From e704a9074a7a40d586d89076223522f5b0f1bc4a Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sat, 2 May 2026 10:19:11 -0400 Subject: [PATCH] FRE-4533: Merge apps/{api,web,mobile} and shared-db into ShieldAI repo Merge FrenoCorp apps into ShieldAI packages/: - packages/api: merged routes (notifications), middleware (auth, rate-limit, error, logging), config, services (darkwatch, spamshield, voiceprint), tests - packages/web: new SolidJS web app stub - packages/mobile: new SolidJS mobile app stub - packages/shared-db: new Prisma DB package (separate from existing packages/db) - pnpm-workspace.yaml: restored (apps/* removed, already covered by packages/*) Next: reconcile packages/shared-db with packages/db, and fix server.ts correlationRoutes import --- apps/api/package.json | 29 -- apps/api/src/routes/darkwatch.routes.ts | 285 ------------------ apps/api/src/routes/index.ts | 142 --------- apps/api/src/routes/spamshield.routes.ts | 252 ---------------- apps/api/src/routes/voiceprint.routes.ts | 257 ---------------- apps/api/tsconfig.json | 12 - .../sms-classifier-race-condition.test.ts | 0 .../api/src/__tests__/spam-rate-limit.test.ts | 0 .../api/src/config/api.config.ts | 0 {apps => packages}/api/src/config/redis.ts | 0 {apps => packages}/api/src/index.ts | 0 .../api/src/middleware/auth.middleware.ts | 0 .../middleware/error-handling.middleware.ts | 0 .../api/src/middleware/logging.middleware.ts | 0 .../src/middleware/rate-limit.middleware.ts | 0 .../middleware/spam-rate-limit.middleware.ts | 0 .../api/src/routes/notifications.routes.ts | 0 .../src/services/darkwatch/alert.pipeline.ts | 0 .../api/src/services/darkwatch/index.ts | 0 .../src/services/darkwatch/scan.service.ts | 0 .../services/darkwatch/scheduler.service.ts | 0 .../services/darkwatch/watchlist.service.ts | 0 .../src/services/darkwatch/webhook.service.ts | 0 .../src/services/spamshield/feature-flags.ts | 0 .../api/src/services/spamshield/index.ts | 0 .../spamshield/spamshield.audit-logger.ts | 0 .../services/spamshield/spamshield.config.ts | 0 .../spamshield/spamshield.error-handler.ts | 0 .../services/spamshield/spamshield.service.ts | 0 .../api/src/services/voiceprint/index.ts | 0 .../services/voiceprint/voiceprint.config.ts | 0 .../voiceprint/voiceprint.feature-flags.ts | 0 .../services/voiceprint/voiceprint.service.ts | 0 {apps => packages}/mobile/package.json | 0 {apps => packages}/web/package.json | 0 pnpm-workspace.yaml | 1 - 36 files changed, 978 deletions(-) delete mode 100644 apps/api/package.json 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/spamshield.routes.ts delete mode 100644 apps/api/src/routes/voiceprint.routes.ts delete mode 100644 apps/api/tsconfig.json rename {apps => packages}/api/src/__tests__/sms-classifier-race-condition.test.ts (100%) rename {apps => packages}/api/src/__tests__/spam-rate-limit.test.ts (100%) rename {apps => packages}/api/src/config/api.config.ts (100%) rename {apps => packages}/api/src/config/redis.ts (100%) rename {apps => packages}/api/src/index.ts (100%) rename {apps => packages}/api/src/middleware/auth.middleware.ts (100%) rename {apps => packages}/api/src/middleware/error-handling.middleware.ts (100%) rename {apps => packages}/api/src/middleware/logging.middleware.ts (100%) rename {apps => packages}/api/src/middleware/rate-limit.middleware.ts (100%) rename {apps => packages}/api/src/middleware/spam-rate-limit.middleware.ts (100%) rename {apps => packages}/api/src/routes/notifications.routes.ts (100%) rename {apps => packages}/api/src/services/darkwatch/alert.pipeline.ts (100%) rename {apps => packages}/api/src/services/darkwatch/index.ts (100%) rename {apps => packages}/api/src/services/darkwatch/scan.service.ts (100%) rename {apps => packages}/api/src/services/darkwatch/scheduler.service.ts (100%) rename {apps => packages}/api/src/services/darkwatch/watchlist.service.ts (100%) rename {apps => packages}/api/src/services/darkwatch/webhook.service.ts (100%) rename {apps => packages}/api/src/services/spamshield/feature-flags.ts (100%) rename {apps => packages}/api/src/services/spamshield/index.ts (100%) rename {apps => packages}/api/src/services/spamshield/spamshield.audit-logger.ts (100%) rename {apps => packages}/api/src/services/spamshield/spamshield.config.ts (100%) rename {apps => packages}/api/src/services/spamshield/spamshield.error-handler.ts (100%) rename {apps => packages}/api/src/services/spamshield/spamshield.service.ts (100%) rename {apps => packages}/api/src/services/voiceprint/index.ts (100%) rename {apps => packages}/api/src/services/voiceprint/voiceprint.config.ts (100%) rename {apps => packages}/api/src/services/voiceprint/voiceprint.feature-flags.ts (100%) rename {apps => packages}/api/src/services/voiceprint/voiceprint.service.ts (100%) rename {apps => packages}/mobile/package.json (100%) rename {apps => packages}/web/package.json (100%) diff --git a/apps/api/package.json b/apps/api/package.json deleted file mode 100644 index 855aa4c..0000000 --- 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/routes/darkwatch.routes.ts b/apps/api/src/routes/darkwatch.routes.ts deleted file mode 100644 index 161b020..0000000 --- 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 cfde1ed..0000000 --- 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/spamshield.routes.ts b/apps/api/src/routes/spamshield.routes.ts deleted file mode 100644 index 6534852..0000000 --- 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 dcdd483..0000000 --- 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/tsconfig.json b/apps/api/tsconfig.json deleted file mode 100644 index e229935..0000000 --- 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/api/src/__tests__/sms-classifier-race-condition.test.ts b/packages/api/src/__tests__/sms-classifier-race-condition.test.ts similarity index 100% rename from apps/api/src/__tests__/sms-classifier-race-condition.test.ts rename to packages/api/src/__tests__/sms-classifier-race-condition.test.ts diff --git a/apps/api/src/__tests__/spam-rate-limit.test.ts b/packages/api/src/__tests__/spam-rate-limit.test.ts similarity index 100% rename from apps/api/src/__tests__/spam-rate-limit.test.ts rename to packages/api/src/__tests__/spam-rate-limit.test.ts diff --git a/apps/api/src/config/api.config.ts b/packages/api/src/config/api.config.ts similarity index 100% rename from apps/api/src/config/api.config.ts rename to packages/api/src/config/api.config.ts diff --git a/apps/api/src/config/redis.ts b/packages/api/src/config/redis.ts similarity index 100% rename from apps/api/src/config/redis.ts rename to packages/api/src/config/redis.ts diff --git a/apps/api/src/index.ts b/packages/api/src/index.ts similarity index 100% rename from apps/api/src/index.ts rename to packages/api/src/index.ts diff --git a/apps/api/src/middleware/auth.middleware.ts b/packages/api/src/middleware/auth.middleware.ts similarity index 100% rename from apps/api/src/middleware/auth.middleware.ts rename to packages/api/src/middleware/auth.middleware.ts diff --git a/apps/api/src/middleware/error-handling.middleware.ts b/packages/api/src/middleware/error-handling.middleware.ts similarity index 100% rename from apps/api/src/middleware/error-handling.middleware.ts rename to packages/api/src/middleware/error-handling.middleware.ts diff --git a/apps/api/src/middleware/logging.middleware.ts b/packages/api/src/middleware/logging.middleware.ts similarity index 100% rename from apps/api/src/middleware/logging.middleware.ts rename to packages/api/src/middleware/logging.middleware.ts diff --git a/apps/api/src/middleware/rate-limit.middleware.ts b/packages/api/src/middleware/rate-limit.middleware.ts similarity index 100% rename from apps/api/src/middleware/rate-limit.middleware.ts rename to packages/api/src/middleware/rate-limit.middleware.ts diff --git a/apps/api/src/middleware/spam-rate-limit.middleware.ts b/packages/api/src/middleware/spam-rate-limit.middleware.ts similarity index 100% rename from apps/api/src/middleware/spam-rate-limit.middleware.ts rename to packages/api/src/middleware/spam-rate-limit.middleware.ts diff --git a/apps/api/src/routes/notifications.routes.ts b/packages/api/src/routes/notifications.routes.ts similarity index 100% rename from apps/api/src/routes/notifications.routes.ts rename to packages/api/src/routes/notifications.routes.ts diff --git a/apps/api/src/services/darkwatch/alert.pipeline.ts b/packages/api/src/services/darkwatch/alert.pipeline.ts similarity index 100% rename from apps/api/src/services/darkwatch/alert.pipeline.ts rename to packages/api/src/services/darkwatch/alert.pipeline.ts diff --git a/apps/api/src/services/darkwatch/index.ts b/packages/api/src/services/darkwatch/index.ts similarity index 100% rename from apps/api/src/services/darkwatch/index.ts rename to packages/api/src/services/darkwatch/index.ts diff --git a/apps/api/src/services/darkwatch/scan.service.ts b/packages/api/src/services/darkwatch/scan.service.ts similarity index 100% rename from apps/api/src/services/darkwatch/scan.service.ts rename to packages/api/src/services/darkwatch/scan.service.ts diff --git a/apps/api/src/services/darkwatch/scheduler.service.ts b/packages/api/src/services/darkwatch/scheduler.service.ts similarity index 100% rename from apps/api/src/services/darkwatch/scheduler.service.ts rename to packages/api/src/services/darkwatch/scheduler.service.ts diff --git a/apps/api/src/services/darkwatch/watchlist.service.ts b/packages/api/src/services/darkwatch/watchlist.service.ts similarity index 100% rename from apps/api/src/services/darkwatch/watchlist.service.ts rename to packages/api/src/services/darkwatch/watchlist.service.ts diff --git a/apps/api/src/services/darkwatch/webhook.service.ts b/packages/api/src/services/darkwatch/webhook.service.ts similarity index 100% rename from apps/api/src/services/darkwatch/webhook.service.ts rename to packages/api/src/services/darkwatch/webhook.service.ts diff --git a/apps/api/src/services/spamshield/feature-flags.ts b/packages/api/src/services/spamshield/feature-flags.ts similarity index 100% rename from apps/api/src/services/spamshield/feature-flags.ts rename to packages/api/src/services/spamshield/feature-flags.ts diff --git a/apps/api/src/services/spamshield/index.ts b/packages/api/src/services/spamshield/index.ts similarity index 100% rename from apps/api/src/services/spamshield/index.ts rename to packages/api/src/services/spamshield/index.ts diff --git a/apps/api/src/services/spamshield/spamshield.audit-logger.ts b/packages/api/src/services/spamshield/spamshield.audit-logger.ts similarity index 100% rename from apps/api/src/services/spamshield/spamshield.audit-logger.ts rename to packages/api/src/services/spamshield/spamshield.audit-logger.ts diff --git a/apps/api/src/services/spamshield/spamshield.config.ts b/packages/api/src/services/spamshield/spamshield.config.ts similarity index 100% rename from apps/api/src/services/spamshield/spamshield.config.ts rename to packages/api/src/services/spamshield/spamshield.config.ts diff --git a/apps/api/src/services/spamshield/spamshield.error-handler.ts b/packages/api/src/services/spamshield/spamshield.error-handler.ts similarity index 100% rename from apps/api/src/services/spamshield/spamshield.error-handler.ts rename to packages/api/src/services/spamshield/spamshield.error-handler.ts diff --git a/apps/api/src/services/spamshield/spamshield.service.ts b/packages/api/src/services/spamshield/spamshield.service.ts similarity index 100% rename from apps/api/src/services/spamshield/spamshield.service.ts rename to packages/api/src/services/spamshield/spamshield.service.ts diff --git a/apps/api/src/services/voiceprint/index.ts b/packages/api/src/services/voiceprint/index.ts similarity index 100% rename from apps/api/src/services/voiceprint/index.ts rename to packages/api/src/services/voiceprint/index.ts diff --git a/apps/api/src/services/voiceprint/voiceprint.config.ts b/packages/api/src/services/voiceprint/voiceprint.config.ts similarity index 100% rename from apps/api/src/services/voiceprint/voiceprint.config.ts rename to packages/api/src/services/voiceprint/voiceprint.config.ts diff --git a/apps/api/src/services/voiceprint/voiceprint.feature-flags.ts b/packages/api/src/services/voiceprint/voiceprint.feature-flags.ts similarity index 100% rename from apps/api/src/services/voiceprint/voiceprint.feature-flags.ts rename to packages/api/src/services/voiceprint/voiceprint.feature-flags.ts diff --git a/apps/api/src/services/voiceprint/voiceprint.service.ts b/packages/api/src/services/voiceprint/voiceprint.service.ts similarity index 100% rename from apps/api/src/services/voiceprint/voiceprint.service.ts rename to packages/api/src/services/voiceprint/voiceprint.service.ts diff --git a/apps/mobile/package.json b/packages/mobile/package.json similarity index 100% rename from apps/mobile/package.json rename to packages/mobile/package.json diff --git a/apps/web/package.json b/packages/web/package.json similarity index 100% rename from apps/web/package.json rename to packages/web/package.json diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1d8028e..22373e7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,3 @@ packages: - - "apps/*" - "packages/*" - "services/*"