Files
ShieldAI/packages/api/src/routes/darkwatch.routes.ts
Michael Freno 24bc9c235f Consolidate @shieldai/db and @shieldsai/shared-db packages (FRE-4603)
- Merged singleton pattern + type exports from shared-db
- Kept FieldEncryptionService from original db package
- Upgraded to Prisma v6.2.0 (newer version)
- Adopted shared-db's complete schema for multi-service platform
- Updated 17 consumer imports across darkwatch, voiceprint, jobs, api
- Standardized on @shieldai/db namespace

Files changed:
- packages/db/package.json (v0.1.0 → v0.2.0)
- packages/db/src/index.ts (consolidated exports)
- packages/db/prisma/schema.prisma (merged schema)
- packages/db/prisma/seed.ts (updated for new schema)
- 17 consumer files updated

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-02 15:06:02 -04:00

286 lines
9.6 KiB
TypeScript

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<string | null> => {
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<string, unknown>;
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<string, unknown> | 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 });
}
});
}