import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; import { correlationService } from "@shieldai/correlation"; type AuthUser = { id?: string }; function getUserId(request: FastifyRequest): string | undefined { return (request.user as AuthUser | undefined)?.id; } export function correlationRoutes(fastify: FastifyInstance) { fastify.get("/dashboard", async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const timeWindow = parseInt( (request.query as Record).timeWindow as string ) || 60; const data = await correlationService.getDashboardData(userId, timeWindow); return reply.send(data); }); fastify.get("/groups", async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const query = request.query as Record; const result = await correlationService.getCorrelationGroups({ userId, status: (query.status as any) || undefined, timeWindowMinutes: query.timeWindow ? parseInt(query.timeWindow) : 60, limit: query.limit ? parseInt(query.limit) : 50, offset: query.offset ? parseInt(query.offset) : 0, }); return reply.send(result); }); fastify.get( "/groups/:groupId", { schema: { params: { type: "object", properties: { groupId: { type: "string", format: "uuid" }, }, required: ["groupId"], }, }, }, async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const groupId = (request.params as Record).groupId; const group = await correlationService.getGroupById(groupId, userId); if (!group) { return reply.code(404).send({ error: "Correlation group not found" }); } return reply.send(group); } ); fastify.patch( "/groups/:groupId/resolve", { schema: { params: { type: "object", properties: { groupId: { type: "string", format: "uuid" }, }, required: ["groupId"], }, body: { type: "object", properties: { status: { type: "string", enum: ["RESOLVED", "ACTIVE"] }, }, additionalProperties: false, }, }, }, async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const groupId = (request.params as Record).groupId; const body = request.body as Record | undefined; const status = body?.status || "RESOLVED"; const group = await correlationService.resolveGroup( groupId, userId, status ); if (!group) { return reply.code(404).send({ error: "Correlation group not found" }); } return reply.send(group); } ); fastify.get("/alerts", async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const query = request.query as Record; const result = await correlationService.getCorrelatedAlerts({ userId, source: (query.source as any) || undefined, category: (query.category as any) || undefined, severity: (query.severity as any) || undefined, timeWindowMinutes: query.timeWindow ? parseInt(query.timeWindow) : 60, limit: query.limit ? parseInt(query.limit) : 50, offset: query.offset ? parseInt(query.offset) : 0, }); return reply.send(result); }); fastify.post( "/ingest/darkwatch", { schema: { body: { type: "object", properties: { sourceAlertId: { type: "string" }, exposureId: { type: "string" }, breachName: { type: "string", maxLength: 500 }, severity: { type: "string", maxLength: 20 }, channel: { type: "string", maxLength: 50 }, dataType: { type: "array", items: { type: "string" } }, dataSource: { type: "string", maxLength: 100 }, }, required: ["sourceAlertId", "breachName", "severity", "channel"], additionalProperties: false, }, }, }, async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const body = request.body as Record; const alert = await correlationService.ingestDarkWatchAlert( userId, body.sourceAlertId as string, { exposureId: body.exposureId as string, breachName: body.breachName as string, severity: body.severity as string, channel: body.channel as string, dataType: body.dataType as string[] | undefined, dataSource: body.dataSource as string | undefined, } ); return reply.code(201).send(alert); } ); fastify.post( "/ingest/spamshield", { schema: { body: { type: "object", properties: { sourceAlertId: { type: "string" }, phoneNumber: { type: "string", maxLength: 20 }, decision: { type: "string", enum: ["BLOCK", "FLAG", "ALLOW"] }, confidence: { type: "number", minimum: 0, maximum: 1 }, reasons: { type: "array", items: { type: "string" } }, channel: { type: "string", enum: ["call", "sms"] }, hiyaReputationScore: { type: "number" }, truecallerSpamScore: { type: "number" }, }, required: ["sourceAlertId", "phoneNumber", "decision", "confidence"], additionalProperties: false, }, }, }, async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const body = request.body as Record; const alert = await correlationService.ingestSpamShieldAlert( userId, body.sourceAlertId as string, { phoneNumber: body.phoneNumber as string, decision: body.decision as string, confidence: body.confidence as number, reasons: body.reasons as string[] | undefined, channel: body.channel as "call" | "sms" | undefined, hiyaReputationScore: body.hiyaReputationScore as | number | undefined, truecallerSpamScore: body.truecallerSpamScore as | number | undefined, } ); return reply.code(201).send(alert); } ); fastify.post( "/ingest/voiceprint", { schema: { body: { type: "object", properties: { sourceAlertId: { type: "string" }, jobId: { type: "string" }, verdict: { type: "string", enum: ["SYNTHETIC", "NATURAL", "UNCERTAIN"], }, syntheticScore: { type: "number", minimum: 0, maximum: 1 }, confidence: { type: "number", minimum: 0, maximum: 1 }, matchedEnrollmentId: { type: "string" }, matchedSimilarity: { type: "number" }, analysisType: { type: "string", maxLength: 50 }, }, required: [ "sourceAlertId", "jobId", "verdict", "syntheticScore", "confidence", ], additionalProperties: false, }, }, }, async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const body = request.body as Record; const alert = await correlationService.ingestVoicePrintAlert( userId, body.sourceAlertId as string, { jobId: body.jobId as string, verdict: body.verdict as string, syntheticScore: body.syntheticScore as number, confidence: body.confidence as number, matchedEnrollmentId: body.matchedEnrollmentId as | string | undefined, matchedSimilarity: body.matchedSimilarity as number | undefined, analysisType: body.analysisType as string | undefined, } ); return reply.code(201).send(alert); } ); fastify.post( "/ingest/call-analysis", { schema: { body: { type: "object", properties: { sourceAlertId: { type: "string" }, callId: { type: "string" }, eventType: { type: "string", maxLength: 100 }, mosScore: { type: "number", minimum: 1, maximum: 5 }, anomaly: { type: "string", maxLength: 500 }, sentiment: { type: "object", properties: { label: { type: "string", maxLength: 50 }, score: { type: "number", minimum: 0, maximum: 1 }, }, }, }, required: ["sourceAlertId", "callId"], additionalProperties: false, }, }, }, async (request, reply) => { const userId = getUserId(request); if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const body = request.body as Record; const alert = await correlationService.ingestCallAnalysisAlert( userId, body.sourceAlertId as string, { callId: body.callId as string, eventType: body.eventType as string | undefined, mosScore: body.mosScore as number | undefined, anomaly: body.anomaly as string | undefined, sentiment: body.sentiment as | { label: string; score: number } | undefined, } ); return reply.code(201).send(alert); } ); }