diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..a0ddcca --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,12 @@ +node_modules +**/node_modules +.git +dist +build +coverage +*.log +.DS_Store +Thumbs.db +.env +.env.* +!.env.example diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..72cb42c --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,43 @@ +# ── Server ────────────────────────────────────────────────────────────── +NODE_ENV=development +PORT=5000 + +# ── Database ──────────────────────────────────────────────────────────── +# MongoDB connection string (Atlas or local). +MONGODB_URI=mongodb://localhost:27017/specter + +# ── Auth ──────────────────────────────────────────────────────────────── +# Secret used to sign JWTs. Use a long, random value in production. +JWT_SECRET=32de172f98c9aecfc18e797f496565763fbf1eed29cd701c31937101cea68981 + + +# ── URLs ──────────────────────────────────────────────────────────────── +# Used to build Stripe redirect/return URLs and CORS origin. +CLIENT_URL=http://localhost:5173 +SERVER_URL=http://localhost:5000 +FRONTEND_URL=http://localhost:5173 + +# ── Wire API (existing intelligence provider) ────────────────────────── +WIRE_API_KEY=ask_01998127762224e740966ba700dd379ea7c003fbe37d072fc98d129fc96d9392 + +# ── AI (optional — falls back to rule-based analysis if unset) ──────── +ANTHROPIC_API_KEY= + +# ── Stripe ────────────────────────────────────────────────────────────── +# Secret key from the Stripe Dashboard (Developers → API keys). Required +# for checkout/portal/cancel/resume to work — without it, billing routes +# return 503 and the app behaves as free-tier-only. +STRIPE_SECRET_KEY= + +# Publishable key — not used by the backend directly, but kept here so +# both frontend and backend can be configured from one place if you later +# add client-side Stripe.js elements. +STRIPE_PUBLISHABLE_KEY= + +# Signing secret for verifying webhook payloads (Developers → Webhooks → +# your endpoint → "Signing secret"). Required for /api/billing/webhook. +STRIPE_WEBHOOK_SECRET= + +# Price ID for the Specter Pro plan ($1.99/month), created in the Stripe +# Dashboard under Products. Required for checkout to work. +STRIPE_PRICE_ID= diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..cb6a3ed --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,23 @@ +# --- Specter backend: production image --- +FROM node:20-alpine AS base +WORKDIR /app + +FROM base AS deps +COPY package*.json ./ +RUN npm ci --omit=dev + +FROM base AS runner +ENV NODE_ENV=production +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Runs as a non-root user inside the container +RUN addgroup -g 1001 -S nodejs && adduser -S specter -u 1001 +USER specter + +EXPOSE 5000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD node -e "require('http').get('http://localhost:5000/api/health/live', r => process.exit(r.statusCode===200?0:1)).on('error', () => process.exit(1))" + +CMD ["node", "src/index.js"] diff --git a/backend/package-lock.json b/backend/package-lock.json index 28d6250..c0d8d99 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,13 +12,15 @@ "@google/generative-ai": "^0.24.1", "axios": "^1.6.0", "bcryptjs": "^2.4.3", + "compression": "^1.8.1", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.0.0" + "mongoose": "^8.0.0", + "stripe": "^22.3.2" }, "devDependencies": { "nodemon": "^3.0.2" @@ -312,6 +314,45 @@ "node": ">= 0.8" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1370,6 +1411,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1686,6 +1736,23 @@ "node": ">= 0.8" } }, + "node_modules/stripe": { + "version": "22.3.2", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.3.2.tgz", + "integrity": "sha512-O13QOvgEIQvDlTy6Ubb5kB980wpbhmoZNsgCXKILjCMZS67f+bW+6w99k3gnSi/N1lkryoj1WYdpGT5Wc5edjg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/backend/package.json b/backend/package.json index 305ccb6..c269d6d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,16 +11,18 @@ "seed": "node src/scripts/seed.js" }, "dependencies": { - "express": "^4.18.2", + "@google/generative-ai": "^0.24.1", + "axios": "^1.6.0", + "bcryptjs": "^2.4.3", + "compression": "^1.8.1", "cors": "^2.8.5", "dotenv": "^16.3.1", - "mongoose": "^8.0.0", - "bcryptjs": "^2.4.3", - "jsonwebtoken": "^9.0.2", - "axios": "^1.6.0", + "express": "^4.18.2", "express-rate-limit": "^7.1.5", "helmet": "^7.1.0", - "@google/generative-ai": "^0.24.1" + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.0.0", + "stripe": "^22.3.2" }, "devDependencies": { "nodemon": "^3.0.2" @@ -33,4 +35,4 @@ ], "author": "Specter Team", "license": "MIT" -} \ No newline at end of file +} diff --git a/backend/src/config/validateEnv.js b/backend/src/config/validateEnv.js index e45bce1..4ce1ca0 100644 --- a/backend/src/config/validateEnv.js +++ b/backend/src/config/validateEnv.js @@ -36,6 +36,11 @@ export function validateEnvironment() { console.warn(''); } + if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET || !process.env.STRIPE_PRICE_ID) { + console.warn(' Stripe is not fully configured — billing/checkout routes will return 503.'); + console.warn(' Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and STRIPE_PRICE_ID to enable Specter Pro.\n'); + } + console.log('✓ Environment validated'); console.log(` Wire base: ${process.env.WIRE_API_BASE || 'https://api.anakin.io/v1 (default)'}`); console.log(` Key prefix: ${process.env.WIRE_API_KEY?.slice(0, 15)}...\n`); diff --git a/backend/src/middleware/premium.js b/backend/src/middleware/premium.js new file mode 100644 index 0000000..cc674d8 --- /dev/null +++ b/backend/src/middleware/premium.js @@ -0,0 +1,59 @@ +/** + * Subscription-gating middleware. Routes should use these instead of + * hand-checking `req.user.subscriptionTier` inline — keeps the free/pro + * distinction in one place. + * + * All three assume `authMiddleware` has already run and set `req.userId`. + */ +import { User } from '../models/index.js'; +import { isPro, hasCreditsRemaining } from '../services/creditsService.js'; + +async function loadUser(req, res) { + const user = await User.findById(req.userId); + if (!user) { + res.status(404).json({ error: 'User not found' }); + return null; + } + req.currentUser = user; // cache for the route handler, avoids a second lookup + return user; +} + +/** Blocks the request unless the user has an active Pro subscription. */ +export async function requirePremium(req, res, next) { + const user = await loadUser(req, res); + if (!user) return; + + if (!isPro(user)) { + return res.status(402).json({ + error: 'This feature requires Specter Pro.', + code: 'PREMIUM_REQUIRED' + }); + } + next(); +} + +/** Blocks the request if a free user has exhausted their investigation credits. Pro users always pass. */ +export async function requireCredits(req, res, next) { + const user = await loadUser(req, res); + if (!user) return; + + if (!hasCreditsRemaining(user)) { + return res.status(402).json({ + error: 'You have no investigation credits remaining. Upgrade to Specter Pro for unlimited investigations.', + code: 'CREDITS_EXHAUSTED', + creditsRemaining: 0 + }); + } + next(); +} + +/** Blocks the request unless the user has any recognized subscription record (free counts). Mostly a sanity guard for billing-only routes. */ +export async function requireSubscription(req, res, next) { + const user = await loadUser(req, res); + if (!user) return; + + if (!user.subscriptionStatus) { + return res.status(403).json({ error: 'No subscription record found for this account.' }); + } + next(); +} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 8a74cb9..e9c5400 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -33,12 +33,57 @@ const userSchema = new mongoose.Schema({ }, investigationLimit: { type: Number, - default: 10 + default: 7 }, investigationsUsed: { type: Number, default: 0 }, + // --- Stripe / billing --- + stripeCustomerId: { + type: String, + default: null, + index: true + }, + stripeSubscriptionId: { + type: String, + default: null, + index: true + }, + subscriptionStatus: { + type: String, + enum: ['free', 'active', 'trialing', 'past_due', 'cancelled', 'expired', 'incomplete'], + default: 'free' + }, + subscriptionPlan: { + type: String, + enum: ['free', 'specter_pro'], + default: 'free' + }, + subscriptionCurrentPeriodEnd: { + type: Date, + default: null + }, + cancelAtPeriodEnd: { + type: Boolean, + default: false + }, + creditsRemaining: { + type: Number, + default: 7 + }, + trialCreditsGranted: { + type: Boolean, + default: true + }, + lastCreditReset: { + type: Date, + default: Date.now + }, + billingEmail: { + type: String, + default: null + }, apiKey: { type: String, unique: true, @@ -190,6 +235,12 @@ anakinJobId: String, type: Boolean, default: false }, + // Guards against double-deducting credits if completion processing ever + // runs more than once for the same investigation (retry, webhook replay, etc). + creditConsumed: { + type: Boolean, + default: false + }, tags: [String], createdAt: { type: Date, @@ -292,6 +343,11 @@ const activityLogSchema = new mongoose.Schema({ } }, { timestamps: false }); +// Matches the exact query pattern used by /analytics/timeline and the +// History page (find by userId, sort by timestamp desc) — more efficient +// than relying on the two separate single-field indexes above for that query. +activityLogSchema.index({ userId: 1, timestamp: -1 }); + // SAVED ENTITY MODEL const savedEntitySchema = new mongoose.Schema({ @@ -338,6 +394,18 @@ savedEntitySchema.index({ userId: 1, entityValue: 1 }, { unique: true }); // CREATE MODELS +// WEBHOOK EVENT MODEL (Stripe idempotency) +// Stripe can and does redeliver webhooks (retries, manual replay from the +// dashboard). We record each processed event ID so handlers never apply the +// same event twice. +const webhookEventSchema = new mongoose.Schema({ + stripeEventId: { type: String, required: true, unique: true, index: true }, + type: { type: String, required: true }, + processedAt: { type: Date, default: Date.now } +}); + +export const WebhookEvent = mongoose.model('WebhookEvent', webhookEventSchema); + export const User = mongoose.model('User', userSchema); export const Investigation = mongoose.model('Investigation', investigationSchema); export const ThreatReport = mongoose.model('ThreatReport', threatReportSchema); diff --git a/backend/src/routes/analytics.js b/backend/src/routes/analytics.js index aeb8b78..5614ccd 100644 --- a/backend/src/routes/analytics.js +++ b/backend/src/routes/analytics.js @@ -1,38 +1,40 @@ import express from 'express'; import { Investigation, ThreatReport, ActivityLog } from '../models/index.js'; import mongoose from 'mongoose'; +import { logger } from '../utils/logger.js'; const router = express.Router(); router.get('/overview', async (req, res) => { try { - const totalInvestigations = await Investigation.countDocuments({ userId: req.userId }); - const threatsDetected = await Investigation.countDocuments({ - userId: req.userId, - riskScore: { $gte: 50 } - }); - const reportsGenerated = await ThreatReport.countDocuments({ userId: req.userId }); - - const avgRiskScore = await Investigation.aggregate([ - { $match: { userId: new mongoose.Types.ObjectId(req.userId) } }, - { $group: { _id: null, avg: { $avg: '$riskScore' } } } - ]); - - const threatLevelDistribution = await Investigation.aggregate([ - { $match: { userId: new mongoose.Types.ObjectId(req.userId) } }, - { $group: { _id: '$threatLevel', count: { $sum: 1 } } } - ]); - + const userObjectId = new mongoose.Types.ObjectId(req.userId); + + const [totalInvestigations, threatsDetected, reportsGenerated, avgRiskScoreAgg, threatLevelDistribution] = + await Promise.all([ + Investigation.countDocuments({ userId: req.userId }), + Investigation.countDocuments({ userId: req.userId, riskScore: { $gte: 50 } }), + ThreatReport.countDocuments({ userId: req.userId }), + Investigation.aggregate([ + { $match: { userId: userObjectId } }, + { $group: { _id: null, avg: { $avg: '$riskScore' } } } + ]), + Investigation.aggregate([ + { $match: { userId: userObjectId } }, + { $group: { _id: '$threatLevel', count: { $sum: 1 } } } + ]) + ]); + res.json({ overview: { totalInvestigations, threatsDetected, reportsGenerated, - avgRiskScore: avgRiskScore[0]?.avg || 0, + avgRiskScore: avgRiskScoreAgg[0]?.avg || 0, threatDistribution: threatLevelDistribution } }); } catch (error) { + logger.error(error.message, 'analytics.overview'); res.status(500).json({ error: 'Failed to fetch analytics' }); } }); @@ -41,10 +43,12 @@ router.get('/timeline', async (req, res) => { try { const timeline = await ActivityLog.find({ userId: req.userId }) .sort({ timestamp: -1 }) - .limit(50); + .limit(50) + .lean(); res.json({ timeline }); } catch (error) { + logger.error(error.message, 'analytics.timeline'); res.status(500).json({ error: 'Failed to fetch timeline' }); } }); diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index bfaa1d6..b3a18af 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -2,6 +2,7 @@ import express from 'express'; import jwt from 'jsonwebtoken'; import crypto from 'crypto'; import { User, ActivityLog } from '../models/index.js'; +import { logger } from '../utils/logger.js'; const router = express.Router(); @@ -13,6 +14,28 @@ const generateToken = (userId, email) => { ); }; +// Shared shape for every endpoint that returns a user object, so the +// frontend always sees the same billing fields regardless of which call +// (register/login/profile) populated `user` in AuthContext. +function serializeUser(user) { + return { + id: user._id, + email: user.email, + displayName: user.displayName, + subscriptionTier: user.subscriptionTier, + investigationLimit: user.investigationLimit, + investigationsUsed: user.investigationsUsed, + creditsRemaining: user.creditsRemaining, + subscriptionStatus: user.subscriptionStatus, + subscriptionPlan: user.subscriptionPlan, + subscriptionCurrentPeriodEnd: user.subscriptionCurrentPeriodEnd, + cancelAtPeriodEnd: user.cancelAtPeriodEnd, + settings: user.settings, + lastLogin: user.lastLogin, + createdAt: user.createdAt + }; +} + // REGISTER @@ -59,15 +82,10 @@ router.post('/register', async (req, res) => { message: 'User registered successfully', token, apiKey, - user: { - id: user._id, - email: user.email, - displayName: user.displayName, - subscriptionTier: user.subscriptionTier - } + user: serializeUser(user) }); } catch (error) { - console.error('Registration error:', error); + logger.error(error.message, 'auth.register'); res.status(500).json({ error: 'Registration failed' }); } }); @@ -113,17 +131,10 @@ router.post('/login', async (req, res) => { success: true, message: 'Logged in successfully', token, - user: { - id: user._id, - email: user.email, - displayName: user.displayName, - subscriptionTier: user.subscriptionTier, - investigationLimit: user.investigationLimit, - investigationsUsed: user.investigationsUsed - } + user: serializeUser(user) }); } catch (error) { - console.error('Login error:', error); + logger.error(error.message, 'auth.login'); res.status(500).json({ error: 'Login failed' }); } }); @@ -146,17 +157,7 @@ router.get('/profile', async (req, res) => { } res.json({ - user: { - id: user._id, - email: user.email, - displayName: user.displayName, - subscriptionTier: user.subscriptionTier, - investigationLimit: user.investigationLimit, - investigationsUsed: user.investigationsUsed, - settings: user.settings, - lastLogin: user.lastLogin, - createdAt: user.createdAt - } + user: serializeUser(user) }); } catch (error) { res.status(500).json({ error: 'Failed to fetch profile' }); @@ -186,12 +187,7 @@ router.put('/profile', async (req, res) => { res.json({ success: true, - user: { - id: user._id, - email: user.email, - displayName: user.displayName, - settings: user.settings - } + user: serializeUser(user) }); } catch (error) { res.status(500).json({ error: 'Failed to update profile' }); diff --git a/backend/src/routes/billing.js b/backend/src/routes/billing.js new file mode 100644 index 0000000..2c29744 --- /dev/null +++ b/backend/src/routes/billing.js @@ -0,0 +1,89 @@ +/** + * Billing routes — checkout, portal, cancel/resume, and status. + * All routes here run behind `authMiddleware` (mounted in server.js). + */ +import express from 'express'; +import billingService from '../services/billing/index.js'; +import { requireSubscription } from '../middleware/premium.js'; +import { getCreditsRemaining, isPro } from '../services/creditsService.js'; +import { User } from '../models/index.js'; +import { logger } from '../utils/logger.js'; + +const router = express.Router(); + +// GET /api/billing/status — full plan/credits/renewal snapshot for the Billing page + Dashboard widgets +router.get('/status', async (req, res) => { + try { + const user = await User.findById(req.userId); + if (!user) return res.status(404).json({ error: 'User not found' }); + + const subscription = await billingService.getSubscriptionStatus(req.userId); + + res.json({ + plan: isPro(user) ? 'pro' : 'free', + subscriptionStatus: user.subscriptionStatus || 'free', + cancelAtPeriodEnd: user.cancelAtPeriodEnd || false, + currentPeriodEnd: user.subscriptionCurrentPeriodEnd, + creditsRemaining: isPro(user) ? null : getCreditsRemaining(user), + creditsLimit: user.investigationLimit, + creditsUsed: user.investigationsUsed, + stripeConnected: !!process.env.STRIPE_SECRET_KEY, + ...subscription + }); + } catch (error) { + logger.error(error.message, 'billing.status'); + res.status(500).json({ error: 'Failed to fetch billing status' }); + } +}); + +// POST /api/billing/checkout — creates a Stripe Checkout Session for Specter Pro +router.post('/checkout', async (req, res) => { + try { + if (!process.env.STRIPE_SECRET_KEY) { + return res.status(503).json({ error: 'Billing is not configured on this server yet.' }); + } + const result = await billingService.createCheckoutSession(req.userId); + res.json(result); + } catch (error) { + logger.error(error.message, 'billing.checkout'); + res.status(500).json({ error: error.message || 'Failed to create checkout session' }); + } +}); + +// POST /api/billing/portal — Stripe billing portal (update card, view invoices, self-serve cancel) +router.post('/portal', requireSubscription, async (req, res) => { + try { + if (!process.env.STRIPE_SECRET_KEY) { + return res.status(503).json({ error: 'Billing is not configured on this server yet.' }); + } + const result = await billingService.createBillingPortalSession(req.userId); + res.json(result); + } catch (error) { + logger.error(error.message, 'billing.portal'); + res.status(500).json({ error: error.message || 'Failed to open billing portal' }); + } +}); + +// POST /api/billing/cancel — cancel at period end (stays Pro until renewal date) +router.post('/cancel', async (req, res) => { + try { + await billingService.cancelSubscription(req.userId); + res.json({ success: true, message: 'Subscription will cancel at the end of the current billing period.' }); + } catch (error) { + logger.error(error.message, 'billing.cancel'); + res.status(500).json({ error: error.message || 'Failed to cancel subscription' }); + } +}); + +// POST /api/billing/resume — undo a pending cancellation before the period ends +router.post('/resume', async (req, res) => { + try { + await billingService.resumeSubscription(req.userId); + res.json({ success: true, message: 'Subscription resumed.' }); + } catch (error) { + logger.error(error.message, 'billing.resume'); + res.status(500).json({ error: error.message || 'Failed to resume subscription' }); + } +}); + +export default router; diff --git a/backend/src/routes/investigations.js b/backend/src/routes/investigations.js index b0fc352..0ff8314 100644 --- a/backend/src/routes/investigations.js +++ b/backend/src/routes/investigations.js @@ -1,9 +1,12 @@ import express from 'express'; -import { User, Investigation, ThreatReport, ActivityLog as ActivityLogModel } from '../models/index.js'; +import { Investigation, ThreatReport, ActivityLog as ActivityLogModel } from '../models/index.js'; import wireService from '../services/wireService.js'; import { WireError } from '../services/wireService.js'; import aiService from '../services/aiService.js'; import threatAnalysisService from '../services/threatAnalysisService.js'; +import { requireCredits } from '../middleware/premium.js'; +import { consumeCredit } from '../services/creditsService.js'; +import { logger } from '../utils/logger.js'; const investRouter = express.Router(); @@ -15,7 +18,7 @@ const PROCESSING_CONFIG = { TOTAL_TIMEOUT: 180000 }; -investRouter.post('/start', async (req, res) => { +investRouter.post('/start', requireCredits, async (req, res) => { try { let { targetType, targetValue } = req.body; const userId = req.userId; @@ -24,6 +27,14 @@ investRouter.post('/start', async (req, res) => { return res.status(400).json({ error: 'Target type and value are required' }); } + if (targetType !== 'url') { + return res.status(400).json({ error: `Unsupported target type "${targetType}". Only "url" is currently supported.` }); + } + + if (typeof targetValue !== 'string' || targetValue.length > 2048) { + return res.status(400).json({ error: 'Target value must be a string under 2048 characters.' }); + } + if (targetType === 'url') { try { @@ -34,8 +45,6 @@ investRouter.post('/start', async (req, res) => { } } - const user = await User.findById(userId); - const investigation = new Investigation({ userId, targetType, @@ -45,8 +54,9 @@ investRouter.post('/start', async (req, res) => { await investigation.save(); - user.investigationsUsed += 1; - await user.save(); + // Credits are deducted only once the investigation actually completes + // successfully (see processInvestigation) — not here, and never on + // failure. req.currentUser was already loaded by requireCredits above. await ActivityLogModel.create({ userId, @@ -62,7 +72,7 @@ investRouter.post('/start', async (req, res) => { targetType, targetValue, PROCESSING_CONFIG.TOTAL_TIMEOUT - ).catch(err => console.error(`[PROCESS] Unhandled error for ${investigation._id}:`, err)); + ).catch(err => logger.error(`unhandled error for ${investigation._id}: ${err.message}`, 'investigations')); res.status(201).json({ success: true, @@ -71,7 +81,10 @@ investRouter.post('/start', async (req, res) => { status: 'processing' }); } catch (error) { - console.error('Investigation start error:', error); + logger.error(error.message, 'investigations.start'); + if (error.name === 'ValidationError') { + return res.status(400).json({ error: 'Invalid investigation data', details: error.message }); + } res.status(500).json({ error: 'Failed to start investigation' }); } }); @@ -116,7 +129,7 @@ investRouter.get('/:investigationId', async (req, res) => { } }); } catch (error) { - console.error('Get investigation error:', error); + logger.error(error.message, 'investigations.detail'); res.status(500).json({ error: 'Failed to fetch investigation' }); } }); @@ -124,15 +137,16 @@ investRouter.get('/:investigationId', async (req, res) => { investRouter.get('/', async (req, res) => { try { - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; + const page = Math.max(parseInt(req.query.page) || 1, 1); + const limit = Math.min(Math.max(parseInt(req.query.limit) || 10, 1), 100); const skip = (page - 1) * limit; const [investigations, total] = await Promise.all([ Investigation.find({ userId: req.userId }) .sort({ createdAt: -1 }) .skip(skip) - .limit(limit), + .limit(limit) + .lean(), Investigation.countDocuments({ userId: req.userId }) ]); @@ -152,7 +166,7 @@ investRouter.get('/', async (req, res) => { pagination: { page, limit, total, pages: Math.ceil(total / limit) } }); } catch (error) { - console.error('Get investigations error:', error); + logger.error(error.message, 'investigations.list'); res.status(500).json({ error: 'Failed to fetch investigations' }); } }); @@ -168,7 +182,7 @@ investRouter.put('/:investigationId/bookmark', async (req, res) => { if (!investigation) return res.status(404).json({ error: 'Investigation not found' }); res.json({ success: true, isBookmarked: investigation.isBookmarked }); } catch (error) { - console.error('Bookmark investigation error:', error); + logger.error(error.message, 'investigations.bookmark'); res.status(500).json({ error: 'Failed to bookmark investigation' }); } }); @@ -194,11 +208,8 @@ async function processInvestigation(investigationId, userId, targetType, targetV try { investigation = await Investigation.findById(investigationId); - console.log(`[PROCESS] Starting investigation ${investigationId}`); - console.log(`[PROCESS] Target: ${targetType} / ${targetValue}`); + logger.info(`processing ${investigationId} (${targetType}: ${targetValue})`, 'investigations'); - - console.log(`[PROCESS] STEP 1/3: Calling Wire API (max ${PROCESSING_CONFIG.WIRE_TIMEOUT}ms)...`); const wireStartTime = Date.now(); let wireData; @@ -209,22 +220,13 @@ async function processInvestigation(investigationId, userId, targetType, targetV setTimeout(() => reject(new Error('Wire API timeout')), PROCESSING_CONFIG.WIRE_TIMEOUT) ) ]); - console.log( - '[WIRE DEBUG FULL JSON]', - JSON.stringify(wireData.generatedJson, null, 2) -); - - const wireElapsed = Date.now() - wireStartTime; - console.log(`[PROCESS] Wire API completed in ${wireElapsed}ms`); - console.log(`[PROCESS] Markdown: ${wireData.markdown?.length ?? 0} chars`); - console.log(`[PROCESS] JSON keys: ${Object.keys(wireData.generatedJson || {}).join(', ') || '(none)'}`); } catch (wireErr) { const wireElapsed = Date.now() - wireStartTime; const userMessage = wireErr instanceof WireError ? `Wire API error (${wireErr.code}): ${wireErr.message}` : `Wire API failed after ${wireElapsed}ms: ${wireErr.message}`; - console.error(`[PROCESS] Wire API failed: ${userMessage}`); + logger.error(`${investigationId} failed at wire_api: ${userMessage}`, 'investigations'); investigation.status = 'failed'; investigation.errorMessage = userMessage; @@ -241,10 +243,6 @@ async function processInvestigation(investigationId, userId, targetType, targetV return; } - - console.log(`[PROCESS] STEP 2/3: Running AI analysis (max ${PROCESSING_CONFIG.AI_TIMEOUT}ms)...`); - const aiStartTime = Date.now(); - let aiAnalysis; try { aiAnalysis = await Promise.race([ @@ -253,23 +251,11 @@ async function processInvestigation(investigationId, userId, targetType, targetV setTimeout(() => reject(new Error('AI analysis timeout')), PROCESSING_CONFIG.AI_TIMEOUT) ) ]); - - const aiElapsed = Date.now() - aiStartTime; - console.log(`[PROCESS] AI analysis completed in ${aiElapsed}ms`); - console.log(`[PROCESS] Source: ${aiAnalysis.source}`); - console.log(`[PROCESS] Patterns: ${aiAnalysis.suspiciousPatterns?.length || 0}`); } catch (aiErr) { - const aiElapsed = Date.now() - aiStartTime; - console.warn(`[PROCESS] AI analysis failed after ${aiElapsed}ms: ${aiErr.message}`); - console.log('[PROCESS] Using fallback rule-based analysis...'); - + logger.warn(`${investigationId} AI analysis failed, using rule-based fallback: ${aiErr.message}`, 'investigations'); aiAnalysis = aiService._ruleBasedAnalysis(targetType, targetValue, wireData); } - - console.log(`[PROCESS] STEP 3/3: Calculating threat score (max ${PROCESSING_CONFIG.THREAT_ANALYSIS_TIMEOUT}ms)...`); - const threatStartTime = Date.now(); - let threatAnalysis; try { threatAnalysis = await Promise.race([ @@ -278,16 +264,8 @@ async function processInvestigation(investigationId, userId, targetType, targetV setTimeout(() => reject(new Error('Threat analysis timeout')), PROCESSING_CONFIG.THREAT_ANALYSIS_TIMEOUT) ) ]); - - const threatElapsed = Date.now() - threatStartTime; - console.log(`[PROCESS] Threat analysis completed in ${threatElapsed}ms`); - console.log(`[PROCESS] Risk Score: ${threatAnalysis.riskScore}/100`); - console.log(`[PROCESS] Threat Level: ${threatAnalysis.threatLevel}`); } catch (threatErr) { - const threatElapsed = Date.now() - threatStartTime; - console.warn(`[PROCESS] Threat analysis failed after ${threatElapsed}ms: ${threatErr.message}`); - console.log('[PROCESS] Using default threat analysis...'); - + logger.warn(`${investigationId} threat analysis failed, using default: ${threatErr.message}`, 'investigations'); threatAnalysis = { riskScore: 0, threatLevel: 'unknown', @@ -299,8 +277,6 @@ async function processInvestigation(investigationId, userId, targetType, targetV }; } - - console.log(`[PROCESS] Storing results...`); investigation.linkedIdentities = aiAnalysis.linkedIdentities || []; investigation.suspiciousPatterns = aiAnalysis.suspiciousPatterns || []; investigation.behavioralInsights = aiAnalysis.behavioralInsights || []; @@ -351,6 +327,15 @@ async function processInvestigation(investigationId, userId, targetType, targetV await investigation.save(); + // Deduct exactly one credit now that the investigation has actually + // succeeded — never on failure, and the creditConsumed flag makes this + // safe even if processing were somehow re-triggered for the same doc. + if (!investigation.creditConsumed) { + await consumeCredit(userId); + investigation.creditConsumed = true; + await investigation.save(); + } + await generateThreatReport(investigation); await ActivityLogModel.create({ @@ -364,11 +349,10 @@ async function processInvestigation(investigationId, userId, targetType, targetV } }); - console.log(`[PROCESS] Success: Investigation ${investigationId} completed in ${investigation.processingTime}ms`); - console.log(`[PROCESS] Risk: ${investigation.riskScore}/100 | Level: ${investigation.threatLevel}`); + logger.info(`${investigationId} completed in ${investigation.processingTime}ms — risk ${investigation.riskScore}/100 (${investigation.threatLevel})`, 'investigations'); } catch (error) { - console.error(`[PROCESS] Unexpected error for ${investigationId}: ${error.message}`); + logger.error(`unexpected error for ${investigationId}: ${error.message}`, 'investigations'); if (investigation) { investigation.status = 'failed'; @@ -377,9 +361,9 @@ async function processInvestigation(investigationId, userId, targetType, targetV try { await investigation.save(); - console.log(`[PROCESS] Investigation marked as failed: ${investigation._id}`); + logger.info(`${investigation._id} marked failed`, 'investigations'); } catch (saveErr) { - console.error(`[PROCESS] Failed to save error state: ${saveErr.message}`); + logger.error(`failed to save error state: ${saveErr.message}`, 'investigations'); } } } @@ -405,10 +389,10 @@ async function generateThreatReport(investigation) { }); await report.save(); - console.log(`[REPORT] Generated: ${report._id}`); + logger.info(`report generated: ${report._id}`, 'investigations.report'); return report; } catch (error) { - console.error('[REPORT] Generation failed:', error.message); + logger.error(`report generation failed: ${error.message}`, 'investigations.report'); } } diff --git a/backend/src/routes/reports.js b/backend/src/routes/reports.js index a2e275c..c7306d9 100644 --- a/backend/src/routes/reports.js +++ b/backend/src/routes/reports.js @@ -50,4 +50,37 @@ router.post('/:investigationId/export', async (req, res) => { } }); + +// Previously referenced by /export's downloadUrl but never implemented, +// so every export "succeeded" yet the download link 404'd. Implemented here. +router.get('/:reportId/download/:format', async (req, res) => { + try { + const { reportId, format } = req.params; + const report = await ThreatReport.findOne({ + _id: reportId, + userId: req.userId + }).populate('investigationId'); + + if (!report) { + return res.status(404).json({ error: 'Report not found' }); + } + + const filenameBase = `specter-report-${report._id}`; + + if (format === 'json') { + res.setHeader('Content-Disposition', `attachment; filename="${filenameBase}.json"`); + res.setHeader('Content-Type', 'application/json'); + return res.send(JSON.stringify(report.toObject(), null, 2)); + } + + // markdown / txt / pdf (pdf falls back to the plain-text report content; + // rich PDF rendering is generated client-side to avoid a heavy server dependency) + res.setHeader('Content-Disposition', `attachment; filename="${filenameBase}.${format === 'markdown' ? 'md' : 'txt'}"`); + res.setHeader('Content-Type', 'text/plain'); + return res.send(report.reportContent || 'No report content available.'); + } catch (error) { + res.status(500).json({ error: 'Download failed' }); + } +}); + export default router; \ No newline at end of file diff --git a/backend/src/routes/webhooks.js b/backend/src/routes/webhooks.js new file mode 100644 index 0000000..af4b2c9 --- /dev/null +++ b/backend/src/routes/webhooks.js @@ -0,0 +1,155 @@ +/** + * Stripe webhook endpoint. + * + * Mounted with a raw-body parser (see server.js — this route is exempted + * from the global express.json() middleware) because Stripe's signature + * verification requires the exact, untouched request body bytes. + */ +import express from 'express'; +import billingService from '../services/billing/index.js'; +import { User, WebhookEvent } from '../models/index.js'; +import { resetToFreeTierCredits, FREE_TIER_LIMIT } from '../services/creditsService.js'; +import { logger } from '../utils/logger.js'; + +const router = express.Router(); + +async function findUserForEvent(stripeObject) { + const userId = stripeObject.metadata?.userId || stripeObject.client_reference_id; + if (userId) { + const user = await User.findById(userId); + if (user) return user; + } + // fall back to the Stripe customer id, since subscription/invoice objects + // don't always carry our metadata (e.g. renewal invoices) + const customerId = stripeObject.customer; + if (customerId) return User.findOne({ stripeCustomerId: customerId }); + return null; +} + +function mapStripeStatus(stripeStatus) { + const map = { + active: 'active', + trialing: 'trialing', + past_due: 'past_due', + canceled: 'cancelled', + unpaid: 'past_due', + incomplete: 'incomplete', + incomplete_expired: 'expired', + paused: 'cancelled' + }; + return map[stripeStatus] || 'free'; +} + +async function syncSubscriptionToUser(user, subscription) { + user.stripeSubscriptionId = subscription.id; + user.subscriptionStatus = mapStripeStatus(subscription.status); + user.subscriptionPlan = subscription.status === 'canceled' ? 'free' : 'specter_pro'; + user.subscriptionCurrentPeriodEnd = subscription.current_period_end + ? new Date(subscription.current_period_end * 1000) + : null; + user.cancelAtPeriodEnd = !!subscription.cancel_at_period_end; + + if (user.subscriptionStatus === 'active' || user.subscriptionStatus === 'trialing') { + user.subscriptionTier = 'pro'; + } else if (['cancelled', 'expired'].includes(user.subscriptionStatus)) { + user.subscriptionTier = 'free'; + await resetToFreeTierCredits(user._id); + return; // resetToFreeTierCredits already saved + } + + await user.save(); +} + +router.post('/', async (req, res) => { + const signature = req.headers['stripe-signature']; + + let event; + try { + event = await billingService.verifyWebhook(req.body, signature); + } catch (err) { + logger.error(`signature verification failed: ${err.message}`, 'webhooks'); + return res.status(400).json({ error: `Webhook signature verification failed: ${err.message}` }); + } + + // Idempotency: Stripe redelivers events (retries, dashboard replay). + // If we've already processed this exact event ID, ack and stop. + try { + await WebhookEvent.create({ stripeEventId: event.id, type: event.type }); + } catch (err) { + if (err.code === 11000) { + return res.json({ received: true, duplicate: true }); + } + throw err; + } + + try { + switch (event.type) { + case 'checkout.session.completed': { + // The subscription itself is synced by the subscription.created + // event that Stripe fires immediately after — nothing to do here + // beyond acknowledging receipt. Kept as an explicit case (rather + // than falling into `default`) so it's visible in logs/monitoring. + break; + } + + case 'customer.subscription.created': + case 'customer.subscription.updated': + case 'customer.subscription.resumed': { + const subscription = event.data; + const user = await findUserForEvent(subscription); + if (user) await syncSubscriptionToUser(user, subscription); + break; + } + + case 'customer.subscription.paused': + case 'customer.subscription.deleted': { + const subscription = event.data; + const user = await findUserForEvent(subscription); + if (user) { + user.subscriptionStatus = event.type === 'customer.subscription.paused' ? 'cancelled' : 'expired'; + user.subscriptionTier = 'free'; + user.stripeSubscriptionId = null; + user.subscriptionPlan = 'free'; + await user.save(); + await resetToFreeTierCredits(user._id); + } + break; + } + + case 'invoice.paid': { + const invoice = event.data; + const user = await findUserForEvent(invoice); + if (user && user.subscriptionTier === 'pro') { + // Pro users don't draw from creditsRemaining, but keep the field + // sane in case of a future downgrade. + user.creditsRemaining = FREE_TIER_LIMIT; + await user.save(); + } + break; + } + + case 'invoice.payment_failed': { + const invoice = event.data; + const user = await findUserForEvent(invoice); + if (user) { + user.subscriptionStatus = 'past_due'; + await user.save(); + } + break; + } + + default: + break; // event type not relevant to subscription state + } + + res.json({ received: true }); + } catch (err) { + logger.error(`processing ${event.type} failed: ${err.message}`, 'webhooks'); + // Still 200 here would suppress Stripe retries for a transient DB error, + // so surface a 500 and let Stripe redeliver — our idempotency guard + // above means redelivery is always safe. + res.status(500).json({ error: 'Webhook processing failed' }); + } +}); + +export default router; diff --git a/backend/src/server.js b/backend/src/server.js index f41e947..6e7ee14 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,14 +1,28 @@ import express from 'express'; import cors from 'cors'; +import helmet from 'helmet'; +import compression from 'compression'; import mongoose from 'mongoose'; import jwt from 'jsonwebtoken'; import rateLimit from 'express-rate-limit'; +import { logger } from './utils/logger.js'; const app = express(); app.set('trust proxy', 1); // MIDDLEWARE CONFIGURATION +app.use(helmet({ + contentSecurityPolicy: false // API-only server; the SPA is served separately and sets its own CSP +})); +app.use(compression()); + +app.use((req, res, next) => { + const start = Date.now(); + res.on('finish', () => logger.request(req, res.statusCode, Date.now() - start)); + next(); +}); + app.use(cors({ origin: process.env.FRONTEND_URL || 'http://localhost:5173', credentials: true, @@ -16,7 +30,13 @@ app.use(cors({ })); -app.use(express.json({ limit: '10mb' })); +// Stripe requires the raw, unparsed request body to verify webhook +// signatures, so /api/billing/webhook is exempted from the global JSON +// parser and given its own raw-body parser directly on the route instead. +app.use((req, res, next) => { + if (req.originalUrl === '/api/billing/webhook') return next(); + express.json({ limit: '10mb' })(req, res, next); +}); app.use(express.urlencoded({ limit: '10mb', extended: true })); @@ -32,8 +52,17 @@ const investigationLimiter = rateLimit({ message: 'Investigation rate limit exceeded. Please wait before starting another investigation.' }); +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + message: 'Too many authentication attempts. Please try again later.', + skipSuccessfulRequests: true +}); + app.use('/api/', limiter); app.use('/api/investigations/start', investigationLimiter); +app.use('/api/auth/login', authLimiter); +app.use('/api/auth/register', authLimiter); // DATABASE CONNECTION @@ -86,8 +115,47 @@ const errorHandler = (err, req, res, next) => { // ROUTES +const startedAt = Date.now(); +const packageVersion = process.env.npm_package_version || '1.0.0'; + +// Liveness: process is up and responding. Never checks dependencies — +// used by orchestrators to decide whether to restart the container. +app.get('/api/health/live', (req, res) => { + res.json({ status: 'ok', uptimeSeconds: Math.floor((Date.now() - startedAt) / 1000) }); +}); + +// Readiness: is this instance actually able to serve real traffic. +// Used by load balancers/orchestrators to decide whether to route to it. +app.get('/api/health/ready', (req, res) => { + const dbReady = mongoose.connection.readyState === 1; // 1 = connected + const checks = { + database: dbReady ? 'connected' : 'disconnected', + stripe: process.env.STRIPE_SECRET_KEY ? 'configured' : 'not_configured', + wireApi: process.env.WIRE_API_KEY ? 'configured' : 'not_configured', + }; + const ready = dbReady; + res.status(ready ? 200 : 503).json({ status: ready ? 'ready' : 'not_ready', checks }); +}); + +// General-purpose status endpoint — the one most monitoring tools should point at. app.get('/api/health', (req, res) => { - res.json({ status: 'operational', timestamp: new Date().toISOString() }); + const mem = process.memoryUsage(); + res.json({ + status: 'operational', + version: packageVersion, + environment: process.env.NODE_ENV, + timestamp: new Date().toISOString(), + uptimeSeconds: Math.floor((Date.now() - startedAt) / 1000), + memory: { + rssMB: Math.round(mem.rss / 1024 / 1024), + heapUsedMB: Math.round(mem.heapUsed / 1024 / 1024), + }, + dependencies: { + database: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected', + stripe: process.env.STRIPE_SECRET_KEY ? 'configured' : 'not_configured', + wireApi: process.env.WIRE_API_KEY ? 'configured' : 'not_configured', + } + }); }); import authRoutes from './routes/auth.js'; @@ -95,14 +163,20 @@ import investigationRoutes from './routes/investigations.js'; import reportRoutes from './routes/reports.js'; import analyticsRoutes from './routes/analytics.js'; import wireRoutes from './routes/wire.js'; +import billingRoutes from './routes/billing.js'; +import webhookRoutes from './routes/webhooks.js'; import { validateEnvironment } from './config/validateEnv.js'; +// Raw body required here for Stripe signature verification — must be +// mounted with express.raw() rather than the JSON parser used everywhere else. +app.use('/api/billing/webhook', express.raw({ type: 'application/json' }), webhookRoutes); app.use('/api/auth', authRoutes); app.use('/api/investigations', authMiddleware, investigationRoutes); app.use('/api/reports', authMiddleware, reportRoutes); app.use('/api/analytics', authMiddleware, analyticsRoutes); app.use('/api/wire', authMiddleware, wireRoutes); +app.use('/api/billing', authMiddleware, billingRoutes); app.use((req, res) => { res.status(404).json({ error: 'Route not found' }); @@ -120,7 +194,7 @@ const startServer = async () => { validateEnvironment(); await connectDB(); - app.listen(PORT, () => { + const server = app.listen(PORT, () => { console.log(` ╔══════════════════════════════════════════╗ ║ SPECTER - SERVER STARTED ║ @@ -130,9 +204,35 @@ const startServer = async () => { ║ Database: Connected ║ Wire API: ${process.env.WIRE_API_KEY ? '✓ Key loaded (' + process.env.WIRE_API_KEY.slice(0,12) + '...)' : '✗ NOT SET — investigations will fail'} ║ AI API: ${process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY !== 'your_anthropic_key_here' ? '✓ Configured (free tier)' : '— Not set (rule-based analysis)'} +║ Stripe: ${process.env.STRIPE_SECRET_KEY ? '✓ Configured' : '— Not set (free tier only)'} ╚══════════════════════════════════════════╝ `); }); + + // Graceful shutdown: stop accepting new connections, let in-flight + // requests finish, then close the DB connection before exiting. Container + // orchestrators (Docker, Kubernetes, Render) send SIGTERM before killing. + const shutdown = (signal) => { + logger.info(`${signal} received, shutting down gracefully…`, 'server'); + server.close(async () => { + try { + await mongoose.connection.close(); + logger.info('Shutdown complete.', 'server'); + process.exit(0); + } catch (err) { + logger.error(`Error during shutdown: ${err.message}`, 'server'); + process.exit(1); + } + }); + // Force-exit if shutdown hangs (e.g. a stuck long-poll request) + setTimeout(() => { + logger.error('Forced shutdown after timeout.', 'server'); + process.exit(1); + }, 10000).unref(); + }; + + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('SIGINT', () => shutdown('SIGINT')); }; startServer().catch(err => { diff --git a/backend/src/services/PollingModes-Fixed.js b/backend/src/services/PollingModes-Fixed.js new file mode 100644 index 0000000..9605bfd --- /dev/null +++ b/backend/src/services/PollingModes-Fixed.js @@ -0,0 +1,149 @@ +export const POLLING_MODES = { + HACKATHON: { + // Fast mode - Perfect for hackathons + description: '⚡ Fast mode - Perfect for hackathons', + + // TIME-BASED (not attempt-based) + timeoutMs: 60000, // 1 minute hard deadline + perRequestTimeoutMs: 8000, // 8 seconds per individual request + + // Backoff configuration + baseDelay: 1000, // Start with 1s + backoffIncrement: 500, // +500ms each attempt + maxBackoff: 5000, // Cap at 5s + + // Rate limiting + rateLimitDelay: { min: 5000, max: 8000 }, + + // Metadata (for UI/logging) + maxEstimatedAttempts: 15, // Roughly how many before hitting timeout + maxEstimatedMinutes: 1 + }, + + ULTRA_FAST: { + // Ultra-fast mode - For quick demos + description: '🚀 Ultra-fast mode - For quick demos', + + // TIME-BASED (not attempt-based) + timeoutMs: 45000, // 45 seconds hard deadline + perRequestTimeoutMs: 6000, // 6 seconds per individual request + + // Backoff configuration + baseDelay: 500, // Start with 500ms + backoffIncrement: 200, // +200ms each attempt + maxBackoff: 3000, // Cap at 3s + + // Rate limiting + rateLimitDelay: { min: 3000, max: 5000 }, + + // Metadata (for UI/logging) + maxEstimatedAttempts: 20, // Roughly how many before hitting timeout + maxEstimatedMinutes: 0.75 // 45 seconds + }, + + PRODUCTION: { + // Reliable mode - Better for long tasks + description: '✓ Reliable mode - Better for long tasks', + + // TIME-BASED (not attempt-based) + timeoutMs: 180000, // 3 minutes hard deadline (realistic!) + perRequestTimeoutMs: 15000, // 15 seconds per individual request + + // Backoff configuration + baseDelay: 2000, // Start with 2s + backoffIncrement: 1000, // +1s each attempt + maxBackoff: 10000, // Cap at 10s + + // Rate limiting + rateLimitDelay: { min: 15000, max: 25000 }, + + // Metadata (for UI/logging) + maxEstimatedAttempts: 60, // Roughly how many before hitting timeout + maxEstimatedMinutes: 3 // 180 seconds + } +}; + +/** + * Accurate timeout calculator + * Handles exponential backoff with caps + */ +function calculateMaxDuration(mode) { + let totalMs = 0; + let attempt = 0; + + while (totalMs < mode.timeoutMs) { + const delay = Math.min( + mode.baseDelay + (attempt * mode.backoffIncrement), + mode.maxBackoff + ); + + totalMs += delay; + attempt++; + + // Safety check to prevent infinite loops in calculation + if (attempt > 1000) break; + } + + return { + totalMs, + estimatedAttempts: attempt, + minutes: Math.ceil(totalMs / 60000), + seconds: Math.ceil(totalMs / 1000) + }; +} + +/** + * Validate all modes have accurate metadata + */ +Object.entries(POLLING_MODES).forEach(([modeName, config]) => { + const calculated = calculateMaxDuration(config); + + console.log(`\n[POLLING] Mode: ${modeName}`); + console.log(` ${config.description}`); + console.log(` Timeout: ${config.timeoutMs}ms (${config.timeoutMs / 1000}s)`); + console.log(` Estimated attempts: ~${calculated.estimatedAttempts}`); + console.log(` Estimated duration: ${calculated.minutes}m${(calculated.seconds % 60)}s`); +}); + +// Current active mode +export const CURRENT_MODE = POLLING_MODES.PRODUCTION; + +console.log(`\n[POLLING] ✓ Active mode: ${CURRENT_MODE.description}`); +console.log(`[POLLING] Hard timeout: ${CURRENT_MODE.timeoutMs / 1000}s`); +console.log(`[POLLING] Per-request timeout: ${CURRENT_MODE.perRequestTimeoutMs / 1000}s`); + +/** + * Get active polling configuration + */ +export const getPollingConfig = () => CURRENT_MODE; + +/** + * Switch modes at runtime + */ +export const setPollingMode = (modeName) => { + if (!POLLING_MODES[modeName]) { + throw new Error(`Unknown polling mode: ${modeName}. Available: ${Object.keys(POLLING_MODES).join(', ')}`); + } + // In a real app, you'd update module state or use a singleton + console.log(`[POLLING] Switched to mode: ${POLLING_MODES[modeName].description}`); + return POLLING_MODES[modeName]; +}; + +/** + * Export helper to calculate next backoff delay for a given attempt + */ +export const getBackoffDelay = (attemptNumber, mode = CURRENT_MODE) => { + return Math.min( + mode.baseDelay + (attemptNumber * mode.backoffIncrement), + mode.maxBackoff + ); +}; + +export default { + POLLING_MODES, + CURRENT_MODE, + getPollingConfig, + setPollingMode, + getBackoffDelay, + calculateMaxDuration +}; diff --git a/backend/src/services/aiService.js b/backend/src/services/aiService.js index 8055bbb..b8149a0 100644 --- a/backend/src/services/aiService.js +++ b/backend/src/services/aiService.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import { logger } from '../utils/logger.js'; class AIService { constructor() { @@ -7,15 +8,15 @@ class AIService { this.baseUrl = 'https://generativelanguage.googleapis.com/v1beta'; if (this.apiKey) { - console.log(`[AI] Service initialized – model: ${this.model}`); + logger.info(`service initialized – model: ${this.model}`, 'ai'); } else { - console.log('[AI] No Gemini API key configured – using rule-based Wire data analysis'); + logger.warn('no Gemini API key configured – using rule-based analysis', 'ai'); } } async analyzeTargetWithAI(targetType, targetValue, wireData) { if (!this.apiKey) { - console.log('[AI] Gemini unavailable, using rule-based analysis'); + return this._ruleBasedAnalysis(targetType, targetValue, wireData); } @@ -26,7 +27,7 @@ class AIService { try { return await this._geminiAnalysis(targetType, targetValue, wireData); } catch (err) { - console.warn(`[AI] Analysis failed (${err.message}), falling back to rule-based`); + logger.warn(`analysis failed (${err.message}), falling back to rule-based`, 'ai'); return this._ruleBasedAnalysis(targetType, targetValue, wireData); } } @@ -40,7 +41,7 @@ class AIService { const startTime = Date.now(); try { - console.log(`[AI] Gemini API request (attempt ${retryCount + 1}/3) for: ${targetValue}`); + const response = await axios.post( `${this.baseUrl}/models/${this.model}:generateContent`, @@ -52,10 +53,11 @@ class AIService { } ], generationConfig: { - temperature: 0.1, - maxOutputTokens: 2048, - responseMimeType: "application/json" - } + temperature: 0.1, + maxOutputTokens: 2048 + // REMOVED: responseMimeType - it causes incomplete responses + // Let Gemini return text naturally + } }, { headers: { @@ -78,150 +80,138 @@ class AIService { throw new Error('No response text from Gemini'); } - console.log(`[AI] Gemini response received in ${elapsed}ms`); + logger.debug(`Gemini response received in ${elapsed}ms (${text.length} chars)`, 'ai'); return this._parseResponse(text, targetValue, wireData); } catch (err) { const elapsed = Date.now() - startTime; const errorMsg = err.response?.data?.error?.message || err.message; - console.warn(`[AI] Request failed after ${elapsed}ms: ${errorMsg}`); + logger.warn(`request failed after ${elapsed}ms: ${errorMsg}`, 'ai'); if (retryCount < 2) { - console.log(`[AI] Retrying (attempt ${retryCount + 2}/3) after 2s delay...`); + await new Promise(r => setTimeout(r, 2000)); return this._geminiAnalysis(targetType, targetValue, wireData, retryCount + 1); } - console.error(`[AI] Failed after 3 attempts, using rule-based analysis`); + logger.error('failed after 3 attempts, using rule-based analysis', 'ai'); throw err; } } -_buildPrompt(targetType, targetValue, wireData) { - const wj = wireData?.generatedJson || {}; - - return ` -You are a senior cybersecurity threat analyst. - -Analyze the following website intelligence collected from a live Wire API scrape. - -URL: -${targetValue} - -TITLE: -${wj.title || 'Unknown'} - -DESCRIPTION: -${wj.description || 'None'} - -PAGE TYPE: -${wj.pageType || 'Unknown'} - -METADATA: -${JSON.stringify(wj.metadata || {}, null, 2)} - -LINKS: -${JSON.stringify((wj.links || []).slice(0, 20), null, 2)} + _buildPrompt(targetType, targetValue, wireData) { + const wj = wireData?.generatedJson || {}; -SECTIONS: -${JSON.stringify((wj.sections || []).slice(0, 5), null, 2)} + return `You are a senior cybersecurity threat analyst. Your job is to analyze website data and return ONLY valid JSON. -SCRAPED CONTENT: -${(wireData?.markdown || '').substring(0, 4000)} +CRITICAL: Your entire response must be valid JSON. Do not add any text before or after the JSON. No markdown. No code blocks. Only JSON. -IMPORTANT RULES: +Analyze this website: -1. Use ONLY the supplied data. -2. Do NOT invent information. -3. Do NOT wrap output in markdown. -4. Return VALID JSON ONLY. -5. Every field must exist. +URL: ${targetValue} +Title: ${wj.title || 'Unknown'} +Description: ${wj.description || 'None'} +Page Type: ${wj.pageType || 'Unknown'} +Links: ${JSON.stringify((wj.links || []).slice(0, 5))} +Content Sections: ${JSON.stringify((wj.sections || []).slice(0, 3))} +Content: ${(wireData?.markdown || '').substring(0, 2000)} -Return EXACTLY: +Your response MUST be exactly this JSON structure with no modifications: { - "summary": "brief threat assessment", - "suspiciousPatterns": [], - "behavioralInsights": [], - "linkedIdentities": [], - "recommendations": [] -} -`; + "summary": "one sentence threat assessment", + "suspiciousPatterns": ["pattern1", "pattern2"], + "behavioralInsights": ["insight1"], + "linkedIdentities": ["identity1"], + "recommendations": ["recommendation1"] } +Rules: +1. Return ONLY the JSON object +2. No markdown, no code blocks, no text outside JSON +3. All arrays must exist (use empty arrays [] if none) +4. All strings must be valid UTF-8 +5. Escape any special characters in strings`; + } -_parseResponse(raw, targetValue, wireData) { - try { - let cleaned = raw.trim(); + _parseResponse(raw, targetValue, wireData) { + try { + let cleaned = raw.trim(); + + // Remove markdown code blocks + cleaned = cleaned + .replace(/^```json\n?/i, '') + .replace(/^```\n?/i, '') + .replace(/\n?```$/i, '') + .trim(); + + // Find first { and last } + const start = cleaned.indexOf('{'); + const end = cleaned.lastIndexOf('}'); + + if (start === -1 || end === -1 || start >= end) { + logger.error(`invalid JSON structure (start=${start}, end=${end}): ${cleaned.substring(0, 200)}`, 'ai'); + throw new Error('No valid JSON object found in response'); + } - cleaned = cleaned - .replace(/^```json/i, '') - .replace(/^```/, '') - .replace(/```$/, '') - .trim(); + // Extract JSON + cleaned = cleaned.slice(start, end + 1); - const start = cleaned.indexOf('{'); - const end = cleaned.lastIndexOf('}'); + // Try to parse + let parsed; + try { + parsed = JSON.parse(cleaned); + } catch (parseErr) { + logger.error(`JSON parse error: ${parseErr.message}`, 'ai'); + throw new Error(`Invalid JSON from Gemini: ${parseErr.message}`); + } - if (start === -1 || end === -1) { - throw new Error('No JSON object found'); + // Validate and construct response + return { + summary: + typeof parsed.summary === 'string' + ? parsed.summary + : 'Analysis completed.', + + suspiciousPatterns: + Array.isArray(parsed.suspiciousPatterns) + ? parsed.suspiciousPatterns.filter(p => typeof p === 'string') + : [], + + behavioralInsights: + Array.isArray(parsed.behavioralInsights) + ? parsed.behavioralInsights.filter(i => typeof i === 'string') + : [], + + linkedIdentities: + Array.isArray(parsed.linkedIdentities) + ? parsed.linkedIdentities.map(item => { + if (typeof item === 'string') { + return { username: item, platform: 'Unknown', confidence: 50 }; + } + if (typeof item === 'object' && item !== null) { + return { + username: item.username || item.name || item.value || 'Unknown', + platform: item.platform || item.type || 'Unknown', + confidence: item.confidence ?? 50 + }; + } + return { username: 'Unknown', platform: 'Unknown', confidence: 50 }; + }) + : [], + + recommendations: + Array.isArray(parsed.recommendations) + ? parsed.recommendations.filter(r => typeof r === 'string') + : [], + + source: 'gemini' + }; + } catch (err) { + logger.warn(`failed to parse Gemini response: ${err.message}, using rule-based fallback`, 'ai'); + return this._ruleBasedAnalysis('url', targetValue, wireData); } - - cleaned = cleaned.slice(start, end + 1); - - const parsed = JSON.parse(cleaned); - - return { - summary: - typeof parsed.summary === 'string' - ? parsed.summary - : 'Analysis completed.', - - suspiciousPatterns: - Array.isArray(parsed.suspiciousPatterns) - ? parsed.suspiciousPatterns - : [], - - behavioralInsights: - Array.isArray(parsed.behavioralInsights) - ? parsed.behavioralInsights - : [], - - linkedIdentities: - Array.isArray(parsed.linkedIdentities) - ? parsed.linkedIdentities.map(item => { - if (typeof item === 'string') { - return { username: item, platform: 'Unknown', confidence: 50 }; - } - if (typeof item === 'object' && item !== null) { - return { - username: item.username || item.name || item.value || 'Unknown', - platform: item.platform || item.type || 'Unknown', - confidence: item.confidence ?? 50 - }; - } - return { username: 'Unknown', platform: 'Unknown', confidence: 50 }; - }) - : [], - - recommendations: - Array.isArray(parsed.recommendations) - ? parsed.recommendations - : [], - - source: 'gemini' - }; - } catch (err) { - console.warn(`[AI] Failed to parse Gemini response: ${err.message}`); - console.warn(`[AI] Raw response: ${raw.substring(0, 500)}`); - - return this._ruleBasedAnalysis( - 'url', - targetValue, - wireData - ); } -} _ruleBasedAnalysis(targetType, targetValue, wireData) { if (targetType !== 'url') { @@ -356,4 +346,4 @@ brandNames.forEach(brand => { } } -export default new AIService(); \ No newline at end of file +export default new AIService(); diff --git a/backend/src/services/billing/index.js b/backend/src/services/billing/index.js new file mode 100644 index 0000000..faf257b --- /dev/null +++ b/backend/src/services/billing/index.js @@ -0,0 +1,45 @@ +/** + * Billing service abstraction. + * + * Routes/controllers import from here — never from Stripe directly. If a + * second payment provider is ever needed, only this file changes. + * + * Auto-selects the Stripe provider when STRIPE_SECRET_KEY is configured; + * otherwise falls back to the safe no-op provider (used in local dev + * without Stripe keys, and in the test environment). + */ +import { nullSubscriptionProvider } from './subscriptionProvider.interface.js'; + +let activeProvider = nullSubscriptionProvider; +let initialized = false; + +async function ensureProvider() { + if (initialized) return activeProvider; + initialized = true; + + if (process.env.STRIPE_SECRET_KEY) { + const { stripeSubscriptionProvider } = await import('./stripeSubscriptionProvider.js'); + activeProvider = stripeSubscriptionProvider; + } + return activeProvider; +} + +export function setSubscriptionProvider(provider) { + activeProvider = provider; + initialized = true; +} + +export async function getSubscriptionProvider() { + return ensureProvider(); +} + +export const billingService = { + createCheckoutSession: async (...args) => (await ensureProvider()).createCheckoutSession(...args), + createBillingPortalSession: async (...args) => (await ensureProvider()).createBillingPortalSession(...args), + cancelSubscription: async (...args) => (await ensureProvider()).cancelSubscription(...args), + resumeSubscription: async (...args) => (await ensureProvider()).resumeSubscription(...args), + getSubscriptionStatus: async (...args) => (await ensureProvider()).getSubscriptionStatus(...args), + verifyWebhook: async (...args) => (await ensureProvider()).verifyWebhook(...args), +}; + +export default billingService; diff --git a/backend/src/services/billing/stripeSubscriptionProvider.js b/backend/src/services/billing/stripeSubscriptionProvider.js new file mode 100644 index 0000000..543293d --- /dev/null +++ b/backend/src/services/billing/stripeSubscriptionProvider.js @@ -0,0 +1,111 @@ +/** + * Stripe implementation of the SubscriptionProvider interface + * (see subscriptionProvider.interface.js for the contract). + * + * This is the only file in the codebase that talks to the Stripe SDK + * directly — routes go through services/billing/index.js. + */ +import Stripe from 'stripe'; +import { User } from '../../models/index.js'; +import { resetToFreeTierCredits } from '../creditsService.js'; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2024-06-20' }); + +const PRICE_ID = process.env.STRIPE_PRICE_ID; +const CLIENT_URL = process.env.CLIENT_URL || 'http://localhost:5173'; + +async function ensureStripeCustomer(user) { + if (user.stripeCustomerId) return user.stripeCustomerId; + + const customer = await stripe.customers.create({ + email: user.billingEmail || user.email, + metadata: { userId: user._id.toString() } + }); + + user.stripeCustomerId = customer.id; + await user.save(); + return customer.id; +} + +async function createCheckoutSession(userId) { + const user = await User.findById(userId); + if (!user) throw new Error('User not found'); + if (!PRICE_ID) throw new Error('STRIPE_PRICE_ID is not configured'); + + const customerId = await ensureStripeCustomer(user); + + const session = await stripe.checkout.sessions.create({ + mode: 'subscription', + customer: customerId, + line_items: [{ price: PRICE_ID, quantity: 1 }], + success_url: `${CLIENT_URL}/billing?checkout=success`, + cancel_url: `${CLIENT_URL}/billing?checkout=cancelled`, + client_reference_id: user._id.toString(), + subscription_data: { + metadata: { userId: user._id.toString() } + } + }); + + return { checkoutUrl: session.url }; +} + +async function createBillingPortalSession(userId) { + const user = await User.findById(userId); + if (!user) throw new Error('User not found'); + if (!user.stripeCustomerId) throw new Error('User has no Stripe customer record yet'); + + const session = await stripe.billingPortal.sessions.create({ + customer: user.stripeCustomerId, + return_url: `${CLIENT_URL}/billing` + }); + + return { portalUrl: session.url }; +} + +async function cancelSubscription(userId) { + const user = await User.findById(userId); + if (!user?.stripeSubscriptionId) throw new Error('No active subscription to cancel'); + + await stripe.subscriptions.update(user.stripeSubscriptionId, { cancel_at_period_end: true }); + user.cancelAtPeriodEnd = true; + await user.save(); +} + +async function resumeSubscription(userId) { + const user = await User.findById(userId); + if (!user?.stripeSubscriptionId) throw new Error('No subscription to resume'); + + await stripe.subscriptions.update(user.stripeSubscriptionId, { cancel_at_period_end: false }); + user.cancelAtPeriodEnd = false; + await user.save(); +} + +async function getSubscriptionStatus(userId) { + const user = await User.findById(userId); + if (!user) throw new Error('User not found'); + return { + status: user.subscriptionStatus || 'free', + plan: user.subscriptionPlan || 'free', + currentPeriodEnd: user.subscriptionCurrentPeriodEnd, + cancelAtPeriodEnd: user.cancelAtPeriodEnd + }; +} + +/** Verifies the raw webhook payload against the configured signing secret. Throws on failure — callers must 400 on error. */ +async function verifyWebhook(rawBody, signature) { + const event = stripe.webhooks.constructEvent(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET); + return { type: event.type, data: event.data.object, id: event.id, raw: event }; +} + +export const stripeSubscriptionProvider = { + createCheckoutSession, + createBillingPortalSession, + cancelSubscription, + resumeSubscription, + getSubscriptionStatus, + verifyWebhook, + // exported for the webhook handler, which needs direct access for syncing + _internal: { stripe, ensureStripeCustomer, resetToFreeTierCredits } +}; + +export default stripeSubscriptionProvider; diff --git a/backend/src/services/billing/subscriptionProvider.interface.js b/backend/src/services/billing/subscriptionProvider.interface.js new file mode 100644 index 0000000..a19ff1e --- /dev/null +++ b/backend/src/services/billing/subscriptionProvider.interface.js @@ -0,0 +1,38 @@ +/** + * SubscriptionProvider interface. + * + * This defines the contract any payment provider (Stripe, Paddle, etc.) must + * implement. Nothing here talks to a real payment API yet — it exists so the + * upcoming Stripe integration has a stable shape to implement against instead + * of being wired directly into routes. + * + * When Stripe support is built, create `stripeSubscriptionProvider.js` + * implementing this same method set and swap it in via + * `services/billing/index.js`. + * + * @typedef {Object} SubscriptionProvider + * @property {(userId: string, priceId: string) => Promise<{ checkoutUrl: string }>} createCheckoutSession + * @property {(userId: string) => Promise<{ portalUrl: string }>} createBillingPortalSession + * @property {(userId: string) => Promise} cancelSubscription + * @property {(userId: string) => Promise} resumeSubscription + * @property {(userId: string) => Promise<{ status: 'free'|'active'|'past_due'|'cancelled'|'expired' }>} getSubscriptionStatus + * @property {(rawBody: Buffer, signature: string) => Promise<{ type: string, data: object }>} verifyWebhook + */ + +export const NOT_IMPLEMENTED = () => { + throw new Error( + 'No SubscriptionProvider is configured yet. Stripe integration is a planned next milestone — see services/billing/index.js.' + ); +}; + +/** A provider that safely no-ops everywhere, used until Stripe is wired in. */ +export const nullSubscriptionProvider = { + createCheckoutSession: NOT_IMPLEMENTED, + createBillingPortalSession: NOT_IMPLEMENTED, + cancelSubscription: NOT_IMPLEMENTED, + resumeSubscription: NOT_IMPLEMENTED, + async getSubscriptionStatus() { + return { status: 'free', plan: 'free', currentPeriodEnd: null, cancelAtPeriodEnd: false }; + }, + verifyWebhook: NOT_IMPLEMENTED, +}; diff --git a/backend/src/services/creditsService.js b/backend/src/services/creditsService.js new file mode 100644 index 0000000..0b6907c --- /dev/null +++ b/backend/src/services/creditsService.js @@ -0,0 +1,68 @@ +/** + * Credits service — production implementation. + * + * Centralizes every read/write of investigation credits so no route hand- + * rolls the free/pro distinction. Pro users are unlimited and never touch + * `creditsRemaining`; free users draw down a fixed pool granted at signup. + */ +import { User } from '../models/index.js'; + +export const FREE_TIER_LIMIT = 7; + +export function isPro(user) { + return user?.subscriptionTier === 'pro' && ['active', 'trialing'].includes(user?.subscriptionStatus); +} + +export function getCreditsRemaining(user) { + if (isPro(user)) return Infinity; + return Math.max(user?.creditsRemaining ?? 0, 0); +} + +export function hasCreditsRemaining(user) { + return isPro(user) || getCreditsRemaining(user) > 0; +} + +/** + * Deducts exactly one credit for a completed investigation. Idempotent per + * investigation via the caller passing `investigation.creditConsumed` — this + * function itself just performs the atomic decrement and never goes below 0. + * No-op for Pro users. + */ +export async function consumeCredit(userId) { + const user = await User.findById(userId); + if (!user) return null; + if (isPro(user)) return user; + + if (user.creditsRemaining > 0) { + user.creditsRemaining -= 1; + user.investigationsUsed += 1; + await user.save(); + } + return user; +} + +/** + * Restores credits to the free-tier default. Used when a subscription ends + * (downgrade back to free) — does not run on every login, only on explicit + * subscription-state transitions, so it never silently tops someone back up. + */ +export async function resetToFreeTierCredits(userId) { + return User.findByIdAndUpdate( + userId, + { + creditsRemaining: FREE_TIER_LIMIT, + investigationLimit: FREE_TIER_LIMIT, + lastCreditReset: new Date() + }, + { new: true } + ); +} + +export default { + FREE_TIER_LIMIT, + isPro, + getCreditsRemaining, + hasCreditsRemaining, + consumeCredit, + resetToFreeTierCredits +}; diff --git a/backend/src/services/threatAnalysisService.js b/backend/src/services/threatAnalysisService.js index cf77664..9ca4781 100644 --- a/backend/src/services/threatAnalysisService.js +++ b/backend/src/services/threatAnalysisService.js @@ -1,10 +1,11 @@ +import { logger } from '../utils/logger.js'; class ThreatAnalysisService { constructor() { - console.log('[THREAT] Service initialized'); + logger.info('service initialized', 'threat'); } async calculateRisk(targetType, targetValue, wireData, aiAnalysis) { - console.log(`[THREAT] Calculating risk for: ${targetValue}`); + const startTime = Date.now(); if (targetType !== 'url') { @@ -53,7 +54,7 @@ class ThreatAnalysisService { const suspiciousPatterns = aiAnalysis?.suspiciousPatterns || []; const behavioralInsights = aiAnalysis?.behavioralInsights || []; - console.log(`[THREAT] ✓ Scored in ${Date.now() - startTime}ms – ${score}/100 (${threatLevel})`); + logger.debug(`scored in ${Date.now() - startTime}ms – ${score}/100 (${threatLevel})`, 'threat'); return { riskScore: score, diff --git a/backend/src/services/wireService.js b/backend/src/services/wireService.js index 4ede635..c56bcc2 100644 --- a/backend/src/services/wireService.js +++ b/backend/src/services/wireService.js @@ -1,4 +1,6 @@ import axios from 'axios'; +import { getPollingConfig, getBackoffDelay } from './PollingModes-Fixed.js'; +import { logger } from '../utils/logger.js'; export function normalizeUrl(rawUrl) { if (!rawUrl || typeof rawUrl !== 'string') throw new Error('URL is required'); @@ -7,24 +9,25 @@ export function normalizeUrl(rawUrl) { return `https://${trimmed}`; } -// WIRE SERVICE (powered by Anakin.io) +/** + * WIRE SERVICE - Updated to use time-based polling modes + * Powered by Anakin.io + */ class WireService { constructor() { this.apiKey = process.env.WIRE_API_KEY || null; - // Anakin real base URL — can be overridden via WIRE_API_BASE this.baseUrl = (process.env.WIRE_API_BASE || 'https://api.anakin.io/v1').replace(/\/$/, ''); if (!this.apiKey) { - console.error('[WIRE] ❌ WIRE_API_KEY is not set. Investigations will fail.'); + logger.error('WIRE_API_KEY is not set. Investigations will fail.', 'wire'); } else { - console.log(`[WIRE] ✓ Service initialized – endpoint: ${this.baseUrl}`); + logger.info(`service initialized – endpoint: ${this.baseUrl}`, 'wire'); } } - async scrapeUrl(rawUrl) { const url = normalizeUrl(rawUrl); - console.log(`[WIRE] Scraping URL: ${url}`); + logger.debug(`scraping URL: ${url}`, 'wire'); if (!this.apiKey) { throw new WireError( @@ -33,10 +36,8 @@ class WireService { ); } - let jobId; try { - console.log(`[WIRE] Submitting job to ${this.baseUrl}/url-scraper`); const submitRes = await axios.post( `${this.baseUrl}/url-scraper`, @@ -55,7 +56,6 @@ class WireService { } ); - jobId = submitRes.data?.jobId || submitRes.data?.id; if (!jobId) { @@ -65,7 +65,7 @@ class WireService { ); } - console.log(`[WIRE] ✓ Job submitted, ID: ${jobId}`); + logger.debug(`job submitted: ${jobId}`, 'wire'); } catch (err) { if (err instanceof WireError) throw err; @@ -78,10 +78,10 @@ class WireService { ); } - + // Use time-based polling with modes const result = await this.pollJob(jobId); - console.log(`[WIRE] ✓ Scrape completed for: ${url}`); + logger.debug(`scrape completed: ${url}`, 'wire'); return { url, @@ -96,42 +96,75 @@ class WireService { }; } + /** + * Poll job using time-based deadline from polling modes + * Gets config from getPollingConfig() to support runtime mode switching + */ + async pollJob(jobId) { + const pollingConfig = getPollingConfig(); + const startTime = Date.now(); + const deadline = startTime + pollingConfig.timeoutMs; + let attemptNum = 0; + + logger.debug(`polling job ${jobId}`, 'wire'); - async pollJob(jobId, maxAttempts = 60, intervalMs = 3000) { - console.log(`[WIRE] Polling job ${jobId} (max ${maxAttempts} attempts with backoff = ~180s+ timeout)`); + while (Date.now() < deadline) { + attemptNum++; - for (let i = 0; i < maxAttempts; i++) { + // Calculate backoff using polling mode config + const backoffMs = getBackoffDelay(attemptNum - 1, pollingConfig); - const backoffMs = Math.min(1000 + i * 500, 10000); await sleep(backoffMs); + // Check deadline after sleep + const timeRemaining = deadline - Date.now(); + if (timeRemaining <= 0) { + const elapsed = Date.now() - startTime; + throw new WireError( + `Wire job ${jobId} timed out after ${elapsed}ms (timeout: ${pollingConfig.timeoutMs}ms)`, + 'TIMEOUT' + ); + } + let res; try { res = await axios.get( `${this.baseUrl}/url-scraper/${jobId}`, { headers: { 'X-API-Key': this.apiKey }, - timeout: 15000 + timeout: pollingConfig.perRequestTimeoutMs } ); } catch (err) { const httpStatus = err.response?.status; - console.warn(`[WIRE] Poll attempt ${i + 1} error (HTTP ${httpStatus ?? 'none'}): ${err.message}`); - if (i === maxAttempts - 1) { + const elapsed = Date.now() - startTime; + const remaining = deadline - Date.now(); + + logger.warn( + `poll ${attemptNum} failed after ${elapsed}ms (${Math.ceil(remaining / 1000)}s remaining): ${err.message}`, + 'wire' + ); + + // If we're out of time, fail immediately + if (remaining <= 0) { throw new WireError( - `Wire poll failed after ${maxAttempts} attempts: ${err.message}`, + `Wire poll timeout after ${elapsed}ms: ${err.message}`, 'POLL_FAILED', httpStatus ); } + + // Otherwise, continue to next attempt continue; } const status = res.data?.status; - console.log(`[WIRE] Poll ${i + 1}/${maxAttempts} – status: ${status}`); + const elapsed = Date.now() - startTime; + const remaining = Math.ceil((deadline - Date.now()) / 1000); + if (status === 'completed') { - console.log(`[WIRE] ✓ Job ${jobId} completed (${res.data.durationMs ?? '?'}ms)`); + logger.debug(`job ${jobId} completed in ${elapsed}ms`, 'wire'); return res.data; } @@ -141,15 +174,18 @@ class WireService { 'JOB_FAILED' ); } + + // Status is 'processing', loop continues } + // Deadline exceeded + const elapsed = Date.now() - startTime; throw new WireError( - `Wire job ${jobId} timed out after ${maxAttempts} attempts (max ~180s)`, + `Wire job ${jobId} exceeded timeout (${pollingConfig.timeoutMs}ms) after ${attemptNum} attempts in ${elapsed}ms`, 'TIMEOUT' ); } - async gatherData(targetType, targetValue) { if (targetType !== 'url') { throw new WireError( @@ -161,10 +197,7 @@ class WireService { const result = await this.scrapeUrl(targetValue); const enriched = this.enrichWebsiteInfo(result.generatedJson || {}); - console.log(`[WIRE] ✓ Gathered data for ${result.url}`); - console.log(`[WIRE] markdown: ${result.markdown.length} chars`); - console.log(`[WIRE] generatedJson keys: ${Object.keys(result.generatedJson).join(', ') || '(none)'}`); - console.log(`[WIRE] cached: ${result.cached}`); + logger.info(`gathered data for ${result.url}`, 'wire'); return { targetType: 'url', @@ -182,37 +215,28 @@ class WireService { }; } - enrichWebsiteInfo(jsonData) { - const links = jsonData.links || []; + const links = jsonData.links || []; - return { - ...jsonData, - - pageTitle: jsonData.title || '', - - externalLinks: links.map(l => l.url).filter(Boolean), - - externalLinkCount: links.length, - - sectionCount: (jsonData.sections || []).length, - - imageCount: (jsonData.metadata?.imageUrls || []).length, - - hasLoginKeywords: - JSON.stringify(jsonData).toLowerCase().includes('login') || - JSON.stringify(jsonData).toLowerCase().includes('sign in') || - JSON.stringify(jsonData).toLowerCase().includes('password'), - - hasPaymentKeywords: - JSON.stringify(jsonData).toLowerCase().includes('payment') || - JSON.stringify(jsonData).toLowerCase().includes('card') || - JSON.stringify(jsonData).toLowerCase().includes('checkout') - }; -} + return { + ...jsonData, + pageTitle: jsonData.title || '', + externalLinks: links.map(l => l.url).filter(Boolean), + externalLinkCount: links.length, + sectionCount: (jsonData.sections || []).length, + imageCount: (jsonData.metadata?.imageUrls || []).length, + hasLoginKeywords: + JSON.stringify(jsonData).toLowerCase().includes('login') || + JSON.stringify(jsonData).toLowerCase().includes('sign in') || + JSON.stringify(jsonData).toLowerCase().includes('password'), + hasPaymentKeywords: + JSON.stringify(jsonData).toLowerCase().includes('payment') || + JSON.stringify(jsonData).toLowerCase().includes('card') || + JSON.stringify(jsonData).toLowerCase().includes('checkout') + }; + } } - export class WireError extends Error { constructor(message, code, httpStatus) { super(message); @@ -222,6 +246,8 @@ export class WireError extends Error { } } -function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } +function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} export default new WireService(); diff --git a/backend/src/utils/logger.js b/backend/src/utils/logger.js new file mode 100644 index 0000000..a604205 --- /dev/null +++ b/backend/src/utils/logger.js @@ -0,0 +1,40 @@ +/** + * Centralized logger. + * + * Thin wrapper over console.* that adds a level, timestamp, and consistent + * tagging — used instead of raw console.log calls so log verbosity is + * environment-aware (debug is silent outside development) and every line + * has a predictable shape for log aggregation later. + */ +const isProd = process.env.NODE_ENV === 'production'; + +function timestamp() { + return new Date().toISOString(); +} + +function format(level, tag, message) { + return `[${timestamp()}] ${level} ${tag ? `[${tag}] ` : ''}${message}`; +} + +export const logger = { + info(message, tag) { + console.log(format('INFO ', tag, message)); + }, + warn(message, tag) { + console.warn(format('WARN ', tag, message)); + }, + error(message, tag) { + console.error(format('ERROR', tag, message)); + }, + debug(message, tag) { + // Debug is noisy by design — only surface it in non-production so + // production logs stay meaningful rather than spammy. + if (!isProd) console.log(format('DEBUG', tag, message)); + }, + request(req, statusCode, durationMs) { + if (isProd && statusCode < 400) return; // skip logging successful requests in prod to reduce noise + console.log(format('HTTP ', 'request', `${req.method} ${req.originalUrl} ${statusCode} ${durationMs}ms`)); + }, +}; + +export default logger; diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..a0ddcca --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,12 @@ +node_modules +**/node_modules +.git +dist +build +coverage +*.log +.DS_Store +Thumbs.db +.env +.env.* +!.env.example diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..78fe153 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# Base URL the frontend uses to reach the Specter API. +VITE_API_URL=http://localhost:5000/api diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..60a1390 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,20 @@ +# --- Specter frontend: build the SPA, then serve it with nginx --- +FROM node:20-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +ARG VITE_API_URL=/api +ENV VITE_API_URL=$VITE_API_URL +RUN npm run build + +FROM nginx:1.27-alpine AS runner +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -q --spider http://localhost:8080/ || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..bf70581 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,32 @@ +import reactHooks from 'eslint-plugin-react-hooks'; +import react from 'eslint-plugin-react'; +import globals from 'globals'; + +export default [ + { + ignores: ['dist/**', 'node_modules/**'], + }, + { + files: ['src/**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: { ecmaFeatures: { jsx: true } }, + globals: { ...globals.browser, ...globals.es2021 }, + }, + plugins: { + 'react-hooks': reactHooks, + react, + }, + settings: { + react: { version: 'detect' }, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react/jsx-uses-vars': 'error', // recognizes imports used only inside JSX (e.g. ) as "used" + 'react/jsx-uses-react': 'off', // React 17+ automatic JSX runtime — no need for React in scope + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + 'no-undef': 'error', + }, + }, +]; diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..b9fa8fb --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,45 @@ +server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Security headers (frontend serves the SPA only; the API sets its own via Helmet) + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + gzip on; + gzip_vary on; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + + # Long-term caching for hashed build assets + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Never cache the HTML shell — it references the current hashed assets + location = /index.html { + add_header Cache-Control "no-cache"; + } + + # SPA fallback: let React Router handle client-side routes + location / { + try_files $uri $uri/ /index.html; + } + + # Proxies to the backend container by its docker-compose service name. + # If deploying frontend/backend separately (e.g. Vercel + Render), this + # block is unused — set VITE_API_URL to the backend's public URL instead. + location /api/ { + proxy_pass http://backend:5000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1a70cf9..d6c708b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "axios": "^1.6.0", "framer-motion": "^10.16.0", + "gsap": "^3.15.0", + "jspdf": "^4.2.1", "lucide-react": "^0.292.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -20,6 +22,10 @@ "devDependencies": { "@vitejs/plugin-react": "^4.2.0", "autoprefixer": "^10.4.0", + "eslint": "^9.39.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "globals": "^17.7.0", "postcss": "^8.4.0", "terser": "^5.48.0", "vite": "^5.0.0" @@ -737,6 +743,229 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1310,6 +1539,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1337,6 +1593,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1344,6 +1601,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1356,6 +1623,39 @@ "node": ">= 6.0.0" } }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1381,6 +1681,161 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1424,18 +1879,51 @@ "postcss": "^8.1.0" } }, - "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "https-proxy-agent": "^5.0.1", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.33", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", @@ -1461,6 +1949,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1515,6 +2014,25 @@ "dev": true, "license": "MIT" }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1528,6 +2046,33 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1558,6 +2103,43 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1603,6 +2185,26 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1624,6 +2226,13 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1631,6 +2240,43 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1770,6 +2416,60 @@ "node": ">=12" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1793,6 +2493,49 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1814,6 +2557,19 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -1824,6 +2580,16 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1845,6 +2611,94 @@ "dev": true, "license": "ISC" }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1863,6 +2717,34 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -1890,22 +2772,56 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "hasown": "^2.0.2" }, "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", @@ -1939,12 +2855,257 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-equals": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", @@ -1982,6 +3143,31 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -1991,6 +3177,25 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2003,6 +3208,44 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -2023,6 +3266,22 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -2100,6 +3359,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2147,6 +3450,24 @@ "node": ">= 0.4" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2159,11 +3480,99 @@ "node": ">=10.13.0" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2210,6 +3619,20 @@ "node": ">= 0.4" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2223,6 +3646,58 @@ "node": ">= 6" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -2232,6 +3707,66 @@ "node": ">=12" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2244,6 +3779,36 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -2259,6 +3824,57 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2268,6 +3884,42 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2280,13 +3932,233 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/jiti": { @@ -2305,6 +4177,29 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2318,6 +4213,27 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2331,6 +4247,63 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2349,12 +4322,35 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2438,6 +4434,19 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2455,59 +4464,301 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", "funding": [ { "type": "github", - "url": "https://github.com/sponsors/ai" + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" } ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } + "license": "(MIT AND Zlib)" }, - "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=8" } }, "node_modules/path-parse": { @@ -2516,6 +4767,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2552,6 +4810,16 @@ "node": ">= 6" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -2709,6 +4977,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2735,6 +5013,16 @@ "node": ">=10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2755,6 +5043,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -2915,6 +5213,57 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -2936,6 +5285,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2946,6 +5305,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", @@ -3014,6 +5383,61 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3033,6 +5457,154 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3063,6 +5635,142 @@ "source-map": "^0.6.0" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -3085,6 +5793,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -3097,6 +5818,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -3140,7 +5871,6 @@ "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -3161,6 +5891,16 @@ "dev": true, "license": "MIT" }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -3258,6 +5998,116 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -3289,12 +6139,32 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -3378,12 +6248,140 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 8dad678..0614768 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,8 @@ "dependencies": { "axios": "^1.6.0", "framer-motion": "^10.16.0", + "gsap": "^3.15.0", + "jspdf": "^4.2.1", "lucide-react": "^0.292.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -21,6 +23,10 @@ "devDependencies": { "@vitejs/plugin-react": "^4.2.0", "autoprefixer": "^10.4.0", + "eslint": "^9.39.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "globals": "^17.7.0", "postcss": "^8.4.0", "terser": "^5.48.0", "vite": "^5.0.0" diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2d910a1..b5e4d1d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,81 +1,162 @@ +import { Suspense, lazy } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; import { useAuth } from './hooks/useAuth'; -import Navigation from './components/Navigation'; +import { ToastProvider } from './providers/ToastProvider'; +import { SubscriptionProvider } from './providers/SubscriptionProvider'; +import AppShell from './design-system/layout/AppShell'; +import LoadingOverlay from './design-system/ui/LoadingOverlay'; import LandingPage from './pages/LandingPage'; import Auth from './pages/Auth'; import Dashboard from './pages/Dashboard'; -import Investigation from './pages/Investigation'; -import ThreatReport from './pages/ThreatReport'; import './styles/globals.css'; import './styles/animations.css'; -import Settings from './pages/Settings'; import NotFound from './pages/NotFound'; -function ProtectedRoute({ children }) { +// Code-split everything past the first authenticated screen so the initial +// bundle stays small — these pages are only fetched when actually visited. +const Investigation = lazy(() => import('./pages/Investigation')); +const Investigations = lazy(() => import('./pages/Investigations')); +const ThreatReport = lazy(() => import('./pages/ThreatReport')); +const Threats = lazy(() => import('./pages/Threats')); +const Entities = lazy(() => import('./pages/Entities')); +const Analytics = lazy(() => import('./pages/Analytics')); // pulls in recharts — the heaviest page, now split out +const History = lazy(() => import('./pages/History')); +const Profile = lazy(() => import('./pages/Profile')); +const Billing = lazy(() => import('./pages/Billing')); +const Settings = lazy(() => import('./pages/Settings')); + +function ProtectedRoute({ children, breadcrumb }) { const { isAuthenticated, loading } = useAuth(); - + if (loading) { - return
Loading...
; + return ( +
+ Loading… +
+ ); } - - return isAuthenticated ? children : ; + + if (!isAuthenticated) return ; + + return ( + + }>{children} + + ); } function AppRoutes() { const { isAuthenticated } = useAuth(); - + return ( - <> - {isAuthenticated && } - - : } /> - : } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } -/> - } /> - } /> - - + + : } /> + : } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + ); } - + export default function App() { return ( - + + + + + ); -} \ No newline at end of file +} diff --git a/frontend/src/animations/gsap.js b/frontend/src/animations/gsap.js new file mode 100644 index 0000000..5a881c3 --- /dev/null +++ b/frontend/src/animations/gsap.js @@ -0,0 +1,18 @@ +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +let registered = false; + +/** + * Registers GSAP plugins exactly once. Safe to call from every module that + * needs GSAP — subsequent calls are no-ops. + */ +export function ensureGsapRegistered() { + if (registered) return; + gsap.registerPlugin(ScrollTrigger); + registered = true; +} + +ensureGsapRegistered(); + +export { gsap, ScrollTrigger }; diff --git a/frontend/src/animations/hooks.js b/frontend/src/animations/hooks.js new file mode 100644 index 0000000..296f0ec --- /dev/null +++ b/frontend/src/animations/hooks.js @@ -0,0 +1,206 @@ +import { useEffect, useRef } from 'react'; +import { gsap, ScrollTrigger } from './gsap'; +import { ease, duration } from '../design-system/tokens'; + +function prefersReducedMotion() { + return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; +} + +/** + * Fades + lifts an element in on mount. Use for hero text, headings, and + * anything that should feel intentional rather than instant. + */ +export function useFadeUp({ delay = 0, y = 24, disabled = false } = {}) { + const ref = useRef(null); + + useEffect(() => { + if (disabled || !ref.current) return; + if (prefersReducedMotion()) { + gsap.set(ref.current, { opacity: 1, y: 0 }); + return; + } + const ctx = gsap.context(() => { + gsap.fromTo( + ref.current, + { opacity: 0, y }, + { opacity: 1, y: 0, duration: duration.slow, delay, ease: ease.out } + ); + }); + return () => ctx.revert(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [disabled]); + + return ref; +} + +/** + * Staggers the direct children of the returned ref in on mount. Use for + * metric rows, card grids, and lists. + */ +export function useStagger({ delay = 0, y = 16, amount = 0.4, disabled = false } = {}) { + const ref = useRef(null); + + useEffect(() => { + if (disabled || !ref.current) return; + const children = ref.current.children; + if (!children?.length) return; + if (prefersReducedMotion()) { + gsap.set(children, { opacity: 1, y: 0 }); + return; + } + const ctx = gsap.context(() => { + gsap.fromTo( + children, + { opacity: 0, y }, + { opacity: 1, y: 0, duration: duration.base, delay, ease: ease.out, stagger: amount / children.length } + ); + }); + return () => ctx.revert(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [disabled]); + + return ref; +} + +/** + * Reveals an element when it scrolls into view (ScrollTrigger), rather than + * on mount. Use further down long pages so nothing animates off-screen. + */ +export function useRevealOnScroll({ y = 32, start = 'top 85%' } = {}) { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) return; + if (prefersReducedMotion()) { + gsap.set(ref.current, { opacity: 1, y: 0 }); + return; + } + const ctx = gsap.context(() => { + gsap.fromTo( + ref.current, + { opacity: 0, y }, + { + opacity: 1, + y: 0, + duration: duration.slow, + ease: ease.out, + scrollTrigger: { trigger: ref.current, start }, + } + ); + }); + return () => ctx.revert(); + }, [y, start]); + + return ref; +} + +/** + * Splits text into per-word spans and staggers them in. Free-tier + * substitute for the paid GSAP SplitText plugin. + */ +export function useSplitReveal({ delay = 0, disabled = false } = {}) { + const ref = useRef(null); + + useEffect(() => { + if (disabled || !ref.current) return; + const el = ref.current; + const original = el.textContent; + + if (prefersReducedMotion()) { + return; // leave text as-is, no split/animate needed + } + + const words = original.split(' '); + const escapeHtml = (str) => str.replace(/&/g, '&').replace(//g, '>'); + el.innerHTML = words + .map( + (word) => + `${escapeHtml(word)} ` + ) + .join(''); + const inner = el.querySelectorAll('span > span'); + + const ctx = gsap.context(() => { + gsap.fromTo( + inner, + { yPercent: 110, opacity: 0 }, + { yPercent: 0, opacity: 1, duration: duration.slow, delay, ease: ease.out, stagger: 0.035 } + ); + }); + + return () => { + ctx.revert(); + el.textContent = original; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [disabled]); + + return ref; +} + +/** + * Magnetic hover: the element nudges toward the cursor within its bounds. + * Use sparingly — primary CTAs only. No-ops under reduced motion. + */ +export function useMagnetic({ strength = 0.35 } = {}) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el || prefersReducedMotion()) return; + + const onMove = (e) => { + const rect = el.getBoundingClientRect(); + const relX = e.clientX - rect.left - rect.width / 2; + const relY = e.clientY - rect.top - rect.height / 2; + gsap.to(el, { x: relX * strength, y: relY * strength, duration: 0.4, ease: ease.out }); + }; + const onLeave = () => { + gsap.to(el, { x: 0, y: 0, duration: 0.5, ease: 'elastic.out(1, 0.4)' }); + }; + + el.addEventListener('mousemove', onMove); + el.addEventListener('mouseleave', onLeave); + return () => { + el.removeEventListener('mousemove', onMove); + el.removeEventListener('mouseleave', onLeave); + }; + }, [strength]); + + return ref; +} + +/** + * Animates a numeric counter from 0 (or its previous value) to `value`. + */ +export function useAnimatedCounter(value, { decimals = 0, duration: dur = 1.1 } = {}) { + const ref = useRef(null); + const prev = useRef(0); + + useEffect(() => { + if (!ref.current) return; + if (prefersReducedMotion()) { + ref.current.textContent = Number(value).toFixed(decimals); + prev.current = value; + return; + } + const obj = { val: prev.current }; + const ctx = gsap.context(() => { + gsap.to(obj, { + val: value, + duration: dur, + ease: ease.out, + onUpdate: () => { + if (ref.current) ref.current.textContent = obj.val.toFixed(decimals); + }, + }); + }); + prev.current = value; + return () => ctx.revert(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + return ref; +} + +export { gsap, ScrollTrigger }; diff --git a/frontend/src/components/ActivityTimeline.jsx b/frontend/src/components/ActivityTimeline.jsx deleted file mode 100644 index c18ac7a..0000000 --- a/frontend/src/components/ActivityTimeline.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Activity, Zap, AlertTriangle } from 'lucide-react'; -import { motion } from 'framer-motion'; - -export function ActivityTimeline() { - const recentActivities = [ - { icon: AlertTriangle, label: 'Critical threat detected', time: '2m ago', color: 'text-red-400' }, - { icon: Zap, label: 'Investigation completed', time: '15m ago', color: 'text-yellow-400' }, - { icon: AlertTriangle, label: 'Phishing detected', time: '1h ago', color: 'text-orange-400' }, - { icon: Activity, label: 'New investigation started', time: '2h ago', color: 'text-spec-accent' } - ]; - - return ( - -

Activity

- -
- {recentActivities.map((activity, i) => ( - -
- -
-
-

{activity.label}

-

{activity.time}

-
-
- ))} -
-
- ); -} -export default ActivityTimeline; \ No newline at end of file diff --git a/frontend/src/components/DigitalFootprintGraph.jsx b/frontend/src/components/DigitalFootprintGraph.jsx deleted file mode 100644 index 0d39056..0000000 --- a/frontend/src/components/DigitalFootprintGraph.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; -import { motion } from 'framer-motion'; - -export function DigitalFootprintGraph({ linkedIdentities = [] }) { - const platformCounts = linkedIdentities.reduce((acc, id) => { - const existing = acc.find(p => p.name === id.platform); - if (existing) { - existing.value += 1; - } else { - acc.push({ name: id.platform, value: 1 }); - } - return acc; - }, []); - - const COLORS = ['#00ff88', '#00aaff', '#ffaa00', '#ff0055', '#aa00ff']; - - return ( - -

Digital Footprint

- - {linkedIdentities.length === 0 ? ( -

No linked identities found

- ) : ( -
- - - - {platformCounts.map((entry, index) => ( - - ))} - - - - -
- {linkedIdentities.slice(0, 5).map((id, i) => ( - -
-
-

{id.username}

-

{id.platform}

-
-

{id.confidence}%

-
-
- ))} -
-
- )} -
- ); -} -export default DigitalFootprintGraph; \ No newline at end of file diff --git a/frontend/src/components/InvestigationForm.jsx b/frontend/src/components/InvestigationForm.jsx deleted file mode 100644 index 1e3a532..0000000 --- a/frontend/src/components/InvestigationForm.jsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useState } from 'react'; -import { motion } from 'framer-motion'; -import { X, Send } from 'lucide-react'; -import { useInvestigation } from '../hooks/useInvestigation'; -import { useNavigate } from 'react-router-dom'; - -export default function InvestigationForm({ onClose, onSuccess }) { - const [targetType, setTargetType] = useState('url'); - const [targetValue, setTargetValue] = useState(''); - const { startInvestigation, loading, error } = useInvestigation(); - const navigate = useNavigate(); - - const targetTypes = [ - { value: 'url', label: 'URL / Website', placeholder: 'google.com or https://suspicious-site.com' } - ]; - - const handleSubmit = async (e) => { - e.preventDefault(); - if (!targetValue.trim()) return; - - try { - const investigationId = await startInvestigation(targetType, targetValue); - navigate(`/investigation/${investigationId}`); - onSuccess?.(); - } catch (err) { - console.error('Investigation failed:', err); - } - }; - - return ( - - -
- - - - -
-

START INVESTIGATION

-

Analyze any digital entity for threats and suspicious activity

- -
- -
- -
- {targetTypes.map(type => ( - setTargetType(type.value)} - className={`p-3 rounded-lg border-2 transition-all font-medium text-sm font-serif ${ - targetType === type.value - ? 'border-spec-accent bg-spec-accent/10 text-spec-accent' - : 'border-spec-border/50 bg-spec-surface/50 text-gray-400 hover:border-spec-accent/30' - }`} - > - {type.label} - - ))} -
-
- - -
- -