FRE-4529: Transfer ShieldAI code from FrenoCorp repo
Transferred ShieldAI-related files mistakenly placed in ~/code/FrenoCorp:
- Services: spamshield (feature-flags, audit-logger, error-handler), voiceprint (config, service, feature-flags), darkwatch (pipeline, scan, scheduler, watchlist, webhook)
- Packages: shared-analytics, shared-auth, shared-ui, shared-utils (new); shared-billing, jobs supplemented with unique FC files
- Server: alerts (FC version newer), routes (spamshield, darkwatch, voiceprint)
- Config: turbo.json, tsconfig.base.json, vite/vitest configs, drizzle, Dockerfile
- VoicePrint ML service
- Examples
Pending: apps/{api,web,mobile}/ structured merge, shared-db/db mapping
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
285
packages/api/src/routes/darkwatch.routes.ts
Normal file
285
packages/api/src/routes/darkwatch.routes.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
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<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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user