From 8c692d79af0fc8b6ae1b6b0eeb4358d0b86af432 Mon Sep 17 00:00:00 2001 From: myself-aas <129354542+myself-aas@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:47:15 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20full=20code-review=20remediation=20?= =?UTF-8?q?=E2=80=94=20Phases=200-3=20(security,=20hardening,=20maintainab?= =?UTF-8?q?ility,=20quality=20systems)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the complete remediation roadmap from the technical audit in CODE_REVIEW.md (all 5 Criticals, 12 Majors, P3 quality systems), plus the pre-review design-system fixes and the Unsplash->Pexels codemod. Phase 0 — Stop the bleeding: - C3: GitHub webhook HMAC (timing-safe, raw body); unknown repoIds rejected - C2: client identity headers untrusted; constant-time cat_live_ API-key allowlist - C1: /api/payments/verify fails closed; payment webhooks HMAC-verified - C4: /api/state/sync* 401 without a verified token - C5: SSRF DNS pinning (anti-rebinding), fail-closed, TLS verification on Phase 1 — Structural hardening: - firebase-admin ID-token verification; tiers server-derived only - CSP: production drops 'unsafe-inline' (inline-script hashing), no 'unsafe-eval' - per-route body limits + zod validation (telemetry/engines/state) - root + route error boundaries; JSON-LD escaping Phase 2 — Maintainability: - server.ts (2,958 lines) decomposed into server/app.ts + routes/* + core/* - dependency cleanup (dup animation libs, dead editor stack, orphans) - strict: true with 0 tsc errors Phase 3 — Quality systems: - pino structured logging + request IDs + redaction (server); client logger facade -> validated /api/client-logs sink; 43 files off console.* - server-route suite: 168 tests total incl. all Critical flows; coverage gate server/** >=70% lines (72.06%), 94% functions; fixed webhookSecret leak found by the suite (toPublicRepo redaction) - README + docs/ARCHITECTURE.md + docs/ENGINES.md (python-engines decision record; Firebase web config public-by-design note) - bundle-budget ratchet (check:bundle) + Lighthouse config NOTE: .github/workflows/* (CI gate with hard tsc typecheck + Lighthouse job) are authored locally and will follow in an immediate follow-up commit — the GitHub App token requires the 'workflows' permission to push workflow files. Verification: tsc strict 0 | eslint 0 warnings | vitest 168/168 | build ~12s | bundle budgets green | 18-endpoint live smoke green. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .env.example | 26 +- .gitignore | 6 + .lighthouserc.json | 24 + CODE_REVIEW.md | 231 + README.md | 84 +- api/run-engine.ts | 23 +- docs/ARCHITECTURE.md | 90 + docs/ENGINES.md | 39 + eslint.config.js | 1 + index.html | 26 +- lib/engines/eco-carbon.ts | 5 +- lib/engines/health.ts | 3 +- lib/engines/migration.ts | 2 +- package-lock.json | 10332 ++++++++++++++++ package.json | 28 +- prisma/schema.prisma | 265 - pyproject.toml | 33 + replace_images.py | 59 - scripts/check-bundle-budget.mjs | 90 + scripts/replace_images.py | 376 + scripts/requirements-dev.txt | 7 + scripts/tests/test_replace_images.py | 375 + server.ts | 2881 +---- server/app.ts | 178 + server/core/enginesCatalog.ts | 41 + server/core/logger.ts | 86 + server/core/rateLimit.ts | 367 + server/core/runtime.ts | 7 + server/core/ssl.ts | 62 + server/routes/account.ts | 251 + server/routes/clientLogs.ts | 90 + server/routes/engines.ts | 504 + server/routes/github.ts | 480 + server/routes/notifications.ts | 291 + server/routes/payments.ts | 135 + server/routes/plans.ts | 177 + server/routes/reports.ts | 76 + server/routes/stateSync.ts | 156 + server/routes/system.ts | 77 + server/routes/telemetry.ts | 285 + src/App.tsx | 33 +- src/components/DiagnosticEngineCard.tsx | 2 +- src/components/ErrorBoundary.tsx | 116 + src/components/RateLimitModal.tsx | 3 +- src/components/RateLimitThresholdAlert.tsx | 6 +- src/components/TerminalOutput.tsx | 4 +- src/components/admin/BlogManagementView.tsx | 14 +- .../admin/ContactInquiriesAdminView.tsx | 3 +- src/components/admin/SiteMonitoringView.tsx | 30 +- src/components/admin/SystemHealthWidget.tsx | 10 +- src/components/auth/AdminRoute.tsx | 3 +- src/components/auth/AuthDomainModal.tsx | 4 +- src/components/blog/HeroImageLivePreview.tsx | 4 +- .../cards/atomic/TrailCardHeader.tsx | 2 +- .../cards/content/MiniDashboardCard.tsx | 2 +- .../cards/content/TelemetrySwatchCard.tsx | 4 +- .../cards/marketing/IntegrationChip.tsx | 2 +- src/components/cards/marketing/PresetChip.tsx | 6 +- src/components/cards/primitives/CardMedia.tsx | 9 +- .../cards/primitives/StackedDateChip.tsx | 2 +- src/components/cards/types.ts | 2 + .../charts/EdgeLatencyRadarChart.tsx | 2 +- .../charts/OWASPSecurityMatrixChart.tsx | 2 +- src/components/common/BrandLogo.tsx | 2 +- src/components/common/Breadcrumbs.tsx | 3 +- src/components/common/CompareEngineInput.tsx | 69 - .../common/GetInTouchEmailModal.tsx | 7 +- src/components/common/GlobalFaqSection.tsx | 2 +- src/components/common/NewsletterModal.tsx | 3 +- src/components/common/ParallaxCard.tsx | 2 + .../common/PaymentCheckoutModal.tsx | 78 +- .../common/RoleSimulatorFloatingBar.tsx | 2 +- .../common/RouteLoadingSkeleton.tsx | 10 +- src/components/common/SEOHead.tsx | 6 +- .../common/TrialActivationModal.tsx | 7 +- src/components/common/TrialBanner.tsx | 2 +- src/components/dashboard/CommandCenterHUD.tsx | 20 +- .../dashboard/LiveCronLogStream.tsx | 2 +- src/components/docs/DocsLayout.tsx | 2 +- .../home/ArchitectureComparator.tsx | 4 +- .../home/CatalystCarouselSection.tsx | 151 - src/components/home/EnzymeGrid.tsx | 2 +- src/components/home/FeatureBento.tsx | 397 - src/components/home/FeaturedAuditMetrics.tsx | 2 +- src/components/home/FinalCTA.tsx | 4 +- src/components/home/GlobalEdgeRadar.tsx | 303 - src/components/home/HeroSection.tsx | 6 +- src/components/home/LatestBlogsSection.tsx | 11 +- src/components/home/SocialProof.tsx | 2 +- src/components/home/Testimonials.tsx | 4 +- src/components/home/WorkflowSection.tsx | 2 +- src/components/home/index.ts | 15 - src/components/layout/DynamicBanner.tsx | 18 +- src/components/layout/Footer.tsx | 2 +- src/components/layout/GlobalBreadcrumb.tsx | 71 +- src/components/layout/MainMenuOverlay.tsx | 122 +- src/components/layout/NavbarSearch.tsx | 64 +- src/components/layout/StickyHUD.tsx | 4 +- .../legal/CookiePreferenceCenter.tsx | 5 +- src/components/media/AvatarStack.tsx | 5 +- src/components/media/CinematicVideo.tsx | 5 +- src/components/media/PexelsImage.tsx | 7 +- .../pricing/PricingMeshCoverage.tsx | 13 +- src/components/tool/EngineDataTable.tsx | 4 +- src/components/tool/EngineReportDashboard.tsx | 15 +- src/components/ui/3-d-coverflow-carousel.tsx | 10 +- src/components/ui/BenchmarkCard.tsx | 2 +- src/components/ui/CodeBlock.tsx | 2 +- src/components/ui/FullscreenCardShowcase.tsx | 12 +- src/components/ui/FullscreenImageCard.tsx | 2 +- src/components/ui/TelemetryCard.tsx | 4 +- src/components/ui/TerminalInput.tsx | 2 +- src/components/ui/animated-group.tsx | 2 +- src/components/ui/cobe-globe-cdn.tsx | 10 +- src/components/ui/demo.tsx | 10 +- src/components/ui/edge-mesh-globe.tsx | 17 +- src/components/ui/hero-section-6.tsx | 4 +- src/components/ui/index.ts | 1 - src/components/ui/particles-bg.tsx | 2 +- src/components/ui/testimonial.tsx | 1 - .../user/UserAnalyticsDashboard.tsx | 28 +- .../user/UserApiKeyManagementView.tsx | 16 +- .../user/UserBlogManagementView.tsx | 12 +- src/components/user/UserGithubWebhookView.tsx | 5 +- .../user/UserRateLimitAllocationCard.tsx | 3 +- src/context/AuthContext.tsx | 13 +- src/context/RoleSecurityContext.tsx | 3 +- src/context/SubscriptionContext.tsx | 11 +- src/context/ThemeContext.tsx | 12 +- src/data/apiSpecs.ts | 5 +- src/data/engineBlogs.ts | 20 +- src/index.css | 1144 +- src/lib/analyticsEngine.ts | 5 +- src/lib/authHeaders.ts | 34 + src/lib/emailService.ts | 3 +- src/lib/firebase.ts | 77 +- src/lib/logger.ts | 165 + src/lib/media/registry.ts | 8 +- src/lib/networkSecurity.ts | 196 +- src/lib/nodeEngines.ts | 23 +- src/lib/serverAuth.ts | 253 + src/lib/structuredData.ts | 13 + src/lib/utils.ts | 10 + src/lib/validation.ts | 81 + src/lib/webhookSecurity.ts | 55 + src/main.tsx | 10 +- src/pages/BlogEditorPage.tsx | 14 +- src/pages/BlogPostPage.tsx | 6 +- src/pages/BlogsPage.tsx | 19 +- src/pages/ComparePage.tsx | 8 +- src/pages/ContactPage.tsx | 5 +- src/pages/DiagnosticEnginesPage.tsx | 2 +- src/pages/DomainReportArticlePage.tsx | 12 +- src/pages/LoginPage.tsx | 9 +- src/pages/MasterAuditExecutionPage.tsx | 14 +- src/pages/NotFoundPage.tsx | 8 +- src/pages/PlaygroundPage.tsx | 21 +- src/pages/PricingPage.tsx | 6 +- src/pages/ProductsPage.tsx | 3 +- src/pages/ReportPermalinkPage.tsx | 7 +- src/pages/ReportsDirectoryPage.tsx | 3 +- src/pages/SignUpPage.tsx | 15 +- src/pages/ToolPage.tsx | 12 +- src/pages/UserDashboardPage.tsx | 11 +- src/pages/docs/ApiReferenceDoc.tsx | 3 +- src/pages/playground/EnginePlaygroundPage.tsx | 10 +- src/store/middleware/mongoSyncMiddleware.ts | 21 +- src/store/slices/alertsSlice.ts | 3 +- src/store/slices/analyticsSlice.ts | 3 +- src/store/slices/auditsSlice.ts | 3 +- src/store/useAppStore.ts | 19 +- src/styles/card-tokens.css | 5 + src/tests/phase1Hardening.test.tsx | 264 + src/tests/server/api.test.ts | 624 + src/tests/server/rateLimit.test.ts | 214 + .../ui/accessibilityAndContrast.test.tsx | 2 +- src/tests/ui/cardsAndParallax.test.tsx | 1 - .../ui/interactiveEnginesAndForms.test.tsx | 7 +- src/tests/ui/modalsAndOverlays.test.tsx | 2 +- src/tests/ui/trailCard.test.tsx | 2 +- src/tests/webhookSecurity.test.ts | 179 + tailwind.config.js | 79 - tailwind.config.ts | 122 - tsconfig.json | 32 +- vite.config.ts | 3 +- vitest.config.ts | 17 + 186 files changed, 18607 insertions(+), 5458 deletions(-) create mode 100644 .lighthouserc.json create mode 100644 CODE_REVIEW.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/ENGINES.md create mode 100644 package-lock.json delete mode 100644 prisma/schema.prisma create mode 100644 pyproject.toml delete mode 100644 replace_images.py create mode 100644 scripts/check-bundle-budget.mjs create mode 100644 scripts/replace_images.py create mode 100644 scripts/requirements-dev.txt create mode 100644 scripts/tests/test_replace_images.py create mode 100644 server/app.ts create mode 100644 server/core/enginesCatalog.ts create mode 100644 server/core/logger.ts create mode 100644 server/core/rateLimit.ts create mode 100644 server/core/runtime.ts create mode 100644 server/core/ssl.ts create mode 100644 server/routes/account.ts create mode 100644 server/routes/clientLogs.ts create mode 100644 server/routes/engines.ts create mode 100644 server/routes/github.ts create mode 100644 server/routes/notifications.ts create mode 100644 server/routes/payments.ts create mode 100644 server/routes/plans.ts create mode 100644 server/routes/reports.ts create mode 100644 server/routes/stateSync.ts create mode 100644 server/routes/system.ts create mode 100644 server/routes/telemetry.ts create mode 100644 src/components/ErrorBoundary.tsx delete mode 100644 src/components/common/CompareEngineInput.tsx delete mode 100644 src/components/home/CatalystCarouselSection.tsx delete mode 100644 src/components/home/FeatureBento.tsx delete mode 100644 src/components/home/GlobalEdgeRadar.tsx delete mode 100644 src/components/home/index.ts delete mode 100644 src/components/ui/testimonial.tsx create mode 100644 src/lib/authHeaders.ts create mode 100644 src/lib/logger.ts create mode 100644 src/lib/serverAuth.ts create mode 100644 src/lib/structuredData.ts create mode 100644 src/lib/validation.ts create mode 100644 src/lib/webhookSecurity.ts create mode 100644 src/tests/phase1Hardening.test.tsx create mode 100644 src/tests/server/api.test.ts create mode 100644 src/tests/server/rateLimit.test.ts create mode 100644 src/tests/webhookSecurity.test.ts delete mode 100644 tailwind.config.js delete mode 100644 tailwind.config.ts diff --git a/.env.example b/.env.example index 08c6c84..fa6c60f 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,4 @@ CATALYST_WEBHOOK_URL= -DATABASE_URL= DODOPAY_API_KEY= DODOPAY_SANDBOX_MODE= DODOPAY_WEBHOOK_SECRET= @@ -27,4 +26,27 @@ VITE_FIREBASE_AUTH_DOMAIN= VITE_FIREBASE_MEASUREMENT_ID= VITE_FIREBASE_MESSAGING_SENDER_ID= VITE_FIREBASE_PROJECT_ID= -VITE_FIREBASE_STORAGE_BUCKET= \ No newline at end of file +VITE_FIREBASE_STORAGE_BUCKET= + +# --- Phase 0 security hardening ------------------------------------------- +# GitHub webhook HMAC secret (sha256 signatures are verified over the raw body). +# Configure this in your GitHub repo webhook settings; leave empty to reject +# all GitHub webhook deliveries. +GITHUB_WEBHOOK_SECRET= + +# Webhook signing secrets for payment gateways (HMAC-SHA256 over raw body). +# Per-gateway value wins over the shared value; unset => deliveries rejected (503). +PAYMENTS_WEBHOOK_SECRET= +PAYMENTS_WEBHOOK_SECRET_2CHECKOUT= +PAYMENTS_WEBHOOK_SECRET_DODOPAY= + +# Comma-separated allowlist of valid ``cat_live_...`` API keys. Unset => the +# API-key rate tier is disabled entirely (Phase 1 replaces this with hashed, +# persisted keys). +VALID_API_KEYS= + +# Firebase Admin service account for server-side ID token verification +# (state sync auth). Either inline JSON (optionally base64) or a file path. +# Unset => /api/state/sync responds 401 to everyone (fail closed). +FIREBASE_SERVICE_ACCOUNT_JSON= +FIREBASE_SERVICE_ACCOUNT_PATH= diff --git a/.gitignore b/.gitignore index 9e438ac..c32df22 100644 --- a/.gitignore +++ b/.gitignore @@ -312,3 +312,9 @@ Thumbs.db .cache .temp + +# Python tooling artifacts +__pycache__/ +*.pyc +.coverage +coverage/ diff --git a/.lighthouserc.json b/.lighthouserc.json new file mode 100644 index 0000000..f8c4176 --- /dev/null +++ b/.lighthouserc.json @@ -0,0 +1,24 @@ +{ + "ci": { + "collect": { + "staticDistDir": "./dist", + "numberOfRuns": 3, + "settings": { + "preset": "desktop" + } + }, + "assert": { + "assertions": { + "categories:performance": ["warn", { "minScore": 0.5 }], + "categories:accessibility": ["error", { "minScore": 0.9 }], + "categories:best-practices": ["error", { "minScore": 0.85 }], + "categories:seo": ["error", { "minScore": 0.85 }], + "resource-summary:script-size": ["off"], + "resource-summary:total-size": ["off"] + } + }, + "upload": { + "target": "temporary-public-storage" + } + } +} diff --git a/CODE_REVIEW.md b/CODE_REVIEW.md new file mode 100644 index 0000000..b408f8e --- /dev/null +++ b/CODE_REVIEW.md @@ -0,0 +1,231 @@ +# Code Review: CatalystLab — Full-Codebase Technical Audit + +> Reviewer methodology: [code-reviewer skill](https://github.com/Jeffallan/claude-skills/blob/main/skills/code-reviewer/SKILL.md) +> (Context → Structure → Details → Tests → Report). Scope: entire repository +> (~75,700 LOC of TS/TSX across `server.ts`, `src/`, `api/`, `lib/`, `app/`, plus +> Firestore rules, Prisma schema, Python tooling). Commit reviewed: `6bdf506`. + +--- + +**Remediation status:** Phase 0 ✅ (`4568112` — fail-closed auth, webhook HMAC, SSRF DNS pinning) · **Phase 1 ✅** (identity middleware + server-derived tiers, CSP hardening, zod validation + per-route body limits, error boundaries, CI gate, JSON-LD escaping) · **Phase 2 ✅** (⑫ store/middleware decomposition `7de284a`; ⑬ dead-code cleanup `00bf4c1`; ⑭ `strict: true` ramps to 0 errors — CI typecheck restored as hard gate) · **Phase 3 ✅** (⑮ structured logging `418f9d6`; ⑯ server-route suite + 72% coverage gate `eab9e56` — found and fixed a webhookSecret leak; ⑰ docs `25e41d5`; ⑱ bundle budget + Lighthouse — see commits after). All 5 Criticals closed; follow-up candidates: `noUncheckedIndexedAccess` ramp, initial-chunk splitting (entry 1009 kB), hashing persisted API keys. + +--- + +## Summary + +**Intent recap (one sentence):** CatalystLab is a Vite 6 + React 18 SPA with an integrated Express 4 server (`server.ts`) that runs website-audit "engines" against user-supplied URLs, metered by a tiered rate limiter, with Firebase auth/Firestore on the client, MongoDB analytics, and demo payment-gateway integrations. + +**Overall assessment:** The frontend is in solid shape — code-split, accessibility-aware, XSS-safe markdown rendering, well-structured components, 59 green tests. The security posture of the **server layer is not production-ready**: there is no server-side authentication verification anywhere, entitlements and tier limits are derived from client-supplied headers, webhooks are unverified, payment verification is a stub that always succeeds, and SSRF validation has exploitable TOCTOU/fail-open gaps. None of these block the current demo deployment, but all of them are real defects in shipped code. + +**Verdict**: [ ] Approve | [x] **Request Changes** | [ ] Comment + +Severity counts: **Critical 5 · Major 12 · Minor 9** + +--- + +## Critical Issues (Must Fix) + +### C1. `[server.ts:2868-2880]` Payments: `/api/payments/verify` grants entitlements unconditionally +- **Current**: The endpoint echoes back any `orderId`/`planId`/`billingCycle` and returns `success: true` with an `activeUntil` timestamp — no gateway API call, no signature check, no order lookup: + ```ts + app.post('/api/payments/verify', express.json(), async (req, res) => { + const { orderId, planId, billingCycle, gateway } = req.body; + res.json({ success: true, message: `Payment successfully verified...`, + activeUntil: Date.now() + (billingCycle === 'annual' ? 365 : 30) * 86400000 }); + }); + ``` + Both payment webhooks (`:2839`, `:2851`) are no-op loggers, and gateway credentials fall back to `DEMO_2CO` / `DEMO_DODO` (`:2799`, `:2806`) — fail-open config. +- **Suggested**: Verify server-side before granting anything: query the gateway's checkout/session API with the API key (`process.env.DODOPAY_API_KEY` must be **required**, not defaulted), or verify the webhook HMAC and drive entitlements *only* from verified webhooks. Fail closed when env is missing: + ```ts + const apiKey = process.env.DODOPAY_API_KEY; + if (!apiKey) { res.status(503).json({ success: false, error: 'Payments not configured' }); return; } + const session = await fetch(`${DODO_API}/checkouts/${orderId}`, { headers: { Authorization: `Bearer ${apiKey}` } }) + .then(r => r.json()); + if (session.status !== 'succeeded') { res.status(402).json({ success: false, error: 'Payment not settled' }); return; } + ``` +- **Impact**: Any anonymous client can self-activate Pro/Enterprise with one POST. Combined with C2 it upgrades rate limits and any plan-gated UI. + +### C2. `[server.ts:120-160]` AuthN: tier/superadmin resolved from spoofable client headers +- **Current**: `resolveClientIdentity()` reads `userEmail`, `userId`, `subscriptionPlan`, `isTrialActive` from request body/headers/query with zero verification. `x-user-email: shuvoasifahmed@gmail.com` → `burstMax: Infinity, limit: null`. Any `x-api-key` starting with `cat_live_` → `api_pro` tier without checking a key store. The client confirms this model (`UserRateLimitAllocationCard.tsx:76` sends `'x-user-email': user?.email`). +- **Suggested**: Verify a Firebase ID token server-side (`firebase-admin` is already in `package.json` but **never imported**): + ```ts + import { getAuth } from 'firebase-admin/auth'; + const token = await getAuth().verifyIdToken(bearerToken); // throws on forgery + // tier from Firestore subscription doc keyed by token.uid — never from headers + ``` + API keys must be validated against persisted hashes (`crypto.timingSafeEqual`), not prefix matching. +- **Impact**: Complete rate-limit/privilege bypass; superadmin is one spoofed header away. The audit engines perform outbound requests + CPU work, so this is a resource-exhaustion vector against the whole service. + +### C3. `[server.ts:2365-2367]` GitHub webhook HMAC never verified +- **Current**: The signature header is read and then never used; `crypto` is imported at `:11` and never called. Unknown `repoId`s are auto-provisioned with an empty secret (`:2372-2379`), and the demo repo's secret is hardcoded in source (`:2238`). +- **Suggested**: + ```ts + const expected = 'sha256=' + crypto.createHmac('sha256', repo.webhookSecret).update(rawBodyBuffer).digest('hex'); + if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature || ''))) { + res.status(401).json({ error: 'Invalid signature' }); return; + } + ``` + Note: signature verification needs the **raw body**, so mount `express.raw({ type: 'application/json' })` on the webhook route instead of the parsed JSON body. Reject unknown `repoId`s. +- **Impact**: Anyone can forge push/PR events, trigger engine scans (cost), and inject fake repository telemetry into the SSE stream consumed by the dashboard. + +### C4. `[server.ts:1111-1236]` `/api/state/sync` — unauthenticated IDOR on user data +- **Current**: `ownerId` is taken from `req.query` (default `usr_default`) and used to read **and write/delete** domains, goals, alerts, user preferences, and audit records across three handlers (GET `:1111`, POST `:1159`, DELETE `:1224`). No token, no session, no ownership check. +- **Suggested**: Derive `ownerId` exclusively from a verified token (`const ownerId = (await verifyToken(req)).uid`). Reject requests without a valid token with 401 — never default to `usr_default`. +- **Impact**: Any anonymous caller can enumerate (`GET /api/state/sync?ownerId=`), overwrite, or delete any user's persisted state when MongoDB is configured. Data breach + tampering. + +### C5. `[src/lib/networkSecurity.ts:100-151]` + `[server.ts:1427-1443]` SSRF guard: DNS-rebinding TOCTOU and fail-open design +- **Current**: `validatePublicUrl()` resolves DNS and checks the IP — then the request code calls `client.request(parsedUrl, …)` which **resolves DNS a second time**. An attacker controlling a short-TTL DNS record passes validation (public IP) and gets the server to connect to `169.254.169.254` (cloud metadata) or internal services. Two further gaps: (a) if `dnsLookup` throws, the URL is **allowed** (`:141-149` — fail-open); (b) `/api/check-url` disables TLS verification (`rejectUnauthorized: false`, `server.ts:1438`). +- **Suggested**: Pin the validated IP for the actual connection: + ```ts + const { address } = await dnsLookup(hostname); // validated + const agent = new https.Agent({ lookup: () => ({ address, family: 4 }), rejectUnauthorized: true }); + const request = client.request(parsedUrl, { ...opts, agent }, cb); // reconnects to the SAME IP + ``` + Return `{ valid: false }` on DNS failure (fail closed), and re-check the pinned IP with `isPrivateIp(address)` immediately before connecting. +- **Impact**: Classic SSRF bypass chain → cloud credential theft in production deployments, internal network probing from the audit endpoints. + +--- + +## Major Issues (Should Fix) + +### M1. `[server.ts:510-556]` CSP and framing: XSS/clickjacking protection largely neutralized +`scriptSrc` includes `'unsafe-inline'` **and** `'unsafe-eval'`; `frameAncestors` ends with `"*"` and `frameguard: false`. Any injected script executes; any site can frame the app (auth-related UI included). Move inline bootstrap JSON to `application/json` script tags (no CSP exemption needed) with nonces for the rest, and replace `"*"` with explicit origins (ai.studio, run.app) — if AI Studio embedding is a hard requirement, document it and allowlist precisely. + +### M2. Global 10 MB JSON body limit + unvalidated telemetry schema +`express.json({ limit: '10mb' })` (`:560`) applies to every route; the public telemetry endpoint (`ACAO *`) ingests `event: any` into `queueEvent` (`analyticsEngine.ts:182`). Suggested: default `express.json({ limit: '256kb' })`, raise per-route only where needed; validate telemetry with a schema (zod) and cap `props`/`vitals` size. Impact: memory/CPU amplification per request (burst caps mitigate but don't bound body size). + +### M3. `[server.ts]` God file: 2,906 lines, ~45 routes, all inline +Single responsibility is violated at file scale — telemetry, notifications, state sync, plans, engines, payments, GitHub SSE, API keys, and static serving all live in one closure. This is why C1–C4 coexist unnoticed. Extract route factories (`registerPaymentRoutes(app, deps)`, `registerStateRoutes(app, db)`…) into `server/` modules; this also unlocks route-level testing. + +### M4. `server.ts` vs `api/*` — duplicated serverless twins that have already drifted +`api/run-engine.ts` reimplements engine dispatch with its own CORS (including the invalid `Access-Control-Allow-Origin: *` + `Allow-Credentials: true` combo, `api/run-engine.ts:23-24`) and its own validation, while `server.ts` has a parallel implementation. One shared handler module consumed by both entry points removes drift risk. Also: `api/run-engine.ts` has **no rate limiting** (in-memory stores don't survive serverless), and it references `python-engines/` scripts that **do not exist in the repo** (`find` confirms) — the primary code path always falls back silently. + +### M5. No React error boundary anywhere +`grep ErrorBoundary|componentDidCatch` → zero hits. One render exception in any of 279 source files white-screens the SPA (lazy routes make this worse: a chunk-load failure on deploy is a guaranteed blank page for open tabs). Add a top-level boundary + per-route boundaries with a "reload" recovery action. + +### M6. `[tsconfig.json]` `"strict": false` on a 75k-LOC codebase +Known baseline of 87 pre-existing `tsc` errors, 31 `: any` in `server.ts` alone, 66 empty `catch {}` blocks swallowing failures (server + src), 148 client `console.*` calls. Ramp plan: enable `strict` (fix incrementally, folder by folder) → add `noUncheckedIndexedAccess` → replace empty catches with logged, typed errors → introduce `pino` (server) and a logger facade (client) with levels and redaction. + +### M7. JSON-LD injection hardening (`Breadcrumbs.tsx:57`, `GlobalBreadcrumb.tsx:532`) +Labels derive from `location.pathname` (URL-controlled). `JSON.stringify` does **not** escape `<`, so a crafted path fragment containing `` can break out of the `application/ld+json` context. One-line fix in both files: +```ts +dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema).replace(/` — `any` payload defeats TS; define a `ConnectedRepo` interface. +8. `[analyticsEngine.ts:182]` `queueEvent(event: any)` — define `TelemetryEvent` type and validate at the boundary; events are silently dropped when Mongo is absent (fine, but log a counter). +9. `[index.html]` `theme-color` triplicate works but the non-media fallback duplicates the dark value; harmless, tidy when convenient. + +--- + +## Positive Feedback + +- **Firestore rules are genuinely good**: deny-by-default `match /{document=**} { allow read, write: if false; }`, per-collection field validators (types, sizes, enum statuses), and `ownerId == request.auth.uid` binding on almost every write. This is the correct mental model — the Express layer should copy it. +- **SSRF guard exists and is unit-tested**: `networkSecurity.ts` covers RFC 1918, loopback, link-local/metadata `169.254.0.0/16`, CGNAT `100.64/10`, IPv6 ULA/link-local, IPv6-mapped IPv4, and multicast/reserved — with dedicated tests (`src/tests/networkSecurity.test.ts`). The gaps in C5 are refinements, not absences. +- **Safe-by-construction rendering**: `MarkdownRenderer` parses markdown into React elements (auto-escaped) instead of `marked` + `innerHTML`; email-HTML preview goes through `DOMPurify.sanitize`. +- **Command injection defenses done right**: `api/run-engine.ts` validates `engine` against a fixed map and uses `execFile('python3', [scriptPath, url])` — argv array, no shell, timeout, `maxBuffer` cap. +- **Serious security middleware**: Helmet with full CSP directive set, HSTS preload, Permissions-Policy, COOP/CORP tuning, and prod sourcemap blocking (`server.ts:2889-2892`). +- **Thoughtful rate-limit design**: tiered daily unit budgets with UTC-midnight reset, hourly stale-entry cleanup (`server.ts:428-435`), burst caps, Upstash Redis integration path, and status surfaced to clients via `/api/rate-limit/status`. +- **Frontend performance**: `React.lazy` on the heavy routes (`App.tsx:29-33`), `manualChunks` vendor splitting, and the earlier UI/a11y hardening (focus states, contrast remaps, aria coverage) — 59/59 tests green including a dedicated accessibility suite. +- **Python tooling exemplifies the target bar**: mypy `--strict` clean, black/ruff clean, 100% branch coverage, deterministic codemod with `--check` CI mode. +- **Zero TODO/FIXME debt**, consistent folder conventions, `.env.example` provided. + +--- + +## Questions for Author + +1. **Payments**: Is the payments flow intentionally a demo stub for now? If it will ever go live, C1 is the first blocker — should entitlements be driven by verified gateway webhooks or by server-side checkout-session polling? +2. **Auth direction**: Was `firebase-admin` installed intending to add server-side token verification (which would fix C2/C4 in one move), or can it be removed? +3. **Canonical server**: Which deployment is real — `server.ts` (Express) or `api/*` (Vercel serverless)? They've already drifted; should one be deleted? +4. **`python-engines/`**: Eight Python engine scripts are referenced (`api/run-engine.ts:15-24`) but absent from the repo — intentionally excluded, or lost? +5. **Framing**: Is `frameAncestors: *` + `frameguard: false` a hard requirement of AI Studio embedding? If yes, can we allowlist the specific studio origins? +6. **Superadmin emails**: Should these move to Firebase custom claims (firestore.rules already checks claims first) so the plaintext email lists can leave the codebase? + +--- + +## Test Coverage Assessment + +- [x] Happy path tested (UI routes, engine UI flows, networkSecurity/rateLimiter/telemetryParser units) +- [x] Error cases tested (invalid UTF-8 handling, rate-limit rejections, URL validation rejections) +- [ ] **Edge cases tested** — no tests for: forged webhook events, spoofed identity headers, state-sync cross-owner access, payment verify abuse, SSRF DNS-rebinding +- [x] Integration tests present (jsdom component suites with user-event flows) +- [ ] **Server route tests** — none exist; the entire `server.ts` surface (the location of every Critical) is untested +- [x] Tooling tests (Python codemod: 32 tests, 100% branch, mypy strict) + +**Recommended first tests** (each maps to a Critical): webhook signature rejection → identity spoof → state-sync IDOR → payment verify → SSRF rebinding simulation (mock `dns.lookup`). + +--- + +## Checklist + +- [ ] No security vulnerabilities — **5 criticals open** +- [ ] Performance acceptable — 10 MB body limit, duplicated animation libs, no route-level error recovery (low severity today) +- [ ] Code is readable — frontend yes; `server.ts` god file needs decomposition +- [ ] Tests are adequate — frontend yes; server criticals untested +- [x] Documentation present — `.env.example`, engine docs pages; README stub needs expansion +- [x] Positive patterns acknowledged — Firestore rules, SSRF guard, safe rendering, rate-limit design + +--- + +## Step-by-Step Remediation Roadmap + +### Phase 0 — Stop the bleeding (P0, ~1–2 days) +1. **C3**: Verify GitHub webhook HMACs with `crypto.timingSafeEqual` over the raw body; reject unknown `repoId`s; move the demo secret to env. +2. **C2 (partial)**: Require `x-api-key` values to match a persisted, hashed key set (or disable key tier until real); stop reading `subscriptionPlan`/`isTrialActive`/superadmin email from client input. +3. **C1**: Make `/api/payments/verify` fail closed (`503 Payments not configured` until gateway verification is implemented); add signature checks to both payment webhooks. +4. **C4**: Return `401` from all `/api/state/sync*` handlers unless a verified token supplies `ownerId`. +5. **C5**: Pin DNS in the request agent; fail closed on DNS errors; set `rejectUnauthorized: true`. + +### Phase 1 — Structural hardening (P1, ~1 week) +6. Add `firebase-admin/auth` token verification middleware; derive identity/tier server-side from Firestore (fixes C2 fully). +7. CSP: nonces over `unsafe-inline`, drop `unsafe-eval`, allowlist `frame-ancestors`. +8. Body limits per route (default 256 kb) + zod schemas for telemetry and engine payloads. +9. Error boundaries (root + per-route) with recovery UI. +10. CI gate workflow: eslint + tsc + vitest + build (+ mypy/pytest for `scripts/`), plus `replace_images.py --check`. +11. Escape `<` in both JSON-LD emitters (M7). + +### Phase 2 — Maintainability (P2, ~1–2 weeks) +12. Decompose `server.ts` into `server/routes/*` + `server/services/*` (M3); share handlers between Express and `api/*` (M4); delete the loser of the two. +13. Dependency cleanup (M9): drop or adopt `firebase-admin`, pick one animation lib and one editor, remove Prisma schema or wire it, move `@types/*` to devDeps, delete orphaned components. +14. TypeScript ramp: `strict: true` → fix 87 errors in batches → `noUncheckedIndexedAccess` → typed error handling replacing 66 empty catches. + +### Phase 3 — Quality systems (P3, ongoing) +15. Structured logging (pino + request IDs) replacing 148 client/10 server `console.*` calls; client error reporting boundary. +16. Server-route test suite (the five Critical-mapped tests first), target >70% coverage on `server/` modules. +17. Docs: real README, architecture diagram, "Firebase web config is public by design" note, engine inventory (including the missing `python-engines/` decision). +18. Bundle budget CI check (chunks currently exceed 500 kB warning) and a Lighthouse regression job. + +--- + +*Review artifacts: all findings reproducible at commit `6bdf506` on branch `arena/01a0544f-catalystlab`. Line numbers refer to `server.ts` (2,906 lines), `src/lib/networkSecurity.ts` (151 lines), and files as named.* diff --git a/README.md b/README.md index 8efae5a..9ea10e3 100644 --- a/README.md +++ b/README.md @@ -1 +1,83 @@ -# CatalystLab \ No newline at end of file +# CatalystLab + +Website-quality intelligence platform: a Vite 6 + React 18 SPA with an integrated Express 4 server that runs website-audit **engines** against user-supplied URLs, metered by a tiered rate limiter, with Firebase auth/Firestore on the client, MongoDB analytics, and fail-closed payment-gateway integrations. + +> Review remediation status: **Phase 0 ✅ · Phase 1 ✅ · Phase 2 ✅ · Phase 3 ✅** — see [`CODE_REVIEW.md`](./CODE_REVIEW.md) for the full audit and roadmap. + +## Quickstart + +```bash +npm ci +cp .env.example .env # all variables optional — every integration fails closed +GITHUB_WEBHOOK_SECRET=dev-secret npm run dev # Express (port 3000) + Vite middleware +``` + +- Dev server: `http://localhost:3000` (PORT/HOST honor env — `PORT`/`HOST`). +- Without credentials the app runs in **degraded-but-working mode**: analytics stay in-memory, emails dispatch in Mailgun mock mode, payments return `503 Payments not configured`, and state sync answers `401` until a Firebase service account is configured. + +## Scripts + +| Command | What it does | +| --- | --- | +| `npm run dev` | tsx server.ts (Express + Vite middleware, HMR off for proxy compatibility) | +| `npm run build` | Production bundle to `dist/` | +| `npm test` | Vitest suite (168 tests: UI + server route suite) | +| `npm run test:coverage` | Same suite with v8 coverage, thresholds gated on `server/**` | +| `npm run lint` | ESLint (0 warnings tolerated) | +| `npm run check:bundle` | Enforce JS bundle-size budgets after `npm run build` | + +CI (`.github/workflows/ci.yml`) runs lint, `tsc --noEmit` (**hard gate** — the tree compiles clean under `strict: true`), the test suite, and the production build. + +## Environment variables + +Everything is optional; behavior when unset is listed. See [`.env.example`](./.env.example) for the full list. + +| Group | Variables | Unset behavior | +| --- | --- | --- | +| Firebase (client) | `VITE_FIREBASE_*` | Auth/FS features degrade gracefully | +| Firebase Admin (server) | `FIREBASE_SERVICE_ACCOUNT_JSON` / `_PATH` | `/api/state/sync` → `401` for everyone (fail closed) | +| MongoDB | `MONGODB_URI`, `MONGODB_DB_NAME` | Analytics buffer in memory (zero-cost mode) | +| Mailgun | `MAILGUN_*` | Emails dispatch in **mock mode** (logged, never sent) | +| Payments | `V2CHECKOUT_*`, `DODOPAY_*`, `PAYMENTS_WEBHOOK_SECRET*` | Checkout/verify → `503`; webhooks → `401` | +| GitHub webhooks | `GITHUB_WEBHOOK_SECRET` | All webhook deliveries rejected | +| Rate-limit API keys | `VALID_API_KEYS` (comma-separated `cat_live_…`) | `api_pro` tier disabled entirely | +| Logging | `LOG_LEVEL` (`debug`/`info`/`warn`/`error`), `PORT`, `HOST` | `info` in production, `debug` otherwise | + +## Architecture + +One HTTP server hosts both the Express API and the SPA (static `dist/` in production, Vite middleware in development). The full diagram and data flows live in [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md); the audit-engine inventory is in [`docs/ENGINES.md`](./docs/ENGINES.md). + +``` +server.ts process entrypoint: HTTP server, Vite/static wiring, PORT/HOST +server/app.ts createApp(): helmet CSP, body limits, identity, routes, 404/errors +server/core/ logger (pino), rate limiter, engine catalog, SSL probe, runtime +server/routes/ telemetry · stateSync · plans · engines · reports · account · + github · payments · notifications · system · clientLogs +src/lib/serverAuth.ts Firebase ID-token verification → server-derived identity/tiers +lib/engines/ the actual audit-engine implementations (TypeScript) +src/lib/networkSecurity.ts SSRF guard: DNS pinning, private-range blocking, size caps +src/ React SPA (279 files) — pages, components, stores, engines UI +``` + +## Security model (summary) + +- **Identity**: tiers are derived server-side from verified Firebase ID tokens (`firebase-admin`); client headers (`x-user-email`, `subscription-plan`, …) are never trusted. Superadmin requires a signed custom claim. +- **Payments**: fail closed. `/api/payments/verify` never grants entitlements; webhooks require HMAC-SHA256 over the raw body; missing gateway credentials → `503`. +- **Webhooks**: GitHub deliveries verify `x-hub-signature-256` over the raw body; unknown `repoId`s are rejected, never auto-provisioned. Repo secrets are never echoed back to clients. +- **SSRF**: every engine request goes through the guard — scheme allowlist, private/loophead range blocking, DNS resolution pinned to the validated address (anti-rebinding), response-size caps, TLS verification on. +- **Rate limiting**: per-identity daily unit budgets + 60s burst windows, in-memory, keyed by server-derived identity; visitor budget 20 units/day. +- **Secrets hygiene**: the Firebase **web** config (`firebase-applet-config.json`, `VITE_FIREBASE_*`) is public by design — it identifies the Firebase project and is safe to ship in the client bundle. The Admin **service account** is the credential, kept out of Git and provided via `FIREBASE_SERVICE_ACCOUNT_*`. + +## Observability + +- Server: structured pino logs (JSON lines), one line per request with `x-request-id` correlation, credential-header redaction (`LOG_LEVEL` to tune). +- Client: `src/lib/logger.ts` facade — console passthrough in dev; in production, redacted/deduplicated warn/error batches ship to `POST /api/client-logs` (validated, rate-limited) via `sendBeacon`/`fetch keepalive`. + +## Testing + +```bash +npm test # 168 tests — UI (jsdom) + server routes (node + supertest) +npm run test:coverage # coverage gate: server/** ≥70% lines/statements/functions +``` + +The server suite covers every security-critical flow: payment fail-closed behavior, webhook HMAC verification (valid/forged/unknown), identity-spoof resistance, state-sync auth, SSRF blocking, telemetry schema validation, and the client-log sink. diff --git a/api/run-engine.ts b/api/run-engine.ts index a51b03a..4921a85 100644 --- a/api/run-engine.ts +++ b/api/run-engine.ts @@ -7,26 +7,15 @@ import path from 'path'; const execFileAsync = promisify(execFile); -const ENGINE_SCRIPT_MAP: Record = { - health: 'website_health.py', - latency: 'edge_latency.py', - ai_ready: 'ai_readiness.py', - repo: 'repo_scanner.py', - eco: 'eco_carbon_audit.py', - compliance: 'compliance_risk_audit.py', - migration: 'platform_migration_audit.py', - llmo: 'llmo_optimizer.py' -}; +import { ENGINE_SCRIPT_MAP } from '../server/core/enginesCatalog'; export default async function handler(req: any, res: any) { - // Enable CORS for Vercel - res.setHeader('Access-Control-Allow-Credentials', 'true'); + // CORS: this endpoint is a public POST API. Credentials are intentionally + // NOT allowed (invalid with a wildcard origin per the fetch spec), and the + // method list is limited to what the endpoint actually serves. res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT'); - res.setHeader( - 'Access-Control-Allow-Headers', - 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' - ); + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Api-Key, Authorization'); if (req.method === 'OPTIONS') { res.status(200).end(); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..1583cb4 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,90 @@ +# Architecture + +CatalystLab is a single-process deployable: one HTTP server hosts the Express API and the React SPA. + +```mermaid +flowchart LR + subgraph Browser + SPA["React 18 SPA
(Vite 6 build, code-split routes)"] + SW["Service Worker
(offline asset + telemetry doc cache)"] + CL["logger facade
src/lib/logger.ts"] + end + + subgraph Server["Express (server/app.ts) — one process, one port"] + MW["helmet CSP · body limits 256kb/2mb
request-ID logging · attachIdentity"] + subgraph Routes["server/routes/*"] + T["telemetry"] + SS["stateSync"] + EN["engines"] + GH["github"] + PAY["payments"] + ACC["account"] + NOT["notifications"] + SYS["system"] + CLS["clientLogs"] + end + RL["core/rateLimit
daily units + 60s bursts"] + AUTH["lib/serverAuth
firebase-admin verifyIdToken"] + end + + subgraph Engines["Audit engines (lib/engines/*)"] + GUARD["networkSecurity SSRF guard
DNS pin + private-range block"] + E1["health · migration · repo-hygiene"] + E2["eco-carbon · compliance · ai-readiness
ai-search · edge-latency"] + end + + FB[("Firebase Auth
+ Firestore")] + MG[("MongoDB
time-series analytics")] + MGUN["Mailgun API"] + GW["2Checkout / Dodo
gateways"] + TARGET["Target websites"] + GHAPI["GitHub webhook deliveries"] + + SPA -->|"Bearer ID token"| MW + MW --> AUTH --> FB + MW --> RL + MW --> Routes + SPA -->|"audit request"| EN --> GUARD --> TARGET + EN -->|"report"| SPA + T --> MG + SS --> FB + SS --> MG + PAY --> GW + GHAPI -->|"HMAC sha256 over raw body"| GH + NOT --> MGUN + CL -->|"batched warn/error
POST /api/client-logs"| CLS + SW -.-> SPA +``` + +## Request lifecycle + +1. **Transport**: `server.ts` binds `PORT`/`HOST` (env-driven, defaults `3000`/`0.0.0.0`) and mounts either Vite middleware (dev) or static `dist/` (production) behind the same Express app built by `createApp()`. +2. **Security middleware** (in order): helmet CSP — production drops `'unsafe-inline'` by allowlisting startup-computed hashes of the theme-bootstrap script; per-route JSON body limits (256 KB default, 2 MB only for state-sync bulk mutations); structured request logging with `x-request-id` (inbound IDs honored when they match a safe pattern); `attachIdentity` verifies the Firebase ID token once and attaches the server-derived identity (plan, trial, superadmin claim). +3. **Routing**: decomposed route modules register under `/api/*` (+ legacy `/stats/*`, `/telemetry/*`). An API 404 catch-all always answers JSON — the SPA fallback can never shadow an API route. A terminal error handler degrades gracefully when MongoDB is offline. +4. **Rate limiting**: engine-scan routes run `createEngineRateLimitMiddleware`, which resolves the caller's tier (visitor → free/starter/pro/team/enterprise/api_pro → superadmin) from the attached identity, charges units from the daily budget, enforces the 60-second burst window, and answers `429` with a machine-readable envelope. +5. **Engine execution**: `server/routes/engines.ts` validates the target URL, runs the SSRF guard (scheme allowlist, private-range block, DNS resolve → validate → pin the socket to that address), then dispatches to the TypeScript engine in `lib/engines/*` and returns a structured report. Redirects are never auto-followed. + +## Identity and trust boundaries + +- The **only** server-trusted inputs are: verified Firebase ID tokens (via `firebase-admin`), HMAC-verified webhook payloads, and the constant-time-checked `cat_live_` API-key allowlist (`VALID_API_KEYS`). +- Everything client-supplied — headers like `x-user-email`, plan strings, tier hints — is ignored for authorization. Quota/tier introspection endpoints reflect only the server-derived identity. +- Superadmin is terminal: unlimited budget (`limit: null`, `burstMax: Infinity`), granted exclusively by a signed custom claim on the verified token. + +## Telemetry pipeline + +First-party, ad-blocker-proof: the SPA and a tiny served script (`/api/telemetry.js`) POST events to `/api/telemetry/event` (also `/api/event`, `/stats/event`). Events are zod-validated and dropped silently when malformed, bot/prefetch traffic is filtered before any processing, geo/UA enrichment is local (`geoip-lite` + `ua-parser-js`), and events queue into MongoDB time-series collections when configured (in-memory otherwise). Query pipelines (`/api/analytics/stats`, `/api/analytics/realtime`, `/api/analytics/anomalies/check`) read the same store. + +## Edge mesh (presentation layer) + +The dashboard visualizes a 42-PoP edge mesh (`src/lib/edge/pops.ts`) on an interactive cobe globe (`EdgeMeshGlobe`), with plan-tier-driven PoP visibility, projection-based overlay chips, and the telemetry HUD. The mesh is a presentation/simulation feature of the product UI — engine scans execute from this single server process. + +## Observability + +- **Server**: pino JSON logs; per-request line with method/url/status/duration/requestId; credential headers redacted; level escalates with status (≥500 error, ≥400 warn). `LOG_LEVEL` controls verbosity. +- **Client**: `src/lib/logger.ts` — dev passthrough; production batches redacted, deduplicated warn/error events to `/api/client-logs` (schema-validated, 64 KB cap, 30 req/5 min per identity) with `sendBeacon` on pagehide. Global `error` and `unhandledrejection` hooks plus `ErrorBoundary` report through the same facade. + +## Deployment notes + +- Single container/process; no serverless entrypoints are load-bearing (`api/*.ts` Vercel twins are dormant; the Express server is canonical). +- `NODE_ENV=production` requires a prior `npm run build` (CSP hashing reads `dist/index.html`). +- All integrations fail closed or degrade to mock modes — a fresh deployment with zero env vars serves the full SPA and public audit surfaces safely. diff --git a/docs/ENGINES.md b/docs/ENGINES.md new file mode 100644 index 0000000..2af8cf1 --- /dev/null +++ b/docs/ENGINES.md @@ -0,0 +1,39 @@ +# Audit engine inventory + +CatalystLab runs website-quality audit engines against user-supplied URLs. Every scan passes through the SSRF guard (`src/lib/networkSecurity.ts`) before any outbound byte is sent: scheme allowlist, private/loopback range blocking, DNS resolve → validate → socket pinning (anti-rebinding), response-size caps, TLS verification on, redirects never auto-followed. + +## Decision record: `python-engines/` + +The original server referenced Python engine scripts (`python-engines/*.py`) that **were never committed to this repository**. During the Phase 2 decomposition the TypeScript implementations in [`lib/engines/`](../lib/engines) were confirmed as the canonical, always-taken code path (the Python dispatch was dead fallback code). The references in `server/core/enginesCatalog.ts` (`ENGINE_SCRIPT_MAP`) are kept as vestigial documentation of the historical naming only; no `.py` file is loaded at runtime. Do not reintroduce a Python engine path without also adding the scripts and a shebang-capable runtime to the deploy image. + +## Server engine catalog (`server/core/enginesCatalog.ts`) + +Each engine has a canonical id and up to one alias (both resolve to the same implementation). + +| Engine id | Alias | Implementation | What it audits | +| --- | --- | --- | --- | +| `health` | `testing_vitals` | `lib/engines/health.ts` | Availability, TLS certificate validity, response status, core health checks | +| `migration` | `planning_arch` | `lib/engines/migration.ts` | Platform/architecture migration readiness (server detection, legacy stack signals) | +| `repo` | `code_quality` | `lib/engines/repo-hygiene.ts` | Repository hygiene surfaces exposed by the site (manifests, source links, metadata) | +| `eco` | `build_eco` | `lib/engines/eco-carbon.ts` | Build/asset carbon efficiency — asset weight rankings, transfer size, carbon estimates | +| `compliance` | `devsecops_compliance` | `lib/engines/compliance.ts` | Security headers, OWASP baseline signals, compliance/privacy markers | +| `ai_ready` | `operations_ai_ready` | `lib/engines/ai-readiness.ts` | AI-discovery readiness (`llms.txt`, structured data, machine-readable metadata) | +| `latency` | `release_edge` | `lib/engines/edge-latency.ts` | Response timing profile and edge-delivery characteristics | +| `ai_search` | — | `lib/engines/ai-search.ts` | AI-search surface readiness (robots directives, crawlable content signals) | +| `llmo` | `evolution_llmo` | *(catalog id; dispatched through the shared pipeline)* | LLMOptimizer-style continuous-evolution scoring | + +## Client-side telemetry engines (`src/data/diagnosticEngines.ts`) + +Nine client-presented diagnostic engine definitions power the playground and dashboard UI (categories: Performance, Security, SEO, Accessibility, DOM & Vitals, …). They render engine cards, presets, and the interactive playground; network-level checks always execute server-side through the catalog above. + +## Execution surfaces + +| Route | Purpose | +| --- | --- | +| `POST /api/run-engine` | Run one engine (`engine` + `url`), rate-limited, SSRF-guarded | +| `POST /api/v1/engines/:engine/scan` | Same pipeline under the versioned surface | +| `POST /api/check-url` | Reachability pre-flight through the SSRF guard (no engine run) | +| `POST /api/monitor/probe` | Uptime-style probe used by the monitoring UI | +| `GET /api/master-audit/stream` | SSE stream for the full 8-engine master audit | + +Rate costs: a single engine scan costs **1 unit**, a master audit **10 units** (see `server/core/rateLimit.ts` for per-tier daily budgets). diff --git a/eslint.config.js b/eslint.config.js index b346e8e..ecdf41c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,6 +7,7 @@ export default tseslint.config( { ignores: [ 'dist/**', + 'coverage/**', 'node_modules/**', '*.config.js', '*.config.ts', diff --git a/index.html b/index.html index e2897a1..79983f5 100644 --- a/index.html +++ b/index.html @@ -8,16 +8,32 @@ - + + + - - - - + + + + + diff --git a/lib/engines/eco-carbon.ts b/lib/engines/eco-carbon.ts index f24634b..f6f3df9 100644 --- a/lib/engines/eco-carbon.ts +++ b/lib/engines/eco-carbon.ts @@ -97,13 +97,14 @@ export async function executeEcoCarbonEngine(targetUrl: string): Promise b.kb - a.kb); + ]; + const assetWeightRankings = [...assetWeightEntries].sort((a, b) => b.kb - a.kb); // Grade & Percentile let ecoGrade: EcoCarbonMetrics['ecoGrade'] = 'A+'; diff --git a/lib/engines/health.ts b/lib/engines/health.ts index d9112ca..c37a8f5 100644 --- a/lib/engines/health.ts +++ b/lib/engines/health.ts @@ -1,4 +1,5 @@ import * as cheerio from 'cheerio'; +import type { Element as DomElement } from 'domhandler'; import type { WebsiteHealthMetrics, EngineResult } from '../../types/telemetry'; const HTTP_TIMEOUT_MS = 12000; @@ -40,7 +41,7 @@ export async function executeHealthEngine(targetUrl: string): Promise, currentDepth: number) { + function calculateDepth(element: cheerio.Cheerio, currentDepth: number) { if (currentDepth > maxDepth) maxDepth = currentDepth; element.children().each((_, child) => { if (child.type === 'tag') { diff --git a/lib/engines/migration.ts b/lib/engines/migration.ts index 74b1097..917d908 100644 --- a/lib/engines/migration.ts +++ b/lib/engines/migration.ts @@ -11,7 +11,7 @@ export async function executeMigrationEngine(targetUrl: string): Promise=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.10", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "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", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "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", + "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", + "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", + "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/js": { + "version": "9.39.5", + "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", + "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", + "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/@exodus/bytes": { + "version": "1.15.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.2", + "license": "MIT" + }, + "node_modules/@firebase/ai": { + "version": "2.15.0", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.24", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.30", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.24", + "@firebase/analytics-types": "0.8.5", + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.5", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.16.1", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.13.1", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.7", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.13.1", + "@firebase/app-check-types": "0.5.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.5", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.5", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.17", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.16.1", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.6", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.2" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.10", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.5", + "@firebase/auth-types": "0.13.2", + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { + "version": "1.13.5", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.6", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.2", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.5", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.7.4", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.5", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.7", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/database": "1.1.5", + "@firebase/database-types": "1.0.22", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + }, + "peerDependenciesMeta": { + "@firebase/app": { + "optional": true + }, + "@firebase/app-compat": { + "optional": true + } + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.22", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.6", + "@firebase/util": "1.15.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.17.1", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "@firebase/webchannel-wrapper": "1.0.7", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "re2js": "^2.8.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.13", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/firestore": "4.17.1", + "@firebase/firestore-types": "3.0.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.5", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.14.0", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/messaging-interop-types": "0.2.6", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.5.0", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/functions": "0.14.0", + "@firebase/functions-types": "0.6.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.5", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.24", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.24", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/installations-types": "0.5.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.5", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.2", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.13.2", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/messaging-interop-types": "0.2.6", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.29", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/messaging": "0.13.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.6", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.14", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.27", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/performance": "0.7.14", + "@firebase/performance-types": "0.2.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.5", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.9.2", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.29", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/remote-config": "0.9.2", + "@firebase/remote-config-types": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.2", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.5", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.5", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/storage": "0.14.5", + "@firebase/storage-types": "0.8.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.5", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.3", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.7", + "license": "Apache-2.0" + }, + "node_modules/@google-cloud/firestore": { + "version": "8.7.1", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "fast-deep-equal": "^3.1.3", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^5.0.1", + "protobufjs": "^7.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.22.0", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "6.1.1", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "9.15.1", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-logging-utils": { + "version": "0.0.2", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "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", + "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", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "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", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "devOptional": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.5.0", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "license": "BSD-3-Clause" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.4", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "license": "MIT" + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "license": "MIT", + "optional": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/dompurify": { + "version": "3.2.0", + "deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "dompurify": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geoip-lite": { + "version": "1.4.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.4.0", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/request": { + "version": "2.48.13", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/superagent": { + "version": "8.1.11", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz", + "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/superagent/node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz", + "integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "license": "MIT", + "optional": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "license": "MIT", + "optional": true + }, + "node_modules/@types/ua-parser-js": { + "version": "0.7.39", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/type-utils": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.68.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.68.0", + "@typescript-eslint/types": "^8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.68.0", + "@typescript-eslint/tsconfig-utils": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@upstash/core-analytics": { + "version": "v0.0.10", + "license": "MIT", + "dependencies": { + "@upstash/redis": "^1.28.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@upstash/ratelimit": { + "version": "v2.0.8", + "license": "MIT", + "dependencies": { + "@upstash/core-analytics": "^0.0.10" + }, + "peerDependencies": { + "@upstash/redis": "^1.34.3" + } + }, + "node_modules/@upstash/redis": { + "version": "1.38.3", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", + "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.7", + "vitest": "3.2.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "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-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "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-flatten": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "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", + "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", + "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", + "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", + "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", + "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/arrify": { + "version": "2.0.1", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bson": { + "version": "7.3.2", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "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", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "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/check-error": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cobe": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "devOptional": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "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", + "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", + "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", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "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", + "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/delaunator": { + "version": "5.1.0", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.14", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.416", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "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-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "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/es-toolkit": { + "version": "1.52.0", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/esbuild": { + "version": "0.28.2", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "dev": true, + "license": "MIT", + "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-jsx-a11y": { + "version": "6.10.2", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "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-scope": { + "version": "8.4.0", + "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", + "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", + "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", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.11.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "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/firebase": { + "version": "12.18.0", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.15.0", + "@firebase/analytics": "0.10.24", + "@firebase/analytics-compat": "0.2.30", + "@firebase/app": "0.16.1", + "@firebase/app-check": "0.13.1", + "@firebase/app-check-compat": "0.4.7", + "@firebase/app-compat": "0.5.17", + "@firebase/app-types": "0.9.6", + "@firebase/auth": "1.13.5", + "@firebase/auth-compat": "0.6.10", + "@firebase/data-connect": "0.7.4", + "@firebase/database": "1.1.5", + "@firebase/database-compat": "2.1.7", + "@firebase/firestore": "4.17.1", + "@firebase/firestore-compat": "0.4.13", + "@firebase/functions": "0.14.0", + "@firebase/functions-compat": "0.5.0", + "@firebase/installations": "0.6.24", + "@firebase/installations-compat": "0.2.24", + "@firebase/messaging": "0.13.2", + "@firebase/messaging-compat": "0.2.29", + "@firebase/performance": "0.7.14", + "@firebase/performance-compat": "0.2.27", + "@firebase/remote-config": "0.9.2", + "@firebase/remote-config-compat": "0.2.29", + "@firebase/storage": "0.14.5", + "@firebase/storage-compat": "0.4.5", + "@firebase/util": "1.15.3" + } + }, + "node_modules/firebase-admin": { + "version": "14.3.0", + "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.1.4", + "@firebase/database-types": "^1.0.20", + "fast-deep-equal": "^3.1.1", + "google-auth-library": "^10.6.2", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^4.0.1" + }, + "engines": { + "node": ">=22" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^8.7.1", + "@google-cloud/storage": "^7.22.0" + } + }, + "node_modules/firebase/node_modules/@firebase/auth": { + "version": "1.13.5", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "2.5.6", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/framer-motion": { + "version": "13.1.1", + "license": "MIT", + "dependencies": { + "motion-dom": "^13.1.1", + "motion-utils": "^13.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "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/functional-red-black-tree": { + "version": "1.0.1", + "license": "MIT", + "optional": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "7.3.1", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/node-fetch": { + "version": "3.3.2", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/geoip-lite": { + "version": "2.0.3", + "license": "Apache-2.0", + "dependencies": { + "chalk": "4.1 - 4.1.2", + "iconv-lite": "0.4.13 - 0.6.3", + "ip-address": "^10.2.0", + "lazy": "1.0.11", + "yauzl": "^3.3.1" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "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": { + "version": "10.5.0", + "devOptional": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "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/google-auth-library": { + "version": "10.9.1", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "7.3.1", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "8.1.2", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/node-fetch": { + "version": "3.3.2", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-gax": { + "version": "5.0.8", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "3.0.4", + "protobufjs": "^7.5.4", + "retry-request": "^8.0.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/google-gax/node_modules/gaxios": { + "version": "7.3.1", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "8.1.4", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "7.1.3", + "google-logging-utils": "1.1.3", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata/node_modules/gaxios": { + "version": "7.1.3", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "10.5.0", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/gtoken": { + "version": "8.0.0", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/http-proxy-agent": { + "version": "7.0.2", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-gax/node_modules/node-fetch": { + "version": "3.3.2", + "license": "MIT", + "optional": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-gax/node_modules/retry-request": { + "version": "8.0.4", + "license": "MIT", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/teeny-request": { + "version": "10.1.4", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/gtoken": { + "version": "7.1.0", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.3.0", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "license": "ISC" + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "11.1.18", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "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", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-address": { + "version": "10.7.0", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "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", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "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", + "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", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "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-potential-custom-element-name": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "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", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "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", + "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", + "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-unsafe": { + "version": "2.0.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "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", + "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", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "devOptional": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "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/jackspeak": { + "version": "3.4.3", + "devOptional": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.10", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "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/jsdom": { + "version": "30.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/entities": { + "version": "8.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/undici": { + "version": "8.10.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.8.5", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "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/jwa": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^6.1.3", + "limiter": "^1.1.5", + "lru-cache": "^11.0.0", + "lru-memoizer": "^3.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >= 23.0.0" + } + }, + "node_modules/jwks-rsa/node_modules/lru-cache": { + "version": "11.5.2", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/lazy": { + "version": "1.0.11", + "license": "MIT", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/limiter": { + "version": "1.1.5" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-memoizer": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^11.0.1" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "11.5.2", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lucide-react": { + "version": "1.37.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "devOptional": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mongodb": { + "version": "7.6.0", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.4.11", + "bson": "^7.2.0", + "mongodb-connection-string-url": "^7.0.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": "^7.2.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.2", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "5.1.1", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": { + "version": "7.0.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "14.2.0", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/motion": { + "version": "13.1.1", + "license": "MIT", + "dependencies": { + "framer-motion": "^13.1.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "13.1.1", + "license": "MIT", + "dependencies": { + "motion-utils": "^13.0.0" + } + }, + "node_modules/motion-utils": { + "version": "13.0.0", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "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", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "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-fetch": { + "version": "2.7.0", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.54", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "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", + "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", + "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", + "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/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "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", + "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", + "devOptional": 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", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "devOptional": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "devOptional": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "devOptional": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.62.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "dev": true, + "license": "MIT" + }, + "node_modules/proto3-json-serializer": { + "version": "3.0.4", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/re2js": { + "version": "2.8.6", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.6", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.6", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4", + "react-router": "6.30.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/recharts": { + "version": "3.10.1", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "5.0.4", + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "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/regexp.prototype.flags": { + "version": "1.5.4", + "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/require-directory": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "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/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.63.1", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "license": "BSD-3-Clause" + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "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-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "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", + "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/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "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", + "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", + "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/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "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", + "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", + "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", + "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/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "license": "MIT", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.1.0", + "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-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "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", + "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", + "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-ansi": { + "version": "7.2.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.3.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/strnum": { + "version": "2.4.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "license": "MIT", + "optional": true + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.11", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.11" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.11", + "dev": true, + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.13", + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "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", + "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", + "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", + "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/typescript": { + "version": "5.9.3", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.68.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.68.0", + "@typescript-eslint/parser": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ua-is-frozen": { + "version": "0.1.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/ua-parser-js": { + "version": "2.0.10", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "AGPL-3.0-or-later", + "dependencies": { + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "ua-is-frozen": "^0.1.2" + }, + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "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/uncrypto": { + "version": "0.1.3", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.29.0", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "devOptional": 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", + "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", + "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", + "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", + "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/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "devOptional": true, + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "3.4.0", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.15", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json index ea61556..8c5b9ac 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,12 @@ "start": "node dist/server.cjs", "test": "vitest run", "lint": "eslint .", - "media:check": "node scripts/verify-media.mjs" + "media:check": "node scripts/verify-media.mjs", + "test:coverage": "vitest run --coverage", + "check:bundle": "node scripts/check-bundle-budget.mjs" }, "dependencies": { - "@relume_io/relume-tailwind": "^1.3.0", - "@relume_io/relume-ui": "^1.3.1", "@tailwindcss/vite": "^4.0.0", - "@types/d3": "^7.4.3", - "@types/dompurify": "^3.2.0", - "@types/express": "^4.17.21", - "@types/node": "^26.2.0", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@uiw/react-md-editor": "^4.1.1", "@upstash/ratelimit": "^2.0.8", "@upstash/redis": "^1.38.2", "@vitejs/plugin-react": "^4.3.4", @@ -35,17 +28,14 @@ "express": "^4.19.2", "firebase": "^12.17.1", "firebase-admin": "^14.2.0", - "framer-motion": "^13.1.0", "geoip-lite": "^2.0.3", "helmet": "^8.3.0", - "html2canvas": "^1.4.1", - "jspdf": "^4.2.1", "lucide-react": "^1.33.0", "mongodb": "^7.5.0", "motion": "^13.1.0", + "pino": "^10.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-quill": "^2.0.0", "react-router-dom": "^6.28.0", "recharts": "^3.10.1", "tailwind-merge": "^3.6.0", @@ -54,20 +44,30 @@ "typescript": "^5.7.2", "ua-parser-js": "^2.0.10", "vite": "^6.0.3", + "zod": "^4.5.4", "zustand": "^5.0.15" }, "devDependencies": { "@eslint/js": "^9.20.0", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", + "@types/d3": "^7.4.3", + "@types/dompurify": "^3.2.0", + "@types/express": "^4.17.21", "@types/geoip-lite": "^1.4.4", + "@types/node": "^26.2.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/supertest": "^7.2.1", "@types/ua-parser-js": "^0.7.39", + "@vitest/coverage-v8": "^3.2.7", "esbuild": "^0.28.2", "eslint": "^9.20.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.4", "jsdom": "^30.0.1", "playwright": "^1.62.1", + "supertest": "^7.2.2", "typescript-eslint": "^8.24.0", "vitest": "^3.0.5" } diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 78dc7e2..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,265 +0,0 @@ -// CatalystLab Enterprise Telemetry Platform Database Schema -// Prisma Schema with PostgreSQL / TimescaleDB & ClickHouse compatibility - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -generator client { - provider = "prisma-client-js" - previewFeatures = ["driverAdapters", "fullTextSearchPostgres"] -} - -enum UserRole { - USER - DEVELOPER - ORGANIZATION_ADMIN - ENTERPRISE_AUDITOR - SUPERADMIN -} - -enum SubscriptionTier { - VISITOR - FREE - STARTER - PRO - TEAM - ENTERPRISE -} - -enum AuditStatus { - QUEUED - PROCESSING - COMPLETED - FAILED - PARTIAL -} - -enum EngineId { - HEALTH - AI_READY - REPO - LATENCY - ECO - COMPLIANCE - MIGRATION - AI_SEARCH -} - -enum PostStatus { - DRAFT - PUBLISHED - ARCHIVED -} - -model User { - id String @id @default(cuid()) - name String? - email String @unique - emailVerified DateTime? - image String? - hashedPassword String? - role UserRole @default(USER) - tier SubscriptionTier @default(FREE) - dailyQuotaLimit Int @default(50) - currentDayUnitsUsed Int @default(0) - quotaResetAt DateTime @default(now()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relationships - auditReports AuditReport[] - comparisons SideBySideComparison[] - academicPosts AcademicPost[] - apiTokens ApiToken[] - organizationMembers OrganizationMember[] - rateLimitLogs RateLimitLog[] - - @@index([email]) - @@index([role, tier]) - @@map("users") -} - -model Organization { - id String @id @default(cuid()) - name String - slug String @unique - plan SubscriptionTier @default(TEAM) - logoUrl String? - billingEmail String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - members OrganizationMember[] - reports AuditReport[] - apiTokens ApiToken[] - - @@index([slug]) - @@map("organizations") -} - -model OrganizationMember { - id String @id @default(cuid()) - organizationId String - userId String - role String @default("MEMBER") // ADMIN, MEMBER, VIEWER - createdAt DateTime @default(now()) - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([organizationId, userId]) - @@index([userId]) - @@map("organization_members") -} - -model AuditReport { - id String @id @default(cuid()) - targetUrl String - normalizedUrl String - domainSlug String - overallScore Int @default(0) - grade String @default("F") - status AuditStatus @default(QUEUED) - durationMs Int @default(0) - isPublic Boolean @default(true) - clientIpHash String? - userAgent String? - summaryMarkdown String? @db.Text - metricsSnapshot Json? // Comprehensive JSON snapshot of all 8 engines - - // Relations - userId String? - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - organizationId String? - organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) - - engineResults EngineResult[] - comparisonsAsA SideBySideComparison[] @relation("ComparisonReportA") - comparisonsAsB SideBySideComparison[] @relation("ComparisonReportB") - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([targetUrl, createdAt]) - @@index([domainSlug, createdAt]) - @@index([userId, createdAt]) - @@index([overallScore, grade]) - @@map("audit_reports") -} - -model EngineResult { - id String @id @default(cuid()) - auditReportId String - engineId EngineId - engineName String - category String // Performance, Intelligence, Security, Architecture - status AuditStatus @default(PROCESSING) - score Int @default(0) - executionTimeMs Int @default(0) - metricsJson Json // Structured telemetry metrics specific to this engine - logsJson Json? // Streamed logs & diagnostic traces - errorMessage String? @db.Text - createdAt DateTime @default(now()) - - auditReport AuditReport @relation(fields: [auditReportId], references: [id], onDelete: Cascade) - - @@unique([auditReportId, engineId]) - @@index([engineId, score]) - @@index([auditReportId]) - @@map("engine_results") -} - -model SideBySideComparison { - id String @id @default(cuid()) - targetUrlA String - targetUrlB String - scoreA Int - scoreB Int - deltaScore Int - winner String // "A", "B", or "TIE" - comparisonMatrix Json // Structured diff matrix of all telemetry metrics - - reportAId String? - reportA AuditReport? @relation("ComparisonReportA", fields: [reportAId], references: [id], onDelete: SetNull) - reportBId String? - reportB AuditReport? @relation("ComparisonReportB", fields: [reportBId], references: [id], onDelete: SetNull) - - userId String? - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - - createdAt DateTime @default(now()) - - @@index([targetUrlA, targetUrlB]) - @@index([userId, createdAt]) - @@map("side_by_side_comparisons") -} - -model AcademicPost { - id String @id @default(cuid()) - title String - slug String @unique - excerpt String @db.Text - content String @db.Text - category String - tags String[] - status PostStatus @default(PUBLISHED) - readTimeMinutes Int @default(5) - coverImageUrl String? - isFeatured Boolean @default(false) - viewCount Int @default(0) - - authorId String? - author User? @relation(fields: [authorId], references: [id], onDelete: SetNull) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([slug]) - @@index([category, status]) - @@index([isFeatured, createdAt]) - @@map("academic_posts") -} - -model ApiToken { - id String @id @default(cuid()) - name String - keyPrefix String // e.g. "ctl_live_" - keyHash String @unique // SHA-256 hash of the full token - scopes String[] @default(["telemetry:read", "scan:create"]) - rateLimitOverride Int? - lastUsedAt DateTime? - expiresAt DateTime? - revokedAt DateTime? - - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - organizationId String? - organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) - - createdAt DateTime @default(now()) - - @@index([keyHash]) - @@index([userId]) - @@map("api_tokens") -} - -model RateLimitLog { - id String @id @default(cuid()) - identifier String // IP, Device Fingerprint, or User ID - tier String - endpoint String - unitsCharged Int @default(1) - ipAddress String? - userAgent String? - blocked Boolean @default(false) - createdAt DateTime @default(now()) - - userId String? - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - - @@index([identifier, createdAt]) - @@index([createdAt]) - @@map("rate_limit_logs") -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..63dfdd7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +# Python tooling config for scripts/replace_images.py (inert to the JS toolchain). +# Install dev tools: python3 -m pip install -r scripts/requirements-dev.txt + +[tool.mypy] +python_version = "3.11" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +mypy_path = ["scripts"] +files = ["scripts"] + +[tool.black] +line-length = 100 +target-version = ["py311"] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "I", "UP", "B"] + +[tool.pytest.ini_options] +testpaths = ["scripts/tests"] +pythonpath = ["scripts"] +addopts = "-q" + +[tool.coverage.run] +branch = true + +[tool.coverage.report] +exclude_also = ['if __name__ == "__main__":'] diff --git a/replace_images.py b/replace_images.py deleted file mode 100644 index 2a09856..0000000 --- a/replace_images.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -import re -import random - -pexels_images = [ - "https://images.pexels.com/photos/3183150/pexels-photo-3183150.jpeg", - "https://images.pexels.com/photos/1181675/pexels-photo-1181675.jpeg", - "https://images.pexels.com/photos/1181467/pexels-photo-1181467.jpeg", - "https://images.pexels.com/photos/574070/pexels-photo-574070.jpeg", - "https://images.pexels.com/photos/1181244/pexels-photo-1181244.jpeg", - "https://images.pexels.com/photos/3861958/pexels-photo-3861958.jpeg", - "https://images.pexels.com/photos/2599244/pexels-photo-2599244.jpeg", - "https://images.pexels.com/photos/281260/pexels-photo-281260.jpeg", - "https://images.pexels.com/photos/3182773/pexels-photo-3182773.jpeg", - "https://images.pexels.com/photos/3183132/pexels-photo-3183132.jpeg", - "https://images.pexels.com/photos/3182781/pexels-photo-3182781.jpeg" -] - -pexels_avatars = [ - "https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg", - "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", - "https://images.pexels.com/photos/733872/pexels-photo-733872.jpeg", - "https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg", - "https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg" -] - -def replace_in_file(filepath): - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - - # We want to match Unsplash URLs - # Pattern: https://images.unsplash.com/photo-[a-zA-Z0-9\-]+(\?[^'"]*)? - # Actually just match https://images.unsplash.com/photo- and the rest until quote or end - - def replacer(match): - full_url = match.group(0) - # If it looks like an avatar (has 'face' in the URL or is small) - if 'face' in full_url or 'w=200' in full_url or 'w=96' in full_url: - base = random.choice(pexels_avatars) - else: - base = random.choice(pexels_images) - return base + "?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1" - - new_content = re.sub(r'https://images\.unsplash\.com/photo-[a-zA-Z0-9\-]+(\?[^\'"\`\)]*)?', replacer, content) - - # Check for random unsplash url with template literal in src/components/blog/HeroImageLivePreview.tsx - # https://images.unsplash.com/photo-${...}?auto=format... - new_content = re.sub(r'https://images\.unsplash\.com/photo-\$\{.*?\}(\?[^\'"\`\)]*)?', lambda m: random.choice(pexels_images) + "?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1", new_content) - - if new_content != content: - with open(filepath, 'w', encoding='utf-8') as f: - f.write(new_content) - print(f"Updated {filepath}") - -for root, _, files in os.walk('src'): - for file in files: - if file.endswith(('.ts', '.tsx')): - replace_in_file(os.path.join(root, file)) - diff --git a/scripts/check-bundle-budget.mjs b/scripts/check-bundle-budget.mjs new file mode 100644 index 0000000..f41a8b3 --- /dev/null +++ b/scripts/check-bundle-budget.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +// Phase 3 (item 18): bundle budget gate. Parses dist/ after `vite build`, +// measures raw + gzip sizes, and fails when any budget is exceeded. +// +// Budgets are a regression RATCHET set ~5-10% above current reality (see +// CODE_REVIEW.md item 18). Tighten them after each chunk-splitting win; +// never loosen them to make a failing build pass — split the chunk instead. +import fs from 'node:fs'; +import path from 'node:path'; +import zlib from 'node:zlib'; + +const dist = path.join(process.cwd(), 'dist'); +const assetsDir = path.join(dist, 'assets'); + +if (!fs.existsSync(assetsDir)) { + console.error('✗ dist/assets not found — run `npm run build` first.'); + process.exit(1); +} + +/** + * Budgets in kilobytes. `initial` = scripts referenced by dist/index.html + * (loaded before first paint); everything else is a lazy route chunk. + */ +const BUDGETS = { + initialRawKb: 2100, + initialGzipKb: 540, + totalRawKb: 3400, + totalGzipKb: 900, + largestChunkRawKb: 1050, + largestChunkGzipKb: 280 +}; + +function sizeOf(file) { + const raw = fs.readFileSync(file); + return { raw: raw.length, gzip: zlib.gzipSync(raw, { level: 9 }).length }; +} + +const jsFiles = fs.readdirSync(assetsDir).filter((f) => f.endsWith('.js')); +const sizes = new Map(jsFiles.map((f) => [f, sizeOf(path.join(assetsDir, f))])); + +// Initial chunks come from the built index.html: the module entry script plus +// every statically-imported vendor chunk surfaced as a modulepreload link. +const html = fs.readFileSync(path.join(dist, 'index.html'), 'utf8'); +const initialFiles = new Set(); +for (const match of html.matchAll(/]+src="\/assets\/([^"]+\.js)"/g)) { + if (sizes.has(match[1])) initialFiles.add(match[1]); +} +for (const match of html.matchAll(/]+rel="modulepreload"[^>]+href="\/assets\/([^"]+\.js)"/g)) { + if (sizes.has(match[1])) initialFiles.add(match[1]); +} + +const kb = (bytes) => bytes / 1024; +const sum = (files, key) => files.reduce((acc, f) => acc + sizes.get(f)[key], 0); + +const initialFilesArr = [...initialFiles]; +const allFilesArr = jsFiles; +const largest = allFilesArr.reduce((max, f) => (sizes.get(f).raw > sizes.get(max).raw ? f : max), allFilesArr[0]); + +const measured = { + initialRawKb: kb(sum(initialFilesArr, 'raw')), + initialGzipKb: kb(sum(initialFilesArr, 'gzip')), + totalRawKb: kb(sum(allFilesArr, 'raw')), + totalGzipKb: kb(sum(allFilesArr, 'gzip')), + largestChunkRawKb: kb(sizes.get(largest).raw), + largestChunkGzipKb: kb(sizes.get(largest).gzip) +}; + +const fmt = (v) => `${v.toFixed(0)} kB`; +let failed = false; + +console.log('\nBundle budget report'); +console.log('─────────────────────────────────────────────────────────────'); +for (const [key, budget] of Object.entries(BUDGETS)) { + const value = measured[key]; + const ok = value <= budget; + if (!ok) failed = true; + const overBy = value - budget; + console.log( + `${ok ? '✓' : '✗'} ${key.padEnd(22)} ${fmt(value).padStart(9)} / ${fmt(budget).padEnd(9)} ` + + (ok ? `(${fmt(budget - value)} headroom)` : `OVER by ${fmt(overBy)}`) + ); +} +console.log('─────────────────────────────────────────────────────────────'); +console.log(`initial chunks: ${initialFilesArr.length}, total JS chunks: ${allFilesArr.length}, largest: ${largest}\n`); + +if (failed) { + console.error('✗ Bundle budget exceeded. Split the chunk or tighten code — do not raise the budget to pass.'); + process.exit(1); +} +console.log('✓ All bundle budgets satisfied.'); diff --git a/scripts/replace_images.py b/scripts/replace_images.py new file mode 100644 index 0000000..12f0eb2 --- /dev/null +++ b/scripts/replace_images.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +"""Codemod: migrate Unsplash image URLs to Pexels and keep image sizing sane. + +Replaces ``images.unsplash.com/photo-...`` URLs in source files with curated +Pexels equivalents, and repairs already-migrated Pexels avatar URLs that carry +hero-sized query params. + +Design rules (each addresses a failure mode of the original one-off script): + +- Deterministic: a given source URL always maps to the same Pexels URL + (md5-indexed pick), so re-runs are idempotent and the same image referenced + from multiple files stays visually consistent. +- Avatar-aware: URLs in an avatar-ish context (``authorAvatar``, ``U_FACE``, + ``rounded-full``, ...) get a square face crop; content images keep a width + bucket derived from the source URL instead of a hardcoded 1260x750. +- Repair pass: Pexels avatar URLs that already carry hero sizing are re-cropped + to the square avatar size. +- Safe: ``--dry-run`` / ``--check`` modes, writes only files that actually + changed, preserves line endings byte-for-byte, skips vendor dirs and + ``*.d.ts``. + +Note on concurrency: file I/O here is local and page-cached across a few +hundred small files, so the run is synchronous on purpose -- asyncio would add +complexity without a measurable win for a disk-bound batch rewrite. + +Usage: + python3 scripts/replace_images.py # migrate in place + python3 scripts/replace_images.py --dry-run # preview changes without writing + python3 scripts/replace_images.py --check # exit 1 if anything would change (CI) +""" + +from __future__ import annotations + +import argparse +import hashlib +import logging +import re +import sys +from collections.abc import Iterator, Sequence +from dataclasses import dataclass +from pathlib import Path + +logger = logging.getLogger(__name__) + +# --- Replacement pools (curated, verified-live Pexels photo IDs) ------------- + +PEXELS_CONTENT: tuple[str, ...] = ( + "https://images.pexels.com/photos/3183150/pexels-photo-3183150.jpeg", + "https://images.pexels.com/photos/1181675/pexels-photo-1181675.jpeg", + "https://images.pexels.com/photos/1181467/pexels-photo-1181467.jpeg", + "https://images.pexels.com/photos/574070/pexels-photo-574070.jpeg", + "https://images.pexels.com/photos/1181244/pexels-photo-1181244.jpeg", + "https://images.pexels.com/photos/3861958/pexels-photo-3861958.jpeg", + "https://images.pexels.com/photos/2599244/pexels-photo-2599244.jpeg", + "https://images.pexels.com/photos/281260/pexels-photo-281260.jpeg", + "https://images.pexels.com/photos/3182773/pexels-photo-3182773.jpeg", + "https://images.pexels.com/photos/3183132/pexels-photo-3183132.jpeg", + "https://images.pexels.com/photos/3182781/pexels-photo-3182781.jpeg", +) + +PEXELS_AVATARS: tuple[str, ...] = ( + "https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg", + "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg", + "https://images.pexels.com/photos/733872/pexels-photo-733872.jpeg", + "https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg", + "https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg", +) + +# --- Query params ------------------------------------------------------------ + +CONTENT_QUERY_WIDE = "auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1" +CONTENT_QUERY_MEDIUM = "auto=compress&cs=tinysrgb&w=800&dpr=1" +AVATAR_QUERY = "auto=compress&cs=tinysrgb&w=160&h=160&fit=crop&dpr=2" + +# --- Patterns ---------------------------------------------------------------- + +# Normal URLs, e.g. https://images.unsplash.com/photo-1544025162-d76694265947?w=800 +UNSPLASH_URL = re.compile(r"https://images\.unsplash\.com/photo-[A-Za-z0-9_-]+(?:\?[^\s'\"`\\)]*)?") +# Template-literal URLs, e.g. https://images.unsplash.com/photo-${seed}?w=800 +UNSPLASH_TEMPLATE_URL = re.compile( + r"https://images\.unsplash\.com/photo-\$\{[^}]+\}(?:\?[^\s'\"`\\)]*)?" +) +# Already-migrated Pexels URLs (the repair pass only rewrites their query). +PEXELS_URL = re.compile( + r"https://images\.pexels\.com/photos/(\d+)/pexels-photo-\d+\.jpeg\?[^\s'\"`\\)]*" +) +# Hints that an image renders as a person's avatar rather than content. +AVATAR_CONTEXT = re.compile( + r"avatar|author|profile|testimonial|\bface\b|U_FACE|rounded-full", re.IGNORECASE +) +WIDTH_PARAM = re.compile(r"[?&]w=(\d+)") +COMMENT_PREFIX = re.compile(r"\s*(?://|/\*|\*)") +PHOTO_ID_IN_URL = re.compile(r"photos/(\d+)/") + +# Directories never scanned (vendor/build output). +SKIP_DIRS: frozenset[str] = frozenset( + {"node_modules", ".git", "dist", "coverage", ".next", "build"} +) + + +@dataclass(slots=True) +class Stats: + """Run summary counters. + + Attributes: + files_scanned: Candidate files inspected. + files_changed: Files whose content differed after migration. + migrated: Unsplash URLs (normal + template-literal) replaced. + repaired: Pexels avatar queries re-cropped. + """ + + files_scanned: int = 0 + files_changed: int = 0 + migrated: int = 0 + repaired: int = 0 + + +def photo_id(url: str) -> str: + """Extract the numeric Pexels photo ID from a pool URL. + + Args: + url: A curated pool URL such as ``.../photos/774909/pexels-photo-...``. + + Returns: + The numeric photo ID as a string. + + Raises: + ValueError: If the URL does not contain a photo ID (a programming + error in a pool constant, not a data error). + """ + match = PHOTO_ID_IN_URL.search(url) + if match is None: + raise ValueError(f"pool URL is missing a photo id: {url}") + return match.group(1) + + +# Numeric photo IDs of the avatar pool, used by the repair pass. +AVATAR_IDS: frozenset[str] = frozenset(photo_id(url) for url in PEXELS_AVATARS) + + +def stable_pick(pool: tuple[str, ...], key: str) -> str: + """Deterministically pick from *pool* -- same key, same pick, every run. + + Args: + pool: Candidate replacement URLs. + key: Source string that seeds the pick (the original URL). + + Returns: + ``pool`` entry indexed by the key's md5 digest, so the mapping is + stable across runs, files, and machines. + """ + digest = hashlib.md5(key.encode("utf-8")).hexdigest() + return pool[int(digest, 16) % len(pool)] + + +def context_around(match: re.Match[str]) -> str: + """Return the URL's own line plus one adjacent code line per side. + + Scanning outward stops at the first blank or comment line, and at most + one code line per side is admitted: a section banner like + ``// Pexels Avatars`` belongs to its whole block rather than to the one + URL being classified, and a decorative ``rounded-full`` chip several + lines below a hero image must not reclassify it as an avatar. + + Args: + match: The URL match inside the file's full text. + + Returns: + The URL's own line plus up to one adjacent code line per side. + """ + text = match.string + lines = text.split("\n") + idx = text.count("\n", 0, match.start()) + selected = [lines[idx]] + for step in (-1, 1): + j = idx + step + while 0 <= j < len(lines) and abs(j - idx) <= 1: + line = lines[j] + if not line.strip() or COMMENT_PREFIX.match(line): + break + selected.append(line) + j += step + return "\n".join(selected) + + +def build_replacement(source_url: str, context: str) -> str: + """Map an Unsplash URL to a sized Pexels URL. + + Args: + source_url: The matched Unsplash URL. + context: Nearby source lines, used to detect avatar usage. + + Returns: + The replacement URL: square face crop for avatars, otherwise a width + bucket derived from the source URL's ``w`` parameter. + """ + if AVATAR_CONTEXT.search(context): + return f"{stable_pick(PEXELS_AVATARS, source_url)}?{AVATAR_QUERY}" + width_match = WIDTH_PARAM.search(source_url) + width = int(width_match.group(1)) if width_match else 0 + query = CONTENT_QUERY_MEDIUM if 0 < width <= 800 else CONTENT_QUERY_WIDE + return f"{stable_pick(PEXELS_CONTENT, source_url)}?{query}" + + +def repair_query(match: re.Match[str]) -> str: + """Re-crop a Pexels avatar URL that still carries hero sizing. + + Args: + match: A ``PEXELS_URL`` match within the file text. + + Returns: + The URL unchanged, or rewritten to the square avatar crop when the + photo is from the avatar pool, still has hero sizing, and sits in an + avatar context. + """ + photo, url = match.group(1), match.group(0) + if photo in AVATAR_IDS and CONTENT_QUERY_WIDE in url: + if AVATAR_CONTEXT.search(context_around(match)): + return f"{url.split('?', 1)[0]}?{AVATAR_QUERY}" + return url + + +def migrate_text(text: str) -> tuple[str, int, int]: + """Migrate every image URL found in one file's text. + + Template-literal URLs cannot keep their runtime interpolation (a Pexels + URL needs a concrete photo ID), so they become static content images. + + Args: + text: Full file content. + + Returns: + A ``(new_text, migrated_count, repaired_count)`` tuple. ``new_text`` + equals ``text`` when nothing matched. + """ + migrated = 0 + repaired = 0 + + def sub_template(match: re.Match[str]) -> str: + nonlocal migrated + migrated += 1 + return f"{stable_pick(PEXELS_CONTENT, match.group(0))}?{CONTENT_QUERY_WIDE}" + + def sub_unsplash(match: re.Match[str]) -> str: + nonlocal migrated + migrated += 1 + return build_replacement(match.group(0), context_around(match)) + + def sub_repair(match: re.Match[str]) -> str: + nonlocal repaired + replacement = repair_query(match) + if replacement != match.group(0): + repaired += 1 + return replacement + + text = UNSPLASH_TEMPLATE_URL.sub(sub_template, text) + text = UNSPLASH_URL.sub(sub_unsplash, text) + text = PEXELS_URL.sub(sub_repair, text) + return text, migrated, repaired + + +def iter_source_files(root: Path, exts: frozenset[str]) -> Iterator[Path]: + """Yield candidate source files under *root*. + + Args: + root: Directory scanned recursively. + exts: File-name extensions to keep (leading dot required). + + Yields: + Files whose suffix matches, excluding ``*.d.ts`` and anything inside + ``SKIP_DIRS``. + """ + for path in root.rglob("*"): + if not path.is_file(): + continue + if path.suffix not in exts or path.name.endswith(".d.ts"): + continue + if SKIP_DIRS.intersection(path.parts): + continue + yield path + + +def configure_logging(verbosity: int) -> None: + """Configure module logging from the CLI verbosity flag. + + Operates on this module's logger (not ``basicConfig``, which is a silent + no-op after its first call) so repeated ``main()`` runs reconfigure + correctly. Records still propagate to the root logger for capture by + test harnesses. + + Args: + verbosity: 0 keeps INFO logs (per-file updates, summary), negative + values reduce to WARNING, values above 0 enable DEBUG. + """ + level = logging.WARNING if verbosity < 0 else logging.DEBUG if verbosity > 0 else logging.INFO + pkg_logger = logging.getLogger(__name__) + pkg_logger.setLevel(level) + if not pkg_logger.handlers: + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + pkg_logger.addHandler(handler) + + +def main(argv: Sequence[str] | None = None) -> int: + """Run the codemod over the target tree. + + Args: + argv: CLI arguments; ``None`` means ``sys.argv[1:]``. + + Returns: + Process exit code: 0 on success, 1 when ``--check`` finds pending + changes, 2 on usage errors. + """ + parser = argparse.ArgumentParser( + description="Migrate Unsplash image URLs to Pexels (deterministic, avatar-aware)." + ) + parser.add_argument("--root", type=Path, default=Path("src"), help="directory to scan") + parser.add_argument( + "--ext", nargs="+", default=[".ts", ".tsx"], metavar="EXT", help="file extensions" + ) + parser.add_argument("--dry-run", action="store_true", help="preview changes, write nothing") + parser.add_argument( + "--check", action="store_true", help="exit 1 if any file would change (CI drift guard)" + ) + parser.add_argument("-v", "--verbose", action="count", default=0, help="increase log verbosity") + parser.add_argument("-q", "--quiet", action="store_true", help="only log warnings") + args = parser.parse_args(argv) + + configure_logging(-1 if args.quiet else args.verbose) + + if not args.root.is_dir(): + parser.error(f"not a directory: {args.root}") + + exts = frozenset(ext if ext.startswith(".") else f".{ext}" for ext in args.ext) + stats = Stats() + verb = "would update" if (args.dry_run or args.check) else "updated" + + for path in iter_source_files(args.root, exts): + stats.files_scanned += 1 + try: + # newline='' disables newline translation so files round-trip byte-identically. + with path.open("r", encoding="utf-8", newline="") as handle: + original = handle.read() + except (OSError, UnicodeDecodeError) as exc: + logger.warning("skipped %s: %s", path, exc) + continue + + updated, migrated, repaired = migrate_text(original) + if updated == original: + continue + + stats.files_changed += 1 + stats.migrated += migrated + stats.repaired += repaired + logger.info("%s: %s (%d migrated, %d repaired)", verb, path, migrated, repaired) + if args.dry_run or args.check: + continue + with path.open("w", encoding="utf-8", newline="") as handle: + handle.write(updated) + + logger.info( + "%d files scanned | %d %s | %d URLs migrated | %d avatar queries repaired", + stats.files_scanned, + stats.files_changed, + verb, + stats.migrated, + stats.repaired, + ) + if args.check and stats.files_changed: + logger.warning("check failed: un-migrated or mis-sized image URLs remain") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt new file mode 100644 index 0000000..c8708e5 --- /dev/null +++ b/scripts/requirements-dev.txt @@ -0,0 +1,7 @@ +# Dev tools for validating scripts/replace_images.py +# Usage: python3 -m pip install -r scripts/requirements-dev.txt +black>=24.0 +ruff>=0.5 +mypy>=1.10 +pytest>=8.0 +pytest-cov>=5.0 diff --git a/scripts/tests/test_replace_images.py b/scripts/tests/test_replace_images.py new file mode 100644 index 0000000..0150b3b --- /dev/null +++ b/scripts/tests/test_replace_images.py @@ -0,0 +1,375 @@ +"""Test suite for scripts/replace_images.py. + +Covers migration determinism, avatar classification (including the +comment-banner regression), the Pexels repair pass, CLI modes, and file +safety guarantees (line endings, vendor-dir skips, bad-input tolerance). +""" + +from __future__ import annotations + +import logging +from collections.abc import Callable, Sequence +from pathlib import Path + +import pytest +import replace_images as ri + +# --- Constants used across tests --------------------------------------------- + +CONTENT_MED = "https://images.unsplash.com/photo-ddeeff?w=800" +CONTENT_HERO = "https://images.unsplash.com/photo-112233?w=1600" +CONTENT_NOW = "https://images.unsplash.com/photo-445566" +CONTENT_TINY = "https://images.unsplash.com/photo-aabbcc?w=400" +AVATAR_SRC = "https://images.unsplash.com/photo-998877?w=200" +TEMPLATE_SRC = "https://images.unsplash.com/photo-${seed}?w=800" + +WIDE_QUERY = ri.CONTENT_QUERY_WIDE +MEDIUM_QUERY = ri.CONTENT_QUERY_MEDIUM +AVATAR_QUERY = ri.AVATAR_QUERY + + +# --- Helpers & fixtures ------------------------------------------------------ + + +def build_tree(tmp_path: Path, files: dict[str, str]) -> Path: + """Materialize a fake source tree and return its root directory.""" + root = tmp_path / "src" + for name, content in files.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8", newline="") + return root + + +def read(root: Path, name: str) -> str: + """Read one file from the built tree as UTF-8 text.""" + return (root / name).read_text(encoding="utf-8") + + +@pytest.fixture +def tree(tmp_path: Path) -> Path: + """A representative source tree: content, avatar, template, vendor files.""" + return build_tree( + tmp_path, + { + "app.tsx": ( + f"export const hero = '{CONTENT_HERO}';\n" f"export const card = '{CONTENT_MED}';\n" + ), + "authors.ts": f"export const authorAvatar = '{AVATAR_SRC}';\n", + "registry.ts": ( + f"export const U_NEON = '{CONTENT_NOW}';\n" + "// Pexels Avatars (Faces crop)\n" + f"export const U_FACE_9 = '{CONTENT_NOW}';\n" + ), + "template.tsx": f"const src = `'{TEMPLATE_SRC}'`;\n", + "crlf.ts": f"const img = '{CONTENT_TINY}';\r\nconst ok = 1;\r\n", + "node_modules/pkg/skip.ts": f"const x = '{CONTENT_TINY}';\n", + "types.d.ts": f"declare const x: '{CONTENT_TINY}';\n", + "vendor.d.ts": f"declare const y: '{CONTENT_TINY}';\n", + }, + ) + + +@pytest.fixture +def run(tree: Path) -> Callable[..., int]: + """Invoke ``main`` against the fixture tree with the root preconfigured.""" + + def _run(*extra: str) -> int: + return ri.main(["--root", str(tree), *extra]) + + return _run + + +def main_on(tmp_path: Path, *extra: str) -> int: + """Run the CLI against ``tmp_path``-rooted ``src`` with *extra* flags.""" + argv: Sequence[str] = ["--root", str(tmp_path / "src"), *extra] + return ri.main(argv) + + +# --- Core migration ---------------------------------------------------------- + + +@pytest.mark.parametrize( + ("url", "query"), + [ + (CONTENT_MED, MEDIUM_QUERY), + (CONTENT_HERO, WIDE_QUERY), + (CONTENT_NOW, WIDE_QUERY), + ], + ids=["width-bucket-800", "width-bucket-wide", "no-width-defaults-wide"], +) +def test_content_urls_use_width_buckets(tmp_path: Path, url: str, query: str) -> None: + """Content images get a width bucket derived from the source ``w`` param.""" + build_tree(tmp_path, {"app.tsx": f"const img = '{url}';\n"}) + assert main_on(tmp_path) == 0 + out = read(tmp_path / "src", "app.tsx") + assert url not in out + assert f"?{query}" in out + + +@pytest.mark.parametrize( + "line", + [ + f"export const authorAvatar = '{AVATAR_SRC}';", + f"export const U_FACE_1 = '{AVATAR_SRC}';", + f'const quote = {{ avatar: "{AVATAR_SRC}" }}; // testimonial', + f'', + ], + ids=["authorAvatar", "U_FACE", "testimonial", "rounded-full"], +) +def test_avatar_contexts_get_square_crop(tmp_path: Path, line: str) -> None: + """Avatar-ish contexts receive the square face crop, not hero sizing.""" + build_tree(tmp_path, {"a.ts": line + "\n"}) + assert main_on(tmp_path) == 0 + out = read(tmp_path / "src", "a.ts") + assert "unsplash" not in out + assert AVATAR_QUERY in out + assert WIDE_QUERY not in out + + +def test_mapping_is_deterministic(tmp_path: Path) -> None: + """Same source URL maps to the same Pexels URL across runs and files.""" + files = { + "a.ts": f"const one = '{CONTENT_MED}';\n", + "b.ts": f"const two = '{CONTENT_MED}';\n", + } + root_one = build_tree(tmp_path / "one", files) + root_two = build_tree(tmp_path / "two", files) + assert ri.main(["--root", str(root_one)]) == 0 + assert ri.main(["--root", str(root_two)]) == 0 + urls = [ + ri.PEXELS_URL.search(text).group(0) # type: ignore[union-attr] + for text in ( + (root_one / "a.ts").read_text(encoding="utf-8"), + (root_two / "a.ts").read_text(encoding="utf-8"), + (root_two / "b.ts").read_text(encoding="utf-8"), + ) + ] + assert len(set(urls)) == 1, f"mappings diverged: {urls}" + + +def test_repair_pass_recrops_pexels_avatar(tmp_path: Path) -> None: + """Already-migrated Pexels avatar URLs lose their hero-sized query.""" + wide_avatar = ( + "https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg" f"?{WIDE_QUERY}" + ) + build_tree( + tmp_path, + { + "authors.ts": f"export const authorAvatar = '{wide_avatar}';\n", + "content.ts": ( + "export const img = 'https://images.pexels.com/photos/281260/" + f"pexels-photo-281260.jpeg?{WIDE_QUERY}';\n" + ), + "square.ts": ( + "export const authorAvatar = 'https://images.pexels.com/photos/774909/" + f"pexels-photo-774909.jpeg?{AVATAR_QUERY}';\n" + ), + }, + ) + assert main_on(tmp_path) == 0 + authors = read(tmp_path / "src", "authors.ts") + content = read(tmp_path / "src", "content.ts") + square = read(tmp_path / "src", "square.ts") + assert AVATAR_QUERY in authors + assert WIDE_QUERY in content # content images stay untouched + assert square.endswith(f"?{AVATAR_QUERY}';\n") # already square → unchanged + + +def test_comment_banner_does_not_reclassify_neighbor(tree: Path, run: Callable[..., int]) -> None: + """A section comment classifies its own block only (U_NEON regression).""" + assert run() == 0 + registry = read(tree, "registry.ts") + neon_line = next(line for line in registry.splitlines() if "U_NEON" in line) + face_line = next(line for line in registry.splitlines() if "U_FACE_9" in line) + assert WIDE_QUERY in neon_line # content, despite the Avatars banner above + assert AVATAR_QUERY in face_line + + +def test_distant_rounded_chip_does_not_reclassify_hero(tmp_path: Path) -> None: + """A ``rounded-full`` chip lines below a hero image is out of context.""" + wide_avatar = ( + "https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg" f"?{WIDE_QUERY}" + ) + build_tree( + tmp_path, + { + "page.tsx": ( + "\n" + ' Chip\n' + ) + }, + ) + assert main_on(tmp_path) == 0 + assert WIDE_QUERY in read(tmp_path / "src", "page.tsx") # stays a hero image + + +def test_adjacent_jsx_line_counts_as_context(tmp_path: Path) -> None: + """Multiline JSX usage is classified: className on the next line counts.""" + build_tree( + tmp_path, + {"card.tsx": ("\n")}, + ) + assert main_on(tmp_path) == 0 + out = read(tmp_path / "src", "card.tsx") + assert "unsplash" not in out + assert AVATAR_QUERY in out + + +def test_template_literal_becomes_static_content(tree: Path, run: Callable[..., int]) -> None: + """``photo-${seed}`` URLs become static content images (no interpolation).""" + assert run() == 0 + out = read(tree, "template.tsx") + assert "unsplash" not in out + assert "${" not in out + assert WIDE_QUERY in out + + +# --- CLI modes & safety ------------------------------------------------------ + + +def test_dry_run_reports_without_writing( + tree: Path, run: Callable[..., int], caplog: pytest.LogCaptureFixture +) -> None: + """``--dry-run`` leaves every file byte-identical.""" + before = {p: p.read_bytes() for p in tree.rglob("*") if p.is_file()} + with caplog.at_level(logging.INFO): + assert run("--dry-run") == 0 + after = {p: p.read_bytes() for p in tree.rglob("*") if p.is_file()} + assert before == after + assert any("would update" in record.message for record in caplog.records) + + +def test_check_mode_exit_codes(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: + """``--check`` exits 1 when work is pending and 0 on a clean tree.""" + pending = build_tree(tmp_path / "pending", {"a.ts": f"const u = '{CONTENT_MED}';\n"}) + clean = build_tree(tmp_path / "clean", {"a.ts": "const ok = 1;\n"}) + with caplog.at_level(logging.INFO): + assert ri.main(["--root", str(pending), "--check"]) == 1 + assert ri.main(["--root", str(clean), "--check"]) == 0 + # check mode must not modify the tree + assert CONTENT_MED in read(pending, "a.ts") + + +def test_second_run_is_idempotent(tree: Path, run: Callable[..., int]) -> None: + """A completed migration re-runs as a no-op.""" + assert run() == 0 + snapshot = {p: p.read_bytes() for p in tree.rglob("*") if p.is_file()} + assert run() == 0 + assert snapshot == {p: p.read_bytes() for p in tree.rglob("*") if p.is_file()} + + +def test_crlf_line_endings_preserved(tmp_path: Path) -> None: + """Migrated CRLF files keep their ``\\r\\n`` endings byte-for-byte.""" + build_tree(tmp_path, {"w.ts": f"const img = '{CONTENT_TINY}';\r\nok();\r\n"}) + path = tmp_path / "src" / "w.ts" + crlf_before = path.read_bytes().count(b"\r\n") + assert main_on(tmp_path) == 0 + data = path.read_bytes() + assert data.count(b"\r\n") == crlf_before + assert b"unsplash" not in data + + +def test_skips_vendor_dirs_and_d_ts(tree: Path, run: Callable[..., int]) -> None: + """node_modules content and ``*.d.ts`` files are never touched.""" + assert run() == 0 + assert CONTENT_TINY in read(tree, "node_modules/pkg/skip.ts") + assert CONTENT_TINY in read(tree, "types.d.ts") + assert CONTENT_TINY in read(tree, "vendor.d.ts") + + +def test_invalid_utf8_warns_and_continues(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: + """Undecodable files log a warning and do not abort the run.""" + root = tmp_path / "src" + root.mkdir() + (root / "bad.ts").write_bytes(b"\xff\xfe\x00const broken = 1;\n") + (root / "good.ts").write_text(f"const u = '{CONTENT_MED}';\n", encoding="utf-8") + with caplog.at_level(logging.WARNING): + assert main_on(tmp_path) == 0 + assert any("skipped" in record.message for record in caplog.records) + assert "unsplash" not in read(root, "good.ts") + + +def test_missing_root_is_usage_error(tmp_path: Path) -> None: + """A nonexistent ``--root`` exits with argparse's usage code (2).""" + with pytest.raises(SystemExit) as excinfo: + ri.main(["--root", str(tmp_path / "missing")]) + assert excinfo.value.code == 2 + + +def test_ext_filter(tmp_path: Path) -> None: + """Only extensions passed via ``--ext`` are rewritten.""" + build_tree( + tmp_path, {"a.ts": f"const u = '{CONTENT_MED}';\n", "b.py": f"u = '{CONTENT_MED}'\n"} + ) + assert main_on(tmp_path) == 0 # default exts: .ts/.tsx + assert CONTENT_MED not in read(tmp_path / "src", "a.ts") + assert CONTENT_MED in read(tmp_path / "src", "b.py") + assert main_on(tmp_path, "--ext", ".py") == 0 + assert CONTENT_MED not in read(tmp_path / "src", "b.py") + + +# --- Unit helpers & logging -------------------------------------------------- + + +@pytest.mark.parametrize( + ("url", "expected"), + [ + ("https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?x=1", "774909"), + ("https://images.pexels.com/photos/281260/pexels-photo-281260.jpeg", "281260"), + ], +) +def test_photo_id_extracts_numeric_id(url: str, expected: str) -> None: + """``photo_id`` pulls the numeric ID out of pool URLs.""" + assert ri.photo_id(url) == expected + + +def test_photo_id_raises_on_malformed_pool_url() -> None: + """Malformed pool constants fail fast with a clear error.""" + with pytest.raises(ValueError, match="missing a photo id"): + ri.photo_id("https://images.pexels.com/no-id-here") + + +@pytest.mark.parametrize("key", ["a", "photo-12345", "https://images.unsplash.com/x"]) +def test_stable_pick_stays_in_pool_and_is_stable(key: str) -> None: + """The deterministic pick always lands inside the pool and repeats.""" + assert ri.stable_pick(ri.PEXELS_CONTENT, key) in ri.PEXELS_CONTENT + assert ri.stable_pick(ri.PEXELS_CONTENT, key) == ri.stable_pick(ri.PEXELS_CONTENT, key) + + +@pytest.mark.parametrize( + ("verbosity", "expected"), + [(-1, logging.WARNING), (0, logging.INFO), (3, logging.DEBUG)], +) +def test_configure_logging_levels(verbosity: int, expected: int) -> None: + """Verbosity maps to WARNING/INFO/DEBUG on the module logger.""" + ri.configure_logging(verbosity) + assert logging.getLogger("replace_images").getEffectiveLevel() == expected + + +def test_summary_is_logged( + tree: Path, run: Callable[..., int], caplog: pytest.LogCaptureFixture +) -> None: + """The run summary reports scan and change counts (5/5 for the fixture).""" + with caplog.at_level(logging.INFO): + assert run() == 0 + summaries = [r.message for r in caplog.records if "files scanned" in r.message] + assert summaries, "expected a summary log record" + assert "5 files scanned" in summaries[0] + assert "5 updated" in summaries[0] + + +def test_migrate_text_counts() -> None: + """``migrate_text`` reports per-category counts.""" + text = ( + f"a {CONTENT_MED} b '{TEMPLATE_SRC}' c d " + "https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg" + f"?{WIDE_QUERY} authorAvatar" + ) + _, migrated, repaired = ri.migrate_text(text) + assert migrated == 2 + assert repaired == 1 diff --git a/server.ts b/server.ts index b0cffd9..3bb974a 100644 --- a/server.ts +++ b/server.ts @@ -1,2870 +1,34 @@ -import express, { Request, Response, NextFunction } from 'express'; -import helmet from 'helmet'; +import express from 'express'; +import { createServer as createHttpServer } from 'http'; import { createServer as createViteServer } from 'vite'; import path from 'path'; -import { fileURLToPath } from 'url'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import https from 'https'; -import http from 'http'; -import tls from 'tls'; -import crypto from 'crypto'; -import { URL } from 'url'; -import os from 'os'; -import geoip from 'geoip-lite'; -import { UAParser } from 'ua-parser-js'; -import { runNativeEngine } from './src/lib/nodeEngines'; -import { validatePublicUrl } from './src/lib/networkSecurity'; -import { initAnalyticsDB, getDbInstance, queueEvent, generateVisitorId, getAnalyticsStats, detectTrafficAnomalies, checkMongoDBHealth, getBatchMetrics } from './src/lib/analyticsEngine'; -import { generateWeeklyReportHtml, generateAnomalyAlertHtml, sendEmailViaMailgun, AnalyticsWeeklyData, AnomalyAlertData } from './src/lib/emailService'; -import { sendSlackWebhook, sendDiscordWebhook, sendGenericWebhook, WebhookPayloadData } from './src/lib/webhookService'; +import type { Request, Response, NextFunction } from 'express'; +import { initAnalyticsDB } from './src/lib/analyticsEngine'; +import { createApp } from './server/app'; +import { logger } from './server/core/logger'; import 'dotenv/config'; -const execAsync = promisify(exec); +// Process entrypoint: owns the HTTP server, Vite dev middleware / production +// static serving, and lifecycle concerns only. All Express configuration +// (security headers, body limits, identity, API routes, error fallbacks) +// lives in server/app.ts so the route suite can exercise it headlessly. -const ENGINE_SCRIPT_MAP: Record = { - // Phase 1: Planning & Architecture - migration: 'platform_migration_audit.py', - planning_arch: 'platform_migration_audit.py', - - // Phase 2: Code Quality & Repo - repo: 'repo_scanner.py', - code_quality: 'repo_scanner.py', - - // Phase 3: Build & Asset Efficiency - eco: 'eco_carbon_audit.py', - build_eco: 'eco_carbon_audit.py', - - // Phase 4: Testing & Core Web Vitals - health: 'website_health.py', - testing_vitals: 'website_health.py', - - // Phase 5: Release & Edge Delivery - latency: 'edge_latency.py', - release_edge: 'edge_latency.py', - - // Phase 6: Deployment & DevSecOps - compliance: 'compliance_risk_audit.py', - devsecops_compliance: 'compliance_risk_audit.py', - - // Phase 7: Live Operations & AI Readiness - ai_ready: 'ai_readiness.py', - operations_ai_ready: 'ai_readiness.py', - - // Phase 8: Continuous Evolution & LLMO - llmo: 'llmo_optimizer.py', - evolution_llmo: 'llmo_optimizer.py' -}; - -const SUPERADMIN_EMAILS = [ - 'shuvo.1807016@bau.edu.bd', - 'shuvoasifahmed@gmail.com', - 'asifahmedshuvo.aas@gmail.com', - 'asifahmedshuvo.aa9@gmail.com' -]; - -export const VISITOR_DAILY_UNITS = 20; -export const FREE_USER_DAILY_UNITS = 50; -export const STARTER_DAILY_UNITS = 150; -export const PRO_DAILY_UNITS = 500; -export const TEAM_DAILY_UNITS = 1500; -export const ENTERPRISE_DAILY_UNITS = 5000; -export const PRO_API_DAILY_UNITS = 500; -export const MASTER_AUDIT_COST = 10; -export const SINGLE_ENGINE_COST = 1; - -export const BURST_WINDOW_MS = 60 * 1000; -export const VISITOR_BURST_MAX = 15; -export const USER_BURST_MAX = 45; - -export type RateLimitTier = 'superadmin' | 'enterprise' | 'team' | 'pro' | 'starter' | 'free' | 'visitor' | 'api_pro'; - -interface RateLimitRecord { - unitsUsed: number; - sessionCostMap: Map; - requestTimestamps: number[]; - lastUpdated: number; - tier: RateLimitTier; -} - -// In-memory rate limit ledger: Map -const dailyRateLimitStore = new Map(); - -function getUtcMidnight(): { dateKey: string; resetAt: Date; resetInSeconds: number; formattedResetTime: string } { - const now = new Date(); - const dateKey = now.toISOString().split('T')[0]; - const resetAt = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 0, 0, 0, 0)); - const resetInSeconds = Math.max(1, Math.floor((resetAt.getTime() - now.getTime()) / 1000)); - - const totalMinutes = Math.floor(resetInSeconds / 60); - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - const formattedResetTime = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; - - return { dateKey, resetAt, resetInSeconds, formattedResetTime }; -} - -function resolveClientIdentity(req: Request): { - identifier: string; - tier: RateLimitTier; - tierLabel: string; - subscriptionPlan: string; - isTrialActive: boolean; - limit: number | null; - burstMax: number; - cleanEmail?: string; - userId?: string; - visitorId?: string; - sessionId?: string; -} { - const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || req.socket.remoteAddress || '127.0.0.1'; - const rawEmail = (req.body?.userEmail || req.headers['x-user-email'] || req.query?.userEmail || '') as string; - const rawUserId = (req.body?.userId || req.headers['x-user-id'] || req.query?.userId || '') as string; - const rawVisitorId = (req.body?.visitorId || req.headers['x-visitor-id'] || req.query?.visitorId || '') as string; - const rawSessionId = (req.body?.auditSessionId || req.headers['x-audit-session'] || req.query?.auditSessionId || '') as string; - const apiKey = (req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', '')) as string; - - const rawPlan = (req.body?.subscriptionPlan || req.headers['x-subscription-plan'] || req.query?.subscriptionPlan || 'free') as string; - const rawTrial = req.body?.isTrialActive || req.headers['x-trial-active'] || req.query?.isTrialActive; - const isTrialActive = rawTrial === true || rawTrial === 'true'; - const plan = rawPlan.toLowerCase().trim(); - - const cleanEmail = rawEmail.toLowerCase().trim(); - const isSuperadmin = Boolean(cleanEmail && SUPERADMIN_EMAILS.includes(cleanEmail)); - - if (isSuperadmin) { - return { - identifier: `superadmin_${cleanEmail}`, - tier: 'superadmin', - tierLabel: 'Primary Superadmin', - subscriptionPlan: 'enterprise', - isTrialActive: false, - limit: null, - burstMax: Infinity, - cleanEmail, - userId: rawUserId, - visitorId: rawVisitorId, - sessionId: rawSessionId - }; - } - - if (apiKey && apiKey.startsWith('cat_live_')) { - return { - identifier: `key_${apiKey.substring(0, 16)}`, - tier: 'api_pro', - tierLabel: 'Developer API Key', - subscriptionPlan: 'pro', - isTrialActive: false, - limit: PRO_API_DAILY_UNITS, - burstMax: 120, - cleanEmail, - userId: rawUserId, - visitorId: rawVisitorId, - sessionId: rawSessionId - }; - } - - if (rawUserId || cleanEmail) { - let limit = FREE_USER_DAILY_UNITS; - let tier: RateLimitTier = 'free'; - let tierLabel = 'Community User'; - let burstMax = USER_BURST_MAX; - - if (plan === 'enterprise') { - limit = ENTERPRISE_DAILY_UNITS; - tier = 'enterprise'; - tierLabel = isTrialActive ? 'Enterprise (7-Day Trial)' : 'Enterprise Tier'; - burstMax = 500; - } else if (plan === 'team') { - limit = TEAM_DAILY_UNITS; - tier = 'team'; - tierLabel = isTrialActive ? 'Team (7-Day Trial)' : 'Team Tier'; - burstMax = 300; - } else if (plan === 'pro') { - limit = PRO_DAILY_UNITS; - tier = 'pro'; - tierLabel = isTrialActive ? 'Pro (7-Day Trial)' : 'Professional Tier'; - burstMax = 120; - } else if (plan === 'starter') { - limit = STARTER_DAILY_UNITS; - tier = 'starter'; - tierLabel = isTrialActive ? 'Starter (7-Day Trial)' : 'Starter Tier'; - burstMax = 60; - } - - return { - identifier: `user_${rawUserId || cleanEmail}`, - tier, - tierLabel, - subscriptionPlan: plan, - isTrialActive, - limit, - burstMax, - cleanEmail, - userId: rawUserId, - visitorId: rawVisitorId, - sessionId: rawSessionId - }; - } - - return { - identifier: `vis_${rawVisitorId || ip}`, - tier: 'visitor', - tierLabel: 'Guest Visitor', - subscriptionPlan: 'visitor', - isTrialActive: false, - limit: VISITOR_DAILY_UNITS, - burstMax: VISITOR_BURST_MAX, - cleanEmail: undefined, - userId: undefined, - visitorId: rawVisitorId, - sessionId: rawSessionId - }; -} - -function getOrCreateRateLimitRecord(key: string, tier: RateLimitTier): RateLimitRecord { - if (!dailyRateLimitStore.has(key)) { - dailyRateLimitStore.set(key, { - unitsUsed: 0, - sessionCostMap: new Map(), - requestTimestamps: [], - lastUpdated: Date.now(), - tier - }); - } - return dailyRateLimitStore.get(key)!; -} - -// Evaluate Rate Limit & Deduplicate Multi-Engine Sessions -export function evaluateAndChargeRateLimit( - req: Request, - res: Response, - requestedCost: number = 1 -): { - allowed: boolean; - burstExceeded?: boolean; - tier: string; - tierLabel: string; - limit: number | null; - unitsUsed: number; - unitsRemaining: number; - costCharged: number; - resetAt: string; - resetInSeconds: number; - formattedResetTime: string; - error?: string; -} { - const { dateKey, resetAt, resetInSeconds, formattedResetTime } = getUtcMidnight(); - const identity = resolveClientIdentity(req); - const now = Date.now(); - - // Superadmin bypass - if (identity.tier === 'superadmin') { - if (!res.headersSent) { - res.setHeader('X-RateLimit-Limit', 'unlimited'); - res.setHeader('X-RateLimit-Remaining', 'unlimited'); - res.setHeader('X-RateLimit-Used', '0'); - res.setHeader('X-RateLimit-Reset', Math.floor(resetAt.getTime() / 1000).toString()); - res.setHeader('X-RateLimit-Tier', identity.tier); - } - return { - allowed: true, - tier: identity.tier, - tierLabel: identity.tierLabel, - limit: null, - unitsUsed: 0, - unitsRemaining: Infinity, - costCharged: 0, - resetAt: resetAt.toISOString(), - resetInSeconds, - formattedResetTime - }; - } - - const storeKey = `${dateKey}_${identity.identifier}`; - const record = getOrCreateRateLimitRecord(storeKey, identity.tier); - - // 1. Burst Rate Limiting Check (Sliding 60s Window) - record.requestTimestamps = record.requestTimestamps.filter(t => now - t < BURST_WINDOW_MS); - if (record.requestTimestamps.length >= identity.burstMax) { - if (!res.headersSent) { - res.setHeader('Retry-After', '10'); - res.setHeader('X-RateLimit-Limit', String(identity.limit)); - res.setHeader('X-RateLimit-Remaining', String(Math.max(0, (identity.limit || 0) - record.unitsUsed))); - res.setHeader('X-RateLimit-Used', String(record.unitsUsed)); - res.setHeader('X-RateLimit-Reset', Math.floor(resetAt.getTime() / 1000).toString()); - res.setHeader('X-RateLimit-Tier', identity.tier); - } - - return { - allowed: false, - burstExceeded: true, - tier: identity.tier, - tierLabel: identity.tierLabel, - limit: identity.limit, - unitsUsed: record.unitsUsed, - unitsRemaining: Math.max(0, (identity.limit || 0) - record.unitsUsed), - costCharged: 0, - resetAt: resetAt.toISOString(), - resetInSeconds: 10, - formattedResetTime, - error: `Burst rate limit exceeded (${identity.burstMax} req/min). Please pause for 10 seconds.` - }; - } - - // 2. Session-Based Cost Calculation (Smart Deduplication) - let costToCharge = requestedCost; - const sessionId = identity.sessionId; - - if (sessionId) { - const previousBilled = record.sessionCostMap.get(sessionId) || 0; - if (previousBilled >= MASTER_AUDIT_COST) { - // Already paid for full master audit session — all sibling engine calls under same session are complimentary - costToCharge = 0; - } else if (previousBilled > 0) { - costToCharge = Math.max(0, requestedCost - previousBilled); - } - } - - const limit = identity.limit || FREE_USER_DAILY_UNITS; - const projectedUsed = record.unitsUsed + costToCharge; - - // 3. Quota Exceeded Check - if (projectedUsed > limit) { - if (!res.headersSent) { - res.setHeader('Retry-After', String(resetInSeconds)); - res.setHeader('X-RateLimit-Limit', String(limit)); - res.setHeader('X-RateLimit-Remaining', '0'); - res.setHeader('X-RateLimit-Used', String(record.unitsUsed)); - res.setHeader('X-RateLimit-Reset', Math.floor(resetAt.getTime() / 1000).toString()); - res.setHeader('X-RateLimit-Tier', identity.tier); - } - - const errorMessage = identity.tier === 'free' - ? `Daily compute quota exhausted (${limit} units / 5 Master Audits / 50 Single Engines). Resets at midnight UTC. Upgrade your plan or activate a 7-day free trial at /pricing.` - : identity.tier === 'visitor' - ? `Daily visitor limit exhausted (${limit} units / 2 Master Audits / 20 Single Engines). Sign in with Google to unlock 50 units/day.` - : `Daily quota limit exhausted (${limit} units) for ${identity.tierLabel}. Resets at midnight UTC.`; - - return { - allowed: false, - tier: identity.tier, - tierLabel: identity.tierLabel, - limit, - unitsUsed: record.unitsUsed, - unitsRemaining: 0, - costCharged: 0, - resetAt: resetAt.toISOString(), - resetInSeconds, - formattedResetTime, - error: errorMessage - }; - } - - // 4. Record Successful Request - record.requestTimestamps.push(now); - record.unitsUsed += costToCharge; - record.lastUpdated = now; - if (sessionId) { - const prev = record.sessionCostMap.get(sessionId) || 0; - record.sessionCostMap.set(sessionId, prev + costToCharge); - } - - const remaining = Math.max(0, limit - record.unitsUsed); - - // Set Standard HTTP Rate Limit Headers - if (!res.headersSent) { - res.setHeader('X-RateLimit-Limit', String(limit)); - res.setHeader('X-RateLimit-Remaining', String(remaining)); - res.setHeader('X-RateLimit-Used', String(record.unitsUsed)); - res.setHeader('X-RateLimit-Reset', Math.floor(resetAt.getTime() / 1000).toString()); - res.setHeader('X-RateLimit-Tier', identity.tier); - res.setHeader('RateLimit-Policy', `${limit};w=86400`); - } - - return { - allowed: true, - tier: identity.tier, - tierLabel: identity.tierLabel, - limit, - unitsUsed: record.unitsUsed, - unitsRemaining: remaining, - costCharged: costToCharge, - resetAt: resetAt.toISOString(), - resetInSeconds, - formattedResetTime - }; -} - -// Express Rate-Limiting Middleware for Engine Endpoints -export function createEngineRateLimitMiddleware(options: { cost?: number; isMaster?: boolean } = {}) { - return (req: Request, res: Response, next: express.NextFunction) => { - const cost = options.isMaster ? MASTER_AUDIT_COST : (options.cost || SINGLE_ENGINE_COST); - const result = evaluateAndChargeRateLimit(req, res, cost); - - if (!result.allowed) { - res.status(429).json({ - success: false, - rateLimitExceeded: true, - tier: result.tier, - tierLabel: result.tierLabel, - limit: result.limit, - used: result.unitsUsed, - remaining: result.unitsRemaining, - resetAt: result.resetAt, - resetInSeconds: result.resetInSeconds, - formattedResetTime: result.formattedResetTime, - error: result.error - }); - return; - } - - (req as any).rateLimitStatus = result; - next(); - }; -} - -// Clean old rate limit map entries every hour -setInterval(() => { - const { dateKey } = getUtcMidnight(); - for (const key of dailyRateLimitStore.keys()) { - if (!key.startsWith(dateKey)) { - dailyRateLimitStore.delete(key); - } - } -}, 1000 * 60 * 60); - -const serverStartTime = Date.now(); -let totalAuditsExecuted = 0; - -// Helper to check SSL certificate days remaining -function getSslDetails(hostname: string, port = 443): Promise<{ valid: boolean; daysRemaining?: number; issuer?: string }> { - return new Promise((resolve) => { - let resolved = false; - const safeResolve = (result: { valid: boolean; daysRemaining?: number; issuer?: string }) => { - if (resolved) return; - resolved = true; - resolve(result); - }; - - try { - const socket = tls.connect( - { - host: hostname, - port, - servername: hostname, - timeout: 4000 - }, - () => { - try { - const cert = socket.getPeerCertificate(); - if (cert && cert.valid_to) { - const validTo = new Date(cert.valid_to); - const now = new Date(); - const diffTime = validTo.getTime() - now.getTime(); - const daysRemaining = Math.max(0, Math.floor(diffTime / (1000 * 60 * 60 * 24))); - socket.destroy(); - safeResolve({ - valid: daysRemaining > 0, - daysRemaining, - issuer: typeof cert.issuer === 'object' && cert.issuer !== null - ? Array.isArray(cert.issuer.O) ? cert.issuer.O.join(', ') : (cert.issuer.O || cert.issuer.CN ? String(cert.issuer.O || cert.issuer.CN) : undefined) - : String(cert.issuer) - }); - return; - } - } catch { - // fallback - } - socket.destroy(); - safeResolve({ valid: true }); - } - ); - - socket.on('error', () => { - socket.destroy(); - safeResolve({ valid: false, daysRemaining: 0 }); - }); - - socket.on('timeout', () => { - socket.destroy(); - safeResolve({ valid: false, daysRemaining: 0 }); - }); - } catch { - safeResolve({ valid: false }); - } - }); -} - -async function startServer() { +async function startServer(): Promise { // Initialize analytics DB asynchronously without blocking server startup - initAnalyticsDB().catch((err) => { - console.warn('[Analytics DB] Startup init skipped/deferred:', err?.message || err); - }); - - const app = express(); - const PORT = 3000; - const HOST = '0.0.0.0'; - - // OWASP Security Hardening Middleware with Helmet - app.use( - helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://apis.google.com", "https://*.googleapis.com", "https://*.gstatic.com"], - styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://*.gstatic.com"], - fontSrc: ["'self'", "data:", "https://fonts.gstatic.com", "https://fonts.googleapis.com"], - imgSrc: ["'self'", "data:", "blob:", "https:", "http:"], - connectSrc: [ - "'self'", - "https://*.googleapis.com", - "https://*.firebaseio.com", - "wss://*.firebaseio.com", - "https://*.cloudfunctions.net", - "https://identitytoolkit.googleapis.com", - "https://securetoken.googleapis.com", - "https://firestore.googleapis.com", - "https://www.catalystlab.tech", - "https://*.run.app", - "ws:", - "wss:" - ], - frameSrc: ["'self'", "https://*.firebaseapp.com", "https://*.google.com"], - frameAncestors: ["'self'", "https://*.google.com", "https://*.googleusercontent.com", "https://*.run.app", "https://ai.studio", "*"], - objectSrc: ["'none'"], - baseUri: ["'self'"], - formAction: ["'self'"], - upgradeInsecureRequests: [], - }, - }, - crossOriginEmbedderPolicy: false, - crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' }, - crossOriginResourcePolicy: { policy: 'cross-origin' }, - hsts: { - maxAge: 63072000, - includeSubDomains: true, - preload: true - }, - referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, - frameguard: false // Permitted for iframe previews in AI Studio - }) - ); - - // Additional defense-in-depth header controls - app.use((req: Request, res: Response, next) => { - res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); - next(); - }); - - app.use(express.json({ limit: '10mb' })); - - // ========================================== - // PHASE 3: FIRST-PARTY PROXY STRATEGY (Ad-Blocker Proof) - // ========================================== - - // Step 1: Serve the Tracking Script as a First-Party Asset (<1KB Vanilla JS) - const serveTelemetryScript = (req: Request, res: Response) => { - const scriptPath = path.join(process.cwd(), 'public', 'telemetry.js'); - res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); - res.setHeader('Cache-Control', 'public, max-age=86400, stale-while-revalidate=604800'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.sendFile(scriptPath); - }; - - app.get('/telemetry.js', serveTelemetryScript); - app.get('/js/telemetry.js', serveTelemetryScript); - app.get('/stats/js', serveTelemetryScript); - app.get('/stats/script.js', serveTelemetryScript); - app.get('/api/telemetry.js', serveTelemetryScript); - - // Step 2: First-Party API Ingestion (Catch encrypted/beaconed payload, filter bots, batch in-memory) - const handleTelemetryEvent = (req: Request, res: Response): void => { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); - - // 1. Comprehensive Bot & Crawler Filtering Check (Block Datacenter/AI Crawlers before DB) - const userAgent = (req.headers['user-agent'] as string) || ''; - const purposeHeader = (req.headers['purpose'] || req.headers['sec-purpose'] || req.headers['x-purpose'] || '') as string; - const isPrefetch = purposeHeader.toLowerCase().includes('preview') || req.headers['x-moz'] === 'prefetch'; - - const botRegex = /bot|crawler|spider|crawling|chatgpt|claude|perplexity|headless|lighthouse|ahrefs|semrush|petalbot|curl|wget|python|go-http|phantom|selenium|puppeteer|googlebot|bingbot|yandex|baidu|slurp|duckduckbot|facebookexternalhit|whatsapp|telegrambot|twitterbot|slackbot|discordbot/i; - - if (!userAgent || isPrefetch || botRegex.test(userAgent)) { - res.status(200).json({ status: 'ignored', reason: 'bot_or_prefetch_traffic' }); // Silently drop bot traffic without processing load - return; - } - - // Support direct JSON object, array of events, and stringified beacon payloads - let rawBody = req.body; - if (typeof rawBody === 'string') { - try { - rawBody = JSON.parse(rawBody); - } catch { - rawBody = {}; - } - } - - // Normalize events array (supports single event, array of events, or { events: [...] }) - let eventsList: any[] = []; - if (Array.isArray(rawBody)) { - eventsList = rawBody; - } else if (rawBody && Array.isArray(rawBody.events)) { - eventsList = rawBody.events; - } else if (rawBody && typeof rawBody === 'object' && Object.keys(rawBody).length > 0) { - eventsList = [rawBody]; - } - - if (eventsList.length === 0) { - res.status(200).json({ status: 'ignored', reason: 'empty_payload' }); - return; - } - - // 2. Local Zero-Cost Geo-IP Resolution (Behind Cloudflare/Vercel proxy headers) - const rawIp = ( - (req.headers['cf-connecting-ip'] as string) || - (req.headers['x-forwarded-for'] as string) || - (req.headers['x-real-ip'] as string) || - req.socket.remoteAddress || - '' - ).split(',')[0].trim(); - - const geo = geoip.lookup(rawIp); - const country = geo ? geo.country : 'Unknown'; - const city = geo ? geo.city : 'Unknown'; - - // 3. User-Agent Parsing (Browser, OS, Device) - const parser = new UAParser(userAgent); - const browser = parser.getBrowser().name || 'Unknown'; - const os = parser.getOS().name || 'Unknown'; - const device = parser.getDevice().type || 'desktop'; - - let processedCount = 0; - - for (const item of eventsList) { - if (!item || typeof item !== 'object') continue; - - const domain = item.domain || (item.url ? (() => { try { return new URL(item.url).hostname; } catch { return 'unknown'; } })() : 'unknown'); - const cleanDomain = domain.replace(/^www\./, ''); - - // 4. Cookieless Privacy Hashing (Daily Salt Rotation - 100% GDPR/ePrivacy Compliant) - const visitor_id = item.visitor_id || generateVisitorId(rawIp, userAgent, cleanDomain); - const currentHour = new Date().toISOString().substring(0, 13); - const session_id = item.session_id || generateVisitorId(rawIp, userAgent + currentHour, cleanDomain); - - let source = 'Direct'; - if (item.referrer) { - try { - source = new URL(item.referrer).hostname; - } catch (e) { - source = String(item.referrer); - } - } - - // 5. In-Memory Batching Queue (Flushes every 3 seconds or 500 events to MongoDB) - queueEvent({ - domain: cleanDomain, - name: item.name || 'pageview', - url: item.url || `https://${cleanDomain}${item.pathname || '/'}`, - pathname: item.pathname || '/', - referrer: item.referrer || null, - browser, - os, - device, - country, - city, - source, - visitor_id, - session_id, - props: item.props || undefined, - vitals: item.vitals || undefined, - timestamp: item.timestamp || undefined - }); - - processedCount++; - } - - // 6. Asynchronous Edge Response - res.status(202).json({ success: true, processed: processedCount }); - }; - - // Support JSON, text, and array bodies for sendBeacon and fetch - const telemetryBodyParsers = express.json({ type: ['application/json', 'text/plain', 'text/json'], limit: '10mb' }); - - // Handle CORS OPTIONS preflight - const handleTelemetryOptions = (req: Request, res: Response) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); - res.status(204).end(); - }; - - app.options('/api/telemetry/event', handleTelemetryOptions); - app.options('/api/event', handleTelemetryOptions); - app.options('/stats/event', handleTelemetryOptions); - app.options('/api/stats/event', handleTelemetryOptions); - - app.post('/api/telemetry/event', telemetryBodyParsers, handleTelemetryEvent); - app.post('/api/event', telemetryBodyParsers, handleTelemetryEvent); - app.post('/stats/event', telemetryBodyParsers, handleTelemetryEvent); - app.post('/api/stats/event', telemetryBodyParsers, handleTelemetryEvent); - - // ========================================== - // PHASE 5: ZERO-COST ANALYTICAL QUERY PIPELINES - // ========================================== - - // Query zero-cost MongoDB time-series aggregations (Cookieless Visitors, Bounce Rate, Session Time) - app.get('/api/analytics/stats', async (req: Request, res: Response): Promise => { - try { - const domain = (req.query.domain as string) || 'all'; - const timeframe = ((req.query.timeframe as string) || '7d') as '24h' | '7d' | '30d' | 'all'; - - const stats = await getAnalyticsStats({ domain, timeframe }); - res.json({ - success: true, - stats - }); - } catch (err: any) { - console.error('Error in /api/analytics/stats:', err); - res.status(500).json({ success: false, error: err.message || 'Failed to query analytics telemetry.' }); - } + initAnalyticsDB().catch((err: unknown) => { + logger.warn({ err }, '[Analytics DB] Startup init skipped/deferred'); }); - // Live Real-Time Active Visitors Pulse - app.get('/api/analytics/realtime', async (req: Request, res: Response): Promise => { - try { - const domain = (req.query.domain as string) || 'all'; - const stats = await getAnalyticsStats({ domain, timeframe: '24h' }); - res.json({ - success: true, - domain, - activeVisitorsNow: stats.activeVisitorsNow, - todayUniqueVisitors: stats.uniqueVisitors, - todayPageviews: stats.totalPageviews, - timestamp: Date.now() - }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Execute Anomaly Detection Check across domains - app.post('/api/analytics/anomalies/check', async (req: Request, res: Response): Promise => { - try { - const { domain = 'all', notify = false, alertEmail, slackWebhookUrl, discordWebhookUrl } = req.body; - const result = await detectTrafficAnomalies(domain); - - let notificationsDispatched = { email: false, slack: false, discord: false }; - - if (notify && result.hasAnomaly && result.type && result.type !== 'healthy') { - const anomalyData: AnomalyAlertData = { - domain: domain === 'all' ? 'all-monitored-domains' : domain, - anomalyType: result.type, - metricName: 'Hourly Ingestion Volume', - currentValue: `${result.currentHourCount} reqs`, - baselineValue: `${result.baselineHourlyAvg} reqs`, - deviationPercentage: result.deviationPercent, - timestamp: result.timestamp, - recommendedAction: result.recommendedAction, - radarUrl: `https://www.catalystlab.tech/dashboard?tab=analytics` - }; - - if (alertEmail) { - const emailHtml = generateAnomalyAlertHtml(anomalyData); - const emailRes = await sendEmailViaMailgun({ - to: alertEmail, - subject: `[CatalystLab Alert] ${result.type === 'traffic_spike' ? 'Traffic Surge' : 'Traffic Drop'} on ${domain}`, - html: emailHtml - }); - notificationsDispatched.email = emailRes.success; - } - - if (slackWebhookUrl) { - const slackRes = await sendSlackWebhook(slackWebhookUrl, { - event: result.type === 'traffic_spike' ? 'anomaly_spike' : 'anomaly_drop', - domain, - title: result.type === 'traffic_spike' ? 'Traffic Surge Detected' : 'Traffic Drop Detected', - summary: `Observed ${result.currentHourCount} reqs/hr vs baseline ${result.baselineHourlyAvg} reqs/hr (${result.deviationPercent > 0 ? '+' : ''}${result.deviationPercent.toFixed(1)}%).`, - severity: result.type === 'traffic_spike' ? 'warning' : 'critical', - metrics: [ - { label: 'Current Volume', value: `${result.currentHourCount} reqs/hr` }, - { label: 'Baseline', value: `${result.baselineHourlyAvg} reqs/hr` }, - { label: 'Deviation', value: `${result.deviationPercent.toFixed(1)}%` } - ] - }); - notificationsDispatched.slack = slackRes.success; - } - - if (discordWebhookUrl) { - const discordRes = await sendDiscordWebhook(discordWebhookUrl, { - event: result.type === 'traffic_spike' ? 'anomaly_spike' : 'anomaly_drop', - domain, - title: result.type === 'traffic_spike' ? 'Traffic Surge Detected' : 'Traffic Drop Detected', - summary: `Observed ${result.currentHourCount} reqs/hr vs baseline ${result.baselineHourlyAvg} reqs/hr (${result.deviationPercent > 0 ? '+' : ''}${result.deviationPercent.toFixed(1)}%).`, - severity: result.type === 'traffic_spike' ? 'warning' : 'critical', - metrics: [ - { label: 'Current Volume', value: result.currentHourCount }, - { label: 'Baseline', value: result.baselineHourlyAvg } - ] - }); - notificationsDispatched.discord = discordRes.success; - } - } - - res.json({ - success: true, - domain, - anomaly: result, - notificationsDispatched - }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // ========================================== - // PHASE 4: NOTIFICATIONS, MAILGUN & WEBHOOKS - // ========================================== - - // Dispatch Weekly Email Dossier via Mailgun - app.post('/api/notifications/email/weekly-digest', async (req: Request, res: Response): Promise => { - try { - const { domain = 'catalystlab.tech', recipientEmail, configOverride } = req.body; - - if (!recipientEmail) { - res.status(400).json({ success: false, error: 'recipientEmail is required.' }); - return; - } - - const stats = await getAnalyticsStats({ domain, timeframe: '7d' }); - const now = new Date(); - const lastWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - - const weeklyData: AnalyticsWeeklyData = { - domain, - startDate: lastWeek.toISOString().split('T')[0], - endDate: now.toISOString().split('T')[0], - uniqueVisitors: stats.uniqueVisitors, - totalPageviews: stats.totalPageviews, - bounceRate: stats.bounceRate, - avgSessionDurationFormatted: stats.avgSessionDurationFormatted, - topPages: stats.topPages, - topSources: stats.sources.map(s => ({ source: s.name, count: s.count, percentage: s.value })), - topCountries: stats.countries, - healthScore: 94, - carbonEmissionsGrams: 0.18, - complianceGrade: 'Grade A+ (OWASP / WCAG Compliant)' - }; - - const html = generateWeeklyReportHtml(weeklyData); - const emailResult = await sendEmailViaMailgun({ - to: recipientEmail, - subject: `📊 CatalystLab Weekly Telemetry Dossier: ${domain}`, - html, - configOverride - }); - - res.json({ - success: emailResult.success, - messageId: emailResult.messageId, - mock: emailResult.mock, - error: emailResult.error, - sentTo: recipientEmail, - timestamp: Date.now() - }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Dispatch Instant Anomaly Alert Email via Mailgun - app.post('/api/notifications/email/anomaly-alert', async (req: Request, res: Response): Promise => { - try { - const { - domain = 'catalystlab.tech', - recipientEmail, - anomalyType = 'traffic_spike', - currentValue = '1,420 reqs/hr', - baselineValue = '480 reqs/hr', - deviationPercentage = 195.8, - recommendedAction = 'Inspect upstream CDN hit ratio, origin server CPU load, and backlink traffic.' - } = req.body; - - if (!recipientEmail) { - res.status(400).json({ success: false, error: 'recipientEmail is required.' }); - return; - } - - const alertData: AnomalyAlertData = { - domain, - anomalyType, - metricName: 'Traffic Ingestion Volume', - currentValue, - baselineValue, - deviationPercentage, - timestamp: new Date().toISOString(), - recommendedAction, - radarUrl: `https://www.catalystlab.tech/dashboard?tab=analytics&domain=${encodeURIComponent(domain)}` - }; - - const html = generateAnomalyAlertHtml(alertData); - const emailResult = await sendEmailViaMailgun({ - to: recipientEmail, - subject: `🚨 [Catalyst Alert] ${anomalyType.replace('_', ' ').toUpperCase()} on ${domain}`, - html - }); - - res.json({ - success: emailResult.success, - messageId: emailResult.messageId, - mock: emailResult.mock, - error: emailResult.error, - sentTo: recipientEmail - }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Test Email Verification Endpoint - app.post('/api/notifications/email/send-test', async (req: Request, res: Response): Promise => { - try { - const { recipientEmail, configOverride } = req.body; - if (!recipientEmail) { - res.status(400).json({ success: false, error: 'recipientEmail is required.' }); - return; - } - - const testHtml = ` -
-

CatalystLab Mailgun Test Dispatch

-

This is a test notification confirming that your Mailgun API pipeline is operational under the GitHub Student Developer Pack.

-
- Status: CONNECTED
- Timestamp: ${new Date().toISOString()}
- Quota: 20,000 Free Emails / Month -
-

CatalystLab Multi-Dimensional Telemetry Platform

-
- `; - - const result = await sendEmailViaMailgun({ - to: recipientEmail, - subject: '✅ CatalystLab Mailgun Connection Test', - html: testHtml, - configOverride - }); - - res.json({ - success: result.success, - messageId: result.messageId, - mock: result.mock, - error: result.error, - recipientEmail - }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Get HTML Preview of Weekly Digest or Anomaly Alert - app.get('/api/notifications/email/preview-html', async (req: Request, res: Response): Promise => { - try { - const type = (req.query.type as string) || 'weekly'; - const domain = (req.query.domain as string) || 'catalystlab.tech'; - - if (type === 'anomaly') { - const html = generateAnomalyAlertHtml({ - domain, - anomalyType: 'traffic_spike', - metricName: 'Traffic Ingestion Volume', - currentValue: '5,820 reqs/hr', - baselineValue: '1,450 reqs/hr', - deviationPercentage: 301.4, - timestamp: new Date().toISOString(), - recommendedAction: 'Verify CDN edge caching hit ratio, inspect origin CPU load, and check for viral backlink surge.', - radarUrl: 'https://www.catalystlab.tech/dashboard' - }); - res.setHeader('Content-Type', 'text/html'); - res.send(html); - return; - } - - const stats = await getAnalyticsStats({ domain, timeframe: '7d' }); - const now = new Date(); - const lastWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - - const html = generateWeeklyReportHtml({ - domain, - startDate: lastWeek.toISOString().split('T')[0], - endDate: now.toISOString().split('T')[0], - uniqueVisitors: stats.uniqueVisitors, - totalPageviews: stats.totalPageviews, - bounceRate: stats.bounceRate, - avgSessionDurationFormatted: stats.avgSessionDurationFormatted, - topPages: stats.topPages, - topSources: stats.sources.map(s => ({ source: s.name, count: s.count, percentage: s.value })), - topCountries: stats.countries, - healthScore: 94, - carbonEmissionsGrams: 0.18, - complianceGrade: 'Grade A+ (100% Pass)' - }); - - res.setHeader('Content-Type', 'text/html'); - res.send(html); - } catch (err: any) { - res.status(500).send(`Error generating email preview: ${err.message}`); - } - }); - - // Generic Webhook Dispatcher - app.post('/api/notifications/webhook/dispatch', async (req: Request, res: Response): Promise => { - try { - const { slackWebhookUrl, discordWebhookUrl, genericWebhookUrl, webhookSecret, payload } = req.body; - const results: Record = {}; - - if (slackWebhookUrl) { - results.slack = await sendSlackWebhook(slackWebhookUrl, payload); - } - if (discordWebhookUrl) { - results.discord = await sendDiscordWebhook(discordWebhookUrl, payload); - } - if (genericWebhookUrl) { - results.generic = await sendGenericWebhook(genericWebhookUrl, payload, webhookSecret); - } - - res.json({ - success: true, - results - }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Test Slack Webhook - app.post('/api/notifications/webhook/test-slack', async (req: Request, res: Response): Promise => { - try { - const { webhookUrl, domain = 'catalystlab.tech' } = req.body; - if (!webhookUrl) { - res.status(400).json({ success: false, error: 'webhookUrl is required.' }); - return; - } - - const result = await sendSlackWebhook(webhookUrl, { - event: 'health_audit_complete', - domain, - title: 'Slack Webhook Verification Test', - summary: 'CatalystLab Slack Webhook pipeline successfully tested and verified.', - severity: 'success', - metrics: [ - { label: 'Integration', value: 'Slack Block Kit' }, - { label: 'Status', value: 'Active / Connected' }, - { label: 'Latency', value: '< 50ms' } - ], - actionUrl: 'https://www.catalystlab.tech/dashboard', - timestamp: Date.now() - }); - - res.json(result); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Test Discord Webhook - app.post('/api/notifications/webhook/test-discord', async (req: Request, res: Response): Promise => { - try { - const { webhookUrl, domain = 'catalystlab.tech' } = req.body; - if (!webhookUrl) { - res.status(400).json({ success: false, error: 'webhookUrl is required.' }); - return; - } - - const result = await sendDiscordWebhook(webhookUrl, { - event: 'health_audit_complete', - domain, - title: 'Discord Webhook Verification Test', - summary: 'CatalystLab Discord Embed Webhook pipeline successfully tested and verified.', - severity: 'success', - metrics: [ - { label: 'Integration', value: 'Discord Rich Embed' }, - { label: 'Status', value: 'Active / Connected' }, - { label: 'Zero-Cost Compute', value: 'Native Fetch' } - ], - actionUrl: 'https://www.catalystlab.tech/dashboard', - timestamp: Date.now() - }); - - res.json(result); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // ========================================== - // PHASE 6: MONGODB DOCUMENT STATE SYNC ENGINE - // ========================================== - - // Full State Reconciliation Query (Initial Load & Sync) - app.get('/api/state/sync', async (req: Request, res: Response): Promise => { - try { - const ownerId = (req.query.ownerId as string) || 'usr_default'; - const db = getDbInstance() || await initAnalyticsDB(); - - if (!db) { - // Fallback default structure - res.json({ - success: true, - mode: 'in_memory_fallback', - state: { - domains: [], - goals: [], - alerts: [], - userPreferences: null, - auditRecords: [] - } - }); - return; - } - - const [domains, goals, alerts, preferences, auditRecords] = await Promise.all([ - db.collection('domains').find({ ownerId }).sort({ createdAt: -1 }).toArray().catch(() => []), - db.collection('goals').find({ ownerId }).sort({ createdAt: -1 }).toArray().catch(() => []), - db.collection('alerts').find({ ownerId }).sort({ createdAt: -1 }).toArray().catch(() => []), - db.collection('user_preferences').findOne({ ownerId }).catch(() => null), - db.collection('audit_results').find({ ownerId }).sort({ createdAt: -1 }).limit(25).toArray().catch(() => []) - ]); - - res.json({ - success: true, - mode: 'mongodb_atlas', - timestamp: Date.now(), - state: { - domains, - goals, - alerts, - userPreferences: preferences, - auditRecords - } - }); - } catch (err: any) { - console.error('[State Sync GET] Error querying MongoDB state:', err); - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Atomic Optimistic State Mutation Persistence (Insert, Update, Delete, Upsert) - app.post('/api/state/sync', async (req: Request, res: Response): Promise => { - try { - const { collection, actionType, documentId, payload, timestamp } = req.body; - const clientMutationId = (req.headers['x-client-mutation-id'] as string) || `mut_${Date.now()}`; - - if (!collection || !actionType || !documentId) { - res.status(400).json({ success: false, error: 'collection, actionType, and documentId are required.' }); - return; - } - - const allowedCollections = ['domains', 'goals', 'alerts', 'user_preferences', 'audit_results', 'monitored_sites']; - if (!allowedCollections.includes(collection)) { - res.status(400).json({ success: false, error: `Invalid collection '${collection}'.` }); - return; - } - - const db = getDbInstance() || await initAnalyticsDB(); - if (!db) { - // Fallback response for in-memory mode - res.json({ - success: true, - mode: 'in_memory_simulated', - mutationId: clientMutationId, - actionType, - documentId, - document: payload - }); - return; - } - - const col = db.collection(collection); - let resultDocument = payload; - - if (actionType === 'insert') { - const docToInsert = { ...payload, id: documentId, createdAt: timestamp || Date.now() }; - delete (docToInsert as any)._id; // prevent duplicate key if already present - await col.updateOne({ id: documentId }, { $set: docToInsert }, { upsert: true }); - resultDocument = docToInsert; - } else if (actionType === 'update' || actionType === 'upsert') { - const updatePayload = { ...payload, updatedAt: timestamp || Date.now() }; - delete (updatePayload as any)._id; - await col.updateOne({ id: documentId }, { $set: updatePayload }, { upsert: true }); - resultDocument = updatePayload; - } else if (actionType === 'delete') { - await col.deleteOne({ id: documentId }); - resultDocument = { id: documentId, deleted: true }; - } - - res.json({ - success: true, - mode: 'mongodb_atlas', - mutationId: clientMutationId, - collection, - actionType, - documentId, - document: resultDocument, - persistedAt: Date.now() - }); - } catch (err: any) { - console.error('[State Sync POST] Error executing mutation on MongoDB:', err); - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Delete document route - app.delete('/api/state/sync/:collection/:id', async (req: Request, res: Response): Promise => { - try { - const { collection, id } = req.params; - const db = getDbInstance() || await initAnalyticsDB(); - if (db) { - await db.collection(collection).deleteOne({ id }); - } - res.json({ success: true, collection, id, deleted: true }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // Rate Limit Status Query Endpoint - app.get('/api/rate-limit/status', (req: Request, res: Response): void => { - const { dateKey, resetAt, resetInSeconds, formattedResetTime } = getUtcMidnight(); - const identity = resolveClientIdentity(req); - - if (identity.tier === 'superadmin') { - res.json({ - success: true, - tier: 'superadmin', - tierLabel: 'Primary Superadmin', - dailyLimit: null, - unitsUsed: 0, - unitsRemaining: Infinity, - masterAuditsRemaining: Infinity, - singleEnginesRemaining: Infinity, - masterAuditCost: MASTER_AUDIT_COST, - singleEngineCost: SINGLE_ENGINE_COST, - burstLimit: Infinity, - burstRemaining: Infinity, - resetAt: resetAt.toISOString(), - resetInSeconds, - formattedResetTime, - isUnlimited: true, - isExceeded: false - }); - return; - } - - const storeKey = `${dateKey}_${identity.identifier}`; - const record = dailyRateLimitStore.get(storeKey); - const unitsUsed = record ? record.unitsUsed : 0; - const limit = identity.limit || FREE_USER_DAILY_UNITS; - const unitsRemaining = Math.max(0, limit - unitsUsed); - - const now = Date.now(); - const recentRequests = record ? record.requestTimestamps.filter(t => now - t < BURST_WINDOW_MS).length : 0; - const burstRemaining = Math.max(0, identity.burstMax - recentRequests); - - res.json({ - success: true, - tier: identity.tier, - tierLabel: identity.tierLabel, - subscriptionPlan: identity.subscriptionPlan, - isTrialActive: identity.isTrialActive, - dailyLimit: limit, - unitsUsed, - unitsRemaining, - masterAuditsRemaining: Math.floor(unitsRemaining / MASTER_AUDIT_COST), - singleEnginesRemaining: Math.floor(unitsRemaining / SINGLE_ENGINE_COST), - masterAuditCost: MASTER_AUDIT_COST, - singleEngineCost: SINGLE_ENGINE_COST, - burstLimit: identity.burstMax, - burstRemaining, - resetAt: resetAt.toISOString(), - resetInSeconds, - formattedResetTime, - isUnlimited: false, - isExceeded: unitsRemaining <= 0 - }); - }); - - // 5-Tier Subscription Plans & Free Trial API - app.get('/api/plans', (_req: Request, res: Response): void => { - res.json({ - success: true, - trialDurationDays: 7, - trialRequiresCreditCard: false, - plans: [ - { - id: 'free', - name: 'Free Community', - tagline: 'Essential developer diagnostics & baseline audits', - monthlyPrice: 0, - annualPrice: 0, - dailyUnits: 50, - monthlyAudits: 150, - burstLimitPerMin: 45, - maxConcurrentEngines: 8, - trialAvailable: false, - features: [ - '50 daily compute units (5 full audits/day)', - 'All 8 zero-overhead diagnostic engines', - 'Full JSON / Markdown / CSV export', - 'Standard edge latency & carbon metrics', - 'Community documentation & GitHub issues' - ] - }, - { - id: 'starter', - name: 'Starter Pro', - tagline: 'Ideal for indie hackers, freelancing engineers & single projects', - monthlyPrice: 9, - annualPrice: 7, - dailyUnits: 150, - monthlyAudits: 450, - burstLimitPerMin: 60, - maxConcurrentEngines: 8, - trialAvailable: true, - badge: 'Most Popular for Solo Devs', - features: [ - '150 daily compute units (15 full audits/day)', - '7-day free trial (no credit card required)', - '3 Monitored domain slots with hourly tracking', - 'Automated Weekly Email Dossier digests', - 'Slack & Discord anomaly webhook alerts', - 'Custom PDF executive audit summaries' - ] - }, - { - id: 'pro', - name: 'Professional Team', - tagline: 'High-speed observability for growing engineering teams', - monthlyPrice: 19, - annualPrice: 15, - dailyUnits: 500, - monthlyAudits: 1500, - burstLimitPerMin: 120, - maxConcurrentEngines: 8, - trialAvailable: true, - badge: 'Recommended', - features: [ - '500 daily compute units (50 full audits/day)', - '7-day free trial (no credit card required)', - '10 Monitored domain slots with continuous health scans', - 'CI/CD GitHub Actions & GitLab Webhook integrations', - 'Full REST API access (cat_live_ developer tokens)', - 'Multi-region edge latency probes across 6 continents', - 'Custom threshold anomaly triggers & priority routing' - ] - }, - { - id: 'team', - name: 'Scale & Growth', - tagline: 'Continuous DevSecOps pipelines & team-wide multi-cloud monitoring', - monthlyPrice: 49, - annualPrice: 39, - dailyUnits: 1500, - monthlyAudits: 4500, - burstLimitPerMin: 300, - maxConcurrentEngines: 8, - trialAvailable: true, - badge: 'High Performance', - features: [ - '1,500 daily compute units (150 full audits/day)', - '7-day free trial (no credit card required)', - '30 Monitored domain slots with 15-minute intervals', - 'Advanced LLMO prompt token benchmarking & cache tracing', - 'Custom compliance rule suites (SOC2 / GDPR / HIPAA)', - 'Unlimited team seat invitations & shared audit vaults', - 'Priority queue execution with zero latency buffering' - ] - }, - { - id: 'enterprise', - name: 'Enterprise Apex', - tagline: 'Mission-critical enterprise governance, unlimited scale & dedicated support', - monthlyPrice: 99, - annualPrice: 79, - dailyUnits: 5000, - monthlyAudits: 15000, - burstLimitPerMin: 500, - maxConcurrentEngines: 8, - trialAvailable: true, - badge: 'Enterprise SLA', - features: [ - '5,000 daily compute units (500 full audits/day)', - '7-day free trial (no credit card required)', - 'Unlimited monitored domains & subdomains', - 'Custom on-premise runner support & VPC peering', - '99.99% uptime SLA & dedicated solution engineer', - 'Role-based access control (RBAC) & SAML/SSO integration', - 'Quarterly architectural review & bespoke engine rules' - ] - } - ] - }); - }); - - // Pre-flight check endpoint for URL connectivity - app.post('/api/check-url', async (req: Request, res: Response): Promise => { - try { - const { url } = req.body; - if (!url || typeof url !== 'string') { - if (!res.headersSent) { - res.status(400).json({ reachable: false, error: 'Target URL is required.' }); - } - return; - } - - // SSRF & loopback address security check - const validation = await validatePublicUrl(url); - if (!validation.valid) { - res.status(400).json({ reachable: false, error: validation.error || 'Invalid or forbidden target URL.' }); - return; - } - - const parsedUrl = new URL(validation.normalizedUrl || url); - const isHttps = parsedUrl.protocol === 'https:'; - const client = isHttps ? https : http; - - const reqOptions = { - method: 'HEAD', - timeout: 4000, - rejectUnauthorized: false - }; - - const request = client.request(parsedUrl, reqOptions, (response) => { - if (res.headersSent) return; - res.json({ reachable: true, status: response.statusCode }); - }); - - request.on('error', (err) => { - if (res.headersSent) return; - res.json({ reachable: false, error: err.message }); - }); - - request.on('timeout', () => { - request.destroy(); - if (res.headersSent) return; - res.json({ reachable: false, error: 'Timeout' }); - }); - - request.end(); - } catch (e: any) { - if (!res.headersSent) { - res.json({ reachable: false, error: e.message }); - } - } - }); - - // Python Engine Execution Endpoint with Rate-Limiting Middleware - app.post('/api/run-engine', createEngineRateLimitMiddleware({ cost: SINGLE_ENGINE_COST }), async (req: Request, res: Response): Promise => { - try { - const { url, engine } = req.body; - - if (!url || typeof url !== 'string') { - res.status(400).json({ success: false, error: 'Target URL is required.' }); - return; - } - - if (!engine || !ENGINE_SCRIPT_MAP[engine]) { - res.status(400).json({ - success: false, - error: `Invalid engine '${engine}'. Valid engines: ${Object.keys(ENGINE_SCRIPT_MAP).join(', ')}` - }); - return; - } - - const isRepoEngine = engine === 'repo' || engine === 'code_quality'; - const validation = await validatePublicUrl(url, isRepoEngine); - if (!validation.valid) { - res.status(400).json({ success: false, error: validation.error || 'Invalid or forbidden target URL.' }); - return; - } - - const targetUrl = validation.normalizedUrl || url; - const rateStatus = (req as any).rateLimitStatus; - totalAuditsExecuted++; - - // Execute Native TypeScript engine directly - const output = await runNativeEngine(targetUrl, engine); - - res.json({ - success: true, - engine, - url: targetUrl, - rateLimit: { - tier: rateStatus?.tier || 'user', - tierLabel: rateStatus?.tierLabel || 'Registered User', - remaining: rateStatus?.unitsRemaining, - limit: rateStatus?.limit, - used: rateStatus?.unitsUsed, - resetAt: rateStatus?.resetAt, - formattedResetTime: rateStatus?.formattedResetTime - }, - output: output || 'Engine completed with no output.' - }); - } catch (err: any) { - console.error(`Error executing engine:`, err); - res.status(500).json({ - success: false, - error: err.stderr || err.message || 'Execution error during telemetry scan.' - }); - } - }); + const app = await createApp(); - // Site Probe for Monitoring & Uptime - app.post('/api/monitor/probe', async (req: Request, res: Response): Promise => { - try { - let { url } = req.body; - if (!url || typeof url !== 'string') { - res.status(400).json({ success: false, error: 'URL is required' }); - return; - } - - let parsedUrl: URL; - try { - let clean = url.trim(); - if (!clean.startsWith('http://') && !clean.startsWith('https://')) { - clean = 'https://' + clean; - } - parsedUrl = new URL(clean); - } catch (e: any) { - res.status(400).json({ success: false, error: 'Invalid URL format' }); - return; - } - - const isHttps = parsedUrl.protocol === 'https:'; - const requestLib = isHttps ? https : http; - const startTime = performance.now(); - - let sslInfo: { valid: boolean; daysRemaining?: number; issuer?: string } = { valid: false }; - if (isHttps) { - sslInfo = await getSslDetails(parsedUrl.hostname, parsedUrl.port ? parseInt(parsedUrl.port) : 443); - } - - const reqPromise = new Promise<{ - statusCode: number; - responseTimeMs: number; - status: 'healthy' | 'degraded' | 'down'; - contentType?: string; - contentLength?: number; - headers: Record; - }>((resolve, reject) => { - const clientReq = requestLib.request( - parsedUrl.toString(), - { - method: 'GET', - headers: { - 'User-Agent': 'CatalystLab-Telemetry-Monitor/2.0 (Uptime-Health-Probe)', - 'Accept': '*/*' - }, - timeout: 10000 - }, - (clientRes) => { - const responseTimeMs = Math.round(performance.now() - startTime); - const statusCode = clientRes.statusCode || 0; - const headers: Record = {}; - for (const [k, v] of Object.entries(clientRes.headers)) { - if (v) headers[k] = Array.isArray(v) ? v.join(', ') : String(v); - } - - let healthStatus: 'healthy' | 'degraded' | 'down' = 'healthy'; - if (statusCode >= 500 || statusCode === 0) { - healthStatus = 'down'; - } else if (statusCode >= 400 || responseTimeMs > 1200) { - healthStatus = 'degraded'; - } - - clientRes.resume(); // consume stream - resolve({ - statusCode, - responseTimeMs, - status: healthStatus, - contentType: headers['content-type'], - contentLength: headers['content-length'] ? parseInt(headers['content-length']) : undefined, - headers - }); - } - ); - - clientReq.on('timeout', () => { - clientReq.destroy(); - reject(new Error('Connection timed out (>10,000ms)')); - }); - - clientReq.on('error', (err) => { - reject(err); - }); - - clientReq.end(); - }); - - try { - const probeData = await reqPromise; - res.json({ - success: true, - url: parsedUrl.toString(), - ...probeData, - sslValid: sslInfo.valid, - sslDaysRemaining: sslInfo.daysRemaining, - sslIssuer: sslInfo.issuer, - timestamp: Date.now() - }); - } catch (err: any) { - const responseTimeMs = Math.round(performance.now() - startTime); - res.json({ - success: false, - url: parsedUrl.toString(), - statusCode: 0, - responseTimeMs, - status: 'down', - error: err.message || 'Connection failed', - timestamp: Date.now() - }); - } - } catch (outerErr: any) { - res.status(500).json({ success: false, error: outerErr.message }); - } - }); - - // System Health & Engine Infrastructure Telemetry - app.get('/api/monitor/system-health', (req: Request, res: Response) => { - const memory = process.memoryUsage(); - res.json({ - status: 'operational', - uptimeSeconds: Math.floor((Date.now() - serverStartTime) / 1000), - memoryUsageMb: { - rss: Math.round(memory.rss / (1024 * 1024)), - heapTotal: Math.round(memory.heapTotal / (1024 * 1024)), - heapUsed: Math.round(memory.heapUsed / (1024 * 1024)) - }, - activeEnginesCount: Object.keys(ENGINE_SCRIPT_MAP).length, - totalAuditsLogged: totalAuditsExecuted, - nodeVersion: process.version, - platform: `${os.type()} ${os.release()} (${os.arch()})`, - timestamp: Date.now() - }); - }); - - // Health check - app.get('/api/health', (req: Request, res: Response) => { - res.json({ status: 'ok', timestamp: Date.now() }); - }); - - // ========================================== - // RESTful API V1 CATALOG & ENDPOINTS - // ========================================== - - // 1. Diagnostic Engines Metadata & Spec - app.get('/api/v1/engines', (req: Request, res: Response) => { - const engines = [ - { id: 'health', name: 'Website Health & DOM Engine', category: 'Performance', script: 'website_health.py', route: '/health', weight: 0.20, description: 'Measures DOM depth, node count, script blocking, and payload size.' }, - { id: 'latency', name: 'Global Edge Latency Radar', category: 'Edge & Network', script: 'edge_latency.py', route: '/latency', weight: 0.20, description: 'Evaluates TTFB, TLS 1.3 resumption, and Anycast routing across 12 worldwide PoPs.' }, - { id: 'ai_ready', name: 'AI Readiness & llms.txt Inspector', category: 'AI & Crawlers', script: 'ai_readiness.py', route: '/ai-readiness', weight: 0.15, description: 'Inspects robots.txt crawler policies, /llms.txt manifests, and JSON-LD schemas.' }, - { id: 'repo', name: 'Git Repository Hygiene & SecOps', category: 'SecOps & Code', script: 'repo_scanner.py', route: '/repo-scanner', weight: 0.15, description: 'Audits open source licenses, SECURITY.md disclosures, Dependabot, and CI/CD.' }, - { id: 'eco', name: 'Eco-Carbon & Green Web Audit', category: 'ESG & Green', script: 'eco_carbon_audit.py', route: '/eco-audit', weight: 0.15, description: 'Calculates energy (kWh) and greenhouse gas emissions (g CO2) via SWD Model v4.' }, - { id: 'compliance', name: 'Compliance, Risk & OWASP SecOps', category: 'Security & Legal', script: 'compliance_risk_audit.py', route: '/compliance', weight: 0.15, description: 'Audits OWASP headers (HSTS, CSP, X-Frame), WCAG 2.2 AA accessibility, and cookies.' }, - { id: 'migration', name: 'Platform Migration & SEO Parity', category: 'Architecture', script: 'platform_migration_audit.py', route: '/migration', weight: 0.15, description: 'Audits CMS re-platforming risk index, 301 permanent redirect matrices, and OpenGraph.' }, - { id: 'llmo', name: 'AI Search Optimization (LLMO)', category: 'AI & Discovery', script: 'llmo_optimizer.py', route: '/llmo', weight: 0.15, description: 'Optimizes content structure for Perplexity, ChatGPT Search, and Gemini citations.' } - ]; - res.json({ success: true, total: engines.length, engines }); - }); - - app.get('/api/v1/engines/:engine', (req: Request, res: Response) => { - const { engine } = req.params; - const script = ENGINE_SCRIPT_MAP[engine]; - if (!script) { - res.status(404).json({ success: false, error: `Engine '${engine}' not found.` }); - return; - } - res.json({ - success: true, - engine, - scriptName: script, - parameters: { - url: { type: 'string', required: true, description: 'Target URL or Git repository URL' }, - auditSessionId: { type: 'string', required: false } - }, - rateLimit: '5 scans/day (Visitor), 10 scans/day (User)' - }); - }); - - // Dedicated Engine Scan endpoint - app.post('/api/v1/engines/:engine/scan', createEngineRateLimitMiddleware({ cost: SINGLE_ENGINE_COST }), async (req: Request, res: Response): Promise => { - const { engine } = req.params; - if (!ENGINE_SCRIPT_MAP[engine]) { - res.status(404).json({ success: false, error: `Engine '${engine}' not found.` }); - return; - } - - try { - const { url } = req.body; - if (!url) { - res.status(400).json({ success: false, error: 'URL parameter is required.' }); - return; - } - - const isRepoEngine = engine === 'repo' || engine === 'code_quality'; - const validation = await validatePublicUrl(url, isRepoEngine); - if (!validation.valid) { - res.status(400).json({ success: false, error: validation.error || 'Invalid or forbidden target URL.' }); - return; - } - - const targetUrl = validation.normalizedUrl || url; - const rateStatus = (req as any).rateLimitStatus; - totalAuditsExecuted++; - - const output = await runNativeEngine(targetUrl, engine); - - res.json({ - success: true, - engine, - url: targetUrl, - rateLimit: { - tier: rateStatus?.tier || 'user', - tierLabel: rateStatus?.tierLabel || 'Registered User', - remaining: rateStatus?.unitsRemaining, - limit: rateStatus?.limit, - used: rateStatus?.unitsUsed, - resetAt: rateStatus?.resetAt, - formattedResetTime: rateStatus?.formattedResetTime - }, - output, - timestamp: Date.now() - }); - } catch (e: any) { - res.status(500).json({ success: false, error: e.message }); - } - }); - - // 2. Master Audit Endpoint - app.post('/api/v1/audit/master', createEngineRateLimitMiddleware({ cost: MASTER_AUDIT_COST, isMaster: true }), async (req: Request, res: Response): Promise => { - try { - const { url } = req.body; - if (!url) { - res.status(400).json({ success: false, error: 'Target URL is required.' }); - return; - } - - const validation = await validatePublicUrl(url); - if (!validation.valid) { - res.status(400).json({ success: false, error: validation.error || 'Invalid or forbidden target URL.' }); - return; - } - - const targetUrl = validation.normalizedUrl || url; - const rateStatus = (req as any).rateLimitStatus; - const primaryEngines = ['health', 'compliance', 'ai_ready', 'latency', 'eco', 'migration', 'llmo', 'repo']; - const results: Record = {}; - - await Promise.allSettled( - primaryEngines.map(async (eng) => { - try { - const out = await runNativeEngine(targetUrl, eng); - results[eng] = { status: 'completed', preview: out ? out.slice(0, 300) : 'Completed', rawOutput: out }; - } catch (err: any) { - results[eng] = { status: 'error', error: err.message }; - } - }) - ); - - res.json({ - success: true, - url: targetUrl, - compositeScore: 92, - grade: 'A', - totalEnginesAudited: primaryEngines.length, - engines: results, - rateLimit: { - tier: rateStatus?.tier || 'user', - tierLabel: rateStatus?.tierLabel || 'Registered User', - remaining: rateStatus?.unitsRemaining, - limit: rateStatus?.limit, - used: rateStatus?.unitsUsed, - resetAt: rateStatus?.resetAt, - formattedResetTime: rateStatus?.formattedResetTime - }, - timestamp: Date.now() - }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - // 2b. Master Audit Real-Time SSE Streaming Orchestrator (/api/v1/audit/master/stream & /api/master-audit/stream) - const handleMasterAuditStream = async (req: Request, res: Response): Promise => { - const rawUrl = (req.query.url as string) || (req.body && req.body.url); - if (!rawUrl) { - res.status(400).json({ success: false, error: 'Target URL is required for stream.' }); - return; - } - - const validation = await validatePublicUrl(rawUrl); - if (!validation.valid) { - res.status(400).json({ success: false, error: validation.error || 'Invalid or forbidden target URL.' }); - return; - } - - const targetUrl = validation.normalizedUrl || rawUrl; - - // Set Server-Sent Events headers - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache, no-transform'); - res.setHeader('Connection', 'keep-alive'); - res.setHeader('X-Accel-Buffering', 'no'); - res.flushHeaders(); - - const sendSse = (eventType: string, data: any) => { - res.write(`event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`); - }; - - sendSse('session_init', { - url: targetUrl, - sessionId: `master_stream_${Date.now()}`, - enginesCount: 8, - timestamp: Date.now() - }); - - const engines = [ - { key: 'health', name: 'VitalZyme (DOM & Core Vitals)' }, - { key: 'compliance', name: 'RiskProtease (OWASP & SSL)' }, - { key: 'ai_ready', name: 'LlmKinase (AI Agents & RAG)' }, - { key: 'latency', name: 'EdgeVmax (CDN & TTFB)' }, - { key: 'eco', name: 'EcoHolo (Carbon & SWD)' }, - { key: 'migration', name: 'SynthShift (Architecture & AST)' }, - { key: 'llmo', name: 'AllosterSearch (Schema & Citations)' }, - { key: 'repo', name: 'GitLygase (Quality & Cyclomatic)' } - ]; - - const completedResults: Record = {}; - - for (let i = 0; i < engines.length; i++) { - const eng = engines[i]; - const progressPct = Math.round(((i) / engines.length) * 100); - - sendSse('engine_start', { - engine: eng.key, - name: eng.name, - index: i, - total: engines.length, - progress: progressPct, - timestamp: Date.now() - }); - - try { - const rawOutput = await runNativeEngine(targetUrl, eng.key); - completedResults[eng.key] = rawOutput; - - sendSse('engine_complete', { - engine: eng.key, - name: eng.name, - status: 'success', - output: rawOutput, - preview: rawOutput ? rawOutput.slice(0, 200) : '', - progress: Math.round(((i + 1) / engines.length) * 100), - timestamp: Date.now() - }); - } catch (err: any) { - sendSse('engine_complete', { - engine: eng.key, - name: eng.name, - status: 'error', - error: err.message || 'Engine execution failed', - progress: Math.round(((i + 1) / engines.length) * 100), - timestamp: Date.now() - }); - } - } - - sendSse('audit_complete', { - success: true, - url: targetUrl, - totalEngines: engines.length, - timestamp: Date.now() - }); - - res.end(); - }; - - app.get('/api/scan/stream', handleMasterAuditStream); - app.post('/api/scan/stream', handleMasterAuditStream); - app.get('/api/v1/audit/master/stream', handleMasterAuditStream); - app.post('/api/v1/audit/master/stream', handleMasterAuditStream); - app.get('/api/master-audit/stream', handleMasterAuditStream); - app.post('/api/master-audit/stream', handleMasterAuditStream); - - // 3. Side-by-side Audit Compare - app.post('/api/v1/audit/compare', async (req: Request, res: Response) => { - const { urlA, urlB } = req.body; - if (!urlA || !urlB) { - res.status(400).json({ success: false, error: 'urlA and urlB parameters are required.' }); - return; - } - res.json({ - success: true, - domainA: { url: urlA, score: 92, status: 'pass' }, - domainB: { url: urlB, score: 88, status: 'pass' }, - winner: urlA, - differential: { - scoreDelta: '+4 pts', - latencyDelta: '-32ms (Faster)' - }, - timestamp: Date.now() - }); - }); - - // 4. Reports & Dossiers - app.get('/api/v1/reports', (req: Request, res: Response) => { - const { search, limit } = req.query; - const sampleReports = [ - { id: 'rep_001', url: 'https://example.com', engine: 'all', score: 92, title: 'Master Multi-Engine Audit: example.com', slug: 'example-com', createdAt: Date.now() - 3600000 }, - { id: 'rep_002', url: 'https://react.dev', engine: 'health', score: 96, title: 'Website Health & DOM: react.dev', slug: 'react-dev', createdAt: Date.now() - 7200000 }, - { id: 'rep_003', url: 'https://github.com', engine: 'repo', score: 94, title: 'Repo Hygiene: github.com', slug: 'github-com', createdAt: Date.now() - 10800000 } - ]; - let filtered = sampleReports; - if (search && typeof search === 'string') { - filtered = filtered.filter(r => r.url.toLowerCase().includes(search.toLowerCase()) || r.slug.includes(search.toLowerCase())); - } - res.json({ - success: true, - count: filtered.length, - reports: filtered.slice(0, Number(limit) || 20) - }); - }); - - app.get('/api/v1/reports/permalink/:slug', (req: Request, res: Response) => { - const { slug } = req.params; - const cleanUrl = 'https://' + slug.replace(/-/g, '.'); - res.json({ - success: true, - id: `rep_${slug}`, - slug, - url: cleanUrl, - engine: 'all', - score: 92, - grade: 'A', - title: `Telemetry Audit Dossier: ${cleanUrl}`, - summary: `Automated 8-engine architecture and telemetry evaluation for ${cleanUrl}. Passed 48 quality assertions.`, - createdAt: Date.now() - }); - }); - - app.post('/api/v1/reports/:id/export', (req: Request, res: Response) => { - const { id } = req.params; - const { format = 'markdown' } = req.body; - res.json({ - success: true, - reportId: id, - format, - content: `# CatalystLab Telemetry Dossier (${id})\nGenerated: ${new Date().toISOString()}\n\n## Summary\n- Composite Quality Score: 92/100 (Grade A)\n- Status: Production Ready\n- Security: OWASP Compliant (HSTS, CSP)` - }); - }); - - // 5. Blogs API - app.get('/api/v1/blogs', (req: Request, res: Response) => { - const articles = [ - { slug: 'dom-recursion-depth-and-mobile-inp', title: 'DOM Recursion Depth: How Deep Nesting Destroys Mobile INP', category: 'Performance', author: 'CatalystLab Telemetry Team', readTime: '6 min read' }, - { slug: 'llms-txt-standard-and-autonomous-crawlers', title: 'The /llms.txt Standard: Preparing Web Architecture for AI Agents', category: 'AI Readiness', author: 'CatalystLab AI Research', readTime: '8 min read' }, - { slug: 'swd-v4-carbon-model-calculations', title: 'Sustainable Web Design (SWD) Model v4: Calculating Digital Carbon', category: 'ESG & Green', author: 'CatalystLab Green Team', readTime: '5 min read' } - ]; - res.json({ success: true, count: articles.length, articles }); - }); - - app.get('/api/v1/blogs/:slug', (req: Request, res: Response) => { - const { slug } = req.params; - res.json({ - success: true, - slug, - title: 'Technical Research Dossier', - content: `# Architectural Deep Dive\nAnalyzing telemetry metrics for modern web performance...`, - author: 'CatalystLab Engineering', - publishedAt: Date.now() - }); - }); - - // 6. Users & API Keys - app.get('/api/v1/users/me', (req: Request, res: Response) => { - const { resetAt, resetInSeconds, formattedResetTime } = getUtcMidnight(); - const identity = resolveClientIdentity(req); - const storeKey = `${getUtcMidnight().dateKey}_${identity.identifier}`; - const record = dailyRateLimitStore.get(storeKey); - const unitsUsed = record ? record.unitsUsed : 0; - const limit = identity.limit || FREE_USER_DAILY_UNITS; - const unitsRemaining = identity.tier === 'superadmin' ? Infinity : Math.max(0, limit - unitsUsed); - - res.json({ - success: true, - user: { - uid: identity.userId || 'usr_developer', - email: identity.cleanEmail || 'developer@example.com', - tier: identity.tier, - tierLabel: identity.tierLabel, - dailyQuotaUnits: limit, - unitsUsedToday: unitsUsed, - unitsRemainingToday: unitsRemaining, - masterAuditsRemaining: identity.tier === 'superadmin' ? Infinity : Math.floor(unitsRemaining / MASTER_AUDIT_COST), - singleEnginesRemaining: identity.tier === 'superadmin' ? Infinity : Math.floor(unitsRemaining / SINGLE_ENGINE_COST), - resetAt: resetAt.toISOString(), - resetInSeconds, - formattedResetTime - } - }); - }); - - app.get('/api/v1/users/me/quota', (req: Request, res: Response) => { - const { resetAt, resetInSeconds, formattedResetTime } = getUtcMidnight(); - const identity = resolveClientIdentity(req); - const storeKey = `${getUtcMidnight().dateKey}_${identity.identifier}`; - const record = dailyRateLimitStore.get(storeKey); - const unitsUsed = record ? record.unitsUsed : 0; - const limit = identity.limit || FREE_USER_DAILY_UNITS; - const unitsRemaining = identity.tier === 'superadmin' ? Infinity : Math.max(0, limit - unitsUsed); - - res.json({ - success: true, - tier: identity.tier, - tierLabel: identity.tierLabel, - limit, - used: unitsUsed, - remaining: unitsRemaining, - masterAuditsRemaining: identity.tier === 'superadmin' ? Infinity : Math.floor(unitsRemaining / MASTER_AUDIT_COST), - singleEnginesRemaining: identity.tier === 'superadmin' ? Infinity : Math.floor(unitsRemaining / SINGLE_ENGINE_COST), - resetAtUtc: resetAt.toISOString(), - resetInSeconds, - formattedResetTime - }); - }); - - app.get('/api/v1/users/me/api-keys', (req: Request, res: Response) => { - const identity = resolveClientIdentity(req); - res.json({ - success: true, - ownerId: identity.userId || 'usr_developer', - keys: [ - { - id: 'key_prod_pipeline_01', - name: 'Production CI/CD Quality Gate', - keyPrefix: 'cat_live_3f9a7b12...', - environment: 'production', - status: 'active', - scopes: ['execute:engines', 'execute:master-audit', 'read:reports'], - dailyComputeLimit: PRO_API_DAILY_UNITS, - whiteLabelConfig: { - organizationName: 'Catalyst Enterprise Systems', - brandHeaderName: 'X-Catalyst-Enterprise', - customWebhookUrl: 'https://api.example.com/webhooks/telemetry-gate' - }, - createdAt: Date.now() - 30 * 24 * 60 * 60 * 1000, - lastRotatedAt: Date.now() - 10 * 24 * 60 * 60 * 1000, - lastUsedAt: Date.now() - 15 * 60 * 1000, - expiresAt: Date.now() + 180 * 24 * 60 * 60 * 1000 - }, - { - id: 'key_staging_radar_02', - name: 'Staging Multi-PoP Radar Probe', - keyPrefix: 'cat_live_8c2d1e90...', - environment: 'staging', - status: 'active', - scopes: ['execute:engines', 'read:monitoring'], - dailyComputeLimit: PRO_API_DAILY_UNITS, - whiteLabelConfig: { - organizationName: 'Staging Quality Ops', - brandHeaderName: 'X-Staging-Quality' - }, - createdAt: Date.now() - 14 * 24 * 60 * 60 * 1000, - lastRotatedAt: null, - lastUsedAt: Date.now() - 2 * 60 * 60 * 1000, - expiresAt: null - } - ] - }); - }); - - app.post('/api/v1/users/me/api-keys', (req: Request, res: Response) => { - const { name = 'CI/CD Pipeline Key', scopes = ['execute:engines', 'read:reports'], environment = 'production', whiteLabelConfig = {} } = req.body; - const randomHex = Math.random().toString(16).substring(2, 14) + Math.random().toString(16).substring(2, 14) + Math.random().toString(16).substring(2, 10); - const keyId = `key_${Date.now()}`; - const secretKey = `cat_live_${randomHex}`; - - res.status(201).json({ - success: true, - keyId, - name, - environment, - scopes, - keyPrefix: secretKey.substring(0, 16) + '...', - secretKey, - dailyComputeLimit: PRO_API_DAILY_UNITS, - whiteLabelConfig, - createdAt: Date.now(), - warning: 'Store this secret key securely. For security, it cannot be displayed again.' - }); - }); - - app.post('/api/v1/users/me/api-keys/:id/rotate', (req: Request, res: Response) => { - const { id } = req.params; - const randomHex = Math.random().toString(16).substring(2, 14) + Math.random().toString(16).substring(2, 14) + Math.random().toString(16).substring(2, 10); - const newSecretKey = `cat_live_${randomHex}`; - - res.json({ - success: true, - keyId: id, - keyPrefix: newSecretKey.substring(0, 16) + '...', - secretKey: newSecretKey, - lastRotatedAt: Date.now(), - status: 'active', - warning: 'Previous key has been rotated. Update your environment variables immediately.' - }); - }); - - app.post('/api/v1/users/me/api-keys/:id/revoke', (req: Request, res: Response) => { - const { id } = req.params; - res.json({ - success: true, - keyId: id, - status: 'revoked', - revokedAt: Date.now(), - message: `API Key '${id}' has been permanently revoked.` - }); - }); - - app.delete('/api/v1/users/me/api-keys/:id', (req: Request, res: Response) => { - const { id } = req.params; - res.json({ - success: true, - keyId: id, - deleted: true, - message: `API Key '${id}' deleted successfully.` - }); - }); - - // 7. Workflows & Automation - app.get('/api/v1/workflows', (req: Request, res: Response) => { - res.json({ - success: true, - workflows: [ - { - id: 'wf_nightly_01', - name: 'Nightly Production Health & TTFB Probe', - targetUrl: 'https://example.com', - schedule: '0 0 * * * (Daily UTC)', - engines: ['health', 'latency', 'compliance'], - alertThreshold: { minScore: 85, maxTtfbMs: 300 }, - active: true, - lastRunStatus: 'passed' - } - ] - }); - }); - - app.post('/api/v1/automation/ci-cd/evaluate', (req: Request, res: Response) => { - const { url, thresholds = {} } = req.body; - if (!url) { - res.status(400).json({ success: false, error: 'URL parameter is required.' }); - return; - } - const minScore = thresholds.minCompositeScore || 85; - const simulatedScore = 92; - const passed = simulatedScore >= minScore; - - res.status(passed ? 200 : 422).json({ - passed, - url, - score: simulatedScore, - assertions: [ - { rule: `minCompositeScore >= ${minScore}`, expected: minScore, actual: simulatedScore, status: passed ? 'pass' : 'fail' }, - { rule: 'maxDomDepth <= 32', expected: 32, actual: 14, status: 'pass' }, - { rule: 'maxTtfbMs <= 350', expected: 350, actual: 142, status: 'pass' }, - { rule: 'requireHsts === true', expected: true, actual: true, status: 'pass' } - ], - summary: passed ? 'All quality assertions passed. CI/CD deployment approved.' : 'Quality gate violated.' - }); - }); - - // 8. Integrations & Webhooks - app.get('/api/v1/integrations', (req: Request, res: Response) => { - res.json({ - success: true, - integrations: [ - { id: 'github-actions', name: 'GitHub Actions Quality Gate', category: 'CI/CD', status: 'available' }, - { id: 'gitlab-ci', name: 'GitLab CI CLI Probe', category: 'CI/CD', status: 'available' }, - { id: 'slack', name: 'Slack Telemetry Webhook', category: 'Alerts', status: 'available' }, - { id: 'discord', name: 'Discord Telemetry Webhook', category: 'Alerts', status: 'available' }, - { id: 'datadog', name: 'Datadog APM & Metrics Exporter', category: 'Observability', status: 'available' } - ] - }); - }); - - app.post('/api/v1/integrations/webhook/test', (req: Request, res: Response) => { - const { targetWebhookUrl } = req.body; - if (!targetWebhookUrl) { - res.status(400).json({ success: false, error: 'targetWebhookUrl is required.' }); - return; - } - res.json({ - success: true, - delivered: true, - statusCode: 200, - responseTimeMs: 68, - signatureHeaderSent: 'sha256=3a4b5c6d7e8f9012...', - payloadSent: { - event: 'audit.completed', - url: 'https://example.com', - score: 92, - timestamp: Date.now() - } - }); - }); - - // --- GITHUB REPOSITORY WEBHOOKS & REAL-TIME TELEMETRY ENGINE --- - const githubSseSubscribers = new Set(); - const serverConnectedRepos = new Map([ - [ - 'gh_repo_default_01', - { - id: 'gh_repo_default_01', - name: 'myself-aas/CatalystLab', - repoUrl: 'https://github.com/myself-aas/CatalystLab', - defaultBranch: 'main', - webhookSecret: 'cat_whsec_9f83a12bc4e5', - webhookUrl: '/api/v1/integrations/github/webhook?repoId=gh_repo_default_01', - ownerId: 'usr_default', - status: 'active', - eventsCount: 3, - lastEventAt: Date.now() - 12 * 60 * 1000, - lastScore: 96, - lastStatus: 'passed', - autoScanEngines: ['repo', 'compliance', 'ai_ready', 'eco'], - createdAt: Date.now() - 7 * 24 * 60 * 60 * 1000 - } - ] - ]); - - const serverTelemetryEvents: any[] = [ - { - id: 'gh_evt_init_01', - repoId: 'gh_repo_default_01', - ownerId: 'usr_default', - repoName: 'myself-aas/CatalystLab', - eventType: 'push', - branch: 'main', - commitHash: '8f92a1c', - commitMessage: 'feat(telemetry): integrate real-time AST quality verification gate', - commitUrl: 'https://github.com/myself-aas/CatalystLab/commit/8f92a1c', - author: 'asifahmedshuvo', - score: 96, - status: 'passed', - summary: '0 CVEs detected, 100% license compliance, AI Discovery readiness /llms.txt verified.', - durationMs: 342, - metrics: { - astCodeHygiene: 98, - securityVulnerabilities: 100, - aiReadinessScore: 94, - buildCarbonEco: 96, - coreWebVitalsGate: 92, - edgeLatencyIndex: 98, - testCoverage: 91.4, - cveIssuesDetected: 0, - linesAnalyzed: 14820, - filesScanned: 64 - }, - engineResults: [ - { engineKey: 'repo', engineName: 'GitLygase (AST Code Hygiene)', score: 98, status: 'passed', summary: '0 cyclomatic anomalies, clean dependency manifest.' }, - { engineKey: 'compliance', engineName: 'RiskProtease (Security & CVE)', score: 100, status: 'passed', summary: 'Zero high/crit vulnerabilities across 84 packages.' }, - { engineKey: 'ai_ready', engineName: 'AI Discovery & llms.txt', score: 94, status: 'passed', summary: 'Clean schema markup & agentic prompt manifest.' }, - { engineKey: 'eco', engineName: 'SWD Green Carbon Efficiency', score: 96, status: 'passed', summary: '0.04g CO2/run rating (A+ Sustainable).' } - ], - timestamp: Date.now() - 12 * 60 * 1000 - }, - { - id: 'gh_evt_init_02', - repoId: 'gh_repo_default_01', - ownerId: 'usr_default', - repoName: 'myself-aas/CatalystLab', - eventType: 'pull_request', - branch: 'feature/edge-caching', - prNumber: 42, - prTitle: 'Optimized Anycast edge caching headers and Brotli compression', - prUrl: 'https://github.com/myself-aas/CatalystLab/pull/42', - prAction: 'synchronize', - author: 'dev-contributor', - score: 94, - status: 'passed', - summary: 'Edge TTFB reduced to 18ms. PR verified for automatic merge.', - durationMs: 418, - metrics: { - astCodeHygiene: 94, - securityVulnerabilities: 98, - aiReadinessScore: 92, - buildCarbonEco: 98, - coreWebVitalsGate: 96, - edgeLatencyIndex: 99, - testCoverage: 88.5, - cveIssuesDetected: 0, - linesAnalyzed: 3420, - filesScanned: 18 - }, - engineResults: [ - { engineKey: 'latency', engineName: 'EdgeVmax (Global TTFB)', score: 99, status: 'passed', summary: 'Mean TTFB 18.2ms across 42 Anycast PoPs.' }, - { engineKey: 'eco', engineName: 'SWD Green Carbon Efficiency', score: 98, status: 'passed', summary: 'Brotli compression reduced payload by 38%.' }, - { engineKey: 'compliance', engineName: 'RiskProtease (Security & CVE)', score: 98, status: 'passed', summary: 'Passed CSP nonce and HSTS headers check.' } - ], - timestamp: Date.now() - 45 * 60 * 1000 - } - ]; - - function broadcastGithubTelemetry(event: any) { - const payload = `data: ${JSON.stringify(event)}\n\n`; - githubSseSubscribers.forEach((res) => { - try { - res.write(payload); - } catch (err) { - githubSseSubscribers.delete(res); - } - }); - } - - // SSE Stream for Real-Time GitHub Telemetry Events - app.get('/api/v1/integrations/github/events/stream', (req: Request, res: Response) => { - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - res.flushHeaders?.(); - - // Initial connection handshake - res.write(`event: connected\ndata: ${JSON.stringify({ status: 'connected', timestamp: Date.now() })}\n\n`); - - githubSseSubscribers.add(res); - - // Heartbeat ping every 25s - const pingInterval = setInterval(() => { - try { - res.write(`event: ping\ndata: ${JSON.stringify({ ping: Date.now() })}\n\n`); - } catch { - clearInterval(pingInterval); - githubSseSubscribers.delete(res); - } - }, 25000); - - req.on('close', () => { - clearInterval(pingInterval); - githubSseSubscribers.delete(res); - }); - }); - - // Incoming GitHub Webhook Receiver Handler - const handleGithubWebhook = async (req: Request, res: Response) => { - const githubEvent = (req.headers['x-github-event'] as string) || (req.body?.action ? 'pull_request' : 'push'); - const signature = (req.headers['x-hub-signature-256'] as string) || (req.headers['x-hub-signature'] as string); - const repoIdParam = (req.query?.repoId as string) || req.body?.repoId || 'gh_repo_default_01'; - - const rawBody = req.body || {}; - const repo = serverConnectedRepos.get(repoIdParam) || { - id: repoIdParam, - name: rawBody.repository?.full_name || 'connected/repository', - defaultBranch: rawBody.repository?.default_branch || 'main', - ownerId: 'usr_default', - webhookSecret: '' - }; - - // 1. Handle GitHub Ping event - if (githubEvent === 'ping') { - const pingResponse = { - zen: rawBody.zen || 'Approachable is better than simple.', - hook_id: rawBody.hook_id || Math.floor(Math.random() * 1000000), - message: 'CatalystLab GitHub webhook verified and connected successfully!', - repository: repo.name, - timestamp: Date.now() - }; - res.status(200).json(pingResponse); - return; - } - - // 2. Process push or pull_request event - const startTime = Date.now(); - const isPr = githubEvent === 'pull_request' || Boolean(rawBody.pull_request); - const branch = isPr - ? (rawBody.pull_request?.head?.ref || 'feature-branch') - : (rawBody.ref?.replace('refs/heads/', '') || repo.defaultBranch || 'main'); - - const commitHash = rawBody.head_commit?.id?.substring(0, 7) || rawBody.after?.substring(0, 7) || Math.random().toString(16).substring(2, 9); - const commitMessage = rawBody.head_commit?.message || rawBody.commits?.[0]?.message || 'Auto-scan triggered via GitHub webhook'; - const commitUrl = rawBody.head_commit?.url || (rawBody.repository?.html_url ? `${rawBody.repository.html_url}/commit/${commitHash}` : undefined); - const author = rawBody.head_commit?.author?.name || rawBody.sender?.login || rawBody.pusher?.name || 'github-actor'; - const authorAvatar = rawBody.sender?.avatar_url || `https://github.com/${author}.png`; - - const prNumber = isPr ? (rawBody.pull_request?.number || rawBody.number || 1) : undefined; - const prTitle = isPr ? (rawBody.pull_request?.title || 'Telemetry Gate Evaluation') : undefined; - const prUrl = isPr ? (rawBody.pull_request?.html_url || (rawBody.repository?.html_url ? `${rawBody.repository.html_url}/pull/${prNumber}` : undefined)) : undefined; - const prAction = isPr ? (rawBody.action || 'synchronize') : undefined; - - // Simulate multi-engine telemetry calculation - const baseScore = Math.floor(Math.random() * 10) + 89; // 89 - 98 - const status = baseScore >= 85 ? 'passed' : baseScore >= 70 ? 'warning' : 'failed'; - const durationMs = Math.floor(Math.random() * 200) + 250; - - const telemetryEvent: any = { - id: `gh_evt_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`, - repoId: repo.id, - ownerId: repo.ownerId || 'usr_default', - repoName: rawBody.repository?.full_name || repo.name, - eventType: isPr ? 'pull_request' : 'push', - branch, - commitHash: isPr ? undefined : commitHash, - commitMessage: isPr ? undefined : commitMessage, - commitUrl: isPr ? undefined : commitUrl, - author, - authorAvatar, - prNumber, - prTitle, - prUrl, - prAction, - score: baseScore, - status, - summary: status === 'passed' - ? `Quality Gate PASSED: 0 critical vulnerabilities, ${baseScore}/100 composite score.` - : `Quality Gate ${status.toUpperCase()}: Minor deviations detected (${baseScore}/100).`, - durationMs, - metrics: { - astCodeHygiene: Math.min(100, baseScore + Math.floor(Math.random() * 4)), - securityVulnerabilities: 100, - aiReadinessScore: Math.min(100, baseScore - 2), - buildCarbonEco: Math.min(100, baseScore + 3), - coreWebVitalsGate: Math.min(100, baseScore + 1), - edgeLatencyIndex: Math.min(100, baseScore + 4), - testCoverage: Number((85 + Math.random() * 12).toFixed(1)), - cveIssuesDetected: 0, - linesAnalyzed: Math.floor(Math.random() * 5000) + 10000, - filesScanned: Math.floor(Math.random() * 30) + 20 - }, - engineResults: [ - { - engineKey: 'repo', - engineName: 'GitLygase (AST Code Hygiene)', - score: Math.min(100, baseScore + 2), - status: 'passed', - summary: '0 anti-patterns, compliant cyclomatic complexity.' - }, - { - engineKey: 'compliance', - engineName: 'RiskProtease (Security & CVE)', - score: 100, - status: 'passed', - summary: 'OWASP zero-trust verification & clean package licenses.' - }, - { - engineKey: 'ai_ready', - engineName: 'AI Discovery & Agent Manifest', - score: Math.min(100, baseScore - 2), - status: 'passed', - summary: 'Valid semantic metadata, llms.txt & prompt safety checked.' - }, - { - engineKey: 'eco', - engineName: 'SWD Green Carbon Efficiency', - score: Math.min(100, baseScore + 3), - status: 'passed', - summary: 'Low-carbon asset bundle, optimized edge delivery.' - } - ], - timestamp: Date.now() - }; - - // Update in-memory repo statistics - if (serverConnectedRepos.has(repo.id)) { - const existing = serverConnectedRepos.get(repo.id); - existing.eventsCount = (existing.eventsCount || 0) + 1; - existing.lastEventAt = Date.now(); - existing.lastScore = baseScore; - existing.lastStatus = status; - serverConnectedRepos.set(repo.id, existing); - } - - serverTelemetryEvents.unshift(telemetryEvent); - if (serverTelemetryEvents.length > 100) serverTelemetryEvents.pop(); - - // Broadcast in real-time to all connected dashboard SSE streams! - broadcastGithubTelemetry(telemetryEvent); - - res.status(200).json({ - success: true, - event: githubEvent, - repository: repo.name, - telemetryEvent, - message: 'GitHub webhook ingested and real-time telemetry evaluated.' - }); - }; - - app.post('/api/v1/integrations/github/webhook', handleGithubWebhook); - app.post('/api/webhooks/github', handleGithubWebhook); - - // List connected GitHub Repositories - app.get('/api/v1/integrations/github/repos', (req: Request, res: Response) => { - const repos = Array.from(serverConnectedRepos.values()); - res.json({ - success: true, - count: repos.length, - repos - }); - }); - - // Connect a new GitHub Repository - app.post('/api/v1/integrations/github/repos', (req: Request, res: Response) => { - const { repoUrl, name, defaultBranch = 'main', autoScanEngines } = req.body; - if (!repoUrl && !name) { - res.status(400).json({ success: false, error: 'Repository URL or name is required.' }); - return; - } - - const repoName = name || repoUrl.replace('https://github.com/', '').replace('.git', ''); - const repoId = `gh_repo_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`; - const randomSecret = 'cat_whsec_' + Math.random().toString(16).substring(2, 14) + Math.random().toString(16).substring(2, 10); - const host = req.get('host') || 'localhost:3000'; - const protocol = req.protocol || 'http'; - const webhookUrl = `${protocol}://${host}/api/v1/integrations/github/webhook?repoId=${repoId}`; - - const newRepo = { - id: repoId, - name: repoName, - repoUrl: repoUrl || `https://github.com/${repoName}`, - defaultBranch, - webhookSecret: randomSecret, - webhookUrl, - ownerId: 'usr_default', - status: 'active', - eventsCount: 0, - lastEventAt: null, - lastScore: null, - lastStatus: null, - autoScanEngines: autoScanEngines || ['repo', 'compliance', 'ai_ready', 'eco'], - createdAt: Date.now() - }; - - serverConnectedRepos.set(repoId, newRepo); - - res.status(201).json({ - success: true, - repo: newRepo, - instructions: { - payloadUrl: webhookUrl, - contentType: 'application/json', - secret: randomSecret, - events: ['Just the push event', 'Pull requests'] - } - }); - }); - - // Disconnect GitHub Repository - app.delete('/api/v1/integrations/github/repos/:id', (req: Request, res: Response) => { - const { id } = req.params; - if (serverConnectedRepos.has(id)) { - serverConnectedRepos.delete(id); - res.json({ success: true, message: `Repository '${id}' disconnected successfully.` }); - } else { - res.status(404).json({ success: false, error: 'Repository not found.' }); - } - }); - - // Simulate / Test GitHub Commit or PR Webhook Trigger - app.post('/api/v1/integrations/github/repos/:id/test-payload', async (req: Request, res: Response) => { - const { id } = req.params; - const { eventType = 'push', branch, commitMessage, prTitle, author = 'catalyst-developer' } = req.body; - - const repo = serverConnectedRepos.get(id) || { - id, - name: 'myself-aas/CatalystLab', - defaultBranch: 'main', - ownerId: 'usr_default' - }; - - const targetBranch = branch || (eventType === 'pull_request' ? 'feature/realtime-radar' : repo.defaultBranch || 'main'); - const commitHash = Math.random().toString(16).substring(2, 9); - const simulatedScore = Math.floor(Math.random() * 8) + 91; // 91-98 - const status = simulatedScore >= 85 ? 'passed' : 'warning'; - const isPr = eventType === 'pull_request'; - - const testEvent: any = { - id: `gh_evt_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`, - repoId: repo.id, - ownerId: repo.ownerId || 'usr_default', - repoName: repo.name, - eventType: isPr ? 'pull_request' : 'push', - branch: targetBranch, - commitHash: isPr ? undefined : commitHash, - commitMessage: isPr ? undefined : (commitMessage || 'feat: automated telemetry pipeline validation'), - commitUrl: `https://github.com/${repo.name}/commit/${commitHash}`, - author, - authorAvatar: `https://github.com/${author}.png`, - prNumber: isPr ? Math.floor(Math.random() * 50) + 10 : undefined, - prTitle: isPr ? (prTitle || 'feat: high-precision automated real-time telemetry webhook') : undefined, - prUrl: isPr ? `https://github.com/${repo.name}/pull/45` : undefined, - prAction: isPr ? 'synchronize' : undefined, - score: simulatedScore, - status, - summary: `Real-time webhook simulated: ${simulatedScore}/100 telemetry quality score. All ${isPr ? 'PR merge criteria' : 'commit quality gates'} met.`, - durationMs: Math.floor(Math.random() * 150) + 210, - metrics: { - astCodeHygiene: Math.min(100, simulatedScore + 2), - securityVulnerabilities: 100, - aiReadinessScore: Math.min(100, simulatedScore - 1), - buildCarbonEco: Math.min(100, simulatedScore + 3), - coreWebVitalsGate: 96, - edgeLatencyIndex: 98, - testCoverage: 93.8, - cveIssuesDetected: 0, - linesAnalyzed: 8920, - filesScanned: 32 - }, - engineResults: [ - { engineKey: 'repo', engineName: 'GitLygase (AST Code Hygiene)', score: Math.min(100, simulatedScore + 2), status: 'passed', summary: 'Clean AST syntax, zero high complexity blocks.' }, - { engineKey: 'compliance', engineName: 'RiskProtease (Security & CVE)', score: 100, status: 'passed', summary: 'All 48 dependencies verified safe against CVE feeds.' }, - { engineKey: 'ai_ready', engineName: 'AI Readiness & Agent Manifest', score: Math.min(100, simulatedScore - 1), status: 'passed', summary: 'llms.txt discovery and tool schema verified.' }, - { engineKey: 'eco', engineName: 'SWD Green Carbon Efficiency', score: Math.min(100, simulatedScore + 3), status: 'passed', summary: '0.038g CO2e/run asset profile.' } - ], - timestamp: Date.now() - }; - - // Update repository stats - if (serverConnectedRepos.has(id)) { - const existing = serverConnectedRepos.get(id); - existing.eventsCount = (existing.eventsCount || 0) + 1; - existing.lastEventAt = Date.now(); - existing.lastScore = simulatedScore; - existing.lastStatus = status; - serverConnectedRepos.set(id, existing); - } - - serverTelemetryEvents.unshift(testEvent); - if (serverTelemetryEvents.length > 100) serverTelemetryEvents.pop(); - - // Broadcast live over SSE to active dashboard listeners! - broadcastGithubTelemetry(testEvent); - - res.json({ - success: true, - delivered: true, - simulatedEvent: testEvent, - message: `Simulated GitHub ${eventType} event successfully processed and broadcasted.` - }); - }); - - // Get GitHub Telemetry Events History - app.get('/api/v1/integrations/github/events', (req: Request, res: Response) => { - const { repoId } = req.query; - let events = serverTelemetryEvents; - if (repoId) { - events = events.filter((e) => e.repoId === repoId); - } - res.json({ - success: true, - count: events.length, - events - }); - }); - - // 9. System Health & Probe Aliases - app.get('/api/v1/system/health', async (req: Request, res: Response) => { - const memory = process.memoryUsage(); - const mongoStatus = await checkMongoDBHealth(); - res.json({ - status: 'operational', - uptimeSeconds: Math.floor((Date.now() - serverStartTime) / 1000), - memoryUsageMb: { - rss: Math.round(memory.rss / (1024 * 1024)), - heapTotal: Math.round(memory.heapTotal / (1024 * 1024)), - heapUsed: Math.round(memory.heapUsed / (1024 * 1024)) - }, - activeEnginesCount: Object.keys(ENGINE_SCRIPT_MAP).length, - totalAuditsLogged: totalAuditsExecuted, - database: { - type: 'MongoDB Atlas', - connected: mongoStatus.connected, - databaseName: mongoStatus.database, - pingLatencyMs: mongoStatus.pingMs, - totalAnalyticsEvents: mongoStatus.totalEventsCount, - connectionUri: mongoStatus.uriMasked, - error: mongoStatus.error - }, - nodeVersion: process.version, - platform: `${os.type()} ${os.release()} (${os.arch()})`, - timestamp: Date.now() - }); - }); - - // Dedicated MongoDB & Ingestion Worker Status Route - app.get('/api/v1/database/mongodb/status', async (req: Request, res: Response) => { - const status = await checkMongoDBHealth(); - const batchMetrics = getBatchMetrics(); - res.json({ - success: true, - ...status, - ingestionBatching: batchMetrics - }); - }); - - app.post('/api/v1/system/probe', (req: Request, res: Response) => { - // Re-route to probe handler logic - res.redirect(307, '/api/monitor/probe'); - }); - - // 10. OpenAPI Specification JSON & Postman Collection JSON - app.get('/api/v1/openapi.json', (req: Request, res: Response) => { - res.json({ - openapi: '3.1.0', - info: { - title: 'CatalystLab Telemetry & Quality Intelligence API', - version: '2.4.0', - description: 'Comprehensive, high-precision automated web telemetry API specification for Core Web Vitals, Edge Latency, AI LLM Readiness, SecOps, and Sustainable Carbon metrics.', - contact: { - name: 'CatalystLab Developer Relations', - url: 'https://www.catalystlab.tech/contact', - email: 'support@catalystlab.tech' - } - }, - servers: [ - { url: 'https://www.catalystlab.tech', description: 'Production Anycast Gateway' }, - { url: 'http://localhost:3000', description: 'Local Container Development' } - ] - }); - }); - - // API 404 catch-all — ensures unhandled /api/* or /stats/* or /telemetry/* requests ALWAYS return JSON and NEVER HTML SPA fallback - app.all(['/api/*', '/stats/*', '/telemetry/*'], (req: Request, res: Response) => { - if (!res.headersSent) { - res.status(404).json({ - success: false, - error: `API endpoint '${req.method} ${req.path}' not found.` - }); - } - }); - - // CRITICAL route-level fallback: Handle database queries failing gracefully when MongoDB is offline: - app.use((err: any, req: Request, res: Response, next: express.NextFunction) => { - if (res.headersSent) { - return next(err); - } - if (err.name === 'MongooseError' || err.name === 'MongoNetworkError' || err.message?.includes('buffering timed out') || err.message?.includes('Mongo')) { - console.warn('[AI Studio] Database offline — returning mock empty response'); - if (req.method === 'GET') { - return res.json(req.path.endsWith('s') || req.path.endsWith('s/') ? [] : {}); - } - return res.status(503).json({ error: 'Service temporarily unavailable (database offline)' }); - } - next(err); - }); - - // --- PAYMENT GATEWAYS: 2Checkout (Verifone) & Dodo Payments (Backup) --- - app.post('/api/payments/create-checkout', express.json(), async (req: Request, res: Response): Promise => { - try { - const { planId, billingCycle, gateway, userId, userEmail } = req.body; - if (!planId) { - res.status(400).json({ success: false, error: 'Missing planId' }); - return; - } - - const selectedGateway = gateway === 'dodopay' ? 'dodopay' : '2checkout'; - const orderId = `ord_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`; - - const pricingMap: Record = { - starter: { monthly: 9, annual: 90, name: 'Starter Tier' }, - pro: { monthly: 19, annual: 190, name: 'Pro Tier' }, - team: { monthly: 49, annual: 490, name: 'Team Tier' }, - enterprise: { monthly: 99, annual: 990, name: 'Enterprise Tier' } - }; - - const planDetails = pricingMap[planId] || pricingMap.pro; - const amount = billingCycle === 'annual' ? planDetails.annual : planDetails.monthly; - - if (selectedGateway === '2checkout') { - const merchantCode = process.env.V2CHECKOUT_MERCHANT_CODE || 'DEMO_2CO'; - const isSandbox = process.env.V2CHECKOUT_SANDBOX_MODE !== 'false'; - - res.json({ - success: true, - gateway: '2checkout', - mode: isSandbox ? 'sandbox' : 'live', - checkoutUrl: isSandbox - ? `https://sandbox.2checkout.com/checkout/purchase?merchant=${merchantCode}&tpi=1&prod=${planId}&price=${amount}` - : `https://secure.2checkout.com/checkout/purchase?merchant=${merchantCode}&tpi=1&prod=${planId}&price=${amount}`, - orderId, - amount, - currency: 'USD', - planId, - billingCycle - }); - return; - } else { - const apiKey = process.env.DODOPAY_API_KEY || 'DEMO_DODO'; - const isSandbox = process.env.DODOPAY_SANDBOX_MODE !== 'false'; - - res.json({ - success: true, - gateway: 'dodopay', - mode: isSandbox ? 'sandbox' : 'live', - checkoutUrl: isSandbox - ? `https://test.dodopayments.com/pay/${orderId}?amount=${amount}¤cy=USD&plan=${planId}` - : `https://checkout.dodopayments.com/pay/${orderId}?amount=${amount}¤cy=USD&plan=${planId}`, - orderId, - amount, - currency: 'USD', - planId, - billingCycle - }); - return; - } - } catch (err: any) { - res.status(500).json({ success: false, error: err.message || 'Payment initialization failed' }); - } - }); - - app.post('/api/payments/webhook/2checkout', express.json(), async (req: Request, res: Response): Promise => { - try { - const payload = req.body; - console.log('[2Checkout Webhook Received]:', payload?.message_type || 'IPN_NOTIFICATION'); - res.status(200).json({ status: 'success', received: true }); - } catch (err) { - res.status(500).json({ error: 'Webhook processing error' }); - } - }); - - app.post('/api/payments/webhook/dodopay', express.json(), async (req: Request, res: Response): Promise => { - try { - const payload = req.body; - console.log('[DodoPay Webhook Received]:', payload?.event || 'PAYMENT_SUCCEEDED'); - res.status(200).json({ status: 'success', received: true }); - } catch (err) { - res.status(500).json({ error: 'Webhook processing error' }); - } - }); - - app.post('/api/payments/verify', express.json(), async (req: Request, res: Response): Promise => { - try { - const { orderId, planId, billingCycle, gateway } = req.body; - res.json({ - success: true, - message: `Payment successfully verified via ${gateway === 'dodopay' ? 'Dodo Payments (Backup)' : '2Checkout (Verifone)'}`, - orderId, - planId, - billingCycle, - activeUntil: Date.now() + (billingCycle === 'annual' ? 365 : 30) * 24 * 60 * 60 * 1000 - }); - } catch (err: any) { - res.status(500).json({ success: false, error: err.message }); - } - }); + // 12-factor: PORT/HOST come from the environment (M11), with the historic + // defaults preserved for local development. + const PORT = Number(process.env.PORT) || 3000; + const HOST = process.env.HOST || '0.0.0.0'; // Use one HTTP server for Express and Vite so Vite can attach its HMR WebSocket // upgrade handler in middleware mode. - const httpServer = http.createServer(app); + const httpServer = createHttpServer(app); // Vite Integration if (process.env.NODE_ENV === 'production') { @@ -2885,6 +49,7 @@ async function startServer() { const vite = await createViteServer({ server: { middlewareMode: true, + allowedHosts: true, // The preview proxy does not expose the dev WebSocket upgrade path. // Disable Vite HMR so the browser does not retry a socket that cannot open. hmr: false @@ -2895,11 +60,11 @@ async function startServer() { } httpServer.listen(PORT, HOST, () => { - console.log(`[CatalystLab] Server running at http://${HOST}:${PORT}`); + logger.info({ host: HOST, port: PORT, env: process.env.NODE_ENV || 'development' }, '[CatalystLab] Server started'); }); } -startServer().catch((err) => { - console.error('Fatal server startup failure:', err); +startServer().catch((err: unknown) => { + logger.error({ err }, 'Fatal server startup failure'); process.exit(1); }); diff --git a/server/app.ts b/server/app.ts new file mode 100644 index 0000000..46020db --- /dev/null +++ b/server/app.ts @@ -0,0 +1,178 @@ +import express, { Request, Response, NextFunction } from 'express'; +import helmet from 'helmet'; +import path from 'path'; +import fs from 'fs'; +import crypto from 'crypto'; +import { attachIdentity } from '../src/lib/serverAuth'; +import { requestLoggingMiddleware } from './core/logger'; +import { logger } from './core/logger'; + +// Decomposed route modules (Phase 2) + client-log sink (Phase 3). +import { registerTelemetryRoutes } from './routes/telemetry'; +import { registerNotificationRoutes } from './routes/notifications'; +import { registerStateSyncRoutes } from './routes/stateSync'; +import { registerPlanRoutes } from './routes/plans'; +import { registerEngineRoutes } from './routes/engines'; +import { registerReportRoutes } from './routes/reports'; +import { registerAccountRoutes } from './routes/account'; +import { registerGithubRoutes } from './routes/github'; +import { registerSystemRoutes } from './routes/system'; +import { registerPaymentRoutes } from './routes/payments'; +import { registerClientLogRoutes } from './routes/clientLogs'; + +/** + * Builds the fully-configured Express app (security headers, body limits, + * identity middleware, all API routes, 404 + error fallbacks) without any + * HTTP/Vite/static wiring, so `server.ts` owns only process concerns and the + * route suite (Phase 3, item 16) can drive the app with supertest. + */ +export async function createApp(): Promise { + const app = express(); + const isProduction = process.env.NODE_ENV === 'production'; + + // Compute sha256 hashes of inline ` would terminate the script element early and allow script + * injection into the page (the URL-derived breadcrumb labels make this + * attacker-controllable). Escaping `<` as `\u003c` is JSON-compatible and + * keeps the structured data intact for crawlers while making script + * breakout impossible. + */ +export function serializeJsonLd(data: unknown): string { + return JSON.stringify(data).replace(/ + z + .object({ + url: boundedUrl, + engine: z.enum(engines) + }) + .strip(); + +export const monitorProbeSchema = z + .object({ url: boundedUrl }) + .strip(); + +export const STATE_SYNC_COLLECTIONS = [ + 'domains', + 'goals', + 'alerts', + 'user_preferences', + 'audit_results', + 'monitored_sites' +] as const; + +export const stateSyncSchema = z + .object({ + collection: z.enum(STATE_SYNC_COLLECTIONS), + actionType: z.enum(['insert', 'update', 'delete', 'upsert']), + documentId: z + .string() + .max(128) + .regex(/^[a-zA-Z0-9_\-]+$/, 'documentId may only contain letters, digits, underscore and dash'), + payload: z.record(z.string(), z.unknown()).default({}), + timestamp: z.number().optional() + }) + .strip(); + +/** + * Telemetry event ingestion. Mirrors the fields consumed by + * ``queueEvent`` (analyticsEngine) -- anything else is dropped. + * ``props`` is bounded to 32 scalar entries; ``vitals`` to 8 numeric entries. + */ +const scalar = z.union([z.string().max(512), z.number().finite(), z.boolean()]); +export const telemetryEventSchema = z + .object({ + name: z.string().max(64).optional(), + url: z.string().max(2048).optional(), + pathname: z.string().max(512).optional(), + referrer: z.string().max(2048).optional(), + domain: z.string().max(253).optional(), + visitor_id: z.string().max(128).optional(), + session_id: z.string().max(128).optional(), + timestamp: z.number().optional(), + props: z.record(z.string(), scalar).optional(), + vitals: z.record(z.string(), z.number().finite()).optional() + }) + .strip(); + +export type ParsedTelemetryEvent = z.infer; + +/** Flattened first-issue message for client-facing 400 responses. */ +export function firstIssue(error: z.ZodError): string { + const issue = error.issues[0]; + if (!issue) return 'Invalid request payload.'; + const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''; + return `${path}${issue.message}`; +} diff --git a/src/lib/webhookSecurity.ts b/src/lib/webhookSecurity.ts new file mode 100644 index 0000000..3be2554 --- /dev/null +++ b/src/lib/webhookSecurity.ts @@ -0,0 +1,55 @@ +import crypto from 'crypto'; + +/** + * Result of a webhook signature verification attempt. + */ +export interface WebhookVerificationResult { + valid: boolean; + reason?: string; +} + +/** + * Verifies an HMAC-SHA256 webhook signature over the RAW request body. + * + * Expects GitHub-style signatures: ``sha256=``. The comparison is + * constant-time (``crypto.timingSafeEqual``) and length-guarded so arbitrary + * attacker input cannot leak timing information or crash the comparison. + * + * IMPORTANT: the signature must be computed over the raw bytes exactly as + * received. Use the buffer captured by the body-parser ``verify`` callback + * (``req.rawBody``), not a re-serialized JSON object. + * + * @param rawBody Raw request body buffer (or string) the signature was computed over. + * @param signatureHeader Value of the ``x-hub-signature-256`` style header. + * @param secret The per-repo / per-gateway webhook secret. + */ +export function verifyHmacSha256( + rawBody: Buffer | string, + signatureHeader: string | undefined | null, + secret: string +): WebhookVerificationResult { + if (!secret) { + return { valid: false, reason: 'Webhook secret is not configured on the server.' }; + } + if (!signatureHeader || typeof signatureHeader !== 'string') { + return { valid: false, reason: 'Missing webhook signature header.' }; + } + + const expected = + 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); + const expectedBuf = Buffer.from(expected, 'utf8'); + const receivedBuf = Buffer.from(signatureHeader.trim(), 'utf8'); + + if (expectedBuf.length !== receivedBuf.length || !crypto.timingSafeEqual(expectedBuf, receivedBuf)) { + return { valid: false, reason: 'Webhook signature mismatch.' }; + } + return { valid: true }; +} + +/** + * Signs a payload with HMAC-SHA256 in the same ``sha256=`` format. + * Used by tests and local tooling to construct verifiable webhook requests. + */ +export function signHmacSha256(rawBody: Buffer | string, secret: string): string { + return 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); +} diff --git a/src/main.tsx b/src/main.tsx index 295f94e..810b489 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,22 +6,27 @@ import { SubscriptionProvider } from './context/SubscriptionContext'; import { RoleSecurityProvider } from './context/RoleSecurityContext'; import { ThemeProvider } from './context/ThemeContext'; import App from './App'; +import { ErrorBoundary } from './components/ErrorBoundary'; import './index.css'; +import { logger, installGlobalErrorReporting } from './lib/logger'; // Register Service Worker for offline asset and telemetry doc caching if (typeof window !== 'undefined' && 'serviceWorker' in navigator && process.env.NODE_ENV === 'production') { window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js').catch((error) => { - console.warn('[SW] Registration failed:', error); + navigator.serviceWorker.register('/sw.js').catch((error: unknown) => { + logger.warn('[SW] Registration failed:', error); }); }); } +installGlobalErrorReporting(); + const rootElement = document.getElementById('root'); if (!rootElement) throw new Error('Failed to find the root element'); ReactDOM.createRoot(rootElement).render( + @@ -33,5 +38,6 @@ ReactDOM.createRoot(rootElement).render( + ); diff --git a/src/pages/BlogEditorPage.tsx b/src/pages/BlogEditorPage.tsx index d228d20..076e83f 100644 --- a/src/pages/BlogEditorPage.tsx +++ b/src/pages/BlogEditorPage.tsx @@ -10,7 +10,7 @@ import { SEOHead } from '../components/common/SEOHead'; import { calculateReadingTime } from '../utils/readingTime'; import { HeroImageLivePreview } from '../components/blog/HeroImageLivePreview'; import { - ArrowLeft, + ArrowLeft, Save, Send, Eye, @@ -52,6 +52,8 @@ import { Heading2, Heading3 } from 'lucide-react'; +import { errorMessage } from '../lib/utils'; +import { logger } from '../lib/logger'; const CATEGORIES = [ 'AI & LLMO', @@ -253,7 +255,7 @@ export const BlogEditorPage: React.FC = () => { } }) .catch((err) => { - console.error('Error fetching blog for editing:', err); + logger.error('Error fetching blog for editing:', err); showNotification('Could not load blog post from server.', 'error'); }) .finally(() => { @@ -435,8 +437,8 @@ export const BlogEditorPage: React.FC = () => { }, 800); } } catch (err: unknown) { - console.error('Failed to save article:', err); - showNotification(`Failed to save article: ${err.message || 'Unknown error'}`, 'error'); + logger.error('Failed to save article:', err); + showNotification(`Failed to save article: ${errorMessage(err) || 'Unknown error'}`, 'error'); } finally { setSaving(false); } @@ -455,8 +457,8 @@ export const BlogEditorPage: React.FC = () => { navigate('/blogs'); }, 600); } catch (err: unknown) { - console.error('Delete failed:', err); - showNotification(`Failed to delete: ${err.message}`, 'error'); + logger.error('Delete failed:', err); + showNotification(`Failed to delete: ${errorMessage(err)}`, 'error'); setSaving(false); } }; diff --git a/src/pages/BlogPostPage.tsx b/src/pages/BlogPostPage.tsx index 29f2c4b..90861a5 100644 --- a/src/pages/BlogPostPage.tsx +++ b/src/pages/BlogPostPage.tsx @@ -5,6 +5,7 @@ import { getBlogPostBySlug, getBlogPosts } from '../lib/firebase'; import { useAuth } from '../context/AuthContext'; import { MarkdownRenderer } from '../components/common/MarkdownRenderer'; import { ScanRevealFigure } from '../components/media/ScanRevealFigure'; +import { PexelsImage } from '../components/media/PexelsImage'; import { getBlogCoverImage } from '../utils/blogImageMap'; import { getArticleReadingTime } from '../utils/readingTime'; import { @@ -16,6 +17,7 @@ import { Edit3 } from 'lucide-react'; import { SEOHead } from '../components/common/SEOHead'; +import { logger } from '../lib/logger'; export const BlogPostPage: React.FC = () => { const { slug } = useParams<{ slug: string }>(); @@ -40,7 +42,7 @@ export const BlogPostPage: React.FC = () => { .slice(0, 3); setRelatedPosts(related); } catch (err) { - console.error("Error loading blog post:", err); + logger.error("Error loading blog post:", err); } finally { setLoading(false); } @@ -60,7 +62,7 @@ export const BlogPostPage: React.FC = () => { if (loading) { return (
- progress_activity +

Loading technical briefing...

); diff --git a/src/pages/BlogsPage.tsx b/src/pages/BlogsPage.tsx index 7bae904..981a674 100644 --- a/src/pages/BlogsPage.tsx +++ b/src/pages/BlogsPage.tsx @@ -30,6 +30,7 @@ import { BlogCard } from '../components/cards/content/BlogCard'; import { EnzymeHue } from '../components/cards/types'; import { getBlogCoverImage } from '../utils/blogImageMap'; import { getArticleReadingTime } from '../utils/readingTime'; +import { logger } from '../lib/logger'; export const BlogsPage: React.FC = () => { const { user, isAdmin } = useAuth(); @@ -47,7 +48,7 @@ export const BlogsPage: React.FC = () => { try { const saved = localStorage.getItem('catalyst_bookmarked_blogs'); if (saved) setBookmarkedIds(new Set(JSON.parse(saved))); - } catch (e) { console.error("Ignored error:", e); } + } catch (e) { logger.error("Ignored error:", e); } }, []); const toggleBookmark = (id: string, e: React.MouseEvent) => { @@ -59,7 +60,7 @@ export const BlogsPage: React.FC = () => { else next.add(id); try { localStorage.setItem('catalyst_bookmarked_blogs', JSON.stringify(Array.from(next))); - } catch (e) { console.error("Ignored error:", e); } + } catch (e) { logger.error("Ignored error:", e); } return next; }); }; @@ -105,7 +106,7 @@ export const BlogsPage: React.FC = () => { if (isMounted) setPosts(merged); } catch (err) { - console.warn("Error fetching remote blogs, using local seeds:", err); + logger.warn("Error fetching remote blogs, using local seeds:", err); if (isMounted) setPosts(allFallbackPosts); } finally { if (isMounted) setLoading(false); @@ -289,7 +290,7 @@ export const BlogsPage: React.FC = () => {
{/* Search Input */}
- + { value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search topics, tags, or words..." - className="w-full rounded-xl border border-slate-800 bg-[#080D1A] pl-9 pr-7 py-1.5 text-xs text-white placeholder:text-slate-500 focus:border-[#06B6D4] focus:outline-none transition-colors font-mono" + className="w-full rounded-xl border border-slate-800 bg-[#080D1A] pl-9 pr-7 py-1.5 text-xs text-white placeholder:text-slate-400 focus:border-[#06B6D4] focus:outline-none transition-colors font-mono" /> {searchQuery && ( @@ -342,7 +343,7 @@ export const BlogsPage: React.FC = () => { > {topic.label} {topic.enzyme && ( - + ({topic.enzyme}) )} @@ -363,7 +364,7 @@ export const BlogsPage: React.FC = () => { {
) : (
- +

No articles matched your filter

Try searching for other catalysts like VitalZyme, EcoHolo, or RiskProtease.

diff --git a/src/pages/ComparePage.tsx b/src/pages/ComparePage.tsx index 22836e8..4a99c12 100644 --- a/src/pages/ComparePage.tsx +++ b/src/pages/ComparePage.tsx @@ -3,6 +3,8 @@ import { SideBySideDeltaMatrix } from '../components/telemetry/SideBySideDeltaMa import { SEOHead } from '../components/common/SEOHead'; import { urlToDomainSlug } from '../utils/slugUtils'; import type { MasterTelemetryReport } from '../types/telemetry'; +import { authorizedFetch } from '../lib/authHeaders'; +import { logger } from '../lib/logger'; export const ComparePage: React.FC = () => { const [reportA, setReportA] = useState(null); @@ -27,12 +29,12 @@ export const ComparePage: React.FC = () => { try { const [resA, resB] = await Promise.all([ - fetch('/api/run-engine', { + authorizedFetch('/api/run-engine', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: cleanA, engine: 'health' }), }).then(r => r.json()).catch(() => null), - fetch('/api/run-engine', { + authorizedFetch('/api/run-engine', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: cleanB, engine: 'health' }), @@ -248,7 +250,7 @@ export const ComparePage: React.FC = () => { setReportA(constructedReportA); setReportB(constructedReportB); } catch (err) { - console.error("Side-by-side comparison failed:", err); + logger.error("Side-by-side comparison failed:", err); } finally { setLoading(false); } diff --git a/src/pages/ContactPage.tsx b/src/pages/ContactPage.tsx index 2092a15..9c979f1 100644 --- a/src/pages/ContactPage.tsx +++ b/src/pages/ContactPage.tsx @@ -15,6 +15,7 @@ import { import { LazyReveal } from '../components/common/LazyAnimate'; import { SEOHead } from '../components/common/SEOHead'; import { submitContactInquiry } from '../lib/firebase'; +import { logger } from '../lib/logger'; export const ContactPage: React.FC = () => { const [submitted, setSubmitted] = useState(false); @@ -136,7 +137,7 @@ export const ContactPage: React.FC = () => { setTicketId(generatedTicket); setSubmitted(true); } catch (err: unknown) { - console.warn("Contact inquiry submission warning:", err); + logger.warn("Contact inquiry submission warning:", err); setTicketId(generatedTicket); setSubmitted(true); } finally { @@ -363,7 +364,7 @@ export const ContactPage: React.FC = () => {