FRE-4471: Scaffold DarkWatch MVP — monorepo, schema, services, API routes, tests

- Turborepo monorepo structure (packages: api, db, types, jobs; services: darkwatch)
- Prisma schema: User, WatchListItem, Exposure, Alert, ScanJob models
- WatchListService: CRUD with normalization, dedup, tier-based limits
- HIBPService: API integration with severity scoring
- MatchingEngine: exact-match with content hash dedup
- AlertPipeline: dedup window, email notifications
- ScanService: orchestrates watch list -> HIBP -> match -> alert flow
- BullMQ job workers for scan and alert processing
- Fastify API routes: watchlist, exposures, alerts, scan
- Docker Compose: PostgreSQL 16 + Redis 7
- 15 unit tests passing
- Implementation plan document uploaded
This commit is contained in:
Senior Engineer
2026-04-29 09:47:45 -04:00
committed by Michael Freno
parent f8f90502fa
commit 218de3b03b
40 changed files with 5225 additions and 0 deletions

21
packages/api/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "@shieldai/api",
"version": "0.1.0",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "vitest run",
"lint": "eslint src/"
},
"dependencies": {
"@fastify/cors": "^10.0.1",
"@fastify/helmet": "^13.0.1",
"@fastify/rate-limit": "^9.0.0",
"@fastify/sensible": "^6.0.1",
"@shieldai/db": "0.1.0",
"@shieldai/types": "0.1.0",
"fastify": "^5.2.0",
"@shieldai/darkwatch": "0.1.0"
}
}

View File

@@ -0,0 +1,30 @@
import { FastifyInstance } from "fastify";
import { AlertPipeline } from "@shieldai/darkwatch";
export function alertRoutes(fastify: FastifyInstance) {
const pipeline = new AlertPipeline();
fastify.get("/", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const limit = parseInt(request.query.limit as string) || 50;
const offset = parseInt(request.query.offset as string) || 0;
const alerts = await pipeline.getUserAlerts(userId, limit, offset);
return reply.send(alerts);
});
fastify.patch("/:id/read", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
await pipeline.markRead(request.params.id, userId);
return reply.send({ read: true });
});
}

View File

@@ -0,0 +1,27 @@
import { FastifyInstance } from "fastify";
import { MatchingEngine } from "@shieldai/darkwatch";
export function exposureRoutes(fastify: FastifyInstance) {
const engine = new MatchingEngine();
fastify.get("/", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const exposures = await engine.getExposuresForUser(userId);
return reply.send(exposures);
});
fastify.get("/:id", async (request, reply) => {
const exposure = await engine.getExposureById(request.params.id);
if (!exposure) {
return reply.code(404).send({ error: "Exposure not found" });
}
return reply.send(exposure);
});
}

View File

@@ -0,0 +1,15 @@
import { FastifyInstance } from "fastify";
export function darkwatchRoutes(fastify: FastifyInstance) {
fastify.register(async (root) => {
const watchlist = (await import("./watchlist.routes")).watchlistRoutes;
const exposures = (await import("./exposure.routes")).exposureRoutes;
const alerts = (await import("./alert.routes")).alertRoutes;
const scans = (await import("./scan.routes")).scanRoutes;
root.register(watchlist, { prefix: "/watchlist" });
root.register(exposures, { prefix: "/exposures" });
root.register(alerts, { prefix: "/alerts" });
root.register(scans, { prefix: "/scan" });
}, { prefix: "/api/v1/darkwatch" });
}

View File

@@ -0,0 +1,32 @@
import { FastifyInstance } from "fastify";
import { ScanService } from "@shieldai/darkwatch";
import { DataSource } from "@shieldai/types";
export function scanRoutes(fastify: FastifyInstance) {
const scanService = new ScanService();
fastify.post("/", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const body = request.body as { source?: string };
const source = body.source ? (body.source as DataSource) : undefined;
const resultCount = await scanService.runScan(userId, source);
return reply.code(200).send({ scanned: true, resultCount });
});
fastify.get("/history", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const history = await scanService.getScanHistory(userId);
return reply.send(history);
});
}

View File

@@ -0,0 +1,41 @@
import { FastifyInstance } from "fastify";
import { WatchListService } from "@shieldai/darkwatch";
import { IdentifierType } from "@shieldai/types";
export function watchlistRoutes(fastify: FastifyInstance) {
const service = new WatchListService();
fastify.post("/", async (request, reply) => {
const body = request.body as { identifierType: string; identifierValue: string };
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const item = await service.addItem(userId, body.identifierType as IdentifierType, body.identifierValue);
return reply.code(201).send(item);
});
fastify.get("/", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const items = await service.listItems(userId);
return reply.send(items);
});
fastify.delete("/:id", async (request, reply) => {
const userId = (request.user as { id: string })?.id;
if (!userId) {
return reply.code(401).send({ error: "User not authenticated" });
}
const result = await service.removeItem(userId, request.params.id);
return reply.send({ count: result.count });
});
}

View File

@@ -0,0 +1,31 @@
import Fastify from "fastify";
import cors from "@fastify/cors";
import helmet from "@fastify/helmet";
import sensible from "@fastify/sensible";
import { darkwatchRoutes } from "./routes";
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL || "info",
},
});
async function bootstrap() {
await app.register(cors, { origin: true });
await app.register(helmet);
await app.register(sensible);
await app.register(darkwatchRoutes);
app.get("/health", async () => ({ status: "ok", timestamp: new Date().toISOString() }));
try {
await app.listen({ port: parseInt(process.env.PORT || "3000", 10), host: "0.0.0.0" });
app.log.info(`Server listening on port ${process.env.PORT || 3000}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
}
bootstrap();

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}