From d6b9d96c39830b93e01a56ee69efce3ddbadcfb3 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sat, 2 May 2026 10:34:58 -0400 Subject: [PATCH] Phase C: Prune FrenoCorp to only owned code after ShieldAI/Scripter migration Removed ShieldAI artifacts: - apps/api/, apps/web/, apps/mobile/ - packages/ (all 8 shared packages) - services/voiceprint-ml/ - server/alerts/, server/webrtc/ - examples/ Removed Scripter artifacts: - marketing/ - tasks/ Updated root configs: - Renamed package.json from shieldsai-monorepo to frenocorp - Updated tsconfig.json to include agents/ instead of src/ - Updated vite.config.ts aliases to reference agents/, analysis/, plans/ --- .../projects/fre-4529-dir-cleanup/items.yaml | 35 + .../projects/fre-4529-dir-cleanup/summary.md | 15 + agents/cto/memory/2026-05-02.md | 166 + .../node_modules/.vite/vitest/results.json | 1 - apps/api/package.json | 29 - .../sms-classifier-race-condition.test.ts | 144 - .../api/src/__tests__/spam-rate-limit.test.ts | 98 - apps/api/src/config/api.config.ts | 55 - apps/api/src/config/redis.ts | 18 - apps/api/src/index.ts | 106 - apps/api/src/middleware/auth.middleware.ts | 86 - .../middleware/error-handling.middleware.ts | 62 - apps/api/src/middleware/logging.middleware.ts | 66 - .../src/middleware/rate-limit.middleware.ts | 116 - .../middleware/spam-rate-limit.middleware.ts | 164 - apps/api/src/routes/darkwatch.routes.ts | 285 -- apps/api/src/routes/index.ts | 142 - apps/api/src/routes/notifications.routes.ts | 213 - apps/api/src/routes/spamshield.routes.ts | 252 - apps/api/src/routes/voiceprint.routes.ts | 257 - .../src/services/darkwatch/alert.pipeline.ts | 174 - apps/api/src/services/darkwatch/index.ts | 5 - .../src/services/darkwatch/scan.service.ts | 220 - .../services/darkwatch/scheduler.service.ts | 155 - .../services/darkwatch/watchlist.service.ts | 97 - .../src/services/darkwatch/webhook.service.ts | 226 - .../src/services/spamshield/feature-flags.ts | 227 - apps/api/src/services/spamshield/index.ts | 26 - .../spamshield/spamshield.audit-logger.ts | 118 - .../services/spamshield/spamshield.config.ts | 163 - .../spamshield/spamshield.error-handler.ts | 118 - .../services/spamshield/spamshield.service.ts | 462 -- apps/api/src/services/voiceprint/index.ts | 30 - .../services/voiceprint/voiceprint.config.ts | 102 - .../voiceprint/voiceprint.feature-flags.ts | 7 - .../services/voiceprint/voiceprint.service.ts | 594 --- apps/api/tsconfig.json | 12 - apps/mobile/package.json | 22 - apps/web/package.json | 24 - examples/call-analysis-example.ts | 90 - package-lock.json | 4159 +---------------- package.json | 4 +- packages/jobs/package.json | 25 - packages/jobs/src/darkwatch.jobs.ts | 173 - packages/jobs/src/index.ts | 9 - packages/jobs/src/voiceprint.jobs.ts | 93 - packages/jobs/tsconfig.json | 12 - packages/shared-analytics/package.json | 19 - .../src/config/analytics.config.ts | 132 - packages/shared-analytics/src/index.ts | 18 - .../src/services/ga4.service.ts | 104 - .../src/services/mixpanel.service.ts | 117 - .../shared-analytics/src/utils/phone-hash.ts | 12 - packages/shared-analytics/tsconfig.json | 12 - packages/shared-auth/package.json | 18 - .../shared-auth/src/config/auth.config.ts | 114 - packages/shared-auth/src/index.ts | 25 - .../src/middleware/auth.middleware.ts | 62 - .../shared-auth/src/models/auth.models.ts | 81 - packages/shared-auth/tsconfig.json | 12 - packages/shared-billing/package.json | 15 - .../src/config/billing.config.ts | 90 - packages/shared-billing/src/index.ts | 22 - .../src/middleware/billing.middleware.ts | 68 - .../src/services/billing.services.ts | 223 - packages/shared-billing/tsconfig.json | 12 - packages/shared-db/drizzle.config.ts | 12 - packages/shared-db/package.json | 23 - packages/shared-db/prisma/schema.prisma | 437 -- packages/shared-db/src/client.ts | 50 - packages/shared-db/src/index.ts | 21 - packages/shared-db/tsconfig.json | 12 - packages/shared-notifications/package.json | 28 - .../src/config/notification.config.ts | 92 - packages/shared-notifications/src/index.ts | 11 - .../src/services/email.service.ts | 171 - .../src/services/notification.service.ts | 271 -- .../src/services/push.service.ts | 262 -- .../src/services/sms.service.ts | 175 - .../src/types/notification.types.ts | 133 - packages/shared-notifications/tsconfig.json | 12 - packages/shared-ui/package.json | 17 - packages/shared-utils/package.json | 16 - server/alerts/alert-server.ts | 415 -- server/webrtc/signaling-server.ts | 259 - services/voiceprint-ml/Dockerfile | 15 - services/voiceprint-ml/main.py | 172 - services/voiceprint-ml/requirements.txt | 8 - tsconfig.json | 2 +- vite.config.ts | 6 +- 90 files changed, 269 insertions(+), 13164 deletions(-) create mode 100644 agents/cto/life/projects/fre-4529-dir-cleanup/items.yaml create mode 100644 agents/cto/life/projects/fre-4529-dir-cleanup/summary.md create mode 100644 agents/cto/memory/2026-05-02.md delete mode 100644 apps/api/node_modules/.vite/vitest/results.json delete mode 100644 apps/api/package.json delete mode 100644 apps/api/src/__tests__/sms-classifier-race-condition.test.ts delete mode 100644 apps/api/src/__tests__/spam-rate-limit.test.ts delete mode 100644 apps/api/src/config/api.config.ts delete mode 100644 apps/api/src/config/redis.ts delete mode 100644 apps/api/src/index.ts delete mode 100644 apps/api/src/middleware/auth.middleware.ts delete mode 100644 apps/api/src/middleware/error-handling.middleware.ts delete mode 100644 apps/api/src/middleware/logging.middleware.ts delete mode 100644 apps/api/src/middleware/rate-limit.middleware.ts delete mode 100644 apps/api/src/middleware/spam-rate-limit.middleware.ts delete mode 100644 apps/api/src/routes/darkwatch.routes.ts delete mode 100644 apps/api/src/routes/index.ts delete mode 100644 apps/api/src/routes/notifications.routes.ts delete mode 100644 apps/api/src/routes/spamshield.routes.ts delete mode 100644 apps/api/src/routes/voiceprint.routes.ts delete mode 100644 apps/api/src/services/darkwatch/alert.pipeline.ts delete mode 100644 apps/api/src/services/darkwatch/index.ts delete mode 100644 apps/api/src/services/darkwatch/scan.service.ts delete mode 100644 apps/api/src/services/darkwatch/scheduler.service.ts delete mode 100644 apps/api/src/services/darkwatch/watchlist.service.ts delete mode 100644 apps/api/src/services/darkwatch/webhook.service.ts delete mode 100644 apps/api/src/services/spamshield/feature-flags.ts delete mode 100644 apps/api/src/services/spamshield/index.ts delete mode 100644 apps/api/src/services/spamshield/spamshield.audit-logger.ts delete mode 100644 apps/api/src/services/spamshield/spamshield.config.ts delete mode 100644 apps/api/src/services/spamshield/spamshield.error-handler.ts delete mode 100644 apps/api/src/services/spamshield/spamshield.service.ts delete mode 100644 apps/api/src/services/voiceprint/index.ts delete mode 100644 apps/api/src/services/voiceprint/voiceprint.config.ts delete mode 100644 apps/api/src/services/voiceprint/voiceprint.feature-flags.ts delete mode 100644 apps/api/src/services/voiceprint/voiceprint.service.ts delete mode 100644 apps/api/tsconfig.json delete mode 100644 apps/mobile/package.json delete mode 100644 apps/web/package.json delete mode 100644 examples/call-analysis-example.ts delete mode 100644 packages/jobs/package.json delete mode 100644 packages/jobs/src/darkwatch.jobs.ts delete mode 100644 packages/jobs/src/index.ts delete mode 100644 packages/jobs/src/voiceprint.jobs.ts delete mode 100644 packages/jobs/tsconfig.json delete mode 100644 packages/shared-analytics/package.json delete mode 100644 packages/shared-analytics/src/config/analytics.config.ts delete mode 100644 packages/shared-analytics/src/index.ts delete mode 100644 packages/shared-analytics/src/services/ga4.service.ts delete mode 100644 packages/shared-analytics/src/services/mixpanel.service.ts delete mode 100644 packages/shared-analytics/src/utils/phone-hash.ts delete mode 100644 packages/shared-analytics/tsconfig.json delete mode 100644 packages/shared-auth/package.json delete mode 100644 packages/shared-auth/src/config/auth.config.ts delete mode 100644 packages/shared-auth/src/index.ts delete mode 100644 packages/shared-auth/src/middleware/auth.middleware.ts delete mode 100644 packages/shared-auth/src/models/auth.models.ts delete mode 100644 packages/shared-auth/tsconfig.json delete mode 100644 packages/shared-billing/package.json delete mode 100644 packages/shared-billing/src/config/billing.config.ts delete mode 100644 packages/shared-billing/src/index.ts delete mode 100644 packages/shared-billing/src/middleware/billing.middleware.ts delete mode 100644 packages/shared-billing/src/services/billing.services.ts delete mode 100644 packages/shared-billing/tsconfig.json delete mode 100644 packages/shared-db/drizzle.config.ts delete mode 100644 packages/shared-db/package.json delete mode 100644 packages/shared-db/prisma/schema.prisma delete mode 100644 packages/shared-db/src/client.ts delete mode 100644 packages/shared-db/src/index.ts delete mode 100644 packages/shared-db/tsconfig.json delete mode 100644 packages/shared-notifications/package.json delete mode 100644 packages/shared-notifications/src/config/notification.config.ts delete mode 100644 packages/shared-notifications/src/index.ts delete mode 100644 packages/shared-notifications/src/services/email.service.ts delete mode 100644 packages/shared-notifications/src/services/notification.service.ts delete mode 100644 packages/shared-notifications/src/services/push.service.ts delete mode 100644 packages/shared-notifications/src/services/sms.service.ts delete mode 100644 packages/shared-notifications/src/types/notification.types.ts delete mode 100644 packages/shared-notifications/tsconfig.json delete mode 100644 packages/shared-ui/package.json delete mode 100644 packages/shared-utils/package.json delete mode 100644 server/alerts/alert-server.ts delete mode 100644 server/webrtc/signaling-server.ts delete mode 100644 services/voiceprint-ml/Dockerfile delete mode 100644 services/voiceprint-ml/main.py delete mode 100644 services/voiceprint-ml/requirements.txt diff --git a/agents/cto/life/projects/fre-4529-dir-cleanup/items.yaml b/agents/cto/life/projects/fre-4529-dir-cleanup/items.yaml new file mode 100644 index 000000000..0329f2f8e --- /dev/null +++ b/agents/cto/life/projects/fre-4529-dir-cleanup/items.yaml @@ -0,0 +1,35 @@ +items: + - id: fre-4529-analysis + type: task + content: Analyzed recent code influx in FrenoCorp repo from parallel agent work + status: completed + created_at: 2026-05-02 + tags: [repo-cleanup, analysis, cto] + + - id: fre-4529-plan + type: document + content: Created cleanup plan with 4 phases (inventory, structural, architecture, quality) + status: completed + created_at: 2026-05-02 + tags: [repo-cleanup, planning] + + - id: fre-4530-naming + type: child_issue + content: FRE-4530 - Resolve FrenoCorp vs ShieldAI naming + status: delegated + assignee: Founding Engineer + created_at: 2026-05-02 + + - id: fre-4531-structural + type: child_issue + content: FRE-4531 - Consolidate duplicates, move files, prune branches + status: delegated + assignee: Senior Engineer + created_at: 2026-05-02 + + - id: fre-4532-todo + type: child_issue + content: FRE-4532 - Audit TODO placeholders + status: delegated + assignee: Senior Engineer + created_at: 2026-05-02 diff --git a/agents/cto/life/projects/fre-4529-dir-cleanup/summary.md b/agents/cto/life/projects/fre-4529-dir-cleanup/summary.md new file mode 100644 index 000000000..db9cdbc10 --- /dev/null +++ b/agents/cto/life/projects/fre-4529-dir-cleanup/summary.md @@ -0,0 +1,15 @@ +# FRE-4529: Repo Cleanup Analysis + +**Status**: Complete +**Date**: 2026-05-02 +**Role**: CTO + +Diagnosed the "WTF happened" in the FrenoCorp repo. Found 7 structural problems from rapid parallel agent work. Created cleanup plan and 3 child issues. + +## Key Outputs +- Analysis comment on [FRE-4529](/FRE/issues/FRE-4529) +- Cleanup plan document on [FRE-4529#document-plan](/FRE/issues/FRE-4529#document-plan) +- Child issues: + - [FRE-4530](/FRE/issues/FRE-4530) - Naming resolution + - [FRE-4531](/FRE/issues/FRE-4531) - Structural cleanup + - [FRE-4532](/FRE/issues/FRE-4532) - TODO audit diff --git a/agents/cto/memory/2026-05-02.md b/agents/cto/memory/2026-05-02.md new file mode 100644 index 000000000..3d35c604b --- /dev/null +++ b/agents/cto/memory/2026-05-02.md @@ -0,0 +1,166 @@ +# 2026-05-02 + +## FRE-4529 - WTF happened in the FrenoCorp Dir? +**Status**: In progress +**Wake reason**: issue_assigned + +Analyzed the repository for the "WTF happened" issue. Found: +- Massive influx of ShieldAI-related code from multiple agents over the past week +- ~38 source files changed in today's auto-commit alone (2,040+ lines added) +- Multiple architectural issues: naming confusion, duplicate services, scattered code, TODO placeholders + +Created cleanup plan document and posted to issue. + +## Key Findings +1. Repo name is "FrenoCorp" but packages use `@shieldsai/` scope +2. Duplicated: call-analysis in `src/lib/call-analysis/` vs `server/webrtc/` +3. Mixed architecture patterns across services +4. Loose root-level files from individual agent work +5. Stale branches (`simple`, `complicated`) with divergent content +6. Empty `tasks/` directory +7. Multiple TODO placeholders in production code + +--- + +## Second heartbeat: FRE-4529 reopened + +**Wake**: issue_reopened_via_comment — user corrected: ShieldAI is at ~/code/ShieldAI + +**Key correction**: ShieldAI = separate repo. FrenoCorp has **divergent independent implementations** of ShieldAI services, not copies. Services flattened as modules in `apps/api/src/services/` vs standalone microservices in ShieldAI. + +**Revised findings**: +1. Overlap: spamshield, voiceprint, darkwatch, shared-billing, shared-notifications, shared-db, jobs, alerts, webrtc — all exist in both repos with *different code* +2. Wrong npm scope: `@shieldsai/` used in FrenoCorp packages +3. Unique to FrenoCorp (keep): apps/web, apps/mobile, src-tauri, agents/, server/trpc, server/websocket, shared-analytics, shared-auth, shared-ui, shared-utils + +**Action**: Posted correction comment, updated plan doc (rev 2), raised board approval for direction choice (Option A: consume ShieldAI as dependency vs Option B: divorce cleanly). Awaiting board decision. + +--- + +## Third heartbeat: issue_continuation_needed + +**Wake**: issue_continuation_needed — board approval still pending, no new comments. + +**Action**: Set FRE-4529 to `blocked` with the board approval as the blocker. Child issues also stalled. Woke by harness but no actionable work until board decides. + +--- + +## Fourth heartbeat: issue_reopened_via_comment + +**Wake**: Board gave direction! ShieldAI code was added mistakenly — move to ~/code/ShieldAI. Scripter code also in FrenoCorp — move to ~/code/scripter. Only agents/ and analysis/ stay. Favor newer for conflicts. + +**Full inventory completed**: +- ShieldAI: apps/{api,web,mobile}, packages/ (8), services/voiceprint-ml, server/{alerts,webrtc}, examples/, root config +- Scripter: src/, src-tauri/, server/{trpc,websocket,types}, public/, brand/, marketing/, docs/, dist/, scripts/, index.html, root *.md +- Keep: agents/, analysis/, memory/, plans/ +- pop: no code found in FrenoCorp +- Tasks empty — delete + +**Action**: Posted comprehensive analysis comment and updated plan to rev 3. Asked board if src-tauri ("frenocorp-desktop") is FrenoCorp's own app or should merge into Scripter. + +--- + +## Fifth heartbeat: issue_children_completed + +**Wake**: child issues FRE-4530/4531/4532 cancelled (old plan superseded). Proceeded to execute Phase A1. + +**Phase A1 executed**: Transferred 45 files from FrenoCorp → ~/code/ShieldAI (commit 1e42c4a): +- Root configs, services (spamshield/voiceprint/darkwatch extras), VoicePrint ML, server/alerts, routes, 4 new packages, supplemented 2 existing packages, examples +- Favor newer policy applied correctly + +**Created new child issues**: +- FRE-4533 → Senior Engineer: Merge apps/{api,web,mobile} + shared-db into ShieldAI +- FRE-4534 → Founding Engineer: Move Scripter code to ~/code/scripter +- Phase C (FrenoCorp prune) — blocked until A+B done + +**Open question pending**: src-tauri identity (frenocorp-desktop vs scripter) + +--- + +## Sixth heartbeat: issue_assigned - FRE-4533 Phase A2 + +**Wake**: FRE-4533 assigned to CTO (was previously planned for Senior Engineer). + +**Action**: Executed Phase A2 — merged apps/{api,web,mobile} and shared-db from FrenoCorp → ShieldAI. + +**ShieldAI commits**: +- 1197fe4 (reverted): First attempt put files in apps/ not packages/ +- e704a90 (final): Properly merged into packages/ (api, web, mobile, shared-db) + +**Key details**: +- apps/api → merged into packages/api/ (added middleware, config, services/{darkwatch,spamshield,voiceprint}, tests, notifications route) +- apps/web → packages/web/ stub +- apps/mobile → packages/mobile/ stub +- packages/shared-db → preserved as-is alongside packages/db (needs reconciliation) +- ShieldAI's package.json and tsconfig for api were restored after accidental overwrite + +**Follow-up items documented on issue**: +1. Reconcile shared-db with db (Prisma schema merge) +2. Fix server.ts correlationRoutes import +3. Build out web/mobile stubs +4. Phase C (FrenoCorp prune) — blocked on FRE-4534 + +## Sixth heartbeat (14:15 UTC) — FRE-4534 Phase B: Move Scripter code + +**Wake reason**: issue_assigned +**Previous run**: Founding Engineer run "succeeded" but no detail captured. + +### Analysis +- Scripter repo exists at ~/code/scripter (git@git.freno.me:Mike/Scripter.git) — independent project +- FrenoCorp scripter files still in place; need to move +- Policy: "favor newer" for conflicts +- server/trpc/ structure differs completely between repos (flat routers vs modular) +- server/types/project.ts imported by src/components/characters/ — cross-dependency + +### Actions Taken +1. **Created child issue** [FRE-4535](/FRE/issues/FRE-4535) → Founding Engineer for overlap comparison work (src/, src-tauri/, server/trpc/, marketing/, public/) +2. **Moved non-overlapping files** from FrenoCorp → scripter, committed in both repos: + - brand/, scripts/, server/types/, server/websocket/, .eslintrc.json, FRE-4510-IMPLEMENTATION.md + - Merged .gitignore +3. **Removed from FrenoCorp** (git rm, commit 4e07718e6) +4. **Committed to scripter** (commit 5b128d9) +5. **Posted progress comment** on FRE-4534 + +### CTO Oversight +- 50 open issues total, 15 in in_review — code review pipeline is backed up +- Senior Engineer on FRE-4473 (blocked), Security Reviewer on FRE-4498 +- Founding Engineer now has FRE-4535 as sole active task +- Critical issues all CMO/CEO-owned (Product Hunt launch) + +### Next +- Founding Engineer picks up [FRE-4535](/FRE/issues/FRE-4535) +- Need Phase C issue (FrenoCorp prune) once A+B done +- Code review pipeline needs attention — 15 items queued + +## Seventh heartbeat (continuation) — FRE-4534 Phase B follow-up + +### Status +- **FRE-4535** (child, Founding Engineer) — checked out, in_progress. No completion yet. +- **FRE-4533** (Phase A2: ShieldAI merge) — **done** ✅ +- **Created FRE-4536** (Phase C: FrenoCorp prune) — blocked on FRE-4534, assigned to Founding Engineer + - Blocked by: FRE-4534 via blockedByIssueIds + - Will auto-wake Founding Engineer when FRE-4534 completes + +### Next +- Wait for Founding Engineer to complete FRE-4535 +- FRE-4534 stays in_progress until FRE-4535 completes +- FRE-4536 auto-wakes when FRE-4534 → done + +## Eighth heartbeat (liveness continuation) — FRE-4534 Phase B completed + +### Actions +- FRE-4535 (Founding Engineer) still had no progress since creation → took over the overlap work directly +- **Remaining overlap items moved** from FrenoCorp → scripter: + - src/, src-tauri/, server/trpc/ (archived to legacy/), marketing/, docs/, public/, dist/, index.html + - Favor-newer applied: scripter's modular structure preserved, FrenoCorp extras merged +- **FrenoCorp cleaned**: `git rm` of all scripter files (373 deletions, commit 0cc005414) +- **Scripter updated**: 155 new files committed (df1360a) +- **FRE-4534 marked done** ✅ +- **FRE-4535 marked cancelled** (superseded by direct CTO action) +- **FRE-4536** (Phase C) auto-woken → Founding Engineer now has it in_progress + +### Final tally +Total scripter files moved to ~/code/scripter across all heartbeats: +- brand/, scripts/, server/types/, server/websocket/, .eslintrc.json, FRE-4510-IMPLEMENTATION.md (heartbeat 1) +- marketing/ (66 files), docs/, public/manifest.json, src-tauri extras, src/ extras, server/trpc/legacy/ (heartbeat 2) +- .gitignore merged diff --git a/apps/api/node_modules/.vite/vitest/results.json b/apps/api/node_modules/.vite/vitest/results.json deleted file mode 100644 index a3798586d..000000000 --- a/apps/api/node_modules/.vite/vitest/results.json +++ /dev/null @@ -1 +0,0 @@ -{"version":"1.6.1","results":[[":src/__tests__/spam-rate-limit.test.ts",{"duration":41,"failed":false}]]} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json deleted file mode 100644 index 855aa4c3b..000000000 --- a/apps/api/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "api", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc", - "lint": "eslint src/" - }, - "dependencies": { - "@fastify/cors": "^11.2.0", - "@fastify/helmet": "^13.0.2", - "@shieldsai/shared-analytics": "*", - "@shieldsai/shared-auth": "*", - "@shieldsai/shared-billing": "*", - "@shieldsai/shared-db": "*", - "@shieldsai/shared-notifications": "*", - "@shieldsai/shared-utils": "*", - "fastify": "^4.25.0", - "fastify-plugin": "^4.5.0", - "ioredis": "^5.3.0" - }, - "devDependencies": { - "@types/node": "^25.6.0", - "tsx": "^4.7.1", - "typescript": "^5.3.3" - } -} diff --git a/apps/api/src/__tests__/sms-classifier-race-condition.test.ts b/apps/api/src/__tests__/sms-classifier-race-condition.test.ts deleted file mode 100644 index 675c75010..000000000 --- a/apps/api/src/__tests__/sms-classifier-race-condition.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { SMSClassifierService } from '../services/spamshield/spamshield.service'; - -// Mock shared-db before anything else (Prisma client is not generated in test env) -vi.mock('@shieldsai/shared-db', () => ({ - prisma: {}, - SpamFeedback: {}, -})); - -// Mock the feature flags module to control enableMLClassifier -vi.mock('../services/spamshield/spamshield.config', () => ({ - spamShieldEnv: { - SPAM_THRESHOLD_AUTO_BLOCK: 0.85, - SPAM_THRESHOLD_FLAG: 0.6, - }, - spamFeatureFlags: { - enableMLClassifier: true, - }, - SpamDecision: { - ALLOW: 'allow', - FLAG: 'flag', - BLOCK: 'block', - CHALLENGE: 'challenge', - }, - SpamLayer: { - NUMBER_REPUTATION: 'number_reputation', - CONTENT_CLASSIFICATION: 'content_classification', - BEHAVIORAL_ANALYSIS: 'behavioral_analysis', - COMMUNITY_INTELLIGENCE: 'community_intelligence', - }, - ConfidenceLevel: { - LOW: 'low', - MEDIUM: 'medium', - HIGH: 'high', - VERY_HIGH: 'very_high', - }, - spamRateLimits: {}, -})); - -describe('SMSClassifierService', () => { - let classifier: SMSClassifierService; - let initializeCalls: number; - let initializeDelay: Promise; - - beforeEach(() => { - // Re-import after mock to get fresh module state - initializeCalls = 0; - initializeDelay = new Promise(resolve => setTimeout(resolve, 50)); - - classifier = new SMSClassifierService(); - // Override initialize to track calls and add delay - classifier.initialize = async () => { - initializeCalls++; - await initializeDelay; - }; - }); - - describe('initialization race condition', () => { - it('should call initialize only once under concurrent classify calls', async () => { - const promises = Array.from({ length: 10 }, () => - classifier.classify('ACT NOW - Limited offer!'), - ); - - const results = await Promise.all(promises); - - expect(initializeCalls).toBe(1); - expect(results).toHaveLength(10); - results.forEach(r => { - expect(r).toHaveProperty('isSpam'); - expect(r).toHaveProperty('confidence'); - expect(r).toHaveProperty('spamFeatures'); - }); - }); - - it('should handle interleaved calls after partial initialization', async () => { - const batch1 = Array.from({ length: 5 }, () => - classifier.classify('First batch message'), - ); - - await Promise.all(batch1); - - expect(initializeCalls).toBe(1); - - const batch2 = Array.from({ length: 5 }, () => - classifier.classify('Second batch message'), - ); - - await Promise.all(batch2); - - // initialize should still only have been called once - expect(initializeCalls).toBe(1); - }); - - it('should return consistent results for same input under concurrency', async () => { - const text = 'URGENT: Click http://example.com now!'; - const promises = Array.from({ length: 20 }, () => - classifier.classify(text), - ); - - const results = await Promise.all(promises); - - const firstResult = results[0]; - results.forEach((r, i) => { - expect(r.isSpam).toBe(firstResult.isSpam); - expect(r.confidence).toBe(firstResult.confidence); - expect(r.spamFeatures).toEqual(firstResult.spamFeatures); - }); - }); - - it('should handle rapid sequential calls without re-initializing', async () => { - for (let i = 0; i < 50; i++) { - await classifier.classify(`Message ${i}`); - } - - expect(initializeCalls).toBe(1); - }); - }); - - describe('feature extraction', () => { - it('should detect URL presence', async () => { - const result = await classifier.classify('Visit www.example.com'); - expect(result.spamFeatures).toContain('url_present'); - }); - - it('should detect urgency keywords', async () => { - const result = await classifier.classify('Act now! This offer is urgent.'); - expect(result.spamFeatures).toContain('urgency_keyword'); - }); - - it('should detect excessive capitalization', async () => { - const result = await classifier.classify('BUY THIS NOW!!!'); - expect(result.spamFeatures).toContain('excessive_caps'); - }); - - it('should detect multiple features', async () => { - const result = await classifier.classify( - 'URGENT: Visit www.example.com NOW!!!', - ); - expect(result.spamFeatures).toContain('url_present'); - expect(result.spamFeatures).toContain('urgency_keyword'); - expect(result.spamFeatures).toContain('excessive_caps'); - }); - }); -}); diff --git a/apps/api/src/__tests__/spam-rate-limit.test.ts b/apps/api/src/__tests__/spam-rate-limit.test.ts deleted file mode 100644 index b907d462d..000000000 --- a/apps/api/src/__tests__/spam-rate-limit.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; -import { RedisRateLimiter } from '../middleware/spam-rate-limit.middleware'; -import { redis } from '../config/redis'; - -describe('RedisRateLimiter', () => { - const testKey = 'test-client'; - const limiter = new RedisRateLimiter(); - - beforeAll(async () => { - await redis.connect(); - }); - - afterAll(async () => { - await redis.quit(); - }); - - beforeEach(async () => { - await redis.del('spamshield:ratelimit:test-client'); - await redis.del('spamshield:ratelimit:daily:test-client'); - }); - - afterEach(async () => { - await redis.del('spamshield:ratelimit:test-client'); - await redis.del('spamshield:ratelimit:daily:test-client'); - }); - - describe('checkLimit (per-minute)', () => { - it('should allow requests within the limit', async () => { - const result = await limiter.checkLimit(testKey, 60, 10); - - expect(result.remaining).toBe(9); - expect(result.retryAfter).toBeUndefined(); - }); - - it('should decrement remaining on each request', async () => { - const result1 = await limiter.checkLimit(testKey, 60, 10); - const result2 = await limiter.checkLimit(testKey, 60, 10); - - expect(result1.remaining).toBe(9); - expect(result2.remaining).toBe(8); - }); - - it('should exceed limit after max requests', async () => { - for (let i = 0; i < 10; i++) { - await limiter.checkLimit(testKey, 60, 10); - } - - const result = await limiter.checkLimit(testKey, 60, 10); - - expect(result.remaining).toBe(0); - expect(result.retryAfter).toBeGreaterThan(0); - }); - - it('should return retry-after when limit is exceeded', async () => { - for (let i = 0; i < 10; i++) { - await limiter.checkLimit(testKey, 60, 10); - } - - const result = await limiter.checkLimit(testKey, 60, 10); - - expect(result.retryAfter).toBeGreaterThan(0); - expect(result.retryAfter).toBeLessThanOrEqual(60000); - }); - }); - - describe('checkDailyLimit', () => { - it('should allow requests within daily limit', async () => { - const result = await limiter.checkDailyLimit(testKey, 100); - - expect(result.remaining).toBe(99); - expect(result.retryAfter).toBeUndefined(); - }); - - it('should exceed daily limit after max requests', async () => { - for (let i = 0; i < 100; i++) { - await limiter.checkDailyLimit(testKey, 100); - } - - const result = await limiter.checkDailyLimit(testKey, 100); - - expect(result.remaining).toBe(0); - expect(result.retryAfter).toBeGreaterThan(0); - }); - }); - - describe('reset', () => { - it('should clear the rate limit counter', async () => { - await limiter.checkLimit(testKey, 60, 10); - await limiter.checkLimit(testKey, 60, 10); - - await limiter.reset(testKey); - - const result = await limiter.checkLimit(testKey, 60, 10); - - expect(result.remaining).toBe(9); - }); - }); -}); diff --git a/apps/api/src/config/api.config.ts b/apps/api/src/config/api.config.ts deleted file mode 100644 index 8812edbcd..000000000 --- a/apps/api/src/config/api.config.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { z } from 'zod'; - -// Environment variables -const envSchema = z.object({ - NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), - PORT: z.string().transform(Number).default(3000), - HOST: z.string().default('0.0.0.0'), - API_RATE_LIMIT_WINDOW: z.string().transform(Number).default(60000), // 1 minute - API_RATE_LIMIT_MAX_REQUESTS: z.string().transform(Number).default(100), - CORS_ORIGIN: z.string().default('http://localhost:5173'), -}); - -export const apiEnv = envSchema.parse({ - NODE_ENV: process.env.NODE_ENV, - PORT: process.env.PORT, - HOST: process.env.HOST, - API_RATE_LIMIT_WINDOW: process.env.API_RATE_LIMIT_WINDOW, - API_RATE_LIMIT_MAX_REQUESTS: process.env.API_RATE_LIMIT_MAX_REQUESTS, - CORS_ORIGIN: process.env.CORS_ORIGIN, -}); - -// Rate limit configuration by tier -export const rateLimitConfig = { - basic: { - windowMs: 60000, // 1 minute - maxRequests: 100, - }, - plus: { - windowMs: 60000, - maxRequests: 500, - }, - premium: { - windowMs: 60000, - maxRequests: 2000, - }, -}; - -// API versioning configuration -export const apiVersioning = { - defaultVersion: '1', - headerName: 'X-API-Version', - queryParam: 'api-version', -}; - -// Logging configuration -export const loggingConfig = { - level: apiEnv.NODE_ENV === 'production' ? 'info' : 'debug', - transport: apiEnv.NODE_ENV === 'development' ? { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: true, - }, - } : undefined, -}; diff --git a/apps/api/src/config/redis.ts b/apps/api/src/config/redis.ts deleted file mode 100644 index 754b2d520..000000000 --- a/apps/api/src/config/redis.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Redis } from 'ioredis'; - -const redisHost = process.env.REDIS_HOST || 'localhost'; -const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10); - -export const redis = new Redis({ - host: redisHost, - port: redisPort, - retryStrategy: (times: number) => Math.min(times * 50, 2000), - lazyConnect: true, -}); - -export async function getRedisConnection(): Promise { - if (redis.status === 'wait' || redis.status === 'connecting') { - await redis.connect(); - } - return redis; -} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts deleted file mode 100644 index e0b718fff..000000000 --- a/apps/api/src/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -import Fastify from 'fastify'; -import cors from '@fastify/cors'; -import helmet from '@fastify/helmet'; -import { authMiddleware } from './middleware/auth.middleware'; -import { rateLimitMiddleware } from './middleware/rate-limit.middleware'; -import { spamRateLimitMiddleware } from './middleware/spam-rate-limit.middleware'; -import { errorHandlingMiddleware } from './middleware/error-handling.middleware'; -import { loggingMiddleware } from './middleware/logging.middleware'; -import { apiEnv, loggingConfig } from './config/api.config'; -import { routes } from './routes'; - -const fastify = Fastify({ - logger: loggingConfig, - ignoreTrailingSlash: true, - maxParamLength: 500, -}); - -// Register plugins -async function registerPlugins() { - // CORS configuration - await fastify.register(cors, { - origin: apiEnv.CORS_ORIGIN, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - credentials: true, - }); - - // Security headers - await fastify.register(helmet, { - global: true, - contentSecurityPolicy: false, - }); - - // Rate limiting - await fastify.register(rateLimitMiddleware); - - // SpamShield rate limiting (Redis-backed) - await fastify.register(spamRateLimitMiddleware); - - // Authentication - await fastify.register(authMiddleware); - - // Logging - await fastify.register(loggingMiddleware); - - // Error handling - await fastify.register(errorHandlingMiddleware); -} - -// Register routes -async function registerRoutes() { - await fastify.register(routes, { prefix: '/api/v1' }); -} - -// Health check endpoint -fastify.get('/health', async () => { - return { status: 'ok', timestamp: new Date().toISOString() }; -}); - -// Root endpoint -fastify.get('/', async () => { - return { - name: 'FrenoCorp API Gateway', - version: '1.0.0', - environment: apiEnv.NODE_ENV, - }; -}); - -// Start server -async function start() { - await registerPlugins(); - await registerRoutes(); - - try { - await fastify.listen({ - port: apiEnv.PORT, - host: apiEnv.HOST, - }); - - console.log(`🚀 API Gateway running at http://${apiEnv.HOST}:${apiEnv.PORT}`); - console.log(`📝 Environment: ${apiEnv.NODE_ENV}`); - console.log(`📊 Rate limit window: ${apiEnv.API_RATE_LIMIT_WINDOW}ms`); - console.log(`📈 Max requests: ${apiEnv.API_RATE_LIMIT_MAX_REQUESTS}`); - } catch (err) { - console.error(err); - process.exit(1); - } -} - -// Graceful shutdown -const gracefulShutdown = async (signal: string) => { - console.log(`\n🛑 ${signal} received, shutting down gracefully...`); - await fastify.close(); - console.log('✅ Server closed'); - process.exit(0); -}; - -process.on('SIGINT', () => gracefulShutdown('SIGINT')); -process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); - -// Export for testing -export { fastify }; - -// Start if running directly -if (process.argv[1] === new URL(import.meta.url).pathname) { - start(); -} diff --git a/apps/api/src/middleware/auth.middleware.ts b/apps/api/src/middleware/auth.middleware.ts deleted file mode 100644 index c14e05829..000000000 --- a/apps/api/src/middleware/auth.middleware.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; - -export interface AuthRequest extends FastifyRequest { - user?: { - id: string; - email: string; - role: string; - organizationId?: string; - }; - apiKey?: string; - authType: 'jwt' | 'api-key' | 'anonymous'; -} - -export async function authMiddleware(fastify: FastifyInstance) { - // Authentication hook - fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - // Skip auth for health checks and root - const publicRoutes = ['/', '/health']; - if (publicRoutes.some((route) => request.url.startsWith(route))) { - authReq.authType = 'anonymous'; - return; - } - - // Try JWT authentication first - const authHeader = request.headers.authorization; - if (authHeader?.startsWith('Bearer ')) { - const token = authHeader.slice(7); - try { - // In production, decode and verify JWT - // For now, we'll attach a placeholder user - authReq.user = { - id: 'user-placeholder', - email: 'user@example.com', - role: 'user', - }; - authReq.authType = 'jwt'; - return; - } catch (err) { - // JWT invalid, continue to API key check - } - } - - // Try API key authentication - const apiKey = request.headers['x-api-key'] as string | undefined; - if (apiKey) { - // In production, validate API key against database - authReq.apiKey = apiKey; - authReq.user = { - id: `api-${apiKey}`, - email: `api-${apiKey}@services.internal`, - role: 'service', - }; - authReq.authType = 'api-key'; - return; - } - - // No auth found - attach anonymous user - authReq.authType = 'anonymous'; - authReq.user = { - id: 'anonymous', - email: 'anonymous@unknown', - role: 'anonymous', - }; - }); - - // Create auth decorator for route-level protection - fastify.decorate('requireAuth', async (request: AuthRequest) => { - if (request.authType === 'anonymous') { - throw { statusCode: 401, message: 'Authentication required' }; - } - return true; - }); - - fastify.decorate('requireRole', (allowedRoles: string[]) => { - return async (request: AuthRequest) => { - if (!request.user?.role || !allowedRoles.includes(request.user.role)) { - throw { - statusCode: 403, - message: `Role ${request.user?.role} not in allowed roles: ${allowedRoles.join(', ')}`, - }; - } - return true; - }; - }); -} diff --git a/apps/api/src/middleware/error-handling.middleware.ts b/apps/api/src/middleware/error-handling.middleware.ts deleted file mode 100644 index 6bd2cbbfa..000000000 --- a/apps/api/src/middleware/error-handling.middleware.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; - -export interface ErrorResponse { - error: string; - message: string; - statusCode: number; - code?: string; - details?: Record; - timestamp: string; - path: string; -} - -export async function errorHandlingMiddleware(fastify: FastifyInstance) { - // Custom error handler - fastify.setErrorHandler((error, request: FastifyRequest, reply: FastifyReply) => { - const response: ErrorResponse = { - error: error.name || 'Internal Server Error', - message: error.message || 'An unexpected error occurred', - statusCode: error.statusCode || 500, - code: (error as any).code, - timestamp: new Date().toISOString(), - path: request.url, - }; - - // Log error - fastify.log.error({ - error: response, - stack: error.stack, - method: request.method, - userAgent: request.headers['user-agent'], - }); - - // Send standardized error response - reply.status(response.statusCode).send(response); - }); - - // 404 handler - fastify.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => { - reply.status(404).send({ - error: 'Not Found', - message: `Route ${request.method} ${request.url} not found`, - statusCode: 404, - timestamp: new Date().toISOString(), - path: request.url, - }); - }); - - // Validation error handler - fastify.addHook('onError', async (request: FastifyRequest, reply: FastifyReply, error) => { - if (error.validation) { - reply.status(400).send({ - error: 'Validation Error', - message: 'Request validation failed', - statusCode: 400, - code: 'VALIDATION_ERROR', - details: error.validation, - timestamp: new Date().toISOString(), - path: request.url, - }); - } - }); -} diff --git a/apps/api/src/middleware/logging.middleware.ts b/apps/api/src/middleware/logging.middleware.ts deleted file mode 100644 index 20809d12f..000000000 --- a/apps/api/src/middleware/logging.middleware.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; - -export interface RequestLog { - method: string; - url: string; - statusCode: number; - responseTime: number; - requestId: string; - userAgent?: string; - clientIp: string; - requestIdHeader?: string; -} - -export async function loggingMiddleware(fastify: FastifyInstance) { - // Generate request ID if not present - fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply, done) => { - const requestId = - request.headers['x-request-id'] || - request.headers['x-correlation-id'] || - `req-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; - - request.headers['x-request-id'] = requestId; - (request as any).requestId = requestId; - - done(); - }); - - // Log request start - fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply) => { - fastify.log.info({ - event: 'request_start', - method: request.method, - url: request.url, - requestId: (request as any).requestId, - userAgent: request.headers['user-agent'], - clientIp: request.ip || request.headers['x-forwarded-for'] || 'unknown', - }); - }); - - // Log response - fastify.addHook('onResponse', (request: FastifyRequest, reply: FastifyReply, done) => { - const log: RequestLog = { - method: request.method, - url: request.url, - statusCode: reply.statusCode, - responseTime: reply.elapsedTime, - requestId: (request as any).requestId, - userAgent: request.headers['user-agent'], - clientIp: request.ip || request.headers['x-forwarded-for'] || 'unknown', - requestIdHeader: request.headers['x-request-id'] as string, - }; - - // Log based on status code - if (reply.statusCode < 300) { - fastify.log.info(log); - } else if (reply.statusCode < 400) { - fastify.log.warn(log); - } else if (reply.statusCode < 500) { - fastify.log.warn(log); - } else { - fastify.log.error(log); - } - - done(); - }); -} diff --git a/apps/api/src/middleware/rate-limit.middleware.ts b/apps/api/src/middleware/rate-limit.middleware.ts deleted file mode 100644 index 433f83373..000000000 --- a/apps/api/src/middleware/rate-limit.middleware.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { apiEnv, rateLimitConfig } from '../config/api.config'; - -// Simple in-memory rate limiter -// In production, this should use Redis or similar distributed store -class RateLimiter { - private store: Map; - - constructor() { - this.store = new Map(); - } - - async checkLimit( - key: string, - windowMs: number, - maxRequests: number - ): Promise<{ remaining: number; resetTime: number; retryAfter?: number }> { - const now = Date.now(); - const current = this.store.get(key); - - if (!current || now > current.resetTime) { - // Reset window - this.store.set(key, { - count: 1, - resetTime: now + windowMs, - }); - - return { - remaining: maxRequests - 1, - resetTime: now + windowMs, - }; - } - - // Increment counter - current.count++; - this.store.set(key, current); - - const remaining = maxRequests - current.count; - - if (current.count > maxRequests) { - return { - remaining: 0, - resetTime: current.resetTime, - retryAfter: current.resetTime - now, - }; - } - - return { - remaining, - resetTime: current.resetTime, - }; - } - - reset(key: string) { - this.store.delete(key); - } -} - -const rateLimiter = new RateLimiter(); - -export async function rateLimitMiddleware(fastify: FastifyInstance) { - fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => { - // Skip rate limiting for health checks - if (request.url === '/health') { - return; - } - - // Get client identifier (IP or API key) - const clientIp = request.ip || request.headers['x-forwarded-for'] || 'unknown'; - const apiKey = request.headers['x-api-key'] as string | undefined; - const key = apiKey ? `api:${apiKey}` : `ip:${clientIp}`; - - // Determine tier based on API key or default to basic - let tier = 'basic'; - if (apiKey) { - // In production, fetch tier from user/service lookup - // For now, use a simple heuristic based on key format - if (apiKey.startsWith('premium_')) { - tier = 'premium'; - } else if (apiKey.startsWith('plus_')) { - tier = 'plus'; - } - } - - const config = rateLimitConfig[tier as keyof typeof rateLimitConfig]; - const result = await rateLimiter.checkLimit( - key, - config.windowMs, - config.maxRequests - ); - - // Set rate limit headers - reply.header('X-RateLimit-Limit', config.maxRequests); - reply.header('X-RateLimit-Remaining', result.remaining); - reply.header('X-RateLimit-Reset', Math.ceil(result.resetTime / 1000)); - - if (result.retryAfter) { - reply.header('Retry-After', Math.ceil(result.retryAfter / 1000)); - reply.code(429); // Too Many Requests - - return { - error: 'Too Many Requests', - message: `Rate limit exceeded. Try again in ${Math.ceil(result.retryAfter / 1000)}s`, - tier, - limit: config.maxRequests, - reset: new Date(result.resetTime).toISOString(), - }; - } - - // Add tier info to request for downstream use - (request as any).rateLimitTier = tier; - }); -} - -// Export for testing -export { rateLimiter }; diff --git a/apps/api/src/middleware/spam-rate-limit.middleware.ts b/apps/api/src/middleware/spam-rate-limit.middleware.ts deleted file mode 100644 index f56e373e5..000000000 --- a/apps/api/src/middleware/spam-rate-limit.middleware.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { redis } from '../config/redis'; -import { spamRateLimits } from '../services/spamshield/spamshield.config'; - -const REDIS_PREFIX = 'spamshield:ratelimit'; - -class RedisRateLimiter { - async checkLimit( - key: string, - windowSeconds: number, - maxRequests: number - ): Promise<{ - remaining: number; - resetTime: number; - retryAfter?: number; - }> { - const redisKey = `${REDIS_PREFIX}:${key}`; - const now = Date.now(); - - const current = await redis.get(redisKey); - const windowStart = now - (now % (windowSeconds * 1000)); - const resetTime = windowStart + windowSeconds * 1000; - - if (!current) { - const expirySeconds = Math.ceil((resetTime - now) / 1000); - await redis.set(redisKey, '1', 'EX', expirySeconds); - - return { - remaining: maxRequests - 1, - resetTime, - }; - } - - const count = parseInt(current, 10) + 1; - await redis.set(redisKey, String(count), 'EX', Math.ceil((resetTime - now) / 1000)); - - const remaining = maxRequests - count; - - if (count > maxRequests) { - return { - remaining: 0, - resetTime, - retryAfter: resetTime - now, - }; - } - - return { - remaining, - resetTime, - }; - } - - async checkDailyLimit( - key: string, - maxPerDay: number - ): Promise<{ - remaining: number; - retryAfter?: number; - }> { - const redisKey = `${REDIS_PREFIX}:daily:${key}`; - const now = Date.now(); - const dayStart = new Date(now); - dayStart.setHours(0, 0, 0, 0); - const dayEnd = new Date(dayStart); - dayEnd.setDate(dayEnd.getDate() + 1); - const resetTime = dayEnd.getTime(); - - const current = await redis.get(redisKey); - const expirySeconds = Math.ceil((resetTime - now) / 1000); - - if (!current) { - await redis.set(redisKey, '1', 'EX', expirySeconds); - - return { - remaining: maxPerDay - 1, - }; - } - - const count = parseInt(current, 10) + 1; - await redis.set(redisKey, String(count), 'EX', expirySeconds); - - const remaining = maxPerDay - count; - - if (count > maxPerDay) { - return { - remaining: 0, - retryAfter: resetTime - now, - }; - } - - return { - remaining, - }; - } - - reset(key: string) { - const redisKey = `${REDIS_PREFIX}:${key}`; - return redis.del(redisKey); - } -} - -export const spamRateLimiter = new RedisRateLimiter(); - -export async function spamRateLimitMiddleware(fastify: FastifyInstance) { - fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => { - const url = request.url || ''; - - if (!url.startsWith('/spamshield')) { - return; - } - - const clientIp = request.ip || (request.headers['x-forwarded-for'] as string) || 'unknown'; - const apiKey = request.headers['x-api-key'] as string | undefined; - const key = apiKey ? `api:${apiKey}` : `ip:${clientIp}`; - - let tier = 'basic'; - if (apiKey) { - if (apiKey.startsWith('premium_')) { - tier = 'premium'; - } else if (apiKey.startsWith('plus_')) { - tier = 'plus'; - } - } - - const config = spamRateLimits[tier as keyof typeof spamRateLimits]; - - const minuteResult = await spamRateLimiter.checkLimit( - key, - 60, - config.analysesPerMinute - ); - - const dailyResult = await spamRateLimiter.checkDailyLimit( - key, - config.analysesPerDay - ); - - reply.header('X-RateLimit-Limit', config.analysesPerMinute); - reply.header('X-RateLimit-Remaining', minuteResult.remaining); - reply.header('X-RateLimit-Reset', Math.ceil(minuteResult.resetTime / 1000)); - reply.header('X-RateLimit-Daily-Limit', config.analysesPerDay); - reply.header('X-RateLimit-Daily-Remaining', dailyResult.remaining); - - const retryAfter = minuteResult.retryAfter || dailyResult.retryAfter; - - if (retryAfter) { - reply.header('Retry-After', Math.ceil(retryAfter / 1000)); - reply.code(429); - - return { - error: 'Too Many Requests', - message: `Spam analysis rate limit exceeded. Try again in ${Math.ceil(retryAfter / 1000)}s`, - tier, - limit: config.analysesPerMinute, - dailyLimit: config.analysesPerDay, - reset: new Date(minuteResult.resetTime).toISOString(), - }; - } - - (request as any).spamRateLimitTier = tier; - }); -} - -export { RedisRateLimiter }; diff --git a/apps/api/src/routes/darkwatch.routes.ts b/apps/api/src/routes/darkwatch.routes.ts deleted file mode 100644 index 161b020d8..000000000 --- a/apps/api/src/routes/darkwatch.routes.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { prisma, SubscriptionTier } from '@shieldsai/shared-db'; -import { tierConfig, SubscriptionTier as BillingTier } from '@shieldsai/shared-billing'; -import { - watchlistService, - scanService, - schedulerService, - webhookService, -} from '../services/darkwatch'; - -export async function darkwatchRoutes(fastify: FastifyInstance) { - const authed = async ( - request: FastifyRequest, - reply: FastifyReply - ): Promise => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - reply.code(401).send({ error: 'User ID required' }); - return null; - } - - const subscription = await prisma.subscription.findFirst({ - where: { userId, status: 'active' }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - reply.code(404).send({ error: 'Active subscription not found' }); - return null; - } - - return subscription.id; - }; - - // GET /darkwatch/watchlist - List watchlist items - fastify.get('/watchlist', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - try { - const items = await watchlistService.getItems(subscriptionId); - return reply.send({ items }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to list watchlist'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /darkwatch/watchlist - Add watchlist item - fastify.post('/watchlist', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const subscription = await prisma.subscription.findFirst({ - where: { userId, status: 'active' }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - return reply.code(404).send({ error: 'Active subscription not found' }); - } - - const body = request.body as { type: string; value: string }; - - if (!body.type || !body.value) { - return reply.code(400).send({ error: 'type and value are required' }); - } - - const maxItems = tierConfig[subscription.tier as BillingTier].features.maxWatchlistItems; - - try { - const item = await watchlistService.addItem( - subscription.id, - body.type, - body.value, - maxItems - ); - return reply.code(201).send({ item }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to add watchlist item'; - return reply.code(422).send({ error: message }); - } - }); - - // DELETE /darkwatch/watchlist/:id - Remove watchlist item - fastify.delete('/watchlist/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - const id = (request.params as { id: string }).id; - - try { - const item = await watchlistService.removeItem(id, subscriptionId); - return reply.send({ item }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to remove watchlist item'; - return reply.code(422).send({ error: message }); - } - }); - - // POST /darkwatch/scan - Trigger on-demand scan - fastify.post('/scan', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - try { - const job = await schedulerService.enqueueOnDemandScan(subscriptionId); - return reply.send({ - job: { - id: job?.id, - status: 'queued', - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to trigger scan'; - return reply.code(422).send({ error: message }); - } - }); - - // GET /darkwatch/scan/schedule - Get scan schedule - fastify.get('/scan/schedule', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - try { - const schedule = await schedulerService.getScanSchedule(subscriptionId); - return reply.send({ schedule }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get schedule'; - return reply.code(500).send({ error: message }); - } - }); - - // GET /darkwatch/exposures - List exposures - fastify.get('/exposures', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - try { - const exposures = await prisma.exposure.findMany({ - where: { subscriptionId }, - orderBy: { detectedAt: 'desc' }, - take: 50, - include: { - watchlistItem: true, - }, - }); - return reply.send({ exposures }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to list exposures'; - return reply.code(500).send({ error: message }); - } - }); - - // GET /darkwatch/alerts - List alerts - fastify.get('/alerts', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - try { - const alerts = await prisma.alert.findMany({ - where: { userId }, - orderBy: { createdAt: 'desc' }, - take: 50, - include: { - exposure: true, - }, - }); - return reply.send({ alerts }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to list alerts'; - return reply.code(500).send({ error: message }); - } - }); - - // PATCH /darkwatch/alerts/:id/read - Mark alert as read - fastify.patch('/alerts/:id/read', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const id = (request.params as { id: string }).id; - - try { - const alert = await prisma.alert.update({ - where: { id }, - data: { isRead: true, readAt: new Date() }, - }); - return reply.send({ alert }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to mark alert as read'; - return reply.code(422).send({ error: message }); - } - }); - - // POST /darkwatch/webhook - External webhook receiver - fastify.post('/webhook', async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as Record; - - const source = typeof body.source === 'string' ? body.source : ''; - const identifier = typeof body.identifier === 'string' ? body.identifier : ''; - const identifierType = typeof body.identifierType === 'string' ? body.identifierType : ''; - const metadata = body.metadata as Record | undefined; - const timestamp = typeof body.timestamp === 'string' ? body.timestamp : new Date().toISOString(); - - if (!source || !identifier || !identifierType) { - return reply.code(400).send({ - error: 'source, identifier, and identifierType are required', - }); - } - - const signature = request.headers['x-webhook-signature'] as string | undefined; - const webhookTimestamp = request.headers['x-webhook-timestamp'] as string | undefined; - - if (!signature || !webhookTimestamp) { - return reply.code(401).send({ error: 'Webhook signature and timestamp required' }); - } - - const valid = await webhookService.verifyWebhookSignature( - JSON.stringify(body), - signature, - webhookTimestamp - ); - if (!valid) { - return reply.code(401).send({ error: 'Invalid webhook signature' }); - } - - try { - const result = await webhookService.processExternalWebhook({ - source, - identifier, - identifierType, - metadata, - timestamp, - }); - - return reply.send({ - processed: true, - exposuresCreated: result.exposuresCreated, - alertsCreated: result.alertsCreated, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Webhook processing failed'; - console.error('[DarkWatch:Webhook] Error:', message); - return reply.code(500).send({ error: 'Webhook processing failed' }); - } - }); - - // POST /darkwatch/scheduler/init - Initialize scheduled scans for all subscriptions - fastify.post('/scheduler/init', async (request: FastifyRequest, reply: FastifyReply) => { - try { - const jobsEnqueued = await schedulerService.scheduleSubscriptionScans(); - return reply.send({ - scheduled: jobsEnqueued.length, - jobs: jobsEnqueued, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Scheduler init failed'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /darkwatch/scheduler/reschedule - Reschedule all scans - fastify.post('/scheduler/reschedule', async (request: FastifyRequest, reply: FastifyReply) => { - try { - const jobsEnqueued = await schedulerService.rescheduleAll(); - return reply.send({ - rescheduled: jobsEnqueued.length, - jobs: jobsEnqueued, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Scheduler reschedule failed'; - return reply.code(500).send({ error: message }); - } - }); -} diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts deleted file mode 100644 index cfde1ed1c..000000000 --- a/apps/api/src/routes/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { authMiddleware, AuthRequest } from './auth.middleware'; -import { voiceprintRoutes } from './voiceprint.routes'; -import { spamshieldRoutes } from './spamshield.routes'; -import { darkwatchRoutes } from './darkwatch.routes'; - -export async function routes(fastify: FastifyInstance) { - // Authenticated routes group - fastify.register( - async (authenticated) => { - // Add auth requirement - authenticated.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { - await fastify.requireAuth(request as AuthRequest); - }); - - // Example authenticated endpoint - authenticated.get('/user/me', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - return { - user: authReq.user, - authType: authReq.authType, - }; - }); - - // Example service endpoint - authenticated.get('/services', async (request: FastifyRequest, reply: FastifyReply) => { - return { - services: [ - { - name: 'user-service', - url: '/api/v1/services/user', - status: 'healthy', - }, - { - name: 'billing-service', - url: '/api/v1/services/billing', - status: 'healthy', - }, - { - name: 'notification-service', - url: '/api/v1/services/notifications', - status: 'healthy', - }, - ], - }; - }); - }, - { prefix: '/auth' } - ); - - // Public API routes - fastify.register( - async (publicRouter) => { - // Version info - publicRouter.get('/info', async () => { - return { - version: '1.0.0', - environment: process.env.NODE_ENV || 'development', - build: process.env.npm_package_version || 'unknown', - }; - }); - - // API documentation - publicRouter.get('/docs', async () => { - return { - title: 'FrenoCorp API Gateway', - version: '1.0.0', - endpoints: { - public: [ - { method: 'GET', path: '/', description: 'Root endpoint' }, - { method: 'GET', path: '/health', description: 'Health check' }, - { method: 'GET', path: '/api/v1/info', description: 'API version info' }, - { method: 'GET', path: '/api/v1/docs', description: 'API documentation' }, - ], - authenticated: [ - { method: 'GET', path: '/api/v1/auth/user/me', description: 'Get current user' }, - { method: 'GET', path: '/api/v1/auth/services', description: 'List available services' }, - ], - }, - }; - }); - }, - { prefix: '/api/v1' } - ); - - // Service proxy placeholder (for future microservice routing) - fastify.register( - async (services) => { - services.get('/services/user', async (request, reply) => { - // In production, proxy to actual user service - return { - service: 'user-service', - message: 'User service endpoint', - timestamp: new Date().toISOString(), - }; - }); - - services.get('/services/billing', async (request, reply) => { - // In production, proxy to actual billing service - return { - service: 'billing-service', - message: 'Billing service endpoint', - timestamp: new Date().toISOString(), - }; - }); - - services.get('/services/notifications', async (request, reply) => { - // In production, proxy to actual notification service - return { - service: 'notification-service', - message: 'Notification service endpoint', - timestamp: new Date().toISOString(), - }; - }); - }, - { prefix: '/api/v1/services' } - ); - - // VoicePrint service routes - fastify.register( - async (voiceprintRouter) => { - await voiceprintRoutes(voiceprintRouter); - }, - { prefix: '/voiceprint' } - ); - - // SpamShield service routes - fastify.register( - async (spamshieldRouter) => { - await spamshieldRoutes(spamshieldRouter); - }, - { prefix: '/spamshield' } - ); - - // DarkWatch service routes - fastify.register( - async (darkwatchRouter) => { - await darkwatchRoutes(darkwatchRouter); - }, - { prefix: '/darkwatch' } - ); -} diff --git a/apps/api/src/routes/notifications.routes.ts b/apps/api/src/routes/notifications.routes.ts deleted file mode 100644 index b99a9919b..000000000 --- a/apps/api/src/routes/notifications.routes.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { FastifyInstance } from 'fastify'; -import { NotificationService } from '@shieldsai/shared-notifications'; - -export async function notificationRoutes(fastify: FastifyInstance): Promise { - let notificationService: NotificationService | undefined; - - // Initialize notification service (will be injected via config) - fastify.addHook('onReady', async () => { - // Notification service will be initialized from config - notificationService = fastify.notificationService; - }); - - /** - * POST /api/v1/notifications/send - * Send a notification to a user - */ - fastify.post( - '/notifications/send', - { - schema: { - body: { - type: 'object', - required: ['userId', 'channel', 'subject', 'body'], - properties: { - userId: { type: 'string' }, - channel: { type: 'string', enum: ['email', 'push', 'sms'] }, - subject: { type: 'string' }, - body: { type: 'string' }, - email: { type: 'string' }, - phone: { type: 'string' }, - fcmToken: { type: 'string' }, - apnsToken: { type: 'string' }, - priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] }, - metadata: { type: 'object' }, - }, - }, - }, - }, - async (request, reply) => { - const { userId, channel, subject, body, priority, metadata } = request.body; - - const recipient = { - userId, - email: request.body.email, - phone: request.body.phone, - fcmToken: request.body.fcmToken, - apnsToken: request.body.apnsToken, - }; - - try { - if (!notificationService) { - return reply.status(503).send({ - success: false, - error: 'Notification service not initialized', - }); - } - - const notifications = await notificationService.sendMultiChannelNotification( - recipient, - channel, - subject, - body, - priority, - metadata - ); - - return reply.send({ - success: true, - notifications, - }); - } catch (error) { - return reply.status(500).send({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - /** - * GET /api/v1/notifications/:userId/preferences - * Get notification preferences for a user - */ - fastify.get( - '/notifications/:userId/preferences', - { - schema: { - params: { - type: 'object', - required: ['userId'], - properties: { - userId: { type: 'string' }, - }, - }, - }, - }, - async (request, reply) => { - const { userId } = request.params; - - try { - if (!notificationService) { - return reply.status(503).send({ - success: false, - error: 'Notification service not initialized', - }); - } - - const preferences = await notificationService.getNotificationPreferences(userId); - - return reply.send({ - success: true, - preferences, - }); - } catch (error) { - return reply.status(500).send({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - /** - * PUT /api/v1/notifications/:userId/preferences - * Update notification preferences for a user - */ - fastify.put( - '/notifications/:userId/preferences', - { - schema: { - params: { - type: 'object', - required: ['userId'], - properties: { - userId: { type: 'string' }, - }, - }, - body: { - type: 'object', - properties: { - email: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - categories: { type: 'array', items: { type: 'string' } }, - }, - }, - push: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - categories: { type: 'array', items: { type: 'string' } }, - }, - }, - sms: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - categories: { type: 'array', items: { type: 'string' } }, - }, - }, - }, - }, - }, - }, - async (request, reply) => { - const { userId } = request.params; - const updates = request.body; - - try { - // TODO: Update preferences in database - return reply.send({ - success: true, - message: 'Preferences updated', - userId, - updates, - }); - } catch (error) { - return reply.status(500).send({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - /** - * GET /api/v1/notifications/config - * Get notification configuration status - */ - fastify.get('/notifications/config', async (request, reply) => { - try { - if (!notificationService) { - return reply.status(503).send({ - success: false, - error: 'Notification service not initialized', - }); - } - - const config = notificationService.getConfigSummary(); - - return reply.send({ - success: true, - config, - }); - } catch (error) { - return reply.status(500).send({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - }); -} diff --git a/apps/api/src/routes/spamshield.routes.ts b/apps/api/src/routes/spamshield.routes.ts deleted file mode 100644 index 65348526e..000000000 --- a/apps/api/src/routes/spamshield.routes.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { - numberReputationService, - smsClassifierService, - callAnalysisService, - spamFeedbackService, -} from '../services/spamshield'; -import { ErrorHandler, SpamErrorCode } from '../services/spamshield/spamshield.error-handler'; - -export async function spamshieldRoutes(fastify: FastifyInstance) { - // Classify SMS text - fastify.post('/sms/classify', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const body = request.body as { text: string }; - - const textValidation = ErrorHandler.validateRequiredField(body.text, 'text'); - if (!textValidation.isValid && textValidation.error) { - ErrorHandler.send(reply, textValidation.error.code, textValidation.error.message, { - field: textValidation.error.field, - status: 400, - }); - return; - } - - try { - const result = await smsClassifierService.classify(body.text); - return reply.send({ - classification: { - isSpam: result.isSpam, - confidence: result.confidence, - spamFeatures: result.spamFeatures, - }, - }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.CLASSIFICATION_FAILED, 'Classification failed', { - status: 422, - }); - } - }); - - // Check number reputation - fastify.post('/number/reputation', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const body = request.body as { phoneNumber: string }; - - const phoneValidation = ErrorHandler.validateRequiredField(body.phoneNumber, 'phoneNumber'); - if (!phoneValidation.isValid && phoneValidation.error) { - ErrorHandler.send(reply, phoneValidation.error.code, phoneValidation.error.message, { - field: phoneValidation.error.field, - status: 400, - }); - return; - } - - try { - const result = await numberReputationService.checkReputation(body.phoneNumber); - return reply.send({ - reputation: { - isSpam: result.isSpam, - confidence: result.confidence, - spamType: result.spamType, - reportCount: result.reportCount, - }, - }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.REPUTATION_CHECK_FAILED, 'Reputation check failed', { - status: 422, - }); - } - }); - - // Analyze incoming call - fastify.post('/call/analyze', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const body = request.body as { - phoneNumber: string; - duration?: number; - callTime: string; - isVoip?: boolean; - }; - - const phoneValidation = ErrorHandler.validateRequiredField(body.phoneNumber, 'phoneNumber'); - const callTimeValidation = ErrorHandler.validateRequiredField(body.callTime, 'callTime'); - - if (!phoneValidation.isValid && phoneValidation.error) { - ErrorHandler.send(reply, phoneValidation.error.code, phoneValidation.error.message, { - field: phoneValidation.error.field, - status: 400, - }); - return; - } - - if (!callTimeValidation.isValid && callTimeValidation.error) { - ErrorHandler.send(reply, callTimeValidation.error.code, callTimeValidation.error.message, { - field: callTimeValidation.error.field, - status: 400, - }); - return; - } - - try { - const result = await callAnalysisService.analyzeCall({ - phoneNumber: body.phoneNumber, - duration: body.duration, - callTime: new Date(body.callTime), - isVoip: body.isVoip, - }); - return reply.send({ - analysis: { - decision: result.decision, - confidence: result.confidence, - reasons: result.reasons, - }, - }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.ANALYSIS_FAILED, 'Call analysis failed', { - status: 422, - }); - } - }); - - // Record spam feedback - fastify.post('/feedback', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const body = request.body as { - phoneNumber: string; - isSpam: boolean; - confidence?: number; - metadata?: Record; - }; - - const phoneValidation = ErrorHandler.validateRequiredField(body.phoneNumber, 'phoneNumber'); - if (!phoneValidation.isValid && phoneValidation.error) { - ErrorHandler.send(reply, phoneValidation.error.code, phoneValidation.error.message, { - field: phoneValidation.error.field, - status: 400, - }); - return; - } - - const isSpamValidation = ErrorHandler.validateBooleanField(body.isSpam, 'isSpam'); - if (!isSpamValidation.isValid && isSpamValidation.error) { - ErrorHandler.send(reply, isSpamValidation.error.code, isSpamValidation.error.message, { - field: isSpamValidation.error.field, - status: 400, - }); - return; - } - - try { - const feedback = await spamFeedbackService.recordFeedback( - userId, - body.phoneNumber, - body.isSpam, - body.confidence, - body.metadata - ); - return reply.code(201).send({ - feedback: { - id: feedback.id, - phoneNumber: feedback.phoneNumber, - isSpam: feedback.isSpam, - createdAt: feedback.createdAt, - }, - }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.FEEDBACK_RECORD_FAILED, 'Feedback recording failed', { - status: 422, - }); - } - }); - - // Get spam history - fastify.get('/history', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const query = request.query as { - limit?: string; - isSpam?: string; - startDate?: string; - }; - - const results = await spamFeedbackService.getSpamHistory(userId, { - limit: query.limit ? parseInt(query.limit, 10) : undefined, - isSpam: query.isSpam !== undefined ? query.isSpam === 'true' : undefined, - startDate: query.startDate ? new Date(query.startDate) : undefined, - }); - - return reply.send({ - history: results.map((r) => ({ - id: r.id, - phoneNumber: r.phoneNumber, - isSpam: r.isSpam, - createdAt: r.createdAt, - })), - }); - }); - - // Get spam statistics - fastify.get('/statistics', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - try { - const stats = await spamFeedbackService.getStatistics(userId); - return reply.send({ statistics: stats }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.ANALYSIS_FAILED, 'Statistics retrieval failed', { - status: 422, - }); - } - }); -} diff --git a/apps/api/src/routes/voiceprint.routes.ts b/apps/api/src/routes/voiceprint.routes.ts deleted file mode 100644 index dcdd4838a..000000000 --- a/apps/api/src/routes/voiceprint.routes.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { - voiceEnrollmentService, - analysisService, - batchAnalysisService, - voicePrintEnv, - AnalysisJobStatus, -} from '../services/voiceprint'; - -export async function voiceprintRoutes(fastify: FastifyInstance) { - // Enroll a new voice profile - fastify.post('/enroll', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const body = request.body as { - name: string; - audio: Buffer; - }; - - if (!body.name || !body.audio) { - return reply.code(400).send({ error: 'name and audio are required' }); - } - - try { - const enrollment = await voiceEnrollmentService.enroll( - userId, - body.name, - body.audio - ); - return reply.code(201).send({ - enrollment: { - id: enrollment.id, - name: enrollment.name, - isActive: enrollment.isActive, - createdAt: enrollment.createdAt, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Enrollment failed'; - return reply.code(422).send({ error: message }); - } - }); - - // List user's voice enrollments - fastify.get('/enrollments', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const isActive = request.query as { isActive?: string }; - const limit = request.query as { limit?: string }; - const offset = request.query as { offset?: string }; - - const enrollments = await voiceEnrollmentService.listEnrollments(userId, { - isActive: isActive.isActive !== undefined - ? isActive.isActive === 'true' - : undefined, - limit: limit.limit ? parseInt(limit.limit, 10) : undefined, - offset: offset.offset ? parseInt(offset.offset, 10) : undefined, - }); - - return reply.send({ - enrollments: enrollments.map((e) => ({ - id: e.id, - name: e.name, - isActive: e.isActive, - createdAt: e.createdAt, - })), - }); - }); - - // Remove an enrollment - fastify.delete('/enrollments/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const enrollmentId = (request.params as { id: string }).id; - - try { - const enrollment = await voiceEnrollmentService.removeEnrollment( - enrollmentId, - userId - ); - return reply.send({ - enrollment: { - id: enrollment.id, - name: enrollment.name, - isActive: enrollment.isActive, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Removal failed'; - return reply.code(404).send({ error: message }); - } - }); - - // Analyze a single audio file - fastify.post('/analyze', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const body = request.body as { - audio: Buffer; - enrollmentId?: string; - audioUrl?: string; - }; - - if (!body.audio) { - return reply.code(400).send({ error: 'audio is required' }); - } - - try { - const result = await analysisService.analyze(userId, body.audio, { - enrollmentId: body.enrollmentId, - audioUrl: body.audioUrl, - }); - return reply.code(201).send({ - analysis: { - id: result.id, - isSynthetic: result.isSynthetic, - confidence: result.confidence, - analysisResult: result.analysisResult, - createdAt: result.createdAt, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Analysis failed'; - return reply.code(422).send({ error: message }); - } - }); - - // Get analysis result by ID - fastify.get('/results/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const analysisId = (request.params as { id: string }).id; - const result = await analysisService.getResult(analysisId, userId); - - if (!result) { - return reply.code(404).send({ error: 'Analysis not found' }); - } - - return reply.send({ - analysis: { - id: result.id, - isSynthetic: result.isSynthetic, - confidence: result.confidence, - analysisResult: result.analysisResult, - createdAt: result.createdAt, - }, - }); - }); - - // Get analysis history - fastify.get('/history', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const query = request.query as { - limit?: string; - offset?: string; - isSynthetic?: string; - }; - - const results = await analysisService.getHistory(userId, { - limit: query.limit ? parseInt(query.limit, 10) : undefined, - offset: query.offset ? parseInt(query.offset, 10) : undefined, - isSynthetic: query.isSynthetic !== undefined - ? query.isSynthetic === 'true' - : undefined, - }); - - return reply.send({ - analyses: results.map((r) => ({ - id: r.id, - isSynthetic: r.isSynthetic, - confidence: r.confidence, - createdAt: r.createdAt, - })), - }); - }); - - // Batch analyze multiple audio files - fastify.post('/batch', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const body = request.body as { - files: Array<{ - name: string; - audio: Buffer; - audioUrl?: string; - }>; - enrollmentId?: string; - }; - - if (!body.files || body.files.length === 0) { - return reply.code(400).send({ error: 'files array is required' }); - } - - try { - const result = await batchAnalysisService.analyzeBatch( - userId, - body.files.map((f) => ({ - name: f.name, - buffer: f.audio, - audioUrl: f.audioUrl, - })), - { - enrollmentId: body.enrollmentId, - } - ); - - return reply.code(201).send({ - jobId: result.jobId, - results: result.results.map((r) => ({ - id: r.id, - isSynthetic: r.isSynthetic, - confidence: r.confidence, - })), - summary: result.summary, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Batch analysis failed'; - return reply.code(422).send({ error: message }); - } - }); -} diff --git a/apps/api/src/services/darkwatch/alert.pipeline.ts b/apps/api/src/services/darkwatch/alert.pipeline.ts deleted file mode 100644 index c91a80992..000000000 --- a/apps/api/src/services/darkwatch/alert.pipeline.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { prisma, AlertType, AlertSeverity } from '@shieldsai/shared-db'; -import { - NotificationService, - NotificationPriority, - loadNotificationConfig, -} from '@shieldsai/shared-notifications'; - -const ALERT_DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000; - -export class AlertPipeline { - private notificationService: NotificationService; - - constructor() { - this.notificationService = new NotificationService(loadNotificationConfig()); - } - - async processNewExposures(exposureIds: string[]) { - const exposures = await prisma.exposure.findMany({ - where: { id: { in: exposureIds }, isFirstTime: true }, - include: { - subscription: { - select: { - id: true, - userId: true, - tier: true, - }, - }, - watchlistItem: true, - }, - }); - - const alertsCreated: Awaited>[] = []; - - for (const exposure of exposures) { - const dedupKey = `exposure:${exposure.subscriptionId}:${exposure.source}:${exposure.identifierHash}`; - - const recentAlert = await prisma.alert.findFirst({ - where: { - subscriptionId: exposure.subscriptionId, - type: AlertType.exposure_detected, - createdAt: { - gte: new Date(Date.now() - ALERT_DEDUP_WINDOW_MS), - }, - }, - orderBy: { createdAt: 'desc' }, - }); - - if (recentAlert) { - continue; - } - - const alert = await prisma.alert.create({ - data: { - subscriptionId: exposure.subscriptionId, - userId: exposure.subscription.userId, - exposureId: exposure.id, - type: AlertType.exposure_detected, - title: this.buildTitle(exposure), - message: this.buildMessage(exposure), - severity: this.mapSeverity(exposure.severity), - channel: this.getChannelsForTier(exposure.subscription.tier), - }, - }); - - alertsCreated.push(alert); - - await this.dispatchNotification(alert, exposure); - } - - return alertsCreated; - } - - async dispatchScanCompleteAlert( - subscriptionId: string, - userId: string, - exposuresFound: number - ) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) return; - - const alert = await prisma.alert.create({ - data: { - subscriptionId, - userId, - type: AlertType.scan_complete, - title: 'DarkWatch Scan Complete', - message: `Scan found ${exposuresFound} new exposure${exposuresFound === 1 ? '' : 's'}.`, - severity: exposuresFound > 0 ? 'warning' : 'info', - channel: this.getChannelsForTier(subscription.tier), - }, - }); - - await this.dispatchNotification(alert, { - source: 'hibp', - severity: 'info', - identifier: '', - dataType: 'email', - } as any); - - return alert; - } - - private async dispatchNotification( - alert: { - userId: string; - channel: string[]; - title: string; - message: string; - severity: AlertSeverity; - }, - exposure: { source: string; severity: string; identifier: string; dataType: string } - ) { - try { - if (!this.notificationService.isFullyConfigured()) return; - - await this.notificationService.sendMultiChannelNotification( - { - userId: alert.userId, - }, - alert.channel as any, - alert.title, - `

${alert.message}

-

Source: ${exposure.source}

-

Severity: ${exposure.severity}

-

Type: ${exposure.dataType}

`, - alert.severity === 'critical' - ? NotificationPriority.HIGH - : NotificationPriority.NORMAL - ); - } catch (error) { - console.error('[AlertPipeline] Notification dispatch error:', error); - } - } - - private buildTitle(exposure: { - source: string; - dataType: string; - severity: string; - }): string { - return `${exposure.severity.toUpperCase()}: ${exposure.dataType} exposure on ${exposure.source}`; - } - - private buildMessage(exposure: { - identifier: string; - source: string; - severity: string; - dataType: string; - }): string { - const masked = exposure.identifier.includes('@') - ? exposure.identifier.replace(/(?<=.{2}).*(?=@)/, '***') - : exposure.identifier.slice(0, 3) + '***'; - - return `Your ${exposure.dataType} (${masked}) was found in a ${exposure.source} breach with ${exposure.severity} severity.`; - } - - private mapSeverity(severity: string): AlertSeverity { - return severity as AlertSeverity; - } - - private getChannelsForTier(tier: string): string[] { - const channelMap: Record = { - basic: ['email'], - plus: ['email', 'push'], - premium: ['email', 'push', 'sms'], - }; - return channelMap[tier] || ['email']; - } -} - -export const alertPipeline = new AlertPipeline(); diff --git a/apps/api/src/services/darkwatch/index.ts b/apps/api/src/services/darkwatch/index.ts deleted file mode 100644 index f18ad02c1..000000000 --- a/apps/api/src/services/darkwatch/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { watchlistService } from './watchlist.service'; -export { scanService } from './scan.service'; -export { schedulerService } from './scheduler.service'; -export { webhookService } from './webhook.service'; -export { alertPipeline } from './alert.pipeline'; diff --git a/apps/api/src/services/darkwatch/scan.service.ts b/apps/api/src/services/darkwatch/scan.service.ts deleted file mode 100644 index d3b518258..000000000 --- a/apps/api/src/services/darkwatch/scan.service.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { prisma, ExposureSource, ExposureSeverity, WatchlistType } from '@shieldsai/shared-db'; -import { createHash } from 'crypto'; - -function hashIdentifier(identifier: string): string { - return createHash('sha256').update(identifier.toLowerCase().trim()).digest('hex'); -} - -function determineSeverity( - source: ExposureSource, - dataType: WatchlistType -): ExposureSeverity { - const criticalSources = [ExposureSource.darkWebForum, ExposureSource.honeypot]; - const warningSources = [ExposureSource.hibp, ExposureSource.shodan]; - const criticalTypes = [WatchlistType.ssn]; - - if (criticalTypes.includes(dataType)) return ExposureSeverity.critical; - if (criticalSources.includes(source)) return ExposureSeverity.critical; - if (warningSources.includes(source)) return ExposureSeverity.warning; - return ExposureSeverity.info; -} - -export class ScanService { - async checkHIBP(email: string): Promise<{ exposed: boolean; sources: string[] }> { - try { - const response = await fetch( - `https://hibp.com/api/v2/${encodeURIComponent(email)}`, - { - headers: { - 'hibp-api-key': process.env.HIBP_API_KEY || '', - Accept: 'application/json', - }, - signal: AbortSignal.timeout(15000), - } - ); - - if (response.status === 404) { - return { exposed: false, sources: [] }; - } - - if (!response.ok) { - console.error(`[ScanService:HIBP] Status ${response.status} for ${email}`); - return { exposed: false, sources: [] }; - } - - const data = await response.json(); - const sources = Array.isArray(data) - ? data.map((p: { Name: string }) => p.Name) - : []; - - return { exposed: sources.length > 0, sources }; - } catch (error) { - console.error('[ScanService:HIBP] Error:', error); - return { exposed: false, sources: [] }; - } - } - - async checkShodan(domain: string): Promise<{ exposed: boolean; ports: string[]; ips: string[] }> { - try { - const response = await fetch( - `https://api.shodan.io/shodan/host/${encodeURIComponent(domain)}`, - { - headers: { - Authorization: `Bearer ${process.env.SHODAN_API_KEY || ''}`, - }, - signal: AbortSignal.timeout(15000), - } - ); - - if (response.status === 404) { - return { exposed: false, ports: [], ips: [] }; - } - - if (!response.ok) { - console.error(`[ScanService:Shodan] Status ${response.status} for ${domain}`); - return { exposed: false, ports: [], ips: [] }; - } - - const data = await response.json(); - return { - exposed: !!data.ip_str, - ports: data.ports?.map(String) || [], - ips: [data.ip_str || ''], - }; - } catch (error) { - console.error('[ScanService:Shodan] Error:', error); - return { exposed: false, ports: [], ips: [] }; - } - } - - async processSubscriptionScan( - subscriptionId: string, - watchlistItems: Awaited> - ): Promise<{ exposuresCreated: number; exposuresUpdated: number }> { - let exposuresCreated = 0; - let exposuresUpdated = 0; - - for (const item of watchlistItems) { - const identifier = item.value; - const identifierHash = hashIdentifier(identifier); - - switch (item.type) { - case WatchlistType.email: { - const hibpResult = await this.checkHIBP(identifier); - if (hibpResult.exposed) { - for (const source of hibpResult.sources) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId, - source: ExposureSource.hibp, - identifierHash, - metadata: { path: ['dbName'], equals: source }, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { detectedAt: new Date() }, - }); - exposuresUpdated++; - } else { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.hibp, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.hibp, item.type), - isFirstTime: true, - metadata: { dbName: source }, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - } - } - break; - } - - case WatchlistType.domain: { - const shodanResult = await this.checkShodan(identifier); - if (shodanResult.exposed) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId, - source: ExposureSource.shodan, - identifierHash, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { - detectedAt: new Date(), - metadata: { ports: shodanResult.ports, ips: shodanResult.ips }, - }, - }); - exposuresUpdated++; - } else { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.shodan, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.shodan, item.type), - isFirstTime: true, - metadata: { ports: shodanResult.ports, ips: shodanResult.ips }, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - } - break; - } - - default: { - const existing = await prisma.exposure.findFirst({ - where: { subscriptionId, watchlistItemId: item.id, identifierHash }, - }); - - if (!existing) { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.darkWebForum, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.darkWebForum, item.type), - isFirstTime: true, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - break; - } - } - } - - return { exposuresCreated, exposuresUpdated }; - } - - async getWatchlistItems(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - }); - } -} - -export const scanService = new ScanService(); diff --git a/apps/api/src/services/darkwatch/scheduler.service.ts b/apps/api/src/services/darkwatch/scheduler.service.ts deleted file mode 100644 index e31725e03..000000000 --- a/apps/api/src/services/darkwatch/scheduler.service.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { prisma, SubscriptionTier, SubscriptionStatus } from '@shieldsai/shared-db'; -import { tierConfig } from '@shieldsai/shared-billing'; -import { darkwatchScanQueue } from '@shieldsai/jobs'; -import { randomUUID } from 'crypto'; - -const CRON_EXPRESSIONS = { - daily: '0 0 * * *', - hourly: '0 * * * *', - realtime: null, -}; - -export class SchedulerService { - async scheduleSubscriptionScans() { - const activeSubscriptions = await prisma.subscription.findMany({ - where: { - tier: { in: [SubscriptionTier.basic, SubscriptionTier.plus, SubscriptionTier.premium] }, - status: SubscriptionStatus.active, - }, - select: { - id: true, - tier: true, - userId: true, - }, - }); - - const jobsEnqueued = []; - - for (const subscription of activeSubscriptions) { - const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency; - const cron = CRON_EXPRESSIONS[frequency]; - - if (!cron) { - continue; - } - - const jobKey = `scheduled-scan:${subscription.id}`; - - try { - await darkwatchScanQueue.add( - 'scheduled-scan', - { - subscriptionId: subscription.id, - tier: subscription.tier, - scanType: 'scheduled', - }, - { - jobId: jobKey, - repeat: { - every: frequency === 'daily' - ? 24 * 60 * 60 * 1000 - : 60 * 60 * 1000, - }, - priority: subscription.tier === SubscriptionTier.premium ? 1 : 3, - } - ); - - jobsEnqueued.push({ - subscriptionId: subscription.id, - tier: subscription.tier, - frequency, - }); - } catch (error) { - if ((error as Error).message?.includes('Duplicate')) { - continue; - } - console.error( - `[SchedulerService] Failed to schedule scan for ${subscription.id}:`, - error - ); - } - } - - return jobsEnqueued; - } - - async enqueueOnDemandScan(subscriptionId: string) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - throw new Error(`Subscription ${subscriptionId} not found`); - } - - return darkwatchScanQueue.add( - 'on-demand-scan', - { - subscriptionId, - tier: subscription.tier, - scanType: 'on-demand', - }, - { - priority: 1, - jobId: `on-demand-scan:${subscriptionId}:${randomUUID()}`, - } - ); - } - - async enqueueRealtimeTrigger(subscriptionId: string, sourceData: Record) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { id: true, tier: true }, - }); - - if (!subscription || subscription.tier !== SubscriptionTier.premium) { - throw new Error('Realtime triggers require Premium tier'); - } - - return darkwatchScanQueue.add( - 'realtime-trigger', - { - subscriptionId, - tier: subscription.tier, - scanType: 'realtime', - sourceData, - }, - { - priority: 0, - jobId: `realtime-trigger:${subscriptionId}:${randomUUID()}`, - } - ); - } - - async rescheduleAll() { - const repeatableJobs = await darkwatchScanQueue.getRepeatableJobs(); - - for (const job of repeatableJobs) { - await darkwatchScanQueue.removeRepeatableByKey(job.key); - } - - return this.scheduleSubscriptionScans(); - } - - async getScanSchedule(subscriptionId: string) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) return null; - - const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency; - - return { - subscriptionId, - tier: subscription.tier, - frequency, - cron: CRON_EXPRESSIONS[frequency], - nextRun: frequency === 'realtime' ? 'event-driven' : CRON_EXPRESSIONS[frequency], - }; - } -} - -export const schedulerService = new SchedulerService(); diff --git a/apps/api/src/services/darkwatch/watchlist.service.ts b/apps/api/src/services/darkwatch/watchlist.service.ts deleted file mode 100644 index caaaf9120..000000000 --- a/apps/api/src/services/darkwatch/watchlist.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { prisma, WatchlistType } from '@shieldsai/shared-db'; -import { createHash } from 'crypto'; - -export function normalizeValue(type: WatchlistType, value: string): string { - const trimmed = value.trim().toLowerCase(); - switch (type) { - case WatchlistType.email: - return trimmed.replace(/\s+/g, ''); - case WatchlistType.phoneNumber: - return trimmed.replace(/[\s\-\(\)]/g, ''); - case WatchlistType.ssn: - return trimmed.replace(/-/g, ''); - case WatchlistType.address: - return trimmed; - case WatchlistType.domain: - return trimmed.replace(/^https?:\/\//, '').replace(/\/.*$/, ''); - default: - return trimmed; - } -} - -export function hashValue(value: string): string { - return createHash('sha256').update(value).digest('hex'); -} - -export class WatchlistService { - async addItem( - subscriptionId: string, - type: WatchlistType, - value: string, - maxItems: number - ) { - const normalized = normalizeValue(type, value); - const itemHash = hashValue(normalized); - - const currentCount = await prisma.watchlistItem.count({ - where: { subscriptionId, isActive: true }, - }); - - if (currentCount >= maxItems) { - throw new Error( - `Watchlist limit reached (${maxItems} items). Upgrade your plan to add more.` - ); - } - - const existing = await prisma.watchlistItem.findFirst({ - where: { subscriptionId, type, hash: itemHash }, - }); - - if (existing) { - if (!existing.isActive) { - return prisma.watchlistItem.update({ - where: { id: existing.id }, - data: { isActive: true }, - }); - } - return existing; - } - - return prisma.watchlistItem.create({ - data: { - subscriptionId, - type, - value: normalized, - hash: itemHash, - }, - }); - } - - async getItems(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - orderBy: { createdAt: 'desc' }, - }); - } - - async removeItem(id: string, subscriptionId: string) { - return prisma.watchlistItem.update({ - where: { id }, - data: { isActive: false }, - }); - } - - async getActiveItemsForScan(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - }); - } - - async getItemCount(subscriptionId: string) { - return prisma.watchlistItem.count({ - where: { subscriptionId, isActive: true }, - }); - } -} - -export const watchlistService = new WatchlistService(); diff --git a/apps/api/src/services/darkwatch/webhook.service.ts b/apps/api/src/services/darkwatch/webhook.service.ts deleted file mode 100644 index 256bd4e3e..000000000 --- a/apps/api/src/services/darkwatch/webhook.service.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { prisma, ExposureSource, ExposureSeverity, WatchlistType, AlertType, AlertSeverity } from '@shieldsai/shared-db'; -import { createHash } from 'crypto'; -import { mixpanelService, EventType } from '@shieldsai/shared-analytics'; - -function hashIdentifier(identifier: string): string { - return createHash('sha256').update(identifier.toLowerCase().trim()).digest('hex'); -} - -function determineSeverity( - source: ExposureSource, - dataType: WatchlistType -): ExposureSeverity { - const criticalSources = [ExposureSource.darkWebForum, ExposureSource.honeypot]; - const warningSources = [ExposureSource.hibp, ExposureSource.shodan]; - const criticalTypes = [WatchlistType.ssn]; - - if (criticalTypes.includes(dataType)) return ExposureSeverity.critical; - if (criticalSources.includes(source)) return ExposureSeverity.critical; - if (warningSources.includes(source)) return ExposureSeverity.warning; - return ExposureSeverity.info; -} - -export interface WebhookPayload { - source: string; - identifier: string; - identifierType: string; - metadata?: Record; - timestamp?: string; -} - -export class WebhookService { - async processExternalWebhook(payload: WebhookPayload): Promise<{ - exposuresCreated: number; - alertsCreated: number; - }> { - const source = this.mapSource(payload.source); - const dataType = this.mapDataType(payload.identifierType); - const identifier = payload.identifier.toLowerCase().trim(); - const identifierHash = hashIdentifier(identifier); - const severity = determineSeverity(source, dataType); - - const matchingItems = await prisma.watchlistItem.findMany({ - where: { - isActive: true, - OR: [ - { hash: identifierHash, type: dataType }, - { value: identifier, type: dataType }, - ], - }, - include: { - subscription: { - select: { - id: true, - tier: true, - userId: true, - }, - }, - }, - }); - - let exposuresCreated = 0; - let alertsCreated = 0; - - for (const item of matchingItems) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId: item.subscriptionId, - source, - identifierHash, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { detectedAt: new Date() }, - }); - continue; - } - - const exposure = await prisma.exposure.create({ - data: { - subscriptionId: item.subscriptionId, - watchlistItemId: item.id, - source, - dataType, - identifier, - identifierHash, - severity, - isFirstTime: true, - metadata: payload.metadata || {}, - detectedAt: new Date(), - }, - }); - - exposuresCreated++; - - const alertChannels = this.getAlertChannelsForTier(item.subscription.tier); - - await prisma.alert.create({ - data: { - subscriptionId: item.subscriptionId, - userId: item.subscription.userId, - exposureId: exposure.id, - type: AlertType.exposure_detected, - title: `New Exposure Detected: ${this.getSourceLabel(source)}`, - message: this.buildAlertMessage(identifier, source, severity), - severity: this.mapAlertSeverity(severity), - channel: alertChannels, - }, - }); - - alertsCreated++; - - await mixpanelService.track(EventType.EXPOSURE_DETECTED, { - userId: item.subscription.userId, - exposureType: dataType, - severity, - source, - subscriptionTier: item.subscription.tier, - }); - } - - return { exposuresCreated, alertsCreated }; - } - - async verifyWebhookSignature( - body: string, - signature: string, - timestamp: string - ): Promise { - const webhookSecret = process.env.DARKWATCH_WEBHOOK_SECRET; - if (!webhookSecret) { - console.warn('[WebhookService] DARKWATCH_WEBHOOK_SECRET not set — signature verification skipped'); - return false; - } - - const expected = createHash('sha256') - .update(`${timestamp}:${body}`) - .digest('hex'); - - return expected === signature; - } - - private mapSource(source: string): ExposureSource { - const sourceMap: Record = { - hibp: ExposureSource.hibp, - 'haveibeenpwned': ExposureSource.hibp, - securitytrails: ExposureSource.securityTrails, - censys: ExposureSource.censys, - 'darkweb-forum': ExposureSource.darkWebForum, - 'darkweb': ExposureSource.darkWebForum, - shodan: ExposureSource.shodan, - honeypot: ExposureSource.honeypot, - }; - - const normalized = source.toLowerCase().replace(/\s+/g, ''); - const mapped = sourceMap[normalized]; - if (!mapped) { - console.warn(`[WebhookService] Unknown source "${source}", falling back to darkWebForum`); - } - return mapped || ExposureSource.darkWebForum; - } - - private mapDataType(type: string): WatchlistType { - const typeMap: Record = { - email: WatchlistType.email, - phone: WatchlistType.phoneNumber, - phonenumber: WatchlistType.phoneNumber, - ssn: WatchlistType.ssn, - address: WatchlistType.address, - domain: WatchlistType.domain, - }; - - const normalized = type.toLowerCase().trim(); - return typeMap[normalized] || WatchlistType.email; - } - - private getAlertChannelsForTier(tier: string): string[] { - const channelMap: Record = { - basic: ['email'], - plus: ['email', 'push'], - premium: ['email', 'push', 'sms'], - }; - return channelMap[tier] || ['email']; - } - - private mapAlertSeverity(severity: ExposureSeverity): AlertSeverity { - return severity as AlertSeverity; - } - - private getSourceLabel(source: ExposureSource): string { - const labels: Record = { - [ExposureSource.hibp]: 'Have I Been Pwned', - [ExposureSource.securityTrails]: 'SecurityTrails', - [ExposureSource.censys]: 'Censys', - [ExposureSource.darkWebForum]: 'Dark Web Forum', - [ExposureSource.shodan]: 'Shodan', - [ExposureSource.honeypot]: 'Honeypot', - }; - return labels[source] || source; - } - - private buildAlertMessage( - identifier: string, - source: ExposureSource, - severity: ExposureSeverity - ): string { - const masked = this.maskIdentifier(identifier); - return `${severity.toUpperCase()}: "${masked}" found in ${this.getSourceLabel(source)}.`; - } - - private maskIdentifier(identifier: string): string { - if (identifier.includes('@')) { - const [user, domain] = identifier.split('@'); - const maskedUser = user.slice(0, 2) + '***' + user.slice(-1); - return `${maskedUser}@${domain}`; - } - if (identifier.length > 8) { - return identifier.slice(0, 3) + '***' + identifier.slice(-2); - } - return identifier; - } -} - -export const webhookService = new WebhookService(); diff --git a/apps/api/src/services/spamshield/feature-flags.ts b/apps/api/src/services/spamshield/feature-flags.ts deleted file mode 100644 index 5c72e6aab..000000000 --- a/apps/api/src/services/spamshield/feature-flags.ts +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Feature Flag Management System - * Centralized feature flag handling with type safety and runtime updates - */ - -import type { z } from 'zod'; - -/** - * Type for feature flag values - */ -export type FeatureFlagValue = boolean | string | number; - -/** - * Interface for a feature flag definition - */ -export interface FeatureFlag { - key: string; - defaultValue: T; - description?: string; - allowedValues?: T[]; // For enum-like flags - category?: string; -} - -/** - * Feature flag registry - stores all defined flags - */ -export interface FeatureFlagRegistry { - [key: string]: FeatureFlag; -} - -/** - * Feature flag resolver - handles flag resolution logic - */ -export class FeatureFlagResolver { - private flags: FeatureFlagRegistry; - private resolvedCache: Map = new Map(); - - constructor(flags: FeatureFlagRegistry) { - this.flags = flags; - } - - /** - * Resolve a feature flag value - * Priority: Environment > Cache > Default - */ - resolve(key: string, defaultValue: T): T { - // Check cache first - if (this.resolvedCache.has(key)) { - return this.resolvedCache.get(key)! as T; - } - - // Check environment variable (allows runtime updates) - const envValue = process.env[`FLAG_${key.toUpperCase()}`]; - if (envValue !== undefined) { - // Try to parse as JSON first, then as boolean, then as string - let parsed: FeatureFlagValue; - try { - parsed = JSON.parse(envValue); - } catch { - parsed = envValue.toLowerCase() === 'true' ? true : - envValue.toLowerCase() === 'false' ? false : - envValue; - } - - // Validate against allowed values if defined - const flag = this.flags[key]; - if (flag && flag.allowedValues && !flag.allowedValues.includes(parsed)) { - console.warn(`Invalid value for flag ${key}: ${parsed}. Using default.`); - parsed = defaultValue as FeatureFlagValue; - } - - this.resolvedCache.set(key, parsed); - return parsed as T; - } - - // Use cached value if available - if (this.resolvedCache.has(key)) { - return this.resolvedCache.get(key)! as T; - } - - // Return default - this.resolvedCache.set(key, defaultValue as FeatureFlagValue); - return defaultValue as T; - } - - /** - * Check if a flag is enabled (boolean check) - */ - isEnabled(key: string, defaultValue: T): T { - return this.resolve(key, defaultValue) as T; - } - - /** - * Get flag definition - */ - getDefinition(key: string): FeatureFlag | undefined { - return this.flags[key]; - } - - /** - * List all registered flags - */ - getAllFlags(): FeatureFlagRegistry { - return { ...this.flags }; - } - - /** - * Clear the resolution cache (useful for testing) - */ - clearCache(): void { - this.resolvedCache.clear(); - } -} - -/** - * Feature flag configuration with pre-defined flags - */ -export const featureFlags: FeatureFlagRegistry = { - // SpamShield Feature Flags - 'spamshield.enable.number.reputation': { - key: 'spamshield_enable_number_reputation', - defaultValue: true, - description: 'Enable number reputation checking (Hiya API integration)', - category: 'spamshield', - }, - 'spamshield.enable.content.classification': { - key: 'spamshield_enable_content_classification', - defaultValue: true, - description: 'Enable SMS content classification (BERT model)', - category: 'spamshield', - }, - 'spamshield.enable.behavioral.analysis': { - key: 'spamshield_enable_behavioral_analysis', - defaultValue: true, - description: 'Enable call behavioral analysis', - category: 'spamshield', - }, - 'spamshield.enable.community.intelligence': { - key: 'spamshield_enable_community_intelligence', - defaultValue: true, - description: 'Enable community intelligence sharing', - category: 'spamshield', - }, - 'spamshield.enable.real.time.blocking': { - key: 'spamshield_enable_real_time_blocking', - defaultValue: true, - description: 'Enable real-time spam blocking', - category: 'spamshield', - }, - 'spamshield.enable.multiple.sources': { - key: 'spamshield_enable_multiple_sources', - defaultValue: false, - description: 'Enable multiple reputation source aggregation (Truecaller, etc.)', - category: 'spamshield', - }, - 'spamshield.enable.ml.classifier': { - key: 'spamshield_enable_ml_classifier', - defaultValue: false, - description: 'Enable ML-based spam classification', - category: 'spamshield', - }, - - // VoicePrint Feature Flags - 'voiceprint.enable.ml.service': { - key: 'voiceprint_enable_ml_service', - defaultValue: false, - description: 'Enable ML service integration for voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.faiss.index': { - key: 'voiceprint_enable_faiss_index', - defaultValue: true, - description: 'Enable FAISS index for voice matching', - category: 'voiceprint', - }, - 'voiceprint.enable.batch.analysis': { - key: 'voiceprint_enable_batch_analysis', - defaultValue: true, - description: 'Enable batch voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.realtime.analysis': { - key: 'voiceprint_enable_realtime_analysis', - defaultValue: false, - description: 'Enable real-time voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.mock.model': { - key: 'voiceprint_enable_mock_model', - defaultValue: true, - description: 'Enable mock model for development', - category: 'voiceprint', - }, - - // General Platform Flags - 'platform.enable.audit.logs': { - key: 'platform_enable_audit_logs', - defaultValue: true, - description: 'Enable comprehensive audit logging', - category: 'platform', - }, - 'platform.enable.kpi.tracking': { - key: 'platform_enable_kpi_tracking', - defaultValue: true, - description: 'Enable KPI snapshot tracking', - category: 'platform', - }, -}; - -/** - * Create a resolver instance with the default flags - */ -export const featureFlagResolver = new FeatureFlagResolver(featureFlags); - -/** - * Convenience function for quick flag checks - */ -export function isFeatureEnabled(key: string, defaultValue: T): T { - return featureFlagResolver.isEnabled(key, defaultValue); -} - -/** - * Check if a flag is enabled with type safety - */ -export function checkFlag(key: string, defaultValue: T): T { - return featureFlagResolver.resolve(key, defaultValue); -} diff --git a/apps/api/src/services/spamshield/index.ts b/apps/api/src/services/spamshield/index.ts deleted file mode 100644 index 917e23787..000000000 --- a/apps/api/src/services/spamshield/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Config -export { - spamShieldEnv, - SpamLayer, - SpamDecision, - ConfidenceLevel, - spamFeatureFlags, - spamRateLimits, - checkFlag, - isFeatureEnabled, -} from './spamshield.config'; - -// Feature flags -export * from './feature-flags'; - -// Services -export { - NumberReputationService, - SMSClassifierService, - CallAnalysisService, - SpamFeedbackService, - numberReputationService, - smsClassifierService, - callAnalysisService, - spamFeedbackService, -} from './spamshield.service'; diff --git a/apps/api/src/services/spamshield/spamshield.audit-logger.ts b/apps/api/src/services/spamshield/spamshield.audit-logger.ts deleted file mode 100644 index dd62ee445..000000000 --- a/apps/api/src/services/spamshield/spamshield.audit-logger.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { createHash } from 'crypto'; - -export type AuditClassificationType = 'sms' | 'call'; - -export interface AuditClassificationEntry { - id: string; - timestamp: string; - type: AuditClassificationType; - phoneNumberHash: string; - decision: 'spam' | 'ham' | 'block' | 'flag' | 'allow'; - confidence: number; - reasons: string[]; - featureFlags: Record; - metadata?: Record; -} - -const MAX_AUDIT_LOG_SIZE = 10_000; - -class AuditLogger { - private entries: AuditClassificationEntry[] = []; - - logClassification(entry: Omit): AuditClassificationEntry { - const record: AuditClassificationEntry = { - id: `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - timestamp: new Date().toISOString(), - ...entry, - }; - - this.entries.push(record); - - if (this.entries.length > MAX_AUDIT_LOG_SIZE) { - this.entries.shift(); - } - - console.log( - `[SpamShield:Audit] type=${record.type} decision=${record.decision} ` + - `confidence=${record.confidence.toFixed(3)} reasons=${record.reasons.join(',') || 'none'} ` + - `phoneHash=${record.phoneNumberHash}` - ); - - return record; - } - - getEntries( - filters?: { - type?: AuditClassificationType; - decision?: string; - startDate?: Date; - endDate?: Date; - limit?: number; - } - ): AuditClassificationEntry[] { - let results = this.entries; - - if (filters?.type) { - results = results.filter(e => e.type === filters.type); - } - - if (filters?.decision) { - results = results.filter(e => e.decision === filters.decision); - } - - if (filters?.startDate) { - results = results.filter(e => new Date(e.timestamp) >= filters.startDate!); - } - - if (filters?.endDate) { - results = results.filter(e => new Date(e.timestamp) <= filters.endDate!); - } - - if (filters?.limit) { - results = results.slice(-filters.limit); - } - - return results; - } - - getSummary(): { - totalEntries: number; - spamCount: number; - hamCount: number; - blockCount: number; - flagCount: number; - allowCount: number; - avgConfidence: number; - } { - const spamCount = this.entries.filter(e => e.decision === 'spam' || e.decision === 'block').length; - const hamCount = this.entries.filter(e => e.decision === 'ham' || e.decision === 'allow').length; - const blockCount = this.entries.filter(e => e.decision === 'block').length; - const flagCount = this.entries.filter(e => e.decision === 'flag').length; - const allowCount = this.entries.filter(e => e.decision === 'allow').length; - const avgConfidence = - this.entries.length > 0 - ? this.entries.reduce((s, e) => s + e.confidence, 0) / this.entries.length - : 0; - - return { - totalEntries: this.entries.length, - spamCount, - hamCount, - blockCount, - flagCount, - allowCount, - avgConfidence: Math.round(avgConfidence * 1000) / 1000, - }; - } - - clear(): void { - this.entries = []; - } -} - -export const spamAuditLogger = new AuditLogger(); - -export function hashPhoneNumber(phoneNumber: string): string { - const hash = createHash('sha256').update(phoneNumber.trim()).digest('hex'); - return `sha256_${hash}`; -} diff --git a/apps/api/src/services/spamshield/spamshield.config.ts b/apps/api/src/services/spamshield/spamshield.config.ts deleted file mode 100644 index af56605b5..000000000 --- a/apps/api/src/services/spamshield/spamshield.config.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { z } from 'zod'; -import { checkFlag } from './feature-flags'; - -// Environment variables for SpamShield -const envSchema = z.object({ - HIYA_API_KEY: z.string(), - HIYA_API_URL: z.string().default('https://api.hiya.com/v1'), - TRUECALLER_API_KEY: z.string().optional(), - BERT_MODEL_PATH: z.string().default('./models/spam-classifier'), - SPAM_THRESHOLD_AUTO_BLOCK: z.string().transform(Number).default(0.85), - SPAM_THRESHOLD_FLAG: z.string().transform(Number).default(0.6), - CALL_ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default(200), -}); - -export const spamShieldEnv = envSchema.parse({ - HIYA_API_KEY: process.env.HIYA_API_KEY, - HIYA_API_URL: process.env.HIYA_API_URL, - TRUECALLER_API_KEY: process.env.TRUECALLER_API_KEY, - BERT_MODEL_PATH: process.env.BERT_MODEL_PATH, - SPAM_THRESHOLD_AUTO_BLOCK: process.env.SPAM_THRESHOLD_AUTO_BLOCK, - SPAM_THRESHOLD_FLAG: process.env.SPAM_THRESHOLD_FLAG, - CALL_ANALYSIS_TIMEOUT_MS: process.env.CALL_ANALYSIS_TIMEOUT_MS, -}); - -// Spam detection layers -export enum SpamLayer { - NUMBER_REPUTATION = 'number_reputation', - CONTENT_CLASSIFICATION = 'content_classification', - BEHAVIORAL_ANALYSIS = 'behavioral_analysis', - COMMUNITY_INTELLIGENCE = 'community_intelligence', -} - -// Spam decision types -export enum SpamDecision { - ALLOW = 'allow', - FLAG = 'flag', - BLOCK = 'block', - CHALLENGE = 'challenge', -} - -// Confidence levels -export enum ConfidenceLevel { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - VERY_HIGH = 'very_high', -} - -// Feature flags for spam detection -// Use the centralized feature flag system from feature-flags.ts -// These are aliases for quick access -export const spamFeatureFlags = { - enableNumberReputation: checkFlag('spamshield.enable.number.reputation', true), - enableContentClassification: checkFlag('spamshield.enable.content.classification', true), - enableBehavioralAnalysis: checkFlag('spamshield.enable.behavioral.analysis', true), - enableCommunityIntelligence: checkFlag('spamshield.enable.community.intelligence', true), - enableRealTimeBlocking: checkFlag('spamshield.enable.real.time.blocking', true), - enableMultipleSources: checkFlag('spamshield.enable.multiple.sources', false), - enableMLClassifier: checkFlag('spamshield.enable.ml.classifier', false), -}; - -// Rate limits for spam analysis -export const spamRateLimits = { - basic: { - analysesPerMinute: 10, - analysesPerDay: 100, - }, - plus: { - analysesPerMinute: 50, - analysesPerDay: 1000, - }, - premium: { - analysesPerMinute: 200, - analysesPerDay: 10000, - }, -}; - -// Default confidence scores for spam detection layers -export const defaultScores = { - // Number reputation service defaults - defaultReputationConfidence: 0.0, - defaultReputationLowConfidence: 0.1, - - // SMS classifier defaults - defaultBaseConfidence: 0.5, - defaultMaxConfidence: 1.0, - - // Feature weights for SMS classification - featureWeights: { - urlPresent: 0.1, - highEmojiDensity: 0.15, - urgencyKeyword: 0.2, - excessiveCaps: 0.15, - }, - - // Call analysis defaults - defaultSpamScore: 0.0, - highReputationThreshold: 0.7, - reputationWeightInCombinedScore: 0.4, - shortDurationScore: 0.2, - voipScore: 0.15, - unusualHoursScore: 0.1, - - // Source combination weights - hiyaWeightInCombinedScore: 0.7, - truecallerWeightInCombinedScore: 0.3, -}; - -// Metadata size limits for SpamFeedback -export const metadataLimits = { - // Maximum size for metadata JSON in bytes - maxMetadataSizeBytes: 4096, - - // Maximum number of keys in metadata object - maxMetadataKeys: 20, - - // Maximum size for individual metadata value in bytes - maxMetadataValueSizeBytes: 512, -}; - -// Standard error codes for spamshield API -export enum SpamErrorCode { - // Client errors (4xx) - INVALID_REQUEST = 'INVALID_REQUEST', - MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD', - UNAUTHORIZED = 'UNAUTHORIZED', - NOT_FOUND = 'NOT_FOUND', - VALIDATION_ERROR = 'VALIDATION_ERROR', - - // Server errors (5xx) - CLASSIFICATION_FAILED = 'CLASSIFICATION_FAILED', - REPUTATION_CHECK_FAILED = 'REPUTATION_CHECK_FAILED', - ANALYSIS_FAILED = 'ANALYSIS_FAILED', - FEEDBACK_RECORD_FAILED = 'FEEDBACK_RECORD_FAILED', - DATABASE_ERROR = 'DATABASE_ERROR', - RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', - SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE', -} - -// Standard error response type -export interface SpamErrorResponse { - error: { - code: SpamErrorCode; - message: string; - field?: string; - timestamp: string; - requestId?: string; - }; -} - -// HTTP status code constants -export const HttpStatus = { - OK: 200, - CREATED: 201, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - UNPROCESSABLE_ENTITY: 422, - TOO_MANY_REQUESTS: 429, - INTERNAL_SERVER_ERROR: 500, - SERVICE_UNAVAILABLE: 503, -}; diff --git a/apps/api/src/services/spamshield/spamshield.error-handler.ts b/apps/api/src/services/spamshield/spamshield.error-handler.ts deleted file mode 100644 index 41b382d8f..000000000 --- a/apps/api/src/services/spamshield/spamshield.error-handler.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { FastifyReply } from 'fastify'; -import { SpamErrorCode, HttpStatus, SpamErrorResponse } from './spamshield.config'; - -export { SpamErrorCode, HttpStatus }; -export type { SpamErrorResponse }; - -/** - * Standardized error response builder for SpamShield API - */ -export class ErrorHandler { - /** - * Create a standard error response - */ - static create( - code: SpamErrorCode, - message: string, - options?: { - field?: string; - requestId?: string; - additionalData?: Record; - } - ): SpamErrorResponse { - return { - error: { - code, - message, - ...(options?.field && { field: options.field }), - timestamp: new Date().toISOString(), - ...(options?.requestId && { requestId: options.requestId }), - }, - }; - } - - /** - * Send a standard error response with appropriate HTTP status code - */ - static send( - reply: FastifyReply, - code: SpamErrorCode, - message: string, - options?: { - field?: string; - status?: number; - requestId?: string; - } - ): void { - const status = options?.status ?? this.getStatusForCode(code); - const errorResponse = this.create(code, message, { - field: options?.field, - requestId: options?.requestId, - }); - reply.code(status).send(errorResponse); - } - - /** - * Map error codes to HTTP status codes - */ - private static getStatusForCode(code: SpamErrorCode): number { - const statusMap: Record = { - // Client errors - [SpamErrorCode.INVALID_REQUEST]: HttpStatus.BAD_REQUEST, - [SpamErrorCode.MISSING_REQUIRED_FIELD]: HttpStatus.BAD_REQUEST, - [SpamErrorCode.UNAUTHORIZED]: HttpStatus.UNAUTHORIZED, - [SpamErrorCode.NOT_FOUND]: HttpStatus.NOT_FOUND, - [SpamErrorCode.VALIDATION_ERROR]: HttpStatus.BAD_REQUEST, - - // Server errors - [SpamErrorCode.CLASSIFICATION_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.REPUTATION_CHECK_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.ANALYSIS_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.FEEDBACK_RECORD_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.DATABASE_ERROR]: HttpStatus.INTERNAL_SERVER_ERROR, - [SpamErrorCode.RATE_LIMIT_EXCEEDED]: HttpStatus.TOO_MANY_REQUESTS, - [SpamErrorCode.SERVICE_UNAVAILABLE]: HttpStatus.SERVICE_UNAVAILABLE, - }; - return statusMap[code] ?? HttpStatus.INTERNAL_SERVER_ERROR; - } - - /** - * Validate required string field - */ - static validateRequiredField( - value: unknown, - fieldName: string - ): { isValid: boolean; error?: { code: SpamErrorCode; message: string; field: string } } { - if (!value || typeof value !== 'string' || value.trim() === '') { - return { - isValid: false, - error: { - code: SpamErrorCode.MISSING_REQUIRED_FIELD, - message: `${fieldName} is required`, - field: fieldName, - }, - }; - } - return { isValid: true }; - } - - /** - * Validate boolean field - */ - static validateBooleanField( - value: unknown, - fieldName: string - ): { isValid: boolean; error?: { code: SpamErrorCode; message: string; field: string } } { - if (value === undefined || value === null || typeof value !== 'boolean') { - return { - isValid: false, - error: { - code: SpamErrorCode.VALIDATION_ERROR, - message: `${fieldName} must be a boolean`, - field: fieldName, - }, - }; - } - return { isValid: true }; - } -} diff --git a/apps/api/src/services/spamshield/spamshield.service.ts b/apps/api/src/services/spamshield/spamshield.service.ts deleted file mode 100644 index 9dd8adda8..000000000 --- a/apps/api/src/services/spamshield/spamshield.service.ts +++ /dev/null @@ -1,462 +0,0 @@ -import { prisma, SpamFeedback } from '@shieldsai/shared-db'; -import { spamShieldEnv, SpamDecision, spamFeatureFlags, defaultScores, metadataLimits } from './spamshield.config'; -import { createHash } from 'crypto'; -import { spamAuditLogger, hashPhoneNumber } from './spamshield.audit-logger'; - -// Number reputation service (Hiya API integration) -export class NumberReputationService { - /** - * Check number reputation using Hiya API - */ - async checkReputation(phoneNumber: string): Promise<{ - isSpam: boolean; - confidence: number; - spamType?: string; - reportCount: number; - }> { - try { - // Only enable if feature flag is set - if (!spamFeatureFlags.enableNumberReputation) { - return { - isSpam: false, - confidence: 0.0, - reportCount: 0, - }; - } - - // TODO: Integrate with Hiya API - // const response = await fetch(`${spamShieldEnv.HIYA_API_URL}/lookup`, { - // headers: { 'X-API-Key': spamShieldEnv.HIYA_API_KEY }, - // method: 'POST', - // body: JSON.stringify({ phone: phoneNumber }), - // }); - - // Simulated response for now - return { - isSpam: false, - confidence: defaultScores.defaultReputationLowConfidence, - spamType: undefined, - reportCount: 0, - }; - } catch (error) { - console.error('Error checking number reputation:', error); - return { - isSpam: false, - confidence: defaultScores.defaultReputationConfidence, - reportCount: 0, - }; - } - } - - /** - * Check number against multiple reputation sources - */ - async checkMultiSource(phoneNumber: string): Promise<{ - hiya: { isSpam: boolean; confidence: number }; - truecaller: { isSpam: boolean; confidence: number } | null; - combinedScore: number; - }> { - // Only enable if feature flag is set - if (!spamFeatureFlags.enableMultipleSources) { - return { - hiya: { isSpam: false, confidence: defaultScores.defaultReputationConfidence }, - truecaller: null, - combinedScore: defaultScores.defaultSpamScore, - }; - } - - const hiyaResult = await this.checkReputation(phoneNumber); - - let truecallerResult: { isSpam: boolean; confidence: number } | null = null; - if (spamShieldEnv.TRUECALLER_API_KEY) { - // TODO: Integrate Truecaller - truecallerResult = { - isSpam: false, - confidence: defaultScores.defaultReputationConfidence, - }; - } - - // Weighted average: Hiya 70%, Truecaller 30% - const combinedScore = hiyaResult.confidence * defaultScores.hiyaWeightInCombinedScore + - (truecallerResult?.confidence ?? defaultScores.defaultReputationConfidence) * defaultScores.truecallerWeightInCombinedScore; - - return { - hiya: { isSpam: hiyaResult.isSpam, confidence: hiyaResult.confidence }, - truecaller: truecallerResult, - combinedScore, - }; - } -} - -// SMS content classifier (BERT-based) -export class SMSClassifierService { - private model: any = null; // BERT model placeholder - private _initPromise: Promise | null = null; - - /** - * Initialize the BERT model (thread-safe via promise deduplication) - */ - async initialize(): Promise { - // TODO: Load BERT model from path - // this.model = await loadBERTModel(spamShieldEnv.BERT_MODEL_PATH); - console.log('SMS classifier initialized'); - } - - /** - * Ensures model is initialized before use. Concurrent callers - * await the same initialization promise to avoid race conditions. - */ - private async ensureInitialized(): Promise { - if (this._initPromise) { - return this._initPromise; - } - this._initPromise = (async () => { - if (this.model) { - return; - } - await this.initialize(); - })(); - return this._initPromise; - } - - /** - * Classify SMS text as spam or ham - */ - async classify( - smsText: string, - phoneNumber?: string - ): Promise<{ - isSpam: boolean; - confidence: number; - spamFeatures: string[]; - }> { - // Only enable if feature flag is set - if (!spamFeatureFlags.enableMLClassifier) { - // Return basic feature-based classification - const features = this.extractFeatures(smsText); - const confidence = this.calculateConfidence(features); - const isSpam = confidence >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK; - - spamAuditLogger.logClassification({ - type: 'sms', - phoneNumberHash: phoneNumber ? hashPhoneNumber(phoneNumber) : 'unknown', - decision: isSpam ? 'spam' : 'ham', - confidence, - reasons: features, - featureFlags: { enableMLClassifier: spamFeatureFlags.enableMLClassifier }, - }); - - return { - isSpam, - confidence, - spamFeatures: features, - }; - } - - await this.ensureInitialized(); - - // Extract features - const features = this.extractFeatures(smsText); - - // TODO: Run through BERT model - // const prediction = await this.model.predict(smsText); - - // Simulated prediction - const confidence = this.calculateConfidence(features); - const isSpam = confidence >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK; - - spamAuditLogger.logClassification({ - type: 'sms', - phoneNumberHash: phoneNumber ? hashPhoneNumber(phoneNumber) : 'unknown', - decision: isSpam ? 'spam' : 'ham', - confidence, - reasons: features, - featureFlags: { enableMLClassifier: spamFeatureFlags.enableMLClassifier }, - }); - - return { - isSpam, - confidence, - spamFeatures: features, - }; - } - - private extractFeatures(text: string): string[] { - const features: string[] = []; - const lowerText = text.toLowerCase(); - - // URL presence - if (/(http|www)\./i.test(text)) { - features.push('url_present'); - } - - // Emoji density - const emojiCount = (text.match(/[\p{Emoji}]/gu) || []).length; - if (emojiCount / text.length > 0.1) { - features.push('high_emoji_density'); - } - - // Urgency keywords - const urgencyWords = ['now', 'urgent', 'limited', 'act fast', 'today']; - if (urgencyWords.some(word => lowerText.includes(word))) { - features.push('urgency_keyword'); - } - - // Excessive capitalization - if (/[A-Z]{3,}/.test(text)) { - features.push('excessive_caps'); - } - - return features; - } - - private calculateConfidence(features: string[]): number { - const baseConfidence = defaultScores.defaultBaseConfidence; - const featureWeights: Record = { - url_present: defaultScores.featureWeights.urlPresent, - high_emoji_density: defaultScores.featureWeights.highEmojiDensity, - urgency_keyword: defaultScores.featureWeights.urgencyKeyword, - excessive_caps: defaultScores.featureWeights.excessiveCaps, - }; - - return Math.min(defaultScores.defaultMaxConfidence, baseConfidence + - features.reduce((sum, f) => sum + (featureWeights[f] || 0), 0)); - } -} - -// Call analysis service -export class CallAnalysisService { - /** - * Analyze incoming call for spam indicators - */ - async analyzeCall(callData: { - phoneNumber: string; - duration?: number; - callTime: Date; - isVoip?: boolean; - }): Promise<{ - decision: SpamDecision; - confidence: number; - reasons: string[]; - }> { - const reasons: string[] = []; - let spamScore = defaultScores.defaultSpamScore; - - // Number reputation check - only if feature flag enabled - if (spamFeatureFlags.enableBehavioralAnalysis) { - const reputationService = new NumberReputationService(); - const reputation = await reputationService.checkMultiSource(callData.phoneNumber); - - if (reputation.combinedScore > defaultScores.highReputationThreshold) { - spamScore += reputation.combinedScore * defaultScores.reputationWeightInCombinedScore; - reasons.push('high_spam_reputation'); - } - } - - // Behavioral analysis - only if feature flag enabled - if (spamFeatureFlags.enableBehavioralAnalysis) { - if (callData.duration && callData.duration < 10) { - spamScore += defaultScores.shortDurationScore; - reasons.push('short_duration'); - } - - if (callData.isVoip) { - spamScore += defaultScores.voipScore; - reasons.push('voip_number'); - } - - // Time-of-day anomaly (simplified) - const hour = callData.callTime.getHours(); - if (hour < 6 || hour > 22) { - spamScore += defaultScores.unusualHoursScore; - reasons.push('unusual_hours'); - } - } - - // Determine decision - let decision: SpamDecision; - if (spamScore >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK) { - decision = SpamDecision.BLOCK; - } else if (spamScore >= spamShieldEnv.SPAM_THRESHOLD_FLAG) { - decision = SpamDecision.FLAG; - } else { - decision = SpamDecision.ALLOW; - } - - spamAuditLogger.logClassification({ - type: 'call', - phoneNumberHash: hashPhoneNumber(callData.phoneNumber), - decision: decision.toLowerCase() as 'block' | 'flag' | 'allow', - confidence: spamScore, - reasons, - featureFlags: { - enableBehavioralAnalysis: spamFeatureFlags.enableBehavioralAnalysis, - enableNumberReputation: spamFeatureFlags.enableNumberReputation, - }, - metadata: { - duration: callData.duration, - isVoip: callData.isVoip, - callTime: callData.callTime.toISOString(), - }, - }); - - return { - decision, - confidence: spamScore, - reasons, - }; - } -} - -// User feedback service -export class SpamFeedbackService { - /** - * Validate metadata size against defined limits - */ - private validateMetadata(metadata?: Record): { - isValid: boolean; - trimmedMetadata?: Record; - reasons?: string[]; - } { - if (!metadata) { - return { isValid: true }; - } - - const reasons: string[] = []; - let trimmedMetadata: Record = metadata; - - // Check number of keys - const keyCount = Object.keys(metadata).length; - if (keyCount > metadataLimits.maxMetadataKeys) { - reasons.push(`Metadata has ${keyCount} keys, exceeding limit of ${metadataLimits.maxMetadataKeys}`); - trimmedMetadata = Object.entries(metadata).slice(0, metadataLimits.maxMetadataKeys); - } - - // Check total JSON size - const jsonSize = JSON.stringify(metadata).length; - if (jsonSize > metadataLimits.maxMetadataSizeBytes) { - reasons.push(`Metadata size ${jsonSize} bytes exceeds limit of ${metadataLimits.maxMetadataSizeBytes} bytes`); - - // Truncate long values - trimmedMetadata = Object.fromEntries( - Object.entries(metadata).map(([key, value]) => { - const valueStr = String(value); - if (valueStr.length > metadataLimits.maxMetadataValueSizeBytes) { - return [key, valueStr.slice(0, metadataLimits.maxMetadataValueSizeBytes)]; - } - return [key, value]; - }) - ); - } - - return { - isValid: reasons.length === 0, - trimmedMetadata, - reasons: reasons.length > 0 ? reasons : undefined, - }; - } - - /** - * Record user feedback on spam detection - */ - async recordFeedback( - userId: string, - phoneNumber: string, - isSpam: boolean, - confidence?: number, - metadata?: Record - ): Promise { - // Validate metadata - const validation = this.validateMetadata(metadata); - const validatedMetadata = validation.trimmedMetadata; - - // Only enable if feature flag is set - if (!spamFeatureFlags.enableCommunityIntelligence) { - // Return a mock feedback for development - return { - id: `mock_${Date.now()}`, - userId, - phoneNumber, - phoneNumberHash: this.hashPhoneNumber(phoneNumber), - isSpam, - confidence, - feedbackType: 'user_confirmation' as const, - metadata: validatedMetadata, - createdAt: new Date(), - updatedAt: new Date(), - }; - } - - const phoneNumberHash = this.hashPhoneNumber(phoneNumber); - - const feedback = await prisma.spamFeedback.create({ - data: { - userId, - phoneNumber, - phoneNumberHash, - isSpam, - confidence, - feedbackType: 'user_confirmation', - metadata: validatedMetadata, - }, - }); - - return feedback; - } - - /** - * Get spam history for a user - */ - async getSpamHistory( - userId: string, - options?: { - limit?: number; - isSpam?: boolean; - startDate?: Date; - } - ): Promise { - return prisma.spamFeedback.findMany({ - where: { - userId, - ...(options?.isSpam !== undefined && { isSpam: options.isSpam }), - ...(options?.startDate && { createdAt: { gte: options.startDate } }), - }, - orderBy: { createdAt: 'desc' }, - take: options?.limit ?? 100, - }); - } - - /** - * Get statistics for a user - */ - async getStatistics(userId: string): Promise<{ - totalAnalyses: number; - spamCount: number; - hamCount: number; - spamPercentage: number; - }> { - const [total, spam] = await Promise.all([ - prisma.spamFeedback.count({ where: { userId } }), - prisma.spamFeedback.count({ where: { userId, isSpam: true } }), - ]); - - return { - totalAnalyses: total, - spamCount: spam, - hamCount: total - spam, - spamPercentage: total > 0 ? (spam / total) * 100 : 0, - }; - } - - private hashPhoneNumber(phoneNumber: string): string { - // SHA-256 hash for phone number fingerprinting - const hash = createHash('sha256').update(phoneNumber).digest('hex'); - return `sha256_${hash}`; - } -} - -// Export instances -export const numberReputationService = new NumberReputationService(); -export const smsClassifierService = new SMSClassifierService(); -export const callAnalysisService = new CallAnalysisService(); -export const spamFeedbackService = new SpamFeedbackService(); diff --git a/apps/api/src/services/voiceprint/index.ts b/apps/api/src/services/voiceprint/index.ts deleted file mode 100644 index 4d40bf7fb..000000000 --- a/apps/api/src/services/voiceprint/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Config -export { - voicePrintEnv, - VoicePrintSource, - AnalysisJobStatus, - DetectionType, - ConfidenceLevel, - audioPreprocessingConfig, - voicePrintFeatureFlags, - voicePrintRateLimits, - checkFlag, - isFeatureEnabled, -} from './voiceprint.config'; - - - -// Services -export { - AudioPreprocessor, - VoiceEnrollmentService, - AnalysisService, - BatchAnalysisService, - EmbeddingService, - FAISSIndex, - audioPreprocessor, - voiceEnrollmentService, - analysisService, - batchAnalysisService, - embeddingService, -} from './voiceprint.service'; diff --git a/apps/api/src/services/voiceprint/voiceprint.config.ts b/apps/api/src/services/voiceprint/voiceprint.config.ts deleted file mode 100644 index f117f5ae3..000000000 --- a/apps/api/src/services/voiceprint/voiceprint.config.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { z } from 'zod'; -import { checkFlag } from './voiceprint.feature-flags'; - -// Environment variables for VoicePrint -const envSchema = z.object({ - ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'), - ML_SERVICE_URL: z.string().default('http://localhost:8001'), - FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'), - AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'), - AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'), - SYNTHETIC_THRESHOLD: z.string().transform(Number).default(0.75), - ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default(3), - ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default(60), - EMBEDDING_DIMENSIONS: z.string().transform(Number).default(192), - BATCH_MAX_FILES: z.string().transform(Number).default(20), - ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default(30000), -}); - -export const voicePrintEnv = envSchema.parse({ - ECAPA_TDNN_MODEL_PATH: process.env.ECAPA_TDNN_MODEL_PATH, - ML_SERVICE_URL: process.env.ML_SERVICE_URL, - FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH, - AUDIO_STORAGE_BUCKET: process.env.AUDIO_STORAGE_BUCKET, - AUDIO_STORAGE_ENDPOINT: process.env.AUDIO_STORAGE_ENDPOINT, - SYNTHETIC_THRESHOLD: process.env.SYNTHETIC_THRESHOLD, - ENROLLMENT_MIN_DURATION_SEC: process.env.ENROLLMENT_MIN_DURATION_SEC, - ENROLLMENT_MAX_DURATION_SEC: process.env.ENROLLMENT_MAX_DURATION_SEC, - EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS, - BATCH_MAX_FILES: process.env.BATCH_MAX_FILES, - ANALYSIS_TIMEOUT_MS: process.env.ANALYSIS_TIMEOUT_MS, -}); - -// Audio source types -export enum VoicePrintSource { - UPLOAD = 'upload', - S3 = 's3', - URL = 'url', - REALTIME = 'realtime', -} - -// Analysis job status -export enum AnalysisJobStatus { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} - -// Detection result types -export enum DetectionType { - SYNTHETIC_VOICE = 'synthetic_voice', - VOICE_CLONE = 'voice_clone', - DEEPFAKE = 'deepfake', - NATURAL = 'natural', -} - -// Confidence levels -export enum ConfidenceLevel { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - VERY_HIGH = 'very_high', -} - -// Audio preprocessing configuration -export const audioPreprocessingConfig = { - sampleRate: 16000, - channels: 1, - bitDepth: 16, - vadThreshold: 0.5, - noiseReduction: true, - maxSilenceDurationMs: 500, -}; - -// Feature flags - use centralized system -export const voicePrintFeatureFlags = { - enableMLService: checkFlag('voiceprint.enable.ml.service', false), - enableFAISSIndex: checkFlag('voiceprint.enable.faiss.index', true), - enableBatchAnalysis: checkFlag('voiceprint.enable.batch.analysis', true), - enableRealtimeAnalysis: checkFlag('voiceprint.enable.realtime.analysis', false), - enableMockModel: checkFlag('voiceprint.enable.mock.model', true), -}; - -// Rate limits for voice analysis -export const voicePrintRateLimits = { - basic: { - analysesPerMinute: 5, - enrollmentsPerDay: 10, - maxAudioFileSizeMB: 50, - }, - plus: { - analysesPerMinute: 30, - enrollmentsPerDay: 50, - maxAudioFileSizeMB: 200, - }, - premium: { - analysesPerMinute: 100, - enrollmentsPerDay: 500, - maxAudioFileSizeMB: 500, - }, -}; diff --git a/apps/api/src/services/voiceprint/voiceprint.feature-flags.ts b/apps/api/src/services/voiceprint/voiceprint.feature-flags.ts deleted file mode 100644 index c4c664d35..000000000 --- a/apps/api/src/services/voiceprint/voiceprint.feature-flags.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * VoicePrint Feature Flags - * Re-exports the checkFlag function from the centralized feature flag system - */ - -// Re-export the checkFlag function from the spamshield feature flags module -export { checkFlag } from '../spamshield/feature-flags'; diff --git a/apps/api/src/services/voiceprint/voiceprint.service.ts b/apps/api/src/services/voiceprint/voiceprint.service.ts deleted file mode 100644 index 5f45dd373..000000000 --- a/apps/api/src/services/voiceprint/voiceprint.service.ts +++ /dev/null @@ -1,594 +0,0 @@ -import { prisma, VoiceEnrollment, VoiceAnalysis } from '@shieldsai/shared-db'; -import { - voicePrintEnv, - AnalysisJobStatus, - DetectionType, - ConfidenceLevel, - audioPreprocessingConfig, - voicePrintFeatureFlags, -} from './voiceprint.config'; -import { checkFlag } from './voiceprint.feature-flags'; - -// Audio preprocessing service -export class AudioPreprocessor { - /** - * Normalize audio to 16kHz mono with VAD and noise reduction. - * Returns preprocessing metadata and the processed audio buffer. - */ - async preprocess( - audioBuffer: Buffer, - options?: { - sourceSampleRate?: number; - channels?: number; - } - ): Promise<{ - buffer: Buffer; - metadata: { - sampleRate: number; - channels: number; - duration: number; - format: string; - }; - }> { - const duration = this.estimateDuration(audioBuffer, options?.sourceSampleRate ?? 44100); - - if (duration < voicePrintEnv.ENROLLMENT_MIN_DURATION_SEC) { - throw new Error( - `Audio too short: ${duration.toFixed(1)}s < ${voicePrintEnv.ENROLLMENT_MIN_DURATION_SEC}s minimum` - ); - } - - if (duration > voicePrintEnv.ENROLLMENT_MAX_DURATION_SEC) { - throw new Error( - `Audio too long: ${duration.toFixed(1)}s > ${voicePrintEnv.ENROLLMENT_MAX_DURATION_SEC}s maximum` - ); - } - - // TODO: Integrate with Python librosa/torchaudio for actual preprocessing - // For MVP, return original buffer with target metadata - return { - buffer: audioBuffer, - metadata: { - sampleRate: audioPreprocessingConfig.sampleRate, - channels: audioPreprocessingConfig.channels, - duration, - format: 'wav', - }, - }; - } - - /** - * Apply Voice Activity Detection to remove silence segments. - */ - async applyVAD(buffer: Buffer): Promise { - // TODO: Integrate with Python webrtcvad or silero-vad - // For MVP, return original buffer - return buffer; - } - - /** - * Estimate audio duration from buffer size and sample rate. - */ - private estimateDuration( - buffer: Buffer, - sampleRate: number - ): number { - const bytesPerSample = 2; - const channels = 1; - const samples = buffer.length / (bytesPerSample * channels); - return samples / sampleRate; - } -} - -// Voice enrollment service -export class VoiceEnrollmentService { - /** - * Enroll a new voice profile from audio data. - */ - async enroll( - userId: string, - name: string, - audioBuffer: Buffer - ): Promise { - const preprocessor = new AudioPreprocessor(); - const processed = await preprocessor.preprocess(audioBuffer); - - const embeddingService = new EmbeddingService(); - const embedding = await embeddingService.extract(processed.buffer); - const voiceHash = this.computeEmbeddingHash(embedding); - - const enrollment = await prisma.voiceEnrollment.create({ - data: { - userId, - name, - voiceHash, - audioMetadata: { - ...processed.metadata, - embeddingDimensions: embedding.length, - enrollmentTimestamp: new Date().toISOString(), - }, - }, - }); - - // Index in FAISS for similarity search - const faissIndex = new FAISSIndex(); - await faissIndex.add(enrollment.id, embedding); - - return enrollment; - } - - /** - * List all enrollments for a user. - */ - async listEnrollments( - userId: string, - options?: { - isActive?: boolean; - limit?: number; - offset?: number; - } - ): Promise { - return prisma.voiceEnrollment.findMany({ - where: { - userId, - ...(options?.isActive !== undefined && { isActive: options.isActive }), - }, - orderBy: { createdAt: 'desc' }, - take: options?.limit ?? 50, - skip: options?.offset ?? 0, - }); - } - - /** - * Get a single enrollment by ID. - */ - async getEnrollment( - enrollmentId: string, - userId: string - ): Promise { - return prisma.voiceEnrollment.findFirst({ - where: { - id: enrollmentId, - userId, - }, - }); - } - - /** - * Remove (deactivate) an enrollment. - */ - async removeEnrollment( - enrollmentId: string, - userId: string - ): Promise { - const enrollment = await this.getEnrollment(enrollmentId, userId); - if (!enrollment) { - throw new Error('Enrollment not found'); - } - - const faissIndex = new FAISSIndex(); - await faissIndex.remove(enrollmentId); - - return prisma.voiceEnrollment.update({ - where: { id: enrollmentId }, - data: { isActive: false }, - }); - } - - /** - * Search for similar enrollments using FAISS. - */ - async findSimilar( - embedding: number[], - topK: number = 5 - ): Promise> { - const faissIndex = new FAISSIndex(); - const results = await faissIndex.search(embedding, topK); - - const enrollmentIds = results.map((r) => r.id); - const enrollments = await prisma.voiceEnrollment.findMany({ - where: { id: { in: enrollmentIds } }, - }); - - return results.map((r, i) => ({ - enrollment: enrollments[i], - similarity: r.similarity, - })); - } - - private computeEmbeddingHash(embedding: number[]): string { - let hash = 0; - for (let i = 0; i < embedding.length; i++) { - hash = ((hash << 5) - hash) + embedding[i]; - hash |= 0; - } - return `vp_${Math.abs(hash).toString(16)}_${embedding.length}`; - } -} - -// Audio analysis service -export class AnalysisService { - /** - * Analyze a single audio file for synthetic voice detection. - */ - async analyze( - userId: string, - audioBuffer: Buffer, - options?: { - enrollmentId?: string; - audioUrl?: string; - } - ): Promise { - const preprocessor = new AudioPreprocessor(); - const processed = await preprocessor.preprocess(audioBuffer); - - const audioHash = this.computeAudioHash(audioBuffer); - - const embeddingService = new EmbeddingService(); - const analysisResult = await embeddingService.analyze(processed.buffer); - - const isSynthetic = analysisResult.confidence >= voicePrintEnv.SYNTHETIC_THRESHOLD; - - const voiceAnalysis = await prisma.voiceAnalysis.create({ - data: { - userId, - enrollmentId: options?.enrollmentId, - audioHash, - isSynthetic, - confidence: analysisResult.confidence, - analysisResult: { - ...analysisResult, - processedMetadata: processed.metadata, - analysisTimestamp: new Date().toISOString(), - modelVersion: 'ecapa-tdnn-v1-mock', - }, - audioUrl: options?.audioUrl ?? '', - }, - }); - - return voiceAnalysis; - } - - /** - * Get analysis result by ID. - */ - async getResult( - analysisId: string, - userId: string - ): Promise { - return prisma.voiceAnalysis.findFirst({ - where: { - id: analysisId, - userId, - }, - }); - } - - /** - * Get analysis history for a user. - */ - async getHistory( - userId: string, - options?: { - limit?: number; - offset?: number; - isSynthetic?: boolean; - } - ): Promise { - return prisma.voiceAnalysis.findMany({ - where: { - userId, - ...(options?.isSynthetic !== undefined && { isSynthetic: options.isSynthetic }), - }, - orderBy: { createdAt: 'desc' }, - take: options?.limit ?? 50, - skip: options?.offset ?? 0, - }); - } - - private computeAudioHash(buffer: Buffer): string { - let hash = 0; - const sampleSize = Math.min(buffer.length, 1024); - for (let i = 0; i < sampleSize; i += 8) { - hash = ((hash << 5) - hash) + buffer.readUInt8(i); - hash |= 0; - } - return `audio_${Math.abs(hash).toString(16)}`; - } -} - -// Batch analysis service -export class BatchAnalysisService { - /** - * Analyze multiple audio files in a batch. - */ - async analyzeBatch( - userId: string, - files: Array<{ - name: string; - buffer: Buffer; - audioUrl?: string; - }>, - options?: { - enrollmentId?: string; - } - ): Promise<{ - jobId: string; - results: VoiceAnalysis[]; - summary: { - total: number; - synthetic: number; - natural: number; - failed: number; - }; - }> { - if (files.length > voicePrintEnv.BATCH_MAX_FILES) { - throw new Error( - `Batch too large: ${files.length} > ${voicePrintEnv.BATCH_MAX_FILES} max` - ); - } - - const analysisService = new AnalysisService(); - const results: VoiceAnalysis[] = []; - let synthetic = 0; - let natural = 0; - let failed = 0; - - for (const file of files) { - try { - const result = await analysisService.analyze(userId, file.buffer, { - enrollmentId: options?.enrollmentId, - audioUrl: file.audioUrl, - }); - results.push(result); - if (result.isSynthetic) { - synthetic++; - } else { - natural++; - } - } catch (error) { - console.error(`Batch analysis failed for ${file.name}:`, error); - failed++; - } - } - - const jobId = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; - - return { - jobId, - results, - summary: { - total: files.length, - synthetic, - natural, - failed, - }, - }; - } -} - -// Embedding service — ECAPA-TDNN inference wrapper -export class EmbeddingService { - private initialized = false; - - /** - * Initialize the ECAPA-TDNN model. - */ - async initialize(): Promise { - if (this.initialized) return; - - // TODO: Connect to Python ML service for real inference - // const response = await fetch(`${voicePrintEnv.ML_SERVICE_URL}/initialize`, { - // method: 'POST', - // body: JSON.stringify({ modelPath: voicePrintEnv.ECAPA_TDNN_MODEL_PATH }), - // }); - - this.initialized = true; - console.log('Embedding service initialized (mock model)'); - } - - /** - * Extract voice embedding from audio. - */ - async extract(audioBuffer: Buffer): Promise { - await this.initialize(); - - // TODO: Call Python ML service - // const response = await fetch(`${voicePrintEnv.ML_SERVICE_URL}/embed`, { - // method: 'POST', - // body: audioBuffer, - // }); - // const data = await response.json(); - // return data.embedding; - - // Mock: generate deterministic embedding based on buffer content - const dims = voicePrintEnv.EMBEDDING_DIMENSIONS; - const embedding: number[] = new Array(dims); - let hash = 0; - for (let i = 0; i < Math.min(audioBuffer.length, 256); i++) { - hash = ((hash << 5) - hash) + audioBuffer[i]; - hash |= 0; - } - for (let i = 0; i < dims; i++) { - hash = ((hash << 5) - hash) + i; - hash |= 0; - embedding[i] = (Math.abs(hash) % 1000) / 1000.0; - } - - // L2 normalize - const norm = Math.sqrt(embedding.reduce((s, v) => s + v * v, 0)); - return embedding.map((v) => v / norm); - } - - /** - * Run full analysis: embedding + synthetic detection. - */ - async analyze(audioBuffer: Buffer): Promise<{ - confidence: number; - detectionType: DetectionType; - features: Record; - embedding: number[]; - }> { - const embedding = await this.extract(audioBuffer); - - // TODO: Run synthetic voice detection model - // For MVP, use heuristic based on embedding statistics - const confidence = this.estimateSyntheticConfidence(audioBuffer, embedding); - const detectionType = - confidence >= voicePrintEnv.SYNTHETIC_THRESHOLD - ? DetectionType.SYNTHETIC_VOICE - : DetectionType.NATURAL; - - const features = this.extractAnalysisFeatures(audioBuffer, embedding); - - return { - confidence, - detectionType, - features, - embedding, - }; - } - - private estimateSyntheticConfidence( - buffer: Buffer, - embedding: number[] - ): number { - // Heuristic features for synthetic detection - const meanAmplitude = - buffer.reduce((s, v) => s + v, 0) / buffer.length / 255; - const embeddingStdDev = - Math.sqrt( - embedding.reduce((s, v) => s + (v - embedding.reduce((a, b) => a + b) / embedding.length) ** 2, 0) / - embedding.length - ) || 0; - - // Combine features into confidence score - const amplitudeScore = Math.abs(meanAmplitude - 0.5) * 2; - const embeddingScore = 1.0 - Math.min(1.0, embeddingStdDev * 2); - - return Math.min( - 1.0, - amplitudeScore * 0.3 + embeddingScore * 0.4 + Math.random() * 0.3 - ); - } - - private extractAnalysisFeatures( - buffer: Buffer, - embedding: number[] - ): Record { - const meanAmplitude = - buffer.reduce((s, v) => s + v, 0) / buffer.length / 255; - const zeroCrossings = buffer.reduce((count, v, i, arr) => { - return i > 0 && ((v - 128) * (arr[i - 1] - 128) < 0) ? count + 1 : count; - }, 0); - - return { - mean_amplitude: meanAmplitude, - zero_crossing_rate: zeroCrossings / buffer.length, - embedding_energy: embedding.reduce((s, v) => s + v * v, 0), - embedding_entropy: this.calculateEntropy(embedding), - }; - } - - private calculateEntropy(values: number[]): number { - const bins = 20; - const histogram = new Array(bins).fill(0); - const min = Math.min(...values); - const max = Math.max(...values); - const range = max - min || 1; - - for (const v of values) { - const bin = Math.min(bins - 1, Math.floor(((v - min) / range) * bins)); - histogram[bin]++; - } - - let entropy = 0; - const total = values.length; - for (const count of histogram) { - if (count > 0) { - const p = count / total; - entropy -= p * Math.log2(p); - } - } - return entropy; - } -} - -// FAISS index wrapper for voice fingerprint matching -export class FAISSIndex { - private indexPath: string; - private initialized = false; - - constructor(path?: string) { - this.indexPath = path ?? voicePrintEnv.FAISS_INDEX_PATH; - } - - /** - * Initialize or load the FAISS index. - */ - async initialize(): Promise { - if (this.initialized) return; - - // TODO: Load FAISS index from disk - // const faiss = require('faiss-node'); - // this.index = faiss.readIndex(this.indexPath); - - this.initialized = true; - console.log(`FAISS index initialized at ${this.indexPath}`); - } - - /** - * Add an enrollment embedding to the index. - */ - async add(enrollmentId: string, embedding: number[]): Promise { - await this.initialize(); - - // TODO: Add to FAISS index - // this.index.add([embedding]); - // Store mapping: enrollmentId -> index position - console.log(`Added enrollment ${enrollmentId} to FAISS index`); - } - - /** - * Remove an enrollment from the index. - */ - async remove(enrollmentId: string): Promise { - await this.initialize(); - - // TODO: Remove from FAISS index - console.log(`Removed enrollment ${enrollmentId} from FAISS index`); - } - - /** - * Search for similar voice embeddings. - */ - async search( - embedding: number[], - topK: number = 5 - ): Promise> { - await this.initialize(); - - // TODO: Query FAISS index - // const [distances, indices] = this.index.search([embedding], topK); - // Map indices back to enrollment IDs - - // Mock: return empty results - return []; - } - - /** - * Save the index to disk. - */ - async save(): Promise { - await this.initialize(); - // TODO: Write FAISS index to disk - console.log(`FAISS index saved to ${this.indexPath}`); - } -} - -// Export singleton instances -export const audioPreprocessor = new AudioPreprocessor(); -export const voiceEnrollmentService = new VoiceEnrollmentService(); -export const analysisService = new AnalysisService(); -export const batchAnalysisService = new BatchAnalysisService(); -export const embeddingService = new EmbeddingService(); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json deleted file mode 100644 index e229935f5..000000000 --- a/apps/api/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/apps/mobile/package.json b/apps/mobile/package.json deleted file mode 100644 index 6d037a923..000000000 --- a/apps/mobile/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "mobile", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint src/" - }, - "dependencies": { - "solid-js": "^1.8.14", - "@shieldsai/shared-auth": "*", - "@shieldsai/shared-ui": "*", - "@shieldsai/shared-utils": "*" - }, - "devDependencies": { - "typescript": "^5.3.3", - "vite": "^5.1.4", - "@types/node": "^25.6.0" - } -} diff --git a/apps/web/package.json b/apps/web/package.json deleted file mode 100644 index fd55f6e90..000000000 --- a/apps/web/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "web", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "lint": "eslint src/" - }, - "dependencies": { - "solid-js": "^1.8.14", - "@shieldsai/shared-auth": "*", - "@shieldsai/shared-ui": "*", - "@shieldsai/shared-utils": "*" - }, - "devDependencies": { - "typescript": "^5.3.3", - "vite": "^5.1.4", - "vite-plugin-solid": "^2.8.2", - "@types/node": "^25.6.0" - } -} diff --git a/examples/call-analysis-example.ts b/examples/call-analysis-example.ts deleted file mode 100644 index a86a8d236..000000000 --- a/examples/call-analysis-example.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Example: Real-Time Call Analysis - * Demonstrates how to use the RealTimeCallAnalysisServer - */ - -import { RealTimeCallAnalysisServer } from '../src/lib/call-analysis/real-time-call-server'; - -async function example() { - // Create and start the server - const server = new RealTimeCallAnalysisServer({ - port: 8089, - enableEchoCancellation: true, - enableNoiseSuppression: true, - enableAutoGainControl: true, - analysisConfig: { - sentimentWindowMs: 5000, - interruptThresholdMs: 200, - overlapThresholdMs: 300, - pauseThresholdMs: 2000, - volumeSpikeThreshold: 0.8, - anomalySensitivity: 'medium', - enableSpeakerDiarization: false, - }, - }); - - // Listen for events - server.on('client:connected', ({ clientId }) => { - console.log(`Client connected: ${clientId}`); - }); - - server.on('client:disconnected', ({ clientId }) => { - console.log(`Client disconnected: ${clientId}`); - }); - - server.on('analysis:alert', ({ clientId, alert }) => { - console.log(`Alert from ${clientId}: ${alert.message} (${alert.severity})`); - }); - - server.on('analysis:result', ({ clientId, status }) => { - console.log(`Analysis status for ${clientId}: ${status}`); - }); - - server.on('analysis:error', ({ clientId, error }) => { - console.error(`Error for ${clientId}:`, error); - }); - - // Start the server - await server.start(); - console.log('Server started, waiting for clients...'); - - // Example: Client connection simulation - const WebSocket = require('ws'); - const client = new WebSocket('ws://localhost:8089?clientId=test-client'); - - client.on('open', () => { - console.log('Client connected'); - - // Start audio capture - client.send(JSON.stringify({ type: 'start' })); - }); - - client.on('message', (data: Buffer) => { - const message = JSON.parse(data.toString()); - console.log('Received:', message.type, message); - - if (message.type === 'alert' || message.type === 'anomaly') { - console.log(` - ${message.alertType}: ${message.message}`); - } - - if (message.type === 'analysis') { - console.log(` - MOS: ${message.callQuality.mosScore}`); - console.log(` - Sentiment: ${message.sentiment.sentiment}`); - console.log(` - Summary: ${message.summary}`); - } - }); - - // Stop after 60 seconds - setTimeout(async () => { - console.log('Stopping server...'); - await server.stop(); - process.exit(0); - }, 60000); -} - -// Run example if called directly -if (require.main === module) { - example().catch(console.error); -} - -export default example; diff --git a/package-lock.json b/package-lock.json index 8548cf29d..41b4b0669 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "shieldsai-monorepo", + "name": "frenocorp", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "shieldsai-monorepo", + "name": "frenocorp", "version": "0.1.0", "license": "MIT", "workspaces": [ @@ -58,6 +58,7 @@ }, "apps/api": { "version": "0.1.0", + "extraneous": true, "dependencies": { "@fastify/cors": "^11.2.0", "@fastify/helmet": "^13.0.2", @@ -77,6 +78,7 @@ }, "apps/mobile": { "version": "0.1.0", + "extraneous": true, "dependencies": { "@shieldsai/shared-auth": "*", "@shieldsai/shared-ui": "*", @@ -91,6 +93,7 @@ }, "apps/web": { "version": "0.1.0", + "extraneous": true, "dependencies": { "@shieldsai/shared-auth": "*", "@shieldsai/shared-ui": "*", @@ -338,6 +341,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -525,17 +529,6 @@ "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", "license": "Apache-2.0" }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -1450,608 +1443,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fastify/ajv-compiler": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", - "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", - "fast-uri": "^2.0.0" - } - }, - "node_modules/@fastify/ajv-compiler/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@fastify/ajv-compiler/node_modules/ajv/node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/@fastify/busboy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", - "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", - "license": "MIT" - }, - "node_modules/@fastify/cors": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz", - "integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fastify-plugin": "^5.0.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/@fastify/cors/node_modules/fastify-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", - "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/error": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", - "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", - "license": "MIT" - }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", - "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", - "license": "MIT", - "dependencies": { - "fast-json-stringify": "^5.7.0" - } - }, - "node_modules/@fastify/helmet": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-13.0.2.tgz", - "integrity": "sha512-tO1QMkOfNeCt9l4sG/FiWErH4QMm+RjHzbMTrgew1DYOQ2vb/6M1G2iNABBrD7Xq6dUk+HLzWW8u+rmmhQHifA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fastify-plugin": "^5.0.0", - "helmet": "^8.0.0" - } - }, - "node_modules/@fastify/helmet/node_modules/fastify-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", - "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", - "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", - "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-types": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.4.tgz", - "integrity": "sha512-crX9TA5SVYZwLPG7/R16IsH8FLlgkPXjJUVhsVpHVDSqJiq3D/NuFTM5ctxGTExXAOeIn//69tQw47CPerM8MQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/logger": "0.5.0" - } - }, - "node_modules/@firebase/auth-interop-types": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", - "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/component": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.2.tgz", - "integrity": "sha512-iyVDGc6Vjx7Rm0cAdccLH/NG6fADsgJak/XW9IA2lPf8AjIlsemOpFGKczYyPHxm4rnKdR8z6sK4+KEC7NwmEg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.15.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.2.tgz", - "integrity": "sha512-lP96CMjMPy/+d1d9qaaHjHHdzdwvEOuyyLq9ehX89e2XMKwS1jHNzYBO+42bdSumuj5ukPbmnFtViZu8YOMT+w==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.2", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.15.0", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database-compat": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.3.tgz", - "integrity": "sha512-GMyfWjD8mehjg/QpNkY/tl9G/MoeugPeg91n9D0atggxbWuKF/2KhVPHZDH+XmoP0EKYqMWYTtKxBsaBaNKLYQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.2", - "@firebase/database": "1.1.2", - "@firebase/database-types": "1.0.19", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.15.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database-types": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.19.tgz", - "integrity": "sha512-FqewjUZmV9LqFfuEnmgdcUpiOUz7qwLXxnm/H8BcMFEzQXtd1yyUDm8ex5VRad2nuTE+ahOuCjUAM/cyDncO+g==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-types": "0.9.4", - "@firebase/util": "1.15.0" - } - }, - "node_modules/@firebase/logger": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", - "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/util": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.0.tgz", - "integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@google-cloud/firestore": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", - "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@opentelemetry/api": "^1.3.0", - "fast-deep-equal": "^3.1.1", - "functional-red-black-tree": "^1.0.1", - "google-gax": "^4.3.3", - "protobufjs": "^7.2.6" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/paginator": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", - "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", - "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", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", - "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", - "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", - "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", - "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", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "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", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "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", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage/node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/@google-cloud/storage/node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "license": "MIT", - "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", - "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/@grpc/grpc-js/node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@grpc/grpc-js/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@grpc/grpc-js/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "optional": true, - "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/@grpc/grpc-js/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@grpc/grpc-js/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "optional": true, - "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/@grpc/grpc-js/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", - "license": "Apache-2.0", - "optional": true, - "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/@grpc/proto-loader/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "optional": true, - "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/@grpc/proto-loader/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@grpc/proto-loader/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "optional": true, - "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/@grpc/proto-loader/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=12" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2114,503 +1505,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", - "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", - "license": "MIT" - }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -2707,17 +1601,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/@libsql/client": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.3.tgz", @@ -2877,27 +1760,6 @@ "win32" ] }, - "node_modules/@lukeed/csprng": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", - "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@lukeed/uuid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@lukeed/uuid/-/uuid-2.0.1.tgz", - "integrity": "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==", - "license": "MIT", - "dependencies": { - "@lukeed/csprng": "^1.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@msgpack/msgpack": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", @@ -2907,233 +1769,12 @@ "node": ">= 10" } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@neon-rs/load": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", "license": "MIT" }, - "node_modules/@next/env": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", - "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", - "license": "MIT", - "peer": true - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", - "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", - "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", - "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", - "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", - "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", - "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", - "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", - "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", @@ -3185,19 +1826,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT", - "optional": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3242,31 +1870,19 @@ "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", "optional": true, + "peer": true, "engines": { "node": ">=8.0.0" } }, - "node_modules/@panva/hkdf": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", - "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "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/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=16.13" }, @@ -3283,16 +1899,18 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@prisma/engines": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", @@ -3304,15 +1922,17 @@ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@prisma/fetch-engine": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", @@ -3323,86 +1943,13 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/debug": "5.22.0" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause", - "optional": true - }, "node_modules/@puppeteer/browsers": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", @@ -4270,96 +2817,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@segment/analytics-core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@segment/analytics-core/-/analytics-core-1.4.1.tgz", - "integrity": "sha512-kV0Pf33HnthuBOVdYNani21kYyj118Fn+9757bxqoksiXoZlYvBsFq6giNdCsKcTIE1eAMqNDq3xE1VQ0cfsHA==", - "license": "MIT", - "dependencies": { - "@lukeed/uuid": "^2.0.0", - "@segment/analytics-generic-utils": "1.1.1", - "dset": "^3.1.2", - "tslib": "^2.4.1" - } - }, - "node_modules/@segment/analytics-generic-utils": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@segment/analytics-generic-utils/-/analytics-generic-utils-1.1.1.tgz", - "integrity": "sha512-THTIzBPHnvu1HYJU3fARdJ3qIkukO3zDXsmDm+kAeUks5R9CBXOQ6rPChiASVzSmwAIIo5uFIXXnCraojlq/Gw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.4.1" - } - }, - "node_modules/@segment/analytics-node": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@segment/analytics-node/-/analytics-node-1.3.0.tgz", - "integrity": "sha512-lRLz1WZaDokMoUe299yP5JkInc3OgJuqNNlxb6j0q22umCiq6b5iDo2gRmFn93reirIvJxWIicQsGrHd93q8GQ==", - "license": "MIT", - "dependencies": { - "@lukeed/uuid": "^2.0.0", - "@segment/analytics-core": "1.4.1", - "@segment/analytics-generic-utils": "1.1.1", - "buffer": "^6.0.3", - "node-fetch": "^2.6.7", - "tslib": "^2.4.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@segment/analytics-node/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "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", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/@shieldsai/jobs": { - "resolved": "packages/jobs", - "link": true - }, - "node_modules/@shieldsai/shared-analytics": { - "resolved": "packages/shared-analytics", - "link": true - }, - "node_modules/@shieldsai/shared-auth": { - "resolved": "packages/shared-auth", - "link": true - }, - "node_modules/@shieldsai/shared-db": { - "resolved": "packages/shared-db", - "link": true - }, - "node_modules/@shieldsai/shared-notifications": { - "resolved": "packages/shared-notifications", - "link": true - }, - "node_modules/@shieldsai/shared-ui": { - "resolved": "packages/shared-ui", - "link": true - }, - "node_modules/@shieldsai/shared-utils": { - "resolved": "packages/shared-utils", - "link": true - }, "node_modules/@sinclair/typebox": { "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", @@ -5197,16 +3654,6 @@ "node": ">= 10" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -5413,13 +3860,6 @@ "@types/responselike": "^1.0.0" } }, - "node_modules/@types/caseless": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", - "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", - "license": "MIT", - "optional": true - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5478,16 +3918,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -5498,19 +3928,6 @@ "@types/node": "*" } }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "25.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", @@ -5529,19 +3946,6 @@ "@types/webrtc": "*" } }, - "node_modules/@types/request": { - "version": "2.48.13", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", - "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.5" - } - }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -5559,13 +3963,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "license": "MIT", - "optional": true - }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -6058,6 +4455,7 @@ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", + "peer": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -6083,12 +4481,6 @@ "node": ">=6" } }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "license": "MIT" - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -6177,61 +4569,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/alien-signals": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.6.tgz", @@ -6269,10 +4606,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/api": { - "resolved": "apps/api", - "link": true - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6290,16 +4623,6 @@ "node": ">=8" } }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "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", @@ -6337,98 +4660,6 @@ "license": "MIT", "optional": true }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "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/avvio": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", - "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", - "license": "MIT", - "dependencies": { - "@fastify/error": "^3.3.0", - "fastq": "^1.17.1" - } - }, - "node_modules/axios": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", - "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/axios/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/axios/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/axios/node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/b4a": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", @@ -6670,15 +4901,6 @@ "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -6859,12 +5081,6 @@ "node": "*" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6899,23 +5115,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/bullmq": { - "version": "5.76.4", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.4.tgz", - "integrity": "sha512-hVAplia7zfN3BxSCgAoRInJnbemfLwJdQLqJy/txEX8UMSTAeg0saPFNGWIlzES/Ct5xQ20TUaik/XwS99DOMA==", - "license": "MIT", - "dependencies": { - "cron-parser": "4.9.0", - "ioredis": "5.10.1", - "msgpackr": "1.11.5", - "node-abort-controller": "3.1.1", - "semver": "7.7.4", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=12.22.0" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -6981,35 +5180,6 @@ "node": ">=6" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "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", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7209,13 +5379,6 @@ "license": "MIT", "peer": true }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true - }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -7259,15 +5422,6 @@ "node": ">=6" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -7286,18 +5440,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", @@ -7409,15 +5551,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/core-js": { "version": "3.47.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", @@ -7436,18 +5569,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7484,12 +5605,6 @@ "node": ">= 14" } }, - "node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7615,24 +5730,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8318,51 +6415,6 @@ } } }, - "node_modules/dset": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", - "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "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", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "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/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8455,51 +6507,6 @@ "stackframe": "^1.3.4" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "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/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -8964,6 +6971,7 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -9025,12 +7033,6 @@ "license": "Apache-2.0", "peer": true }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, "node_modules/extract-zip": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", @@ -9086,31 +7088,11 @@ "node": "> 0.1.90" } }, - "node_modules/farmhash-modern": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", - "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/fast-content-type-parse": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", - "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", - "license": "MIT" - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -9157,76 +7139,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-json-stringify": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", - "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", - "license": "MIT", - "dependencies": { - "@fastify/merge-json-schemas": "^0.1.0", - "ajv": "^8.10.0", - "ajv-formats": "^3.0.1", - "fast-deep-equal": "^3.1.3", - "fast-uri": "^2.1.0", - "json-schema-ref-resolver": "^1.0.1", - "rfdc": "^1.2.0" - } - }, - "node_modules/fast-json-stringify/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fast-json-stringify/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -9234,15 +7146,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "license": "MIT", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, "node_modules/fast-sha256": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", @@ -9256,50 +7159,6 @@ "license": "MIT", "peer": true }, - "node_modules/fast-uri": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", - "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", - "license": "MIT" - }, - "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "path-expression-matcher": "^1.1.3" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", - "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -9309,67 +7168,16 @@ "node": ">= 4.9.1" } }, - "node_modules/fastify": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", - "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/ajv-compiler": "^3.5.0", - "@fastify/error": "^3.4.0", - "@fastify/fast-json-stringify-compiler": "^4.3.0", - "abstract-logging": "^2.0.1", - "avvio": "^8.3.0", - "fast-content-type-parse": "^1.1.0", - "fast-json-stringify": "^5.8.0", - "find-my-way": "^8.0.0", - "light-my-request": "^5.11.0", - "pino": "^9.0.0", - "process-warning": "^3.0.0", - "proxy-addr": "^2.0.7", - "rfdc": "^1.3.0", - "secure-json-parse": "^2.7.0", - "semver": "^7.5.4", - "toad-cache": "^3.3.0" - } - }, - "node_modules/fastify-plugin": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", - "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", - "license": "MIT" - }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/fb-dotslash": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", @@ -9403,29 +7211,6 @@ "pend": "~1.2.0" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "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": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -9494,20 +7279,6 @@ "license": "MIT", "peer": true }, - "node_modules/find-my-way": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", - "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^3.1.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9525,44 +7296,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/firebase-admin": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.8.0.tgz", - "integrity": "sha512-iawoQkmZbsA+2DY5UEuB8f6jSlskzzySoye0D2F6e3zlDZX9DUcXf0HhZqLUn/P6WhLGvTf6ZtCmshZvhAgTYg==", - "license": "Apache-2.0", - "dependencies": { - "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "^2.0.0", - "@firebase/database-types": "^1.0.6", - "farmhash-modern": "^1.1.0", - "fast-deep-equal": "^3.1.1", - "google-auth-library": "^10.6.1", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.1.0", - "node-forge": "^1.4.0", - "uuid": "^11.0.2" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@google-cloud/firestore": "^7.11.0", - "@google-cloud/storage": "^7.19.0" - } - }, - "node_modules/firebase-admin/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -9592,88 +7325,6 @@ "license": "MIT", "peer": true }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -9712,118 +7363,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "license": "MIT", - "optional": true - }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "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/gaxios/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gaxios/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "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/gcp-metadata/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "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/gcp-metadata/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "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/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9852,43 +7391,6 @@ "node": "*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "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", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -10038,300 +7540,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", - "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/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/google-auth-library/node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "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/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "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": "4.6.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", - "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@grpc/grpc-js": "^1.10.9", - "@grpc/proto-loader": "^0.7.13", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "google-auth-library": "^9.3.0", - "node-fetch": "^2.7.0", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^2.0.2", - "protobufjs": "^7.3.2", - "retry-request": "^7.0.0", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "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-gax/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "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-gax/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/google-gax/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis": { - "version": "128.0.0", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-128.0.0.tgz", - "integrity": "sha512-+sLtVYNazcxaSD84N6rihVX4QiGoqRdnlz2SwmQQkadF31XonDfy4ufk3maMg27+FiySrH0rd7V8p+YJG6cknA==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^9.0.0", - "googleapis-common": "^7.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/googleapis-common": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", - "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "gaxios": "^6.0.3", - "google-auth-library": "^9.7.0", - "qs": "^6.7.0", - "url-template": "^2.0.8", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/googleapis-common/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis-common/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "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/googleapis-common/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis-common/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/googleapis/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "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/googleapis/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/got": { "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", @@ -10372,19 +7580,6 @@ "dev": true, "license": "MIT" }, - "node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "license": "MIT", - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -10394,54 +7589,6 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "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.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/helmet": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", - "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/hermes-compiler": { "version": "250829098.0.10", "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.10.tgz", @@ -10524,12 +7671,6 @@ "node": ">= 0.8" } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -10719,30 +7860,6 @@ "loose-envify": "^1.0.0" } }, - "node_modules/ioredis": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", - "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "1.5.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -10753,15 +7870,6 @@ "node": ">= 12" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -11134,15 +8242,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-base64": { "version": "3.7.8", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", @@ -11196,15 +8295,6 @@ "node": ">=6" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -11212,15 +8302,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-ref-resolver": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", - "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -11254,65 +8335,6 @@ "node": ">=6" } }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "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/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "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": "3.2.2", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", - "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", - "license": "MIT", - "dependencies": { - "@types/jsonwebtoken": "^9.0.4", - "debug": "^4.3.4", - "jose": "^4.15.4", - "limiter": "^1.1.5", - "lru-memoizer": "^2.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/kebab-case": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz", @@ -11564,17 +8586,6 @@ "@libsql/win32-x64-msvc": "0.5.29" } }, - "node_modules/light-my-request": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", - "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", - "license": "BSD-3-Clause", - "dependencies": { - "cookie": "^0.7.0", - "process-warning": "^3.0.0", - "set-cookie-parser": "^2.4.1" - } - }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -11603,11 +8614,6 @@ "license": "MIT", "peer": true }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, "node_modules/local-pkg": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", @@ -11647,73 +8653,12 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT", - "optional": true - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "license": "MIT" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -11721,12 +8666,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -11734,13 +8673,6 @@ "license": "MIT", "peer": true }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0", - "optional": true - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11783,34 +8715,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lru-memoizer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", - "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", - "license": "MIT", - "dependencies": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "6.0.0" - } - }, - "node_modules/lru-memoizer/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lru-memoizer/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/ltgt": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", @@ -11818,15 +8722,6 @@ "license": "MIT", "optional": true }, - "node_modules/luxon": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", - "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -11854,15 +8749,6 @@ "license": "Apache-2.0", "peer": true }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -12536,51 +9422,17 @@ "dev": true, "license": "MIT" }, - "node_modules/mobile": { - "resolved": "apps/mobile", - "link": true - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -12636,131 +9488,6 @@ "node": ">= 0.4.0" } }, - "node_modules/next": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", - "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@next/env": "16.2.4", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.4", - "@next/swc-darwin-x64": "16.2.4", - "@next/swc-linux-arm64-gnu": "16.2.4", - "@next/swc-linux-arm64-musl": "16.2.4", - "@next/swc-linux-x64-gnu": "16.2.4", - "@next/swc-linux-x64-musl": "16.2.4", - "@next/swc-win32-arm64-msvc": "16.2.4", - "@next/swc-win32-x64-msvc": "16.2.4", - "sharp": "^0.34.5" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-auth": { - "version": "4.24.14", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.14.tgz", - "integrity": "sha512-YRz6xFDXKUwiXSMMChbrBEWyFktZ1qZXEgeSHQQ3nsy08B4c/xLk6REeutRsIFwkjY/1+ShHnu07DN3JeJguig==", - "license": "ISC", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@panva/hkdf": "^1.0.2", - "cookie": "^0.7.0", - "jose": "^4.15.5", - "oauth": "^0.9.15", - "openid-client": "^5.4.0", - "preact": "^10.6.3", - "preact-render-to-string": "^5.1.19", - "uuid": "^8.3.2" - }, - "peerDependencies": { - "@auth/core": "0.34.3", - "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", - "nodemailer": "^7.0.7", - "react": "^17.0.2 || ^18 || ^19", - "react-dom": "^17.0.2 || ^18 || ^19" - }, - "peerDependenciesMeta": { - "@auth/core": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, - "node_modules/next/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "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", - "peer": true, - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/node-abi": { "version": "3.89.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", @@ -12774,37 +9501,12 @@ "node": ">=10" } }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "license": "MIT" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "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-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -12820,15 +9522,6 @@ } } }, - "node_modules/node-forge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-gyp-build": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", @@ -12841,21 +9534,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -12918,12 +9596,6 @@ "license": "MIT", "peer": true }, - "node_modules/oauth": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", - "license": "MIT" - }, "node_modules/ob1": { "version": "0.84.3", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.3.tgz", @@ -12937,45 +9609,6 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, - "node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/oidc-token-hash": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", - "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", - "license": "MIT", - "engines": { - "node": "^10.13.0 || >=12.0.0" - } - }, - "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.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -13032,39 +9665,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openid-client": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", - "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", - "license": "MIT", - "dependencies": { - "jose": "^4.15.9", - "lru-cache": "^6.0.0", - "object-hash": "^2.2.0", - "oidc-token-hash": "^5.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/openid-client/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/openid-client/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -13154,7 +9754,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -13270,22 +9870,6 @@ "node": ">=8" } }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -13389,59 +9973,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pino": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", - "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.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": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "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/pino/node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -13515,24 +10046,6 @@ "url": "https://opencollective.com/preact" } }, - "node_modules/preact-render-to-string": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", - "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", - "license": "MIT", - "dependencies": { - "pretty-format": "^3.8.0" - }, - "peerDependencies": { - "preact": ">=10" - } - }, - "node_modules/preact-render-to-string/node_modules/pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", - "license": "MIT" - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -13601,9 +10114,10 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -13624,12 +10138,6 @@ "dev": true, "license": "MIT" }, - "node_modules/process-warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", - "license": "MIT" - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -13656,57 +10164,6 @@ "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", "license": "ISC" }, - "node_modules/proto3-json-serializer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", - "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "protobufjs": "^7.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/protobufjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", - "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-agent": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", @@ -13808,21 +10265,6 @@ "node": ">=10.13.0" } }, - "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -13854,12 +10296,6 @@ ], "license": "MIT" }, - "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/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -13985,6 +10421,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "optional": true, "peer": true, "dependencies": { "scheduler": "^0.27.0" @@ -14216,36 +10653,6 @@ "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/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -14262,15 +10669,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -14337,56 +10735,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ret": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", - "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/retry-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", - "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/request": "^2.48.8", - "extend": "^3.0.2", - "teeny-request": "^9.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -14563,24 +10922,6 @@ ], "license": "MIT" }, - "node_modules/safe-regex2": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", - "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", - "license": "MIT", - "dependencies": { - "ret": "~0.4.0" - } - }, - "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/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -14588,25 +10929,12 @@ "license": "MIT", "peer": true }, - "node_modules/scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", - "deprecated": "Just use Node.js's crypto.timingSafeEqual()", - "license": "BSD-3-Clause" - }, "node_modules/sdp": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.2.tgz", "integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==", "license": "MIT" }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "license": "BSD-3-Clause" - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -14757,12 +11085,6 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -14770,63 +11092,6 @@ "license": "ISC", "peer": true }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/sharp/node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -14861,78 +11126,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "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", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -15077,15 +11270,6 @@ "solid-js": "^1.3" } }, - "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": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -15099,6 +11283,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -15114,15 +11299,6 @@ "source-map": "^0.6.0" } }, - "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", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -15160,12 +11336,6 @@ "node": ">=8" } }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, "node_modules/standardwebhooks": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", @@ -15199,16 +11369,6 @@ "license": "BSD-3-Clause", "peer": true }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "license": "MIT", - "optional": true, - "dependencies": { - "stubs": "^3.0.0" - } - }, "node_modules/stream-json": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", @@ -15219,13 +11379,6 @@ "stream-chain": "^2.2.5" } }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT", - "optional": true - }, "node_modules/streamx": { "version": "2.25.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", @@ -15320,26 +11473,6 @@ "dev": true, "license": "MIT" }, - "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", - "license": "MIT", - "optional": true - }, "node_modules/style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -15350,30 +11483,6 @@ "inline-style-parser": "0.1.1" } }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "peer": true, - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, "node_modules/superstruct": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", @@ -15449,79 +11558,6 @@ "node": ">=6" } }, - "node_modules/teeny-request": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", - "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", - "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", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/teeny-request/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -15581,15 +11617,6 @@ "dev": true, "license": "MIT" }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", @@ -15704,15 +11731,6 @@ "node": ">=8.0" } }, - "node_modules/toad-cache": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", - "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -15727,7 +11745,8 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ts-api-utils": { "version": "1.4.3", @@ -15808,49 +11827,6 @@ "@turbo/windows-arm64": "2.9.6" } }, - "node_modules/twilio": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.13.1.tgz", - "integrity": "sha512-sT+PkhptF4Mf7t8eXFFvPQx4w5VHnBIPXbltGPMFRe+R2GxfRdMuFbuNA/cEm0aQR6LFQOn33+fhClg+TjRVqQ==", - "license": "MIT", - "dependencies": { - "axios": "^1.13.5", - "dayjs": "^1.11.9", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^9.0.3", - "qs": "^6.14.1", - "scmp": "^2.1.0", - "xmlbuilder": "^13.0.2" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/twilio/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/twilio/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -15977,12 +11953,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", - "license": "BSD" - }, "node_modules/utf-8-validate": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", @@ -16033,6 +12003,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "license": "MIT", + "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -16813,19 +12784,6 @@ "makeerror": "1.0.12" } }, - "node_modules/web": { - "resolved": "apps/web", - "link": true - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webdriver-bidi-protocol": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", @@ -16837,7 +12795,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/webrtc-adapter": { "version": "9.0.5", @@ -16852,29 +12811,6 @@ "npm": ">=3.10.0" } }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "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", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", @@ -16887,6 +12823,7 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -16982,15 +12919,6 @@ } } }, - "node_modules/xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -17235,7 +13163,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -17285,6 +13213,7 @@ "packages/jobs": { "name": "@shieldsai/jobs", "version": "0.1.0", + "extraneous": true, "dependencies": { "@shieldsai/shared-db": "*", "@shieldsai/shared-utils": "*", @@ -17300,6 +13229,7 @@ "packages/shared-analytics": { "name": "@shieldsai/shared-analytics", "version": "0.1.0", + "extraneous": true, "dependencies": { "@segment/analytics-node": "^1.0.0", "googleapis": "^128.0.0", @@ -17312,6 +13242,7 @@ "packages/shared-auth": { "name": "@shieldsai/shared-auth", "version": "0.1.0", + "extraneous": true, "dependencies": { "next-auth": "^4.24.0", "zod": "^4.3.6" @@ -17323,6 +13254,7 @@ "packages/shared-db": { "name": "@shieldsai/shared-db", "version": "0.1.0", + "extraneous": true, "dependencies": { "@prisma/client": "^5.14.0", "zod": "^4.3.6" @@ -17335,6 +13267,7 @@ "packages/shared-notifications": { "name": "@shieldsai/shared-notifications", "version": "0.1.0", + "extraneous": true, "dependencies": { "firebase-admin": "^13.2.0", "resend": "^6.12.2", @@ -17349,6 +13282,7 @@ "packages/shared-ui": { "name": "@shieldsai/shared-ui", "version": "0.1.0", + "extraneous": true, "dependencies": { "solid-js": "^1.8.14" }, @@ -17359,6 +13293,7 @@ "packages/shared-utils": { "name": "@shieldsai/shared-utils", "version": "0.1.0", + "extraneous": true, "devDependencies": { "typescript": "^5.3.3", "vitest": "^1.3.1" diff --git a/package.json b/package.json index e142fac8f..75030e6cf 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "shieldsai-monorepo", + "name": "frenocorp", "version": "0.1.0", "private": true, - "description": "ShieldAI multi-service SaaS platform", + "description": "FrenoCorp agent orchestration platform", "type": "module", "packageManager": "npm@10.9.0", "workspaces": [ diff --git a/packages/jobs/package.json b/packages/jobs/package.json deleted file mode 100644 index 96f87e8c9..000000000 --- a/packages/jobs/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@shieldsai/jobs", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", - "scripts": { - "start": "tsx src/index.ts", - "dev": "tsx watch src/index.ts" - }, - "dependencies": { - "@shieldsai/shared-analytics": "*", - "@shieldsai/shared-billing": "*", - "@shieldsai/shared-db": "*", - "@shieldsai/shared-utils": "*", - "bullmq": "^5.1.0", - "ioredis": "^5.3.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "tsx": "^4.7.1", - "typescript": "^5.3.3" - } -} diff --git a/packages/jobs/src/darkwatch.jobs.ts b/packages/jobs/src/darkwatch.jobs.ts deleted file mode 100644 index 1b7c4f208..000000000 --- a/packages/jobs/src/darkwatch.jobs.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { prisma, SubscriptionTier } from '@shieldsai/shared-db'; -import { Queue, Worker, Job } from 'bullmq'; -import { Redis } from 'ioredis'; -import { tierConfig, getTierFeatures } from '@shieldsai/shared-billing'; -import { mixpanelService, EventType } from '@shieldsai/shared-analytics'; - -const redisHost = process.env.REDIS_HOST || 'localhost'; -const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10); - -const connection = new Redis({ - host: redisHost, - port: redisPort, - retryStrategy: (times: number) => Math.min(times * 50, 2000), -}); - -const QUEUE_CONFIG = { - darkwatchScan: { - name: 'darkwatch-scan', - concurrency: parseInt(process.env.DARKWATCH_CONCURRENCY || '5', 10), - defaultJobTimeout: parseInt(process.env.DARKWATCH_JOB_TIMEOUT || '120000', 10), - maxAttempts: parseInt(process.env.DARKWATCH_MAX_ATTEMPTS || '3', 10), - }, -}; - -export const darkwatchScanQueue = new Queue( - QUEUE_CONFIG.darkwatchScan.name, - { connection } -); - -async function processDarkwatchScan( - job: Job<{ - subscriptionId: string; - tier: string; - scanType: 'scheduled' | 'on-demand' | 'realtime'; - sourceData?: Record; - }> -) { - const { subscriptionId, tier, scanType, sourceData } = job.data; - - const { scanService } = await import( - '../../../apps/api/src/services/darkwatch/scan.service' - ); - const { alertPipeline } = await import( - '../../../apps/api/src/services/darkwatch/alert.pipeline' - ); - - job.updateProgress(10); - console.log( - `[DarkWatch:Scan] Starting ${scanType} scan for subscription ${subscriptionId} (tier: ${tier})` - ); - - try { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { userId: true, tier: true }, - }); - - if (!subscription) { - job.updateProgress(100); - return { status: 'skipped', reason: 'subscription_not_found' }; - } - - await mixpanelService.track( - EventType.DARK_WEB_SCAN_STARTED, - subscription.userId, - { - scanType, - subscriptionTier: subscription.tier, - } - ); - - job.updateProgress(25); - - const watchlistItems = await scanService.getWatchlistItems(subscriptionId); - - if (watchlistItems.length === 0) { - job.updateProgress(100); - return { status: 'completed', exposuresCreated: 0, exposuresUpdated: 0 }; - } - - job.updateProgress(50); - - const { exposuresCreated, exposuresUpdated } = - await scanService.processSubscriptionScan(subscriptionId, watchlistItems); - - job.updateProgress(80); - - const newExposureIds = await prisma.exposure.findMany({ - where: { - subscriptionId, - isFirstTime: true, - detectedAt: { gte: new Date(Date.now() - 5 * 60 * 1000) }, - }, - select: { id: true }, - }); - - if (newExposureIds.length > 0) { - await alertPipeline.processNewExposures(newExposureIds.map((e) => e.id)); - } - - await alertPipeline.dispatchScanCompleteAlert( - subscriptionId, - subscription.userId, - exposuresCreated - ); - - job.updateProgress(95); - - await mixpanelService.track( - EventType.DARK_WEB_SCAN_COMPLETED, - subscription.userId, - { - scanType, - subscriptionTier: subscription.tier, - exposuresCreated, - exposuresUpdated, - watchlistItemsScanned: watchlistItems.length, - } - ); - - job.updateProgress(100); - - return { - status: 'completed', - exposuresCreated, - exposuresUpdated, - watchlistItemsScanned: watchlistItems.length, - }; - } catch (error) { - const message = error instanceof Error ? error.message : 'Scan failed'; - console.error(`[DarkWatch:Scan] Job ${job.id} failed:`, message); - job.updateProgress(100); - throw new Error(message); - } -} - -export const darkwatchScanWorker = new Worker( - QUEUE_CONFIG.darkwatchScan.name, - processDarkwatchScan, - { - connection, - concurrency: QUEUE_CONFIG.darkwatchScan.concurrency, - limiter: { - max: 20, - duration: 1000, - }, - removeOnComplete: { - age: 7 * 24 * 60 * 60, - count: 1000, - }, - removeOnFail: { - age: 30 * 24 * 60 * 60, - count: 100, - }, - } -); - -darkwatchScanWorker.on('completed', (job, result) => { - console.log(`[DarkWatch:Scan] Job ${job.id} completed:`, result); -}); - -darkwatchScanWorker.on('failed', (job, err) => { - console.error(`[DarkWatch:Scan] Job ${job?.id} failed:`, err.message); -}); - -darkwatchScanWorker.on('error', (err) => { - console.error('[DarkWatch:Scan] Worker error:', err.message); -}); - -export default { - darkwatchScanQueue, - darkwatchScanWorker, -}; diff --git a/packages/jobs/src/index.ts b/packages/jobs/src/index.ts deleted file mode 100644 index 40111361a..000000000 --- a/packages/jobs/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - voiceprintAnalysisQueue, - voiceprintAnalysisWorker, -} from './voiceprint.jobs'; - -export { - darkwatchScanQueue, - darkwatchScanWorker, -} from './darkwatch.jobs'; diff --git a/packages/jobs/src/voiceprint.jobs.ts b/packages/jobs/src/voiceprint.jobs.ts deleted file mode 100644 index f60c3575c..000000000 --- a/packages/jobs/src/voiceprint.jobs.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { prisma } from '@shieldsai/shared-db'; -import { Queue, Worker, Job } from 'bullmq'; -import { Redis } from 'ioredis'; - -// Redis connection -const redisHost = process.env.REDIS_HOST || 'localhost'; -const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10); - -const connection = new Redis({ - host: redisHost, - port: redisPort, - retryStrategy: (times: number) => Math.min(times * 50, 2000), -}); - -// Queue configuration -const QUEUE_CONFIG = { - voiceprintAnalysis: { - name: 'voiceprint-analysis', - concurrency: parseInt(process.env.VOICEPRINT_CONCURRENCY || '3', 10), - defaultJobTimeout: parseInt(process.env.VOICEPRINT_JOB_TIMEOUT || '30000', 10), - maxAttempts: parseInt(process.env.VOICEPRINT_MAX_ATTEMPTS || '3', 10), - }, -}; - -// Create queues -export const voiceprintAnalysisQueue = new Queue( - QUEUE_CONFIG.voiceprintAnalysis.name, - { connection } -); - -// VoicePrint analysis job processor -async function processVoiceprintAnalysis(job: Job<{ - userId: string; - audioBuffer: Buffer; - enrollmentId?: string; - audioUrl?: string; -}>) { - const { userId, audioBuffer, enrollmentId, audioUrl } = job.data; - - // Import analysis service dynamically to avoid circular dependencies - const { analysisService } = await import( - '../../../apps/api/src/services/voiceprint' - ); - - try { - const result = await analysisService.analyze(userId, audioBuffer, { - enrollmentId, - audioUrl, - }); - - return { - analysisId: result.id, - isSynthetic: result.isSynthetic, - confidence: result.confidence, - }; - } catch (error) { - const message = error instanceof Error ? error.message : 'Analysis failed'; - job.updateProgress(100); - throw new Error(message); - } -} - -// Create worker -export const voiceprintAnalysisWorker = new Worker( - QUEUE_CONFIG.voiceprintAnalysis.name, - processVoiceprintAnalysis, - { - connection, - concurrency: QUEUE_CONFIG.voiceprintAnalysis.concurrency, - limiter: { - max: 10, - duration: 1000, - }, - } -); - -// Add event handlers -voiceprintAnalysisWorker.on('completed', (job, result) => { - console.log(`Job ${job.id} completed:`, result); -}); - -voiceprintAnalysisWorker.on('failed', (job, err) => { - console.error(`Job ${job.id} failed:`, err.message); -}); - -voiceprintAnalysisWorker.on('error', (err) => { - console.error('Worker error:', err.message); -}); - -export default { - voiceprintAnalysisQueue, - voiceprintAnalysisWorker, -}; diff --git a/packages/jobs/tsconfig.json b/packages/jobs/tsconfig.json deleted file mode 100644 index e229935f5..000000000 --- a/packages/jobs/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-analytics/package.json b/packages/shared-analytics/package.json deleted file mode 100644 index d2352c6e0..000000000 --- a/packages/shared-analytics/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@shieldsai/shared-analytics", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", - "scripts": { - "lint": "eslint src/" - }, - "dependencies": { - "@segment/analytics-node": "^1.0.0", - "googleapis": "^128.0.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "typescript": "^5.3.3" - } -} diff --git a/packages/shared-analytics/src/config/analytics.config.ts b/packages/shared-analytics/src/config/analytics.config.ts deleted file mode 100644 index 06ca1409d..000000000 --- a/packages/shared-analytics/src/config/analytics.config.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { z } from 'zod'; - -// Environment variables for analytics -const envSchema = z.object({ - MIXPANEL_TOKEN: z.string(), - MIXPANEL_API_SECRET: z.string().optional(), - GA4_MEASUREMENT_ID: z.string(), - GA4_API_SECRET: z.string().optional(), - STRIPE_WEBHOOK_SECRET: z.string(), - ANALYTICS_ENV: z.enum(['development', 'production', 'staging']).default('development'), -}); - -export const analyticsEnv = envSchema.parse({ - MIXPANEL_TOKEN: process.env.MIXPANEL_TOKEN, - MIXPANEL_API_SECRET: process.env.MIXPANEL_API_SECRET, - GA4_MEASUREMENT_ID: process.env.GA4_MEASUREMENT_ID, - GA4_API_SECRET: process.env.GA4_API_SECRET, - STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, - ANALYTICS_ENV: process.env.ANALYTICS_ENV, -}); - -// Event taxonomy -export enum EventType { - // User events - USER_SIGNED_UP = 'user_signed_up', - USER_LOGGED_IN = 'user_logged_in', - USER_LOGGED_OUT = 'user_logged_out', - USER_UPGRADED = 'user_upgraded', - USER_DOWNGRADED = 'user_downgraded', - - // Subscription events - SUBSCRIPTION_CREATED = 'subscription_created', - SUBSCRIPTION_UPDATED = 'subscription_updated', - SUBSCRIPTION_CANCELLED = 'subscription_cancelled', - SUBSCRIPTION_RENEWED = 'subscription_renewed', - - // DarkWatch events - DARK_WEB_SCAN_STARTED = 'dark_web_scan_started', - DARK_WEB_SCAN_COMPLETED = 'dark_web_scan_completed', - EXPOSURE_DETECTED = 'exposure_detected', - EXPOSURE_RESOLVED = 'exposure_resolved', - WATCHLIST_ITEM_ADDED = 'watchlist_item_added', - WATCHLIST_ITEM_REMOVED = 'watchlist_item_removed', - - // VoicePrint events - VOICE_ENROLLED = 'voice_enrolled', - VOICE_ANALYZED = 'voice_analyzed', - VOICE_MATCH_FOUND = 'voice_match_found', - SYNTHETIC_VOICE_DETECTED = 'synthetic_voice_detected', - - // SpamShield events - CALL_ANALYZED = 'call_analyzed', - SMS_ANALYZED = 'sms_analyzed', - SPAM_BLOCKED = 'spam_blocked', - SPAM_FLAGGED = 'spam_flagged', - SPAM_FEEDBACK_SUBMITTED = 'spam_feedback_submitted', - - // KPI events - MRR_UPDATED = 'mrr_updated', - CONVERSION_OCCURRED = 'conversion_occurred', - CHURN_OCCURRED = 'churn_occurred', - REFERRAL_SENT = 'referral_sent', - REFERRAL_CONVERTED = 'referral_converted', -} - -// Event properties schema -export const eventPropertiesSchema = z.object({ - userId: z.string().optional(), - sessionId: z.string().optional(), - timestamp: z.date().optional(), - platform: z.enum(['web', 'mobile', 'desktop', 'api']).optional(), - version: z.string().optional(), - environment: z.string().optional(), -}); - -// KPI definitions -export const kpiDefinitions = { - mau: { - name: 'Monthly Active Users', - description: 'Unique users who performed an action in the last 30 days', - calculation: 'COUNT(DISTINCT userId) WHERE timestamp > NOW() - INTERVAL 30 DAYS', - }, - payingUsers: { - name: 'Paying Users', - description: 'Users with active subscriptions', - calculation: 'COUNT(DISTINCT userId) WHERE subscription.status = "active"', - }, - mrr: { - name: 'Monthly Recurring Revenue', - description: 'Total monthly subscription revenue', - calculation: 'SUM(subscription.amount) WHERE subscription.status = "active"', - }, - conversionRate: { - name: 'Conversion Rate', - description: 'Percentage of free users who upgrade to paid', - calculation: 'COUNT(upgrade events) / COUNT(signup events)', - }, - churn: { - name: 'Churn Rate', - description: 'Percentage of paying users who cancel', - calculation: 'COUNT(cancel events) / COUNT(active subscriptions)', - }, - cac: { - name: 'Customer Acquisition Cost', - description: 'Average cost to acquire a new paying user', - calculation: 'Total marketing spend / COUNT(new paying users)', - }, - ltv: { - name: 'Lifetime Value', - description: 'Average revenue per user over their lifetime', - calculation: 'Average subscription amount / Churn rate', - }, - nps: { - name: 'Net Promoter Score', - description: 'Customer satisfaction metric (-100 to 100)', - calculation: '% Promoters - % Detractors', - }, - viralCoefficient: { - name: 'Viral Coefficient', - description: 'Average number of referrals per user', - calculation: 'COUNT(referral events) / COUNT(users)', - }, -}; - -// Alert thresholds -export const alertThresholds = { - churn: { warning: 0.05, critical: 0.10 }, - conversionRate: { warning: 0.02, critical: 0.01 }, - mrr: { warning: 0.90, critical: 0.80 }, // Percentage of target - nps: { warning: 50, critical: 40 }, - viralCoefficient: { warning: 0.4, critical: 0.3 }, -}; diff --git a/packages/shared-analytics/src/index.ts b/packages/shared-analytics/src/index.ts deleted file mode 100644 index c51357f1e..000000000 --- a/packages/shared-analytics/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Config -export { - analyticsEnv, - EventType, - eventPropertiesSchema, - kpiDefinitions, - alertThresholds, -} from './config/analytics.config'; - -// Services -export { - MixpanelService, - mixpanelService, -} from './services/mixpanel.service'; -export { - GA4Service, - ga4Service, -} from './services/ga4.service'; diff --git a/packages/shared-analytics/src/services/ga4.service.ts b/packages/shared-analytics/src/services/ga4.service.ts deleted file mode 100644 index f8ef9e4d8..000000000 --- a/packages/shared-analytics/src/services/ga4.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { google } from 'googleapis'; -import { analyticsEnv, EventType } from '../config/analytics.config'; - -// GA4 service -export class GA4Service { - private auth: any; - - constructor() { - this.auth = google.auth.fromAPIKey(analyticsEnv.GA4_API_SECRET || 'placeholder'); - } - - /** - * Initialize GA4 client - */ - async initialize(): Promise { - // TODO: Initialize GA4 client with measurement ID - console.log('GA4 client initialized'); - } - - /** - * Send event to GA4 - */ - async sendEvent( - eventName: string, - params: { - client_id: string; - [key: string]: any; - } - ): Promise { - // TODO: Implement GA4 event tracking - // const measurementId = analyticsEnv.GA4_MEASUREMENT_ID; - // await fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${analyticsEnv.GA4_API_SECRET}`, { - // method: 'POST', - // body: JSON.stringify({ - // events: [{ name: eventName, params }], - // }), - // }); - - console.log('GA4 event:', eventName, params); - } - - /** - * Track page view - */ - async trackPageView(clientId: string, path: string, title?: string): Promise { - await this.sendEvent('page_view', { - client_id: clientId, - page_path: path, - page_title: title, - }); - } - - /** - * Track e-commerce purchase - */ - async trackPurchase( - clientId: string, - transactionId: string, - value: number, - currency: string, - items: Array<{ name: string; price: number; quantity: number }> - ): Promise { - await this.sendEvent('purchase', { - client_id: clientId, - transaction_id: transactionId, - value, - currency, - items, - }); - } - - /** - * Track conversion - */ - async trackConversion( - clientId: string, - conversionName: string, - metadata?: Record - ): Promise { - await this.sendEvent('conversion', { - client_id: clientId, - conversion_name: conversionName, - ...metadata, - }); - } - - /** - * Get analytics data (for dashboards) - */ - async getMetrics( - dateRange: { startDate: string; endDate: string }, - metrics: string[], - dimensions?: string[] - ): Promise { - // TODO: Implement GA4 Analytics Data API - return { - rows: [], - totals: [], - }; - } -} - -// Export instance -export const ga4Service = new GA4Service(); diff --git a/packages/shared-analytics/src/services/mixpanel.service.ts b/packages/shared-analytics/src/services/mixpanel.service.ts deleted file mode 100644 index 4ca21023e..000000000 --- a/packages/shared-analytics/src/services/mixpanel.service.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Analytics } from '@segment/analytics-node'; -import { analyticsEnv, EventType, eventPropertiesSchema } from '../config/analytics.config'; -import { hashPhoneNumber } from '../utils/phone-hash'; - -// Mixpanel service -export class MixpanelService { - private client: Analytics; - - constructor() { - this.client = new Analytics({ - apiKey: analyticsEnv.MIXPANEL_TOKEN, - }); - } - - /** - * Track an event in Mixpanel - */ - async track( - event: EventType, - distinctId: string, - properties?: Record - ): Promise { - const validatedProperties = eventPropertiesSchema.parse(properties); - - this.client.track({ - event, - distinctId, - properties: { - ...validatedProperties, - ...properties, - }, - }); - } - - /** - * Identify a user - */ - async identify(userId: string, traits?: Record): Promise { - this.client.identify({ - distinctId: userId, - traits, - }); - } - - /** - * Group users by subscription tier - */ - async group(groupId: string, groupKey: string, traits?: Record): Promise { - this.client.group({ - groupKey, - groupId, - traits, - }); - } - - /** - * Track user sign-up - */ - async userSignedUp(userId: string, plan?: string, referrer?: string): Promise { - await this.track(EventType.USER_SIGNED_UP, userId, { - plan, - referrer, - timestamp: new Date(), - }); - } - - /** - * Track subscription upgrade - */ - async userUpgraded(userId: string, fromTier: string, toTier: string, mrr: number): Promise { - await this.track(EventType.USER_UPGRADED, userId, { - fromTier, - toTier, - mrr, - timestamp: new Date(), - }); - } - - /** - * Track exposure detection - */ - async exposureDetected( - userId: string, - exposureType: string, - severity: string, - source: string - ): Promise { - await this.track(EventType.EXPOSURE_DETECTED, userId, { - exposureType, - severity, - source, - timestamp: new Date(), - }); - } - - /** - * Track spam detection - */ - async spamBlocked(userId: string, phoneNumber: string, confidence: number, method: string): Promise { - await this.track(EventType.SPAM_BLOCKED, userId, { - phoneNumber: hashPhoneNumber(phoneNumber), - confidence, - method, - timestamp: new Date(), - }); - } - - /** - * Flush pending events - */ - async flush(): Promise { - await this.client.flush(); - } -} - -// Export instance -export const mixpanelService = new MixpanelService(); diff --git a/packages/shared-analytics/src/utils/phone-hash.ts b/packages/shared-analytics/src/utils/phone-hash.ts deleted file mode 100644 index accb2c239..000000000 --- a/packages/shared-analytics/src/utils/phone-hash.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Hash a phone number for analytics purposes - * Uses a consistent hashing algorithm to create a deterministic hash - */ -export function hashPhoneNumber(phoneNumber: string): string { - let hash = 0; - for (let i = 0; i < phoneNumber.length; i++) { - hash = ((hash << 5) - hash) + phoneNumber.charCodeAt(i); - hash |= 0; - } - return `hash_${Math.abs(hash)}`; -} diff --git a/packages/shared-analytics/tsconfig.json b/packages/shared-analytics/tsconfig.json deleted file mode 100644 index d822f58c4..000000000 --- a/packages/shared-analytics/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-auth/package.json b/packages/shared-auth/package.json deleted file mode 100644 index a45c56fdb..000000000 --- a/packages/shared-auth/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@shieldsai/shared-auth", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", - "scripts": { - "lint": "eslint src/" - }, - "dependencies": { - "next-auth": "^4.24.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "typescript": "^5.3.3" - } -} diff --git a/packages/shared-auth/src/config/auth.config.ts b/packages/shared-auth/src/config/auth.config.ts deleted file mode 100644 index 9202c658d..000000000 --- a/packages/shared-auth/src/config/auth.config.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { NextAuthOptions } from 'next-auth'; -import CredentialsProvider from 'next-auth/providers/credentials'; -import GoogleProvider from 'next-auth/providers/google'; -import AppleProvider from 'next-auth/providers/apple'; -import { z } from 'zod'; - -// Environment variables -const envSchema = z.object({ - NEXTAUTH_URL: z.string().url(), - NEXTAUTH_SECRET: z.string().min(32), - GOOGLE_CLIENT_ID: z.string(), - GOOGLE_CLIENT_SECRET: z.string(), - APPLE_CLIENT_ID: z.string(), - APPLE_CLIENT_SECRET: z.string(), - DATABASE_URL: z.string().url(), -}); - -export const authEnv = envSchema.parse({ - NEXTAUTH_URL: process.env.NEXTAUTH_URL, - NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, - APPLE_CLIENT_ID: process.env.APPLE_CLIENT_ID, - APPLE_CLIENT_SECRET: process.env.APPLE_CLIENT_SECRET, - DATABASE_URL: process.env.DATABASE_URL, -}); - -// Role-based access control -export type UserRole = 'user' | 'family_admin' | 'family_member' | 'support'; - -export const userRoles: UserRole[] = ['user', 'family_admin', 'family_member', 'support']; - -// Family group types -export type FamilyGroup = { - id: string; - name: string; - members: string[]; // user IDs - createdAt: Date; - updatedAt: Date; -}; - -// NextAuth options -export const authOptions: NextAuthOptions = { - providers: [ - CredentialsProvider({ - name: 'Credentials', - credentials: { - email: { label: 'Email', type: 'email' }, - password: { label: 'Password', type: 'password' }, - }, - async authorize(credentials) { - if (!credentials?.email || !credentials?.password) { - throw new Error('Email and password required'); - } - - // TODO: Validate against database - const user = { - id: '1', - email: credentials.email, - name: credentials.email.split('@')[0], - role: 'user' as UserRole, - }; - - return user; - }, - }), - GoogleProvider({ - clientId: authEnv.GOOGLE_CLIENT_ID, - clientSecret: authEnv.GOOGLE_CLIENT_SECRET, - }), - AppleProvider({ - clientId: authEnv.APPLE_CLIENT_ID, - clientSecret: authEnv.APPLE_CLIENT_SECRET, - }), - ], - session: { - strategy: 'jwt', - maxAge: 30 * 24 * 60 * 60, // 30 days - }, - pages: { - signIn: '/auth/signin', - signOut: '/auth/signout', - error: '/auth/error', - }, - callbacks: { - async jwt({ token, user, account }) { - if (user) { - token.id = user.id; - token.role = (user as any).role; - } - - if (account) { - token.provider = account.provider; - token.accessToken = account.access_token; - } - - return token; - }, - async session({ session, token }) { - if (session.user) { - session.user.id = token.id as string; - session.user.role = token.role as UserRole; - } - - return session; - }, - }, - events: { - async createUser({ user }) { - // TODO: Create default family group - console.log('New user created:', user.email); - }, - }, -}; diff --git a/packages/shared-auth/src/index.ts b/packages/shared-auth/src/index.ts deleted file mode 100644 index c59c5eaad..000000000 --- a/packages/shared-auth/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Config -export { authOptions, authEnv, userRoles } from './config/auth.config'; -export type { UserRole, FamilyGroup } from './config/auth.config'; - -// Middleware -export { withAuth, withRole, protectApiRoute } from './middleware/auth.middleware'; - -// Models -export { - userSchema, - familyGroupSchema, - familyMemberSchema, - sessionSchema, - accountSchema, - createUserSchema, - createFamilyGroupSchema, - addFamilyMemberSchema, -} from './models/auth.models'; -export type { - User, - FamilyGroup as AuthFamilyGroup, - FamilyMember, - Session, - Account, -} from './models/auth.models'; diff --git a/packages/shared-auth/src/middleware/auth.middleware.ts b/packages/shared-auth/src/middleware/auth.middleware.ts deleted file mode 100644 index 35a4abfa8..000000000 --- a/packages/shared-auth/src/middleware/auth.middleware.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { NextRequest, NextResponse } from 'next-auth/react'; -import { UserRole } from '../config/auth.config'; - -/** - * Middleware to protect routes that require authentication - */ -export function withAuth( - request: NextRequest, - options?: { - signInPath?: string; - } -): NextResponse { - const token = request.cookies.get('next-auth.session-token')?.value; - const signInPath = options?.signInPath ?? '/auth/signin'; - - if (!token) { - const signInUrl = new URL(signInPath, request.url); - signInUrl.searchParams.set('callbackUrl', request.nextUrl.pathname); - return NextResponse.redirect(signInUrl); - } - - return NextResponse.next(); -} - -/** - * Middleware to check if user has required role - */ -export function withRole( - response: NextResponse, - request: NextRequest, - requiredRoles: UserRole[] -): NextResponse { - const token = request.cookies.get('next-auth.session-token')?.value; - - if (!token) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - // TODO: Decode JWT and check role - // For now, allow all authenticated users - return response; -} - -/** - * Middleware to protect API routes - */ -export function protectApiRoute(request: NextRequest): NextResponse { - const authHeader = request.headers.get('authorization'); - - if (!authHeader?.startsWith('Bearer ')) { - return NextResponse.json({ error: 'Missing or invalid token' }, { status: 401 }); - } - - const token = authHeader.split(' ')[1]; - - try { - // TODO: Verify JWT token - return NextResponse.next(); - } catch (error) { - return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); - } -} diff --git a/packages/shared-auth/src/models/auth.models.ts b/packages/shared-auth/src/models/auth.models.ts deleted file mode 100644 index 7198b9f3b..000000000 --- a/packages/shared-auth/src/models/auth.models.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { z } from 'zod'; - -// User schema -export const userSchema = z.object({ - id: z.string().uuid(), - email: z.string().email(), - name: z.string().min(1), - image: z.string().url().optional(), - role: z.enum(['user', 'family_admin', 'family_member', 'support']), - emailVerified: z.date().optional(), - createdAt: z.date(), - updatedAt: z.date(), -}); - -export type User = z.infer; - -// Family group schema -export const familyGroupSchema = z.object({ - id: z.string().uuid(), - name: z.string().min(1).max(100), - ownerId: z.string().uuid(), - createdAt: z.date(), - updatedAt: z.date(), -}); - -export type FamilyGroup = z.infer; - -// Family member schema -export const familyMemberSchema = z.object({ - id: z.string().uuid(), - groupId: z.string().uuid(), - userId: z.string().uuid(), - role: z.enum(['owner', 'admin', 'member']), - joinedAt: z.date(), -}); - -export type FamilyMember = z.infer; - -// Session schema -export const sessionSchema = z.object({ - id: z.string().uuid(), - userId: z.string().uuid(), - sessionToken: z.string(), - expires: z.date(), - createdAt: z.date(), -}); - -export type Session = z.infer; - -// Account schema (for OAuth) -export const accountSchema = z.object({ - id: z.string().uuid(), - userId: z.string().uuid(), - provider: z.string(), - providerAccountId: z.string(), - access_token: z.string().optional(), - refresh_token: z.string().optional(), - expires_at: z.number().optional(), - token_type: z.string().optional(), - scope: z.string().optional(), -}); - -export type Account = z.infer; - -// Validation schemas for API -export const createUserSchema = z.object({ - email: z.string().email(), - password: z.string().min(8), - name: z.string().min(1), -}); - -export const createFamilyGroupSchema = z.object({ - name: z.string().min(1).max(100), - ownerId: z.string().uuid(), -}); - -export const addFamilyMemberSchema = z.object({ - groupId: z.string().uuid(), - userId: z.string().uuid(), - role: z.enum(['admin', 'member']).default('member'), -}); diff --git a/packages/shared-auth/tsconfig.json b/packages/shared-auth/tsconfig.json deleted file mode 100644 index d822f58c4..000000000 --- a/packages/shared-auth/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-billing/package.json b/packages/shared-billing/package.json deleted file mode 100644 index 2cf1265ac..000000000 --- a/packages/shared-billing/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@shieldsai/shared-billing", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", - "dependencies": { - "stripe": "^14.0.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "typescript": "^5.3.3" - } -} diff --git a/packages/shared-billing/src/config/billing.config.ts b/packages/shared-billing/src/config/billing.config.ts deleted file mode 100644 index 2715d136a..000000000 --- a/packages/shared-billing/src/config/billing.config.ts +++ /dev/null @@ -1,90 +0,0 @@ -import Stripe from 'stripe'; -import { z } from 'zod'; - -// Environment variables -const envSchema = z.object({ - STRIPE_SECRET_KEY: z.string().startsWith('sk_'), - STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'), - STRIPE_PRICE_ID_BASIC: z.string().startsWith('price_'), - STRIPE_PRICE_ID_PLUS: z.string().startsWith('price_'), - STRIPE_PRICE_ID_PREMIUM: z.string().startsWith('price_'), -}); - -export const billingEnv = envSchema.parse({ - STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, - STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, - STRIPE_PRICE_ID_BASIC: process.env.STRIPE_PRICE_ID_BASIC, - STRIPE_PRICE_ID_PLUS: process.env.STRIPE_PRICE_ID_PLUS, - STRIPE_PRICE_ID_PREMIUM: process.env.STRIPE_PRICE_ID_PREMIUM, -}); - -// Initialize Stripe -export const stripe = new Stripe(billingEnv.STRIPE_SECRET_KEY, { - apiVersion: '2024-06-20', - typescript: true, -}); - -// Subscription tiers -export enum SubscriptionTier { - BASIC = 'basic', - PLUS = 'plus', - PREMIUM = 'premium', -} - -// Tier configuration -export const tierConfig: Record = { - [SubscriptionTier.BASIC]: { - name: 'Basic', - priceId: billingEnv.STRIPE_PRICE_ID_BASIC, - features: { - maxWatchlistItems: 2, - maxFamilyMembers: 1, - darkWebScanFrequency: 'daily', - exposureRetentionMonths: 12, - alertChannels: ['email'], - }, - }, - [SubscriptionTier.PLUS]: { - name: 'Plus', - priceId: billingEnv.STRIPE_PRICE_ID_PLUS, - features: { - maxWatchlistItems: 10, - maxFamilyMembers: 3, - darkWebScanFrequency: 'hourly', - exposureRetentionMonths: 24, - alertChannels: ['email', 'push'], - }, - }, - [SubscriptionTier.PREMIUM]: { - name: 'Premium', - priceId: billingEnv.STRIPE_PRICE_ID_PREMIUM, - features: { - maxWatchlistItems: Infinity, - maxFamilyMembers: Infinity, - darkWebScanFrequency: 'realtime', - exposureRetentionMonths: 60, - alertChannels: ['email', 'push', 'sms'], - }, - }, -}; - -// Feature gating middleware -export function getTierFeatures(tier: SubscriptionTier) { - return tierConfig[tier].features; -} - -export function checkFeatureAccess(tier: SubscriptionTier, feature: string, value: number): boolean { - const features = tierConfig[tier].features; - const featureValue = features[feature as keyof typeof features] as number; - return value <= featureValue; -} diff --git a/packages/shared-billing/src/index.ts b/packages/shared-billing/src/index.ts deleted file mode 100644 index 15ed0bf5d..000000000 --- a/packages/shared-billing/src/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Config -export { - stripe, - billingEnv, - SubscriptionTier, - tierConfig, - getTierFeatures, - checkFeatureAccess, -} from './config/billing.config'; - -// Services -export { - SubscriptionService, - CustomerService, - WebhookService, - subscriptionService, - customerService, - webhookService, -} from './services/billing.services'; - -// Middleware -export { requireTier, checkFeatureLimit } from './middleware/billing.middleware'; diff --git a/packages/shared-billing/src/middleware/billing.middleware.ts b/packages/shared-billing/src/middleware/billing.middleware.ts deleted file mode 100644 index 86bfaa162..000000000 --- a/packages/shared-billing/src/middleware/billing.middleware.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { SubscriptionTier, getTierFeatures } from '../config/billing.config'; - -/** - * Middleware to check if user has access to required tier features - */ -export function requireTier( - request: NextRequest, - requiredTier: SubscriptionTier -): NextResponse | null { - const userTier = request.headers.get('x-user-tier') as SubscriptionTier; - - if (!userTier) { - return NextResponse.json({ error: 'User tier not found' }, { status: 401 }); - } - - const tierOrder: Record = { - [SubscriptionTier.BASIC]: 1, - [SubscriptionTier.PLUS]: 2, - [SubscriptionTier.PREMIUM]: 3, - }; - - if (tierOrder[userTier] < tierOrder[requiredTier]) { - return NextResponse.json( - { - error: `Feature requires ${requiredTier} tier or higher`, - currentTier: userTier, - requiredTier, - }, - { status: 403 } - ); - } - - return null; -} - -/** - * Middleware to check feature limits - */ -export function checkFeatureLimit( - request: NextRequest, - feature: string, - currentValue: number -): NextResponse | null { - const userTier = request.headers.get('x-user-tier') as SubscriptionTier; - - if (!userTier) { - return null; - } - - const features = getTierFeatures(userTier); - const featureLimit = features[feature as keyof typeof features] as number; - - if (currentValue > featureLimit) { - return NextResponse.json( - { - error: `Feature limit exceeded`, - feature, - current: currentValue, - limit: featureLimit, - tier: userTier, - }, - { status: 400 } - ); - } - - return null; -} diff --git a/packages/shared-billing/src/services/billing.services.ts b/packages/shared-billing/src/services/billing.services.ts deleted file mode 100644 index f2f953277..000000000 --- a/packages/shared-billing/src/services/billing.services.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { stripe, SubscriptionTier, tierConfig } from '../config/billing.config'; -import { z } from 'zod'; - -// Subscription service -export class SubscriptionService { - /** - * Create a new subscription for a customer - */ - async createSubscription( - customerId: string, - tier: SubscriptionTier, - metadata?: Record - ): Promise { - const priceId = tierConfig[tier].priceId; - - const subscription = await stripe.subscriptions.create({ - customer: customerId, - items: [{ price: priceId }], - metadata: metadata, - proration_behavior: 'create_prorations', - }); - - return subscription; - } - - /** - * Update a customer's subscription tier - */ - async updateSubscriptionTier( - subscriptionId: string, - newTier: SubscriptionTier - ): Promise { - const newPriceId = tierConfig[newTier].priceId; - - const subscription = await stripe.subscriptions.update(subscriptionId, { - items: [ - { - price: newPriceId, - quantity: 1, - }, - ], - proration_behavior: 'create_prorations', - }); - - return subscription; - } - - /** - * Cancel a subscription - */ - async cancelSubscription( - subscriptionId: string, - atPeriodEnd: boolean = true - ): Promise { - const subscription = await stripe.subscriptions.update(subscriptionId, { - cancel_at_period_end: atPeriodEnd, - }); - - return subscription; - } - - /** - * Get subscription by ID - */ - async getSubscription(subscriptionId: string): Promise { - try { - const subscription = await stripe.subscriptions.retrieve(subscriptionId); - return subscription; - } catch (error) { - if (error instanceof Stripe.errors.StripeInvalidRequestError) { - return null; - } - throw error; - } - } - - /** - * Get customer's current subscription - */ - async getCustomerSubscription(customerId: string): Promise { - const subscriptions = await stripe.subscriptions.list({ - customer: customerId, - status: 'active', - limit: 1, - }); - - return subscriptions.data[0] || null; - } -} - -// Customer service -export class CustomerService { - /** - * Create a new Stripe customer - */ - async createCustomer( - email: string, - name?: string, - metadata?: Record - ): Promise { - const customer = await stripe.customers.create({ - email, - name, - metadata, - }); - - return customer; - } - - /** - * Get or create customer by email - */ - async getOrCreateCustomer( - email: string, - name?: string - ): Promise { - const existingCustomers = await stripe.customers.list({ - email, - limit: 1, - }); - - if (existingCustomers.data.length > 0) { - return existingCustomers.data[0]; - } - - return this.createCustomer(email, name); - } - - /** - * Create a billing portal session - */ - async createBillingPortalSession( - customerId: string, - returnUrl: string - ): Promise { - const session = await stripe.billingPortal.sessions.create({ - customer: customerId, - return_url: returnUrl, - }); - - return session; - } - - /** - * Get customer by ID - */ - async getCustomer(customerId: string): Promise { - try { - const customer = await stripe.customers.retrieve(customerId); - return customer as Stripe.Customer; - } catch (error) { - if (error instanceof Stripe.errors.StripeInvalidRequestError) { - return null; - } - throw error; - } - } -} - -// Webhook service -export class WebhookService { - /** - * Construct webhook event from raw body - */ - constructEvent( - rawBody: Buffer | string, - signature: string - ): Stripe.Event { - return stripe.webhooks.constructEvent( - rawBody, - signature, - process.env.STRIPE_WEBHOOK_SECRET! - ); - } - - /** - * Handle webhook event - */ - async handleWebhook(event: Stripe.Event): Promise { - switch (event.type) { - case 'customer.subscription.created': - case 'customer.subscription.updated': - await this.handleSubscriptionChange(event.data.object); - break; - case 'customer.subscription.deleted': - await this.handleSubscriptionDeleted(event.data.object); - break; - case 'invoice.payment_succeeded': - await this.handlePaymentSucceeded(event.data.object); - break; - case 'invoice.payment_failed': - await this.handlePaymentFailed(event.data.object); - break; - default: - console.log(`Unhandled event type: ${event.type}`); - } - } - - private async handleSubscriptionChange(subscription: Stripe.Subscription) { - console.log(`Subscription ${subscription.id} changed to ${subscription.status}`); - // TODO: Update local database - } - - private async handleSubscriptionDeleted(subscription: Stripe.Subscription) { - console.log(`Subscription ${subscription.id} deleted`); - // TODO: Update local database - } - - private async handlePaymentSucceeded(invoice: Stripe.Invoice) { - console.log(`Payment succeeded for invoice ${invoice.id}`); - // TODO: Update usage tracking - } - - private async handlePaymentFailed(invoice: Stripe.Invoice) { - console.log(`Payment failed for invoice ${invoice.id}`); - // TODO: Send notification to customer - } -} - -// Export instances -export const subscriptionService = new SubscriptionService(); -export const customerService = new CustomerService(); -export const webhookService = new WebhookService(); diff --git a/packages/shared-billing/tsconfig.json b/packages/shared-billing/tsconfig.json deleted file mode 100644 index d822f58c4..000000000 --- a/packages/shared-billing/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-db/drizzle.config.ts b/packages/shared-db/drizzle.config.ts deleted file mode 100644 index 6b8d17eda..000000000 --- a/packages/shared-db/drizzle.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'drizzle-kit'; - -export default defineConfig({ - schema: './prisma/schema.prisma', - out: './migrations', - dialect: 'postgresql', - dbCredentials: { - url: process.env.DATABASE_URL!, - }, - verbose: true, - strict: true, -}); diff --git a/packages/shared-db/package.json b/packages/shared-db/package.json deleted file mode 100644 index 8c4193a82..000000000 --- a/packages/shared-db/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@shieldsai/shared-db", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", - "scripts": { - "db:generate": "prisma generate", - "db:push": "prisma db push", - "db:migrate": "prisma migrate deploy", - "db:studio": "prisma studio", - "db:format": "prisma format" - }, - "dependencies": { - "@prisma/client": "^5.14.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "prisma": "^5.14.0", - "typescript": "^5.3.3" - } -} diff --git a/packages/shared-db/prisma/schema.prisma b/packages/shared-db/prisma/schema.prisma deleted file mode 100644 index edd32afe6..000000000 --- a/packages/shared-db/prisma/schema.prisma +++ /dev/null @@ -1,437 +0,0 @@ -// Prisma schema for ShieldAI -// All models for the multi-service SaaS platform - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -// ============================================ -// User & Authentication Models -// ============================================ - -model User { - id String @id @default(uuid()) - email String @unique - emailVerified DateTime? - name String? - image String? - role UserRole @default(user) - - // Relationships - accounts Account[] - sessions Session[] - familyGroups FamilyGroupMember[] - familyGroupOwned FamilyGroup[] @relation("FamilyGroupOwner") - subscriptions Subscription[] - alerts Alert[] - voiceEnrollments VoiceEnrollment[] - voiceAnalyses VoiceAnalysis[] - spamFeedback SpamFeedback[] - spamRules SpamRule[] - - // Audit - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([email]) - @@index([role]) -} - -enum UserRole { - user - family_admin - family_member - support -} - -model Account { - id String @id @default(uuid()) - userId String - provider String - providerAccountId String - access_token String? - refresh_token String? - expires_at Int? - token_type String? - scope String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([userId, provider, providerAccountId]) - @@index([userId]) -} - -model Session { - id String @id @default(uuid()) - userId String - sessionToken String @unique - expires DateTime - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([sessionToken]) - @@index([userId]) -} - -// ============================================ -// Family & Subscription Models -// ============================================ - -model FamilyGroup { - id String @id @default(uuid()) - name String - ownerId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - owner User @relation("FamilyGroupOwner", fields: [ownerId], references: [id]) - members FamilyGroupMember[] - subscriptions Subscription[] - - @@index([ownerId]) - @@index([name]) -} - -model FamilyGroupMember { - id String @id @default(uuid()) - groupId String - userId String - role FamilyMemberRole @default(member) - joinedAt DateTime @default(now()) - - group FamilyGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([groupId, userId]) - @@index([groupId]) - @@index([userId]) -} - -enum FamilyMemberRole { - owner - admin - member -} - -model Subscription { - id String @id @default(uuid()) - userId String - familyGroupId String? - stripeId String? @unique - tier SubscriptionTier @default(basic) - status SubscriptionStatus @default(active) - currentPeriodStart DateTime - currentPeriodEnd DateTime - cancelAtPeriodEnd Boolean @default(false) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - familyGroup FamilyGroup? @relation(fields: [familyGroupId], references: [id]) - - watchlistItems WatchlistItem[] - exposures Exposure[] - alerts Alert[] - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([familyGroupId]) - @@index([stripeId]) - @@index([tier]) -} - -enum SubscriptionTier { - basic - plus - premium -} - -enum SubscriptionStatus { - active - past_due - canceled - unpaid - trialing -} - -// ============================================ -// DarkWatch Models (Dark Web Monitoring) -// ============================================ - -model WatchlistItem { - id String @id @default(uuid()) - subscriptionId String - type WatchlistType - value String - hash String // SHA-256 hash for deduplication - isActive Boolean @default(true) - - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - exposures Exposure[] - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([subscriptionId, type, hash]) - @@index([subscriptionId]) - @@index([type]) - @@index([hash]) -} - -enum WatchlistType { - email - phoneNumber - ssn - address - domain -} - -model Exposure { - id String @id @default(uuid()) - subscriptionId String - watchlistItemId String? - source ExposureSource - dataType WatchlistType - identifier String - identifierHash String - severity ExposureSeverity @default(info) - metadata Json? // Additional source-specific data - isFirstTime Boolean @default(false) - - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - watchlistItem WatchlistItem? @relation(fields: [watchlistItemId], references: [id]) - alerts Alert[] - - detectedAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([subscriptionId]) - @@index([watchlistItemId]) - @@index([source]) - @@index([severity]) - @@index([detectedAt]) -} - -enum ExposureSource { - hibp // Have I Been Pwned - securityTrails - censys - darkWebForum - shodan - honeypot -} - -enum ExposureSeverity { - info - warning - critical -} - -// ============================================ -// Notification & Alert Models -// ============================================ - -model Alert { - id String @id @default(uuid()) - subscriptionId String - userId String - exposureId String? - type AlertType - title String - message String - severity AlertSeverity @default(info) - isRead Boolean @default(false) - readAt DateTime? - channel AlertChannel[] // Array of notification channels - - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - exposure Exposure? @relation(fields: [exposureId], references: [id]) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([subscriptionId]) - @@index([userId]) - @@index([isRead]) - @@index([createdAt]) -} - -enum AlertType { - exposure_detected - exposure_resolved - scan_complete - subscription_changed - system_warning -} - -enum AlertSeverity { - info - warning - critical -} - -enum AlertChannel { - email - push - sms -} - -// ============================================ -// VoicePrint Models (Voice Cloning Detection) -// ============================================ - -model VoiceEnrollment { - id String @id @default(uuid()) - userId String - name String - voiceHash String // FAISS embedding hash - audioMetadata Json? // Sample rate, duration, etc. - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - analyses VoiceAnalysis[] - - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([voiceHash]) -} - -model VoiceAnalysis { - id String @id @default(uuid()) - enrollmentId String? - userId String - audioHash String // Content hash of audio file - isSynthetic Boolean - confidence Float // 0.0 to 1.0 - analysisResult Json // Full ML analysis results - audioUrl String // S3 storage URL - - enrollment VoiceEnrollment? @relation(fields: [enrollmentId], references: [id]) - user User @relation(fields: [userId], references: [id]) - - createdAt DateTime @default(now()) - - @@index([userId]) - @@index([enrollmentId]) - @@index([audioHash]) -} - -// ============================================ -// SpamShield Models (Spam Detection) -// ============================================ - -model SpamFeedback { - id String @id @default(uuid()) - userId String - phoneNumber String - phoneNumberHash String // SHA-256 hash - isSpam Boolean - confidence Float? // ML model confidence - feedbackType FeedbackType - metadata Json? // Call duration, time, etc. - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([phoneNumberHash]) - @@index([isSpam]) -} - -enum FeedbackType { - initial_detection - user_confirmation - user_rejection - auto_learned -} - -model SpamRule { - id String @id @default(uuid()) - userId String? - isGlobal Boolean @default(false) - ruleType RuleType - pattern String - action RuleAction - priority Int @default(0) - isActive Boolean @default(true) - - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([isGlobal]) - @@index([ruleType]) -} - -enum RuleType { - phoneNumber - areaCode - prefix - pattern - reputation -} - -enum RuleAction { - block - flag - allow - challenge -} - -// ============================================ -// Audit & Analytics Models -// ============================================ - -model AuditLog { - id String @id @default(uuid()) - userId String? - action String - resource String - resourceId String? - changes Json? // Before/after values - metadata Json? - ipAddress String? - userAgent String? - - createdAt DateTime @default(now()) - - @@index([userId]) - @@index([action]) - @@index([resource]) - @@index([createdAt]) -} - -model KPISnapshot { - id String @id @default(uuid()) - date DateTime @unique - metricName String - metricValue Float - metadata Json? - - createdAt DateTime @default(now()) - - @@index([metricName]) - @@index([date]) -} diff --git a/packages/shared-db/src/client.ts b/packages/shared-db/src/client.ts deleted file mode 100644 index 377c6f816..000000000 --- a/packages/shared-db/src/client.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -// Singleton pattern for Prisma Client -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; -}; - -export const prisma = - globalForPrisma.prisma ?? - new PrismaClient({ - log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], - }); - -if (process.env.NODE_ENV === 'development') { - globalForPrisma.prisma = prisma; -} - -// Export types from generated client -export type { - User, - Account, - Session, - FamilyGroup, - FamilyGroupMember, - Subscription, - WatchlistItem, - Exposure, - Alert, - VoiceEnrollment, - VoiceAnalysis, - SpamFeedback, - SpamRule, - AuditLog, - KPISnapshot, - UserRole, - FamilyMemberRole, - SubscriptionTier, - SubscriptionStatus, - WatchlistType, - ExposureSource, - ExposureSeverity, - AlertType, - AlertSeverity, - AlertChannel, - FeedbackType, - RuleType, - RuleAction, -} from '@prisma/client'; - -export * as PrismaModels from '@prisma/client'; diff --git a/packages/shared-db/src/index.ts b/packages/shared-db/src/index.ts deleted file mode 100644 index a41efb09c..000000000 --- a/packages/shared-db/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Re-export Prisma client -export { prisma } from './client'; - -// Export types -export type { - User, - Account, - Session, - FamilyGroup, - FamilyGroupMember, - Subscription, - WatchlistItem, - Exposure, - Alert, - VoiceEnrollment, - VoiceAnalysis, - SpamFeedback, - SpamRule, - AuditLog, - KPISnapshot, -} from './client'; diff --git a/packages/shared-db/tsconfig.json b/packages/shared-db/tsconfig.json deleted file mode 100644 index 33fa9a4a0..000000000 --- a/packages/shared-db/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "prisma"] -} diff --git a/packages/shared-notifications/package.json b/packages/shared-notifications/package.json deleted file mode 100644 index cdb60704d..000000000 --- a/packages/shared-notifications/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@shieldsai/shared-notifications", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": { - "import": "./src/index.ts", - "types": "./src/index.ts" - } - }, - "scripts": { - "build": "tsc", - "lint": "eslint src/" - }, - "dependencies": { - "resend": "^6.12.2", - "firebase-admin": "^13.2.0", - "twilio": "^5.4.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "@types/node": "^25.6.0", - "typescript": "^5.3.3" - } -} diff --git a/packages/shared-notifications/src/config/notification.config.ts b/packages/shared-notifications/src/config/notification.config.ts deleted file mode 100644 index bb289d875..000000000 --- a/packages/shared-notifications/src/config/notification.config.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { RateLimitConfig, NotificationChannel } from '../types/notification.types'; - -// Resend configuration -export interface ResendConfig { - apiKey: string; - fromEmail: string; - fromName: string; -} - -// Firebase Cloud Messaging configuration -export interface FCMConfig { - projectId: string; - privateKey: string; - clientEmail: string; - keyPath?: string; // Path to service account key file -} - -// APNs configuration -export interface APNsConfig { - keyPath: string; // Path to .p8 key file - keyId: string; - teamId: string; - bundleId: string; -} - -// Twilio configuration -export interface TwilioConfig { - accountSid: string; - authToken: string; - fromNumber?: string; // Optional default sender number -} - -// Combined notification config -export interface NotificationConfig { - resend: ResendConfig; - fcm?: FCMConfig; - apns?: APNsConfig; - twilio?: TwilioConfig; - rateLimits: { - email: RateLimitConfig; - push: RateLimitConfig; - sms: RateLimitConfig; - }; -} - -// Default rate limits -export const defaultRateLimits: Record = { - [NotificationChannel.EMAIL]: { - maxPerWindow: 100, - windowMs: 60 * 60 * 1000, // 1 hour - key: 'user', - }, - [NotificationChannel.PUSH]: { - maxPerWindow: 50, - windowMs: 60 * 60 * 1000, // 1 hour - key: 'user', - }, - [NotificationChannel.SMS]: { - maxPerWindow: 20, - windowMs: 60 * 60 * 1000, // 1 hour - key: 'user', - }, -}; - -// Load config from environment variables -export function loadNotificationConfig(): NotificationConfig { - return { - resend: { - apiKey: process.env.RESEND_API_KEY!, - fromEmail: process.env.RESEND_FROM_EMAIL || 'noreply@shieldsai.com', - fromName: process.env.RESEND_FROM_NAME || 'ShieldAI', - }, - fcm: process.env.FCM_PROJECT_ID ? { - projectId: process.env.FCM_PROJECT_ID, - privateKey: process.env.FCM_PRIVATE_KEY!.replace(/\\n/g, '\n'), - clientEmail: process.env.FCM_CLIENT_EMAIL!, - keyPath: process.env.FCM_KEY_PATH, - } : undefined, - apns: process.env.APNS_KEY_PATH ? { - keyPath: process.env.APNS_KEY_PATH, - keyId: process.env.APNS_KEY_ID!, - teamId: process.env.APNS_TEAM_ID!, - bundleId: process.env.APNS_BUNDLE_ID!, - } : undefined, - twilio: process.env.TWILIO_ACCOUNT_SID ? { - accountSid: process.env.TWILIO_ACCOUNT_SID!, - authToken: process.env.TWILIO_AUTH_TOKEN!, - fromNumber: process.env.TWILIO_FROM_NUMBER, - } : undefined, - rateLimits: defaultRateLimits, - }; -} diff --git a/packages/shared-notifications/src/index.ts b/packages/shared-notifications/src/index.ts deleted file mode 100644 index 9c2b03253..000000000 --- a/packages/shared-notifications/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Types -export * from './types/notification.types'; - -// Config -export * from './config/notification.config'; - -// Services -export { EmailService } from './services/email.service'; -export { PushService } from './services/push.service'; -export { SMSService } from './services/sms.service'; -export { NotificationService, createNotificationService } from './services/notification.service'; diff --git a/packages/shared-notifications/src/services/email.service.ts b/packages/shared-notifications/src/services/email.service.ts deleted file mode 100644 index 799e1a9bf..000000000 --- a/packages/shared-notifications/src/services/email.service.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { Resend } from 'resend'; -import { ResendConfig } from '../config/notification.config'; -import { - EmailNotification, - NotificationStatus, - NotificationPriority, - NotificationRecipient, - NotificationChannel, -} from '../types/notification.types'; - -export class EmailService { - private resend: Resend; - private config: ResendConfig; - - constructor(config: ResendConfig) { - this.config = config; - this.resend = new Resend(config.apiKey); - } - - /** - * Send a transactional email - */ - async sendEmail( - recipient: NotificationRecipient, - subject: string, - htmlBody: string, - textBody?: string, - templateId?: string, - attachments?: Array<{ - filename: string; - content: Buffer | string; - mimeType?: string; - }> - ): Promise { - const notification: EmailNotification = { - id: `email_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - userId: recipient.userId, - channel: NotificationChannel.EMAIL, - templateId: templateId || 'custom', - priority: NotificationPriority.NORMAL, - status: NotificationStatus.PENDING, - to: recipient.email!, - subject, - htmlBody, - textBody, - attachments, - createdAt: new Date(), - }; - - try { - const { data, error } = await this.resend.emails.send({ - from: `${this.config.fromName} <${this.config.fromEmail}>`, - to: [recipient.email!], - subject, - html: htmlBody, - text: textBody, - attachments: attachments?.map((att) => ({ - filename: att.filename, - content: typeof att.content === 'string' ? Buffer.from(att.content) : att.content, - mimeType: att.mimeType, - })), - metadata: { - userId: recipient.userId, - notificationId: notification.id, - }, - }); - - if (error) { - notification.status = NotificationStatus.FAILED; - notification.failedAt = new Date(); - notification.errorMessage = error.message; - } else { - notification.status = NotificationStatus.SENT; - notification.sentAt = new Date(); - } - - return notification; - } catch (error) { - notification.status = NotificationStatus.FAILED; - notification.failedAt = new Date(); - notification.errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return notification; - } - } - - /** - * Send email with retry logic - */ - async sendEmailWithRetry( - recipient: NotificationRecipient, - subject: string, - htmlBody: string, - textBody?: string, - maxRetries: number = 3 - ): Promise { - let lastError: Error | undefined; - - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - const result = await this.sendEmail(recipient, subject, htmlBody, textBody); - - if (result.status === NotificationStatus.SENT) { - return result; - } - - lastError = new Error(result.errorMessage); - } catch (error) { - lastError = error as Error; - } - - // Wait before retry (exponential backoff) - await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); - } - - return { - id: `email_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - userId: recipient.userId, - channel: NotificationChannel.EMAIL, - templateId: 'custom', - priority: NotificationPriority.NORMAL, - status: NotificationStatus.FAILED, - to: recipient.email!, - subject, - htmlBody, - textBody, - createdAt: new Date(), - failedAt: new Date(), - errorMessage: lastError?.message, - }; - } - - /** - * Check email delivery status - */ - async getDeliveryStatus(notificationId: string): Promise { - try { - const emailId = notificationId.replace('email_', ''); - const { data } = await this.resend.emails.get(emailId); - - if (data.status === 'sent') { - return NotificationStatus.SENT; - } else if (data.status === 'delivered') { - return NotificationStatus.DELIVERED; - } else if (data.status === 'failed') { - return NotificationStatus.FAILED; - } - - return NotificationStatus.PENDING; - } catch { - return NotificationStatus.PENDING; - } - } - - /** - * Batch send emails - */ - async batchSendEmails( - recipients: Array<{ - recipient: NotificationRecipient; - subject: string; - htmlBody: string; - textBody?: string; - }> - ): Promise { - const promises = recipients.map(async ({ recipient, subject, htmlBody, textBody }) => { - return this.sendEmail(recipient, subject, htmlBody, textBody); - }); - - return Promise.all(promises); - } -} diff --git a/packages/shared-notifications/src/services/notification.service.ts b/packages/shared-notifications/src/services/notification.service.ts deleted file mode 100644 index 47c3edbe1..000000000 --- a/packages/shared-notifications/src/services/notification.service.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { EmailService } from './email.service'; -import { PushService } from './push.service'; -import { SMSService } from './sms.service'; -import { - Notification, - NotificationChannel, - NotificationPreferences, - NotificationRecipient, - NotificationStatus, - NotificationPriority, -} from '../types/notification.types'; -import { NotificationConfig } from '../config/notification.config'; - -/** - * Main notification service that orchestrates all notification channels - */ -export class NotificationService { - private emailService?: EmailService; - private pushService?: PushService; - private smsService?: SMSService; - private config: NotificationConfig; - - constructor(config: NotificationConfig) { - this.config = config; - - // Initialize services based on configuration - if (config.resend) { - this.emailService = new EmailService(config.resend); - } - - if (config.fcm || config.apns) { - this.pushService = new PushService(config.fcm, config.apns); - } - - if (config.twilio) { - this.smsService = new SMSService(config.twilio); - } - } - - /** - * Send notification to all enabled channels for a user - */ - async sendMultiChannelNotification( - recipient: NotificationRecipient, - channel: NotificationChannel | NotificationChannel[], - subject: string, - body: string, - priority: NotificationPriority = NotificationPriority.NORMAL, - metadata?: Record - ): Promise { - const channels = Array.isArray(channel) ? channel : [channel]; - const notifications: Notification[] = []; - - for (const ch of channels) { - const prefs = await this.getNotificationPreferences(recipient.userId); - - if (!this.isChannelEnabled(prefs, ch)) { - continue; - } - - let notification: Notification; - - switch (ch) { - case NotificationChannel.EMAIL: - if (this.emailService && recipient.email) { - notification = await this.emailService.sendEmail( - recipient, - subject, - body, - body // Plain text fallback - ); - notifications.push(notification); - } - break; - - case NotificationChannel.PUSH: - if (this.pushService) { - notification = await this.pushService.sendPush( - recipient, - subject, - body, - metadata as Record, - undefined, - 'default' - ); - notifications.push(notification); - } - break; - - case NotificationChannel.SMS: - if (this.smsService && recipient.phone) { - notification = await this.smsService.sendSMS(recipient, body); - notifications.push(notification); - } - break; - } - } - - return notifications; - } - - /** - * Send email notification - */ - async sendEmail( - recipient: NotificationRecipient, - subject: string, - htmlBody: string, - textBody?: string - ): Promise { - if (!this.emailService) { - return null; - } - - return this.emailService.sendEmail(recipient, subject, htmlBody, textBody); - } - - /** - * Send push notification - */ - async sendPush( - recipient: NotificationRecipient, - title: string, - body: string, - data?: Record - ): Promise { - if (!this.pushService) { - return null; - } - - return this.pushService.sendPush(recipient, title, body, data); - } - - /** - * Send SMS notification - */ - async sendSMS( - recipient: NotificationRecipient, - body: string, - fromNumber?: string - ): Promise { - if (!this.smsService) { - return null; - } - - return this.smsService.sendSMS(recipient, body, fromNumber); - } - - /** - * Get notification preferences for a user - */ - async getNotificationPreferences( - userId: string - ): Promise { - // TODO: Fetch from database - // For now, return default preferences - return { - userId, - email: { - enabled: true, - categories: ['marketing', 'transactional', 'alerts'], - }, - push: { - enabled: true, - categories: ['marketing', 'transactional', 'alerts'], - }, - sms: { - enabled: true, - categories: ['alerts'], - }, - }; - } - - /** - * Check if a channel is enabled for a user - */ - private isChannelEnabled( - prefs: NotificationPreferences, - channel: NotificationChannel - ): boolean { - switch (channel) { - case NotificationChannel.EMAIL: - return prefs.email.enabled; - case NotificationChannel.PUSH: - return prefs.push.enabled; - case NotificationChannel.SMS: - return prefs.sms.enabled; - } - } - - /** - * Deduplicate notifications (prevent duplicate sends) - */ - async deduplicateNotification( - userId: string, - templateId: string, - windowMs: number = 5 * 60 * 1000 // 5 minutes default - ): Promise { - // TODO: Check recent notifications in database - // For now, return true (not a duplicate) - return true; - } - - /** - * Check rate limit for a channel - */ - async checkRateLimit( - userId: string, - channel: NotificationChannel - ): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> { - const rateLimit = this.config.rateLimits[channel]; - - // TODO: Implement actual rate limiting with Redis or database - // For now, return default values - return { - allowed: true, - remaining: rateLimit.maxPerWindow, - resetAt: new Date(Date.now() + rateLimit.windowMs), - }; - } - - /** - * Get email service instance - */ - getEmailService(): EmailService | undefined { - return this.emailService; - } - - /** - * Get push service instance - */ - getPushService(): PushService | undefined { - return this.pushService; - } - - /** - * Get SMS service instance - */ - getSMSService(): SMSService | undefined { - return this.smsService; - } - - /** - * Check if all services are initialized - */ - isFullyConfigured(): boolean { - return !!(this.emailService && this.pushService && this.smsService); - } - - /** - * Get configuration summary - */ - getConfigSummary(): { - email: boolean; - push: boolean; - sms: boolean; - } { - return { - email: !!this.emailService, - push: !!this.pushService, - sms: !!this.smsService, - }; - } -} - -// Export singleton instance creator -export function createNotificationService( - config: NotificationConfig -): NotificationService { - return new NotificationService(config); -} diff --git a/packages/shared-notifications/src/services/push.service.ts b/packages/shared-notifications/src/services/push.service.ts deleted file mode 100644 index 1a77f4fa2..000000000 --- a/packages/shared-notifications/src/services/push.service.ts +++ /dev/null @@ -1,262 +0,0 @@ -import admin from 'firebase-admin'; -import * as path from 'path'; -import { - PushNotification, - NotificationStatus, - NotificationPriority, - NotificationRecipient, - NotificationChannel, -} from '../types/notification.types'; -import { FCMConfig, APNsConfig } from '../config/notification.config'; - -export class PushService { - private fcm?: admin.app.App; - private apnsConfig?: APNsConfig; - - constructor(fcmConfig?: FCMConfig, apnsConfig?: APNsConfig) { - if (fcmConfig) { - // Use named app instance for multi-tenant support - const appName = fcmConfig.keyPath - ? `fcm_${fcmConfig.projectId}` - : 'fcm_default'; - - // Check if app with this name already exists - const existingApp = admin.app(appName); - - if (!existingApp) { - this.fcm = admin.initializeApp({ - credential: admin.credential.cert({ - projectId: fcmConfig.projectId, - privateKey: fcmConfig.privateKey.replace(/\\n/g, '\n'), - clientEmail: fcmConfig.clientEmail, - }), - ...(fcmConfig.keyPath && { - storageBucket: `${fcmConfig.projectId}.appspot.com`, - }), - }, appName); - } else { - this.fcm = existingApp; - } - } - - this.apnsConfig = apnsConfig; - } - - /** - * Send push notification to FCM device - */ - async sendFCMPush( - recipient: NotificationRecipient, - title: string, - body: string, - data?: Record, - badge?: number, - sound?: string - ): Promise { - const notification: PushNotification = { - id: `push_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - userId: recipient.userId, - channel: NotificationChannel.PUSH, - templateId: 'custom', - priority: NotificationPriority.NORMAL, - status: NotificationStatus.PENDING, - title, - body, - data, - badge, - sound, - fcmToken: recipient.fcmToken, - createdAt: new Date(), - }; - - if (!this.fcm || !recipient.fcmToken) { - notification.status = NotificationStatus.FAILED; - notification.failedAt = new Date(); - notification.errorMessage = !this.fcm ? 'FCM not configured' : 'Missing FCM token'; - return notification; - } - - try { - const message: admin.messaging.Message = { - token: recipient.fcmToken, - notification: { - title, - body, - }, - data: data - ? Object.fromEntries( - Object.entries(data).map(([key, value]) => [key, String(value)]) - ) - : undefined, - apns: { - payload: { - aps: { - badge, - sound: sound || 'default', - }, - }, - }, - android: { - priority: 'high', - notification: { - title, - body, - clickAction: 'FLUTTER_NOTIFICATION_CLICK', - }, - }, - }; - - const response = await this.fcm.messaging().send(message); - - notification.status = NotificationStatus.SENT; - notification.sentAt = new Date(); - - return notification; - } catch (error) { - notification.status = NotificationStatus.FAILED; - notification.failedAt = new Date(); - notification.errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return notification; - } - } - - /** - * Send push notification using APNs - */ - async sendAPNSPush( - recipient: NotificationRecipient, - title: string, - body: string, - data?: Record, - badge?: number - ): Promise { - const notification: PushNotification = { - id: `push_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - userId: recipient.userId, - channel: NotificationChannel.PUSH, - templateId: 'custom', - priority: NotificationPriority.NORMAL, - status: NotificationStatus.PENDING, - title, - body, - data, - badge, - apnsToken: recipient.apnsToken, - createdAt: new Date(), - }; - - if (!this.apnsConfig || !recipient.apnsToken) { - notification.status = NotificationStatus.FAILED; - notification.failedAt = new Date(); - notification.errorMessage = !this.apnsConfig ? 'APNs not configured' : 'Missing APNs token'; - return notification; - } - - // FCM supports sending to APNs tokens (iOS devices) - // This leverages FCM's unified push infrastructure for iOS - // APNs token format: device-specific token from iOS - if (this.fcm && recipient.apnsToken) { - const message: admin.messaging.Message = { - token: recipient.apnsToken, - notification: { - title, - body, - }, - data: data - ? Object.fromEntries( - Object.entries(data).map(([key, value]) => [key, String(value)]) - ) - : undefined, - apns: { - payload: { - aps: { - badge, - sound: 'default', - contentAvailable: true, - }, - }, - }, - }; - - try { - await this.fcm.messaging().send(message); - notification.status = NotificationStatus.SENT; - notification.sentAt = new Date(); - } catch (error) { - notification.status = NotificationStatus.FAILED; - notification.failedAt = new Date(); - notification.errorMessage = error instanceof Error ? error.message : 'Unknown error'; - } - } - - return notification; - } - - /** - * Send push notification (auto-detect platform) - */ - async sendPush( - recipient: NotificationRecipient, - title: string, - body: string, - data?: Record, - badge?: number, - sound?: string - ): Promise { - // Prefer APNs for iOS tokens, FCM for Android - if (recipient.apnsToken) { - return this.sendAPNSPush(recipient, title, body, data, badge); - } else if (recipient.fcmToken) { - return this.sendFCMPush(recipient, title, body, data, badge, sound); - } else { - return { - id: `push_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - userId: recipient.userId, - channel: NotificationChannel.PUSH, - templateId: 'custom', - priority: NotificationPriority.NORMAL, - status: NotificationStatus.FAILED, - title, - body, - data, - badge, - sound, - createdAt: new Date(), - failedAt: new Date(), - errorMessage: 'No push token available', - }; - } - } - - /** - * Send broadcast push to multiple devices - */ - async sendBroadcastPush( - recipients: Array, - title: string, - body: string, - data?: Record - ): Promise { - const promises = recipients.map((recipient) => - this.sendPush(recipient, title, body, data) - ); - - return Promise.all(promises); - } - - /** - * Check if FCM is properly configured - */ - isFCMConfigured(): boolean { - return !!this.fcm; - } - -/** - * Shutdown FCM app - */ - async shutdown(): Promise { - if (this.fcm) { - await this.fcm.delete(); - } - } -} diff --git a/packages/shared-notifications/src/services/sms.service.ts b/packages/shared-notifications/src/services/sms.service.ts deleted file mode 100644 index a8b6be07d..000000000 --- a/packages/shared-notifications/src/services/sms.service.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Twilio } from 'twilio'; -import { - SMSNotification, - NotificationStatus, - NotificationPriority, - NotificationRecipient, - NotificationChannel, -} from '../types/notification.types'; -import { TwilioConfig } from '../config/notification.config'; - -export class SMSService { - private twilio: Twilio; - private config: TwilioConfig; - private defaultFromNumber?: string; - - constructor(config: TwilioConfig) { - this.config = config; - this.twilio = new Twilio(config.accountSid, config.authToken); - this.defaultFromNumber = config.fromNumber; - } - - /** - * Send SMS message - */ - async sendSMS( - recipient: NotificationRecipient, - body: string, - fromNumber?: string - ): Promise { - const notification: SMSNotification = { - id: `sms_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - userId: recipient.userId, - channel: NotificationChannel.SMS, - templateId: 'custom', - priority: NotificationPriority.NORMAL, - status: NotificationStatus.PENDING, - to: recipient.phone!, - body, - from: fromNumber || this.defaultFromNumber, - createdAt: new Date(), - }; - - if (!recipient.phone) { - notification.status = NotificationStatus.FAILED; - notification.failedAt = new Date(); - notification.errorMessage = 'Missing phone number'; - return notification; - } - - try { - const message = await this.twilio.messages.create({ - body, - from: fromNumber || this.defaultFromNumber, - to: recipient.phone, - }); - - notification.status = NotificationStatus.SENT; - notification.sentAt = new Date(); - notification.id = message.sid; - - return notification; - } catch (error) { - notification.status = NotificationStatus.FAILED; - notification.failedAt = new Date(); - notification.errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return notification; - } - } - - /** - * Send SMS with delivery status tracking - */ - async sendSMSWithTracking( - recipient: NotificationRecipient, - body: string, - fromNumber?: string - ): Promise { - const notification = await this.sendSMS(recipient, body, fromNumber); - - if (notification.status === NotificationStatus.SENT && notification.id) { - try { - const message = await this.twilio.messages(notification.id).fetch(); - - if (message.status === 'delivered') { - notification.status = NotificationStatus.DELIVERED; - notification.deliveredAt = new Date(message.dateUpdated || message.dateSent); - } - } catch (error) { - console.warn(`Failed to fetch delivery status for SMS ${notification.id}:`, error); - } - } - - return notification; - } - - /** - * Check SMS delivery status - */ - async getDeliveryStatus(smsId: string): Promise { - try { - const message = await this.twilio.messages(smsId).fetch(); - - switch (message.status) { - case 'sent': - case 'delivered': - return NotificationStatus.DELIVERED; - case 'failed': - case 'undelivered': - return NotificationStatus.FAILED; - case 'queued': - case 'sending': - return NotificationStatus.PENDING; - default: - return NotificationStatus.PENDING; - } - } catch { - return NotificationStatus.PENDING; - } - } - - /** - * Send bulk SMS messages - */ - async bulkSendSMS( - recipients: Array<{ - recipient: NotificationRecipient; - body: string; - fromNumber?: string; - }> - ): Promise { - const promises = recipients.map(async ({ recipient, body, fromNumber }) => { - return this.sendSMS(recipient, body, fromNumber); - }); - - return Promise.all(promises); - } - - /** - * Send transactional SMS (e.g., verification codes) - */ - async sendTransactionSMS( - recipient: NotificationRecipient, - template: 'verification' | 'password_reset' | 'welcome', - variables?: Record - ): Promise { - const templates: Record) => string> = { - verification: (vars) => - `Your verification code is: ${vars?.code || '123456'}. Valid for 10 minutes.`, - password_reset: (vars) => - `Password reset requested for ${vars?.email || 'your account'}. Click the link to reset.`, - welcome: (vars) => - `Welcome ${vars?.name || 'there'}! Your account has been created successfully.`, - }; - - const body = templates[template](variables); - - return this.sendSMS(recipient, body); - } - - /** - * Validate phone number format - */ - isValidPhoneNumber(phone: string): boolean { - // Basic E.164 format validation - const e164Regex = /^\+[1-9]\d{1,14}$/; - return e164Regex.test(phone); - } - - /** - * Get Twilio client for advanced operations - */ - getClient(): Twilio { - return this.twilio; - } -} diff --git a/packages/shared-notifications/src/types/notification.types.ts b/packages/shared-notifications/src/types/notification.types.ts deleted file mode 100644 index f2356f4e8..000000000 --- a/packages/shared-notifications/src/types/notification.types.ts +++ /dev/null @@ -1,133 +0,0 @@ -// Notification channels -export enum NotificationChannel { - EMAIL = 'email', - PUSH = 'push', - SMS = 'sms', -} - -// Notification priorities -export enum NotificationPriority { - LOW = 'low', - NORMAL = 'normal', - HIGH = 'high', - URGENT = 'urgent', -} - -// Notification status -export enum NotificationStatus { - PENDING = 'pending', - SENT = 'sent', - DELIVERED = 'delivered', - READ = 'read', - FAILED = 'failed', -} - -// Template types -export enum TemplateType { - WELCOME = 'welcome', - PASSWORD_RESET = 'password_reset', - EMAIL_VERIFICATION = 'email_verification', - SMS_VERIFICATION = 'sms_verification', - PUSH_WELCOME = 'push_welcome', - CUSTOM = 'custom', -} - -// Notification recipient -export interface NotificationRecipient { - userId: string; - email?: string; - phone?: string; - fcmToken?: string; - apnsToken?: string; -} - -// Notification template -export interface NotificationTemplate { - id: string; - type: TemplateType; - channel: NotificationChannel; - subject?: string; // For email - title?: string; // For push - body: string; - locale: string; - variables: Record; - isActive: boolean; -} - -// Notification preferences -export interface NotificationPreferences { - userId: string; - email: { - enabled: boolean; - categories: string[]; // e.g., ['marketing', 'transactional', 'alerts'] - }; - push: { - enabled: boolean; - categories: string[]; - }; - sms: { - enabled: boolean; - categories: string[]; - }; -} - -// Base notification interface -export interface BaseNotification { - id: string; - userId: string; - channel: NotificationChannel; - templateId: string; - priority: NotificationPriority; - status: NotificationStatus; - metadata?: Record; - createdAt: Date; - sentAt?: Date; - deliveredAt?: Date; - readAt?: Date; - failedAt?: Date; - errorMessage?: string; -} - -// Email-specific notification -export interface EmailNotification extends BaseNotification { - channel: NotificationChannel.EMAIL; - to: string; - subject: string; - htmlBody?: string; - textBody?: string; - attachments?: Array<{ - filename: string; - content: Buffer | string; - mimeType?: string; - }>; -} - -// Push notification -export interface PushNotification extends BaseNotification { - channel: NotificationChannel.PUSH; - title: string; - body: string; - data?: Record; - badge?: number; - sound?: string; - fcmToken?: string; - apnsToken?: string; -} - -// SMS notification -export interface SMSNotification extends BaseNotification { - channel: NotificationChannel.SMS; - to: string; - body: string; - from?: string; -} - -// Union type for all notification types -export type Notification = EmailNotification | PushNotification | SMSNotification; - -// Rate limit configuration -export interface RateLimitConfig { - maxPerWindow: number; - windowMs: number; - key: string; // User ID or template ID -} diff --git a/packages/shared-notifications/tsconfig.json b/packages/shared-notifications/tsconfig.json deleted file mode 100644 index e229935f5..000000000 --- a/packages/shared-notifications/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-ui/package.json b/packages/shared-ui/package.json deleted file mode 100644 index dc9680f48..000000000 --- a/packages/shared-ui/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@shieldsai/shared-ui", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "src/index.tsx", - "types": "src/index.tsx", - "scripts": { - "lint": "eslint src/" - }, - "dependencies": { - "solid-js": "^1.8.14" - }, - "devDependencies": { - "typescript": "^5.3.3" - } -} diff --git a/packages/shared-utils/package.json b/packages/shared-utils/package.json deleted file mode 100644 index 6ae9d1451..000000000 --- a/packages/shared-utils/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@shieldsai/shared-utils", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", - "scripts": { - "lint": "eslint src/", - "test": "vitest" - }, - "devDependencies": { - "typescript": "^5.3.3", - "vitest": "^1.3.1" - } -} diff --git a/server/alerts/alert-server.ts b/server/alerts/alert-server.ts deleted file mode 100644 index c55792b35..000000000 --- a/server/alerts/alert-server.ts +++ /dev/null @@ -1,415 +0,0 @@ -/** - * WebSocket Alert Server - * Real-time alert broadcasting for call analysis events and anomalies - * Connects to CallAnalysisEngine and pushes alerts to subscribed clients - */ - -import { WebSocketServer, WebSocket } from 'ws'; -import { CallAnalysisEngine, CallEvent, Anomaly, SentimentAnalysis, AnalysisResult } from '../../src/lib/inference/call-analysis-engine'; -import { jwtVerify, SignJWT } from 'jose'; - -export type AlertType = - | 'anomaly' - | 'call_event' - | 'quality_degraded' - | 'sentiment_shift' - | 'call_summary' - | 'connection' - | 'disconnection'; - -export type AlertSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical'; - -export interface AlertPayload { - id: string; - type: AlertType; - severity: AlertSeverity; - timestamp: number; - callId?: string; - title: string; - message: string; - data: Record; - actionable: boolean; -} - -export interface AlertServerConfig { - port?: number; - enableAuth?: boolean; - jwtSecret?: string; - allowedOrigins?: string[]; - alertCooldownMs?: number; - maxSubscribers?: number; - enableCallCorrelation?: boolean; -} - -export interface SubscriberSession { - ws: WebSocket; - userId?: string; - callIds: Set; - lastAlertTime: Map; - subscribedAt: number; -} - -const DEFAULT_CONFIG: Required = { - port: 8088, - enableAuth: true, - jwtSecret: process.env.ALERT_SERVER_JWT_SECRET || '', - allowedOrigins: ['http://localhost:3000'], - alertCooldownMs: 5000, - maxSubscribers: 100, - enableCallCorrelation: true, -}; - -/** - * JWT verification helper - */ -async function verifyJWT(token: string, secret: string): Promise { - try { - const decoded = await jwtVerify(token, new TextEncoder().encode(secret), { - algorithms: ['HS256'], - }); - return decoded; - } catch (error) { - console.error('[AlertServer] JWT verification failed:', (error as Error).message); - return null; - } -} - -export class AlertServer { - private wss: WebSocketServer | null = null; - private config: Required; - private subscribers: Map = new Map(); - private analysisEngines: Map = new Map(); - private alertHistory: AlertPayload[] = []; - private maxAlertHistory: number = 500; - private isRunning: boolean = false; - - constructor(config: AlertServerConfig = {}) { - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - async start(): Promise { - this.wss = new WebSocketServer({ - port: this.config.port, - maxPayload: 1024 * 1024, - }); - - this.wss.on('connection', (ws: WebSocket, req) => { - this.handleConnection(ws, req); - }); - - this.wss.on('error', (error: Error) => { - console.error(`[AlertServer] WebSocket error: ${error.message}`); - }); - - this.isRunning = true; - console.log(`[AlertServer] Listening on port ${this.config.port}`); - } - - private handleConnection(ws: WebSocket, req: import('http').IncomingMessage): void { - const url = new URL(req.url || '', `http://${req.headers.host}`); - const sessionId = url.searchParams.get('sessionId') || `sub-${Date.now()}-${Math.random().toString(36).slice(2)}`; - let userId = url.searchParams.get('userId') || undefined; - const callId = url.searchParams.get('callId') || undefined; - - // Origin validation - const origin = req.headers.origin; - if (origin && !this.config.allowedOrigins.includes(origin)) { - ws.close(1008, 'Origin not allowed'); - return; - } - - // JWT Authentication (if enabled) - if (this.config.enableAuth && this.config.jwtSecret) { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - ws.close(4001, 'Missing or invalid JWT token'); - return; - } - - const token = authHeader.substring(7); - const decoded = verifyJWT(token, this.config.jwtSecret); - - if (!decoded) { - ws.close(4002, 'Invalid or expired JWT token'); - return; - } - - // Extract user ID from token if present - userId = (decoded as any).sub || userId; - } - - if (this.subscribers.size >= this.config.maxSubscribers) { - ws.close(1013, 'Too many subscribers'); - return; - } - - const session: SubscriberSession = { - ws, - userId, - callIds: callId ? new Set([callId]) : new Set(), - lastAlertTime: new Map(), - subscribedAt: Date.now(), - }; - - this.subscribers.set(sessionId, session); - - ws.send(JSON.stringify({ - type: 'connected', - payload: { sessionId, userId, timestamp: Date.now() }, - })); - - ws.on('message', (data: Buffer | ArrayBuffer) => { - this.handleMessage(sessionId, data); - }); - - ws.on('close', () => { - this.subscribers.delete(sessionId); - console.log(`[AlertServer] Subscriber disconnected: ${sessionId}`); - }); - - ws.on('error', (error: Error) => { - console.error(`[AlertServer] Subscriber error (${sessionId}): ${error.message}`); - }); - - console.log(`[AlertServer] Subscriber connected: ${sessionId}${callId ? ` (call: ${callId})` : ''}`); - } - - private handleMessage(sessionId: string, data: Buffer | ArrayBuffer): void { - try { - const message = JSON.parse(data.toString()); - const session = this.subscribers.get(sessionId); - if (!session) return; - - switch (message.type) { - case 'subscribe': - if (message.callId) { - session.callIds.add(message.callId); - } - break; - - case 'unsubscribe': - if (message.callId) { - session.callIds.delete(message.callId); - } - break; - - case 'ping': - session.ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); - break; - } - } catch (error) { - console.error(`[AlertServer] Message parse error: ${(error as Error).message}`); - } - } - - bindAnalysisEngine(callId: string, engine: CallAnalysisEngine): void { - this.analysisEngines.set(callId, engine); - - engine.on('result', (result: AnalysisResult) => { - this.processAnalysisResult(callId, result); - }); - - engine.on('events', (events: CallEvent[]) => { - events.forEach(event => { - this.sendAlert({ - type: 'call_event', - severity: event.severity as AlertSeverity, - callId, - title: this.formatEventType(event.type), - message: this.formatEventMessage(event), - data: { event, timestamp: event.timestamp }, - actionable: event.severity === 'high', - }); - }); - }); - - engine.on('anomalies', (anomalies: Anomaly[]) => { - anomalies.forEach(anomaly => { - this.sendAlert({ - type: 'anomaly', - severity: anomaly.severity as AlertSeverity, - callId, - title: this.formatAnomalyType(anomaly.type), - message: anomaly.description, - data: { - anomaly, - confidence: anomaly.confidence, - recommendation: anomaly.recommendation, - }, - actionable: anomaly.severity === 'high' || anomaly.severity === 'critical', - }); - }); - }); - - console.log(`[AlertServer] Bound analysis engine for call: ${callId}`); - } - - private processAnalysisResult(callId: string, result: AnalysisResult): void { - if (result.callQuality.mosScore < 3.0) { - this.sendAlert({ - type: 'quality_degraded', - severity: result.callQuality.mosScore < 2.5 ? 'high' : 'medium', - callId, - title: 'Call Quality Degraded', - message: `MOS score: ${result.callQuality.mosScore.toFixed(1)} (threshold: 3.0)`, - data: result.callQuality as unknown as Record, - actionable: true, - }); - } - - if (result.sentiment.sentiment === 'negative' && result.sentiment.confidence > 0.7) { - this.sendAlert({ - type: 'sentiment_shift', - severity: 'medium', - callId, - title: 'Negative Sentiment Detected', - message: `Confidence: ${(result.sentiment.confidence * 100).toFixed(0)}%`, - data: result.sentiment as unknown as Record, - actionable: false, - }); - } - } - - sendAlert(options: { - type: AlertType; - severity: AlertSeverity; - callId?: string; - title: string; - message: string; - data: Record; - actionable: boolean; - }): void { - const cooldownKey = `${options.callId}:${options.type}`; - const now = Date.now(); - - const sessionKeys = Array.from(this.subscribers.keys()); - for (const key of sessionKeys) { - const session = this.subscribers.get(key); - if (!session) continue; - - const lastTime = session.lastAlertTime.get(cooldownKey) || 0; - if (now - lastTime < this.config.alertCooldownMs) continue; - - if (options.callId && session.callIds.size > 0 && !session.callIds.has(options.callId)) continue; - - const alert: AlertPayload = { - id: `alert-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - type: options.type, - severity: options.severity, - timestamp: now, - callId: options.callId, - title: options.title, - message: options.message, - data: options.data, - actionable: options.actionable, - }; - - this.alertHistory.push(alert); - if (this.alertHistory.length > this.maxAlertHistory) { - this.alertHistory.shift(); - } - - if (session.ws.readyState === WebSocket.OPEN) { - session.ws.send(JSON.stringify(alert)); - } - session.lastAlertTime.set(cooldownKey, now); - } - } - - broadcastCallSummary(callId: string, summary: string): void { - this.sendAlert({ - type: 'call_summary', - severity: 'info', - callId, - title: 'Call Analysis Summary', - message: summary, - data: { summary }, - actionable: false, - }); - } - - getAlertHistory(limit: number = 50, callId?: string): AlertPayload[] { - let history = this.alertHistory; - if (callId) { - history = history.filter(a => a.callId === callId); - } - return history.slice(-limit); - } - - getSubscriberCount(): number { - return this.subscribers.size; - } - - getActiveCalls(): string[] { - return Array.from(this.analysisEngines.keys()); - } - - getEngine(callId: string): CallAnalysisEngine | undefined { - return this.analysisEngines.get(callId); - } - - async stop(): Promise { - this.isRunning = false; - - this.subscribers.forEach((session) => { - if (session.ws.readyState === WebSocket.OPEN) { - session.ws.send(JSON.stringify({ - type: 'server_shutdown', - payload: { timestamp: Date.now() }, - })); - session.ws.close(1001, 'Server shutting down'); - } - }); - - this.analysisEngines.forEach((engine) => { - engine.destroy(); - }); - - if (this.wss) { - await new Promise((resolve) => { - this.wss!.close(() => resolve()); - }); - this.wss = null; - } - - console.log('[AlertServer] Stopped'); - } - - private formatEventType(type: string): string { - const labels: Record = { - interrupt: 'Speaker Interrupt', - overlap: 'Speech Overlap', - long_pause: 'Long Pause', - volume_spike: 'Volume Spike', - silence: 'Silence Detected', - speaker_change: 'Speaker Change', - }; - return labels[type] || type; - } - - private formatEventMessage(event: CallEvent): string { - const messages: Record = { - interrupt: `Interrupt detected (${event.duration}ms)`, - overlap: `Speech overlap detected (${event.duration}ms)`, - long_pause: `Pause duration: ${(event.duration / 1000).toFixed(1)}s`, - volume_spike: `Volume spike: ${(event.metadata.level as number)?.toFixed(2) || 'unknown'}`, - silence: `Silence detected for ${(event.duration * 1000).toFixed(0)}ms`, - speaker_change: 'Speaker change detected', - }; - return messages[event.type] || 'Event detected'; - } - - private formatAnomalyType(type: string): string { - const labels: Record = { - background_noise: 'Background Noise', - echo: 'Echo Detected', - distortion: 'Audio Distortion', - dropouts: 'Audio Dropout', - excessive_silence: 'Excessive Silence', - volume_inconsistency: 'Volume Inconsistency', - }; - return labels[type] || type; - } -} - -export default AlertServer; diff --git a/server/webrtc/signaling-server.ts b/server/webrtc/signaling-server.ts deleted file mode 100644 index 79a6c2efb..000000000 --- a/server/webrtc/signaling-server.ts +++ /dev/null @@ -1,259 +0,0 @@ -/** - * WebRTC Signaling Server - * Reuses WebSocket infrastructure for WebRTC signaling - * Handles peer connection negotiation and ICE candidate exchange - */ - -import { WebSocketServer } from 'ws'; -import { Peer } from 'peerjs'; - -type SignalingMessage = { - type: 'offer' | 'answer' | 'ice-candidate' | 'disconnect'; - payload: RTCSessionDescriptionInit | RTCIceCandidate | { peerId: string }; - targetPeerId: string; -}; - -interface PeerConnection { - peer: Peer; - connections: Map; - iceCandidates: Map; -} - -export class WebRTCSignalingServer { - private wss: WebSocketServer; - private peers: Map = new Map(); - private port: number; - - constructor(port: number) { - this.port = port; - this.wss = new WebSocketServer({ port }); - this.initialize(); - } - - private initialize(): void { - console.log(`WebRTC Signaling Server starting on port ${this.port}`); - - this.wss.on('connection', (ws: any, req) => { - const url = new URL(req.url || '', `http://${req.headers.host}`); - const peerId = url.searchParams.get('peerId') || `peer-${Date.now()}-${Math.random()}`; - - console.log(`WebRTC peer connected: ${peerId}`); - - const peerConnection: PeerConnection = { - peer: new Peer(peerId, { - host: 'localhost', - port: this.port, - path: '/webrtc', - }), - connections: new Map(), - iceCandidates: new Map(), - }; - - this.peers.set(peerId, peerConnection); - - // Handle incoming signaling messages - ws.on('message', (data: Buffer) => { - try { - const message: SignalingMessage = JSON.parse(data.toString()); - this.handleSignalingMessage(peerId, message, ws); - } catch (error) { - console.error('Error parsing signaling message:', error); - ws.send(JSON.stringify({ type: 'error', payload: { message: 'Invalid message format' } })); - } - }); - - // Handle disconnection - ws.on('close', () => { - console.log(`WebRTC peer disconnected: ${peerId}`); - this.cleanupPeer(peerId); - }); - - // Send confirmation - ws.send(JSON.stringify({ - type: 'connected', - payload: { peerId } - })); - }); - - console.log(`WebRTC Signaling Server started on port ${this.port}`); - } - - private handleSignalingMessage( - sourcePeerId: string, - message: SignalingMessage, - ws: any - ): void { - const { type, payload, targetPeerId } = message; - const sourceConnection = this.peers.get(sourcePeerId); - - if (!sourceConnection) { - console.warn(`Source peer not found: ${sourcePeerId}`); - return; - } - - switch (type) { - case 'offer': - this.handleOffer(sourcePeerId, targetPeerId, payload as RTCSessionDescriptionInit, ws); - break; - - case 'answer': - this.handleAnswer(sourcePeerId, targetPeerId, payload as RTCSessionDescriptionInit); - break; - - case 'ice-candidate': - this.handleIceCandidate(sourcePeerId, targetPeerId, payload as RTCIceCandidate); - break; - - case 'disconnect': - this.disconnectPeer(sourcePeerId, targetPeerId); - break; - } - } - - private async handleOffer( - sourcePeerId: string, - targetPeerId: string, - offer: RTCSessionDescriptionInit, - ws: any - ): Promise { - console.log(`Offer received from ${sourcePeerId} to ${targetPeerId}`); - - const targetConnection = this.peers.get(targetPeerId); - if (!targetConnection) { - console.warn(`Target peer not found: ${targetPeerId}`); - ws.send(JSON.stringify({ - type: 'error', - payload: { message: `Target peer ${targetPeerId} not found` }, - })); - return; - } - - // Store the connection - if (!targetConnection.connections.has(sourcePeerId)) { - const conn = targetConnection.peer.call(sourcePeerId, new MediaStream()); - targetConnection.connections.set(sourcePeerId, conn); - - // Handle connection events - conn.on('stream', (stream: MediaStream) => { - console.log(`Media stream received: ${targetPeerId} from ${sourcePeerId}`); - }); - - conn.on('close', () => { - console.log(`Connection closed: ${targetPeerId} <-> ${sourcePeerId}`); - }); - - conn.on('error', (error: Error) => { - console.error(`Connection error: ${targetPeerId} <-> ${sourcePeerId}`, error); - }); - - // Send accumulated ICE candidates - const accumulatedCandidates = targetConnection.iceCandidates.get(sourcePeerId) || []; - accumulatedCandidates.forEach(candidate => { - conn.dataChannel.send(JSON.stringify({ - type: 'ice-candidate', - payload: candidate, - targetPeerId, - })); - }); - } - - // Forward offer to target peer - const targetConn = targetConnection.connections.get(sourcePeerId); - if (targetConn) { - (targetConn as any).dataChannel.send(JSON.stringify({ - type: 'offer', - payload: offer, - targetPeerId: targetPeerId, - })); - } - } - - private handleAnswer( - sourcePeerId: string, - targetPeerId: string, - answer: RTCSessionDescriptionInit - ): void { - console.log(`Answer received from ${sourcePeerId} to ${targetPeerId}`); - - const targetConnection = this.peers.get(targetPeerId); - if (targetConnection?.connections.has(sourcePeerId)) { - targetConnection.connections.get(sourcePeerId).send(JSON.stringify({ - type: 'answer', - payload: answer, - targetPeerId: targetPeerId, - })); - } - } - - private handleIceCandidate( - sourcePeerId: string, - targetPeerId: string, - candidate: RTCIceCandidate - ): void { - const targetConnection = this.peers.get(targetPeerId); - - if (!targetConnection) { - return; - } - - // Forward ICE candidate to target peer - if (targetConnection.connections.has(sourcePeerId)) { - const conn = targetConnection.connections.get(sourcePeerId); - if (conn) { - (conn as any).send(JSON.stringify({ - type: 'ice-candidate', - payload: candidate, - targetPeerId: targetPeerId, - })); - } - } - } - - private disconnectPeer(sourcePeerId: string, targetPeerId: string): void { - const sourceConnection = this.peers.get(sourcePeerId); - if (sourceConnection?.connections.has(targetPeerId)) { - sourceConnection.connections.get(targetPeerId).close(); - sourceConnection.connections.delete(targetPeerId); - console.log(`Connection closed: ${sourcePeerId} <-> ${targetPeerId}`); - } - } - - private cleanupPeer(peerId: string): void { - const peerConnection = this.peers.get(peerId); - if (peerConnection) { - // Close all connections - peerConnection.connections.forEach((conn, connectedPeerId) => { - conn.close(); - console.log(`Cleaned up connection: ${peerId} <-> ${connectedPeerId}`); - }); - - // Destroy PeerJS instance - peerConnection.peer.destroy(); - - // Remove from registry - this.peers.delete(peerId); - } - } - - getPeerCount(): number { - return this.peers.size; - } - - getPeers(): string[] { - return Array.from(this.peers.keys()); - } - - getPeerConnections(peerId: string): string[] { - const peerConnection = this.peers.get(peerId); - if (!peerConnection) return []; - return Array.from(peerConnection.connections.keys()); - } - - close(): void { - this.peers.forEach((_, peerId) => this.cleanupPeer(peerId)); - this.wss.close(); - console.log('WebRTC Signaling Server closed'); - } -} - -export default WebRTCSignalingServer; diff --git a/services/voiceprint-ml/Dockerfile b/services/voiceprint-ml/Dockerfile deleted file mode 100644 index 79be4245e..000000000 --- a/services/voiceprint-ml/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -# Create models directory -RUN mkdir -p models - -EXPOSE 8001 - -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/services/voiceprint-ml/main.py b/services/voiceprint-ml/main.py deleted file mode 100644 index d652bf680..000000000 --- a/services/voiceprint-ml/main.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -VoicePrint ML Service — ECAPA-TDNN inference microservice. - -Provides endpoints for: -- Audio preprocessing (VAD, noise reduction, normalization) -- Voice embedding extraction using ECAPA-TDNN -- Synthetic voice detection - -For MVP, uses a mock model. Replace with real ECAPA-TDNN model when available. -""" - -from fastapi import FastAPI, File, UploadFile, HTTPException -from pydantic import BaseModel -from typing import Optional -import numpy as np -import io - -app = FastAPI( - title="VoicePrint ML Service", - description="ECAPA-TDNN inference for voice cloning detection", - version="0.1.0", -) - -# Model configuration -MODEL_PATH = "./models/ecapa-tdnn" -EMBEDDING_DIMENSIONS = 192 -SAMPLE_RATE = 16000 -CHANNELS = 1 - - -class EmbeddingResponse(BaseModel): - embedding: list[float] - duration: float - sample_rate: int - - -class AnalysisResponse(BaseModel): - is_synthetic: bool - confidence: float - detection_type: str - features: dict[str, float] - embedding: list[float] - - -class PreprocessRequest(BaseModel): - sample_rate: int = SAMPLE_RATE - channels: int = CHANNELS - apply_vad: bool = True - noise_reduction: bool = True - - -# Mock model — replace with real ECAPA-TDNN inference -class MockECAPATDNN: - def __init__(self): - self.dimensions = EMBEDDING_DIMENSIONS - self.initialized = False - - def initialize(self): - # TODO: Load real ECAPA-TDNN model - # self.model = torch.load(MODEL_PATH) - self.initialized = True - - def extract_embedding(self, audio_bytes: bytes) -> list[float]: - if not self.initialized: - self.initialize() - - # Mock: generate deterministic embedding based on audio content - hash_val = sum(audio_bytes[:256]) & 0xFFFFFFFF - embedding = [] - for i in range(self.dimensions): - hash_val = ((hash_val << 5) - hash_val + i) & 0xFFFFFFFF - embedding.append((hash_val % 1000) / 1000.0) - - # L2 normalize - norm = np.sqrt(sum(v * v for v in embedding)) - return [v / norm for v in embedding] - - def analyze(self, audio_bytes: bytes) -> dict: - embedding = self.extract_embedding(audio_bytes) - - # Mock: estimate synthetic confidence from audio statistics - mean_amplitude = np.mean(np.frombuffer(audio_bytes[:1024], dtype=np.uint8)) / 255.0 - confidence = min(1.0, abs(mean_amplitude - 0.5) * 2 * 0.3 + np.random.random() * 0.7) - - detection_type = "synthetic_voice" if confidence >= 0.75 else "natural" - - return { - "is_synthetic": confidence >= 0.75, - "confidence": float(confidence), - "detection_type": detection_type, - "features": { - "mean_amplitude": float(mean_amplitude), - "embedding_energy": float(sum(v * v for v in embedding)), - }, - "embedding": embedding, - } - - -model = MockECAPATDNN() - - -@app.get("/health") -async def health(): - return { - "status": "ok", - "model": "ecapa-tdnn-v1-mock", - "initialized": model.initialized, - } - - -@app.post("/initialize") -async def initialize(): - model.initialize() - return {"status": "initialized", "model": "ecapa-tdnn-v1-mock"} - - -@app.post("/preprocess") -async def preprocess(audio: UploadFile = File(...)): - """Preprocess audio: VAD, noise reduction, normalization to 16kHz mono.""" - audio_bytes = await audio.read() - - # TODO: Integrate with librosa/torchaudio for real preprocessing - # audio_array, sr = librosa.load(io.BytesIO(audio_bytes), sr=SAMPLE_RATE, mono=CHANNELS) - - return { - "status": "processed", - "sample_rate": SAMPLE_RATE, - "channels": CHANNELS, - "duration": len(audio_bytes) / (SAMPLE_RATE * 2 * CHANNELS), - } - - -@app.post("/embed", response_model=EmbeddingResponse) -async def extract_embedding(audio: UploadFile = File(...)): - """Extract voice embedding using ECAPA-TDNN.""" - audio_bytes = await audio.read() - - if len(audio_bytes) < SAMPLE_RATE * 2: - raise HTTPException( - status_code=422, - detail=f"Audio too short: minimum {SAMPLE_RATE * 2} bytes (1 second at 16kHz)", - ) - - embedding = model.extract_embedding(audio_bytes) - duration = len(audio_bytes) / (SAMPLE_RATE * 2 * CHANNELS) - - return EmbeddingResponse( - embedding=embedding, - duration=duration, - sample_rate=SAMPLE_RATE, - ) - - -@app.post("/analyze", response_model=AnalysisResponse) -async def analyze_audio(audio: UploadFile = File(...)): - """Analyze audio for synthetic voice detection.""" - audio_bytes = await audio.read() - - if len(audio_bytes) < SAMPLE_RATE * 2 * 3: - raise HTTPException( - status_code=422, - detail=f"Audio too short: minimum {SAMPLE_RATE * 2 * 3} bytes (3 seconds at 16kHz)", - ) - - result = model.analyze(audio_bytes) - - return AnalysisResponse(**result) - - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/services/voiceprint-ml/requirements.txt b/services/voiceprint-ml/requirements.txt deleted file mode 100644 index 626922cd6..000000000 --- a/services/voiceprint-ml/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -fastapi==0.104.1 -uvicorn==0.24.0 -pydantic==2.5.0 -numpy==1.26.0 -librosa==0.10.0 -torch==2.1.0 -faiss-cpu==1.7.4 -python-multipart==0.0.6 diff --git a/tsconfig.json b/tsconfig.json index 706203e5b..2e033b3ea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,6 @@ }, "types": ["vite-plugin-solid"] }, - "include": ["src/**/*", "server/**/*"], + "include": ["agents/**/*"], "exclude": ["node_modules", "dist"] } diff --git a/vite.config.ts b/vite.config.ts index f2c20d083..dd3f835ed 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,9 +6,9 @@ export default defineConfig({ plugins: [solid()], resolve: { alias: { - '@lib': resolve(__dirname, './src/lib'), - '@components': resolve(__dirname, './src/components'), - '@types': resolve(__dirname, './src/types'), + '@agents': resolve(__dirname, './agents'), + '@analysis': resolve(__dirname, './analysis'), + '@plans': resolve(__dirname, './plans'), }, }, build: {