import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { prisma, SubscriptionTier } from '@shieldai/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 }); } }); }