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

View File

@@ -0,0 +1,104 @@
import prisma from "@shieldai/db";
import { ExposureResult, DataSource, Severity } from "@shieldai/types";
import { createHash } from "crypto";
export class MatchingEngine {
async matchExposure(
watchListItemId: string,
dataSource: DataSource,
breachName: string,
exposedAt: Date,
dataType: string[],
severity: Severity,
details?: string
): Promise<ExposureResult | null> {
const contentHash = this.computeContentHash(dataSource, breachName, watchListItemId);
const existing = await prisma.exposure.findUnique({
where: { contentHash },
});
if (existing) {
return null;
}
const exposure = await prisma.exposure.create({
data: {
watchListItemId,
dataSource,
breachName,
exposedAt,
dataType,
severity,
details,
contentHash,
},
});
return {
dataSource: exposure.dataSource,
breachName: exposure.breachName,
exposedAt: exposure.exposedAt,
dataType: exposure.dataType,
severity: exposure.severity,
details: exposure.details || "",
};
}
async getExposuresForUser(userId: string): Promise<ExposureResult[]> {
const items = await prisma.watchListItem.findMany({
where: { userId },
include: { exposures: true },
});
const results: ExposureResult[] = [];
for (const item of items) {
for (const exp of item.exposures) {
results.push({
dataSource: exp.dataSource,
breachName: exp.breachName,
exposedAt: exp.exposedAt,
dataType: exp.dataType,
severity: exp.severity,
details: exp.details || "",
});
}
}
return results.sort((a, b) => {
const severityOrder = { CRITICAL: 0, WARNING: 1, INFO: 2 };
return severityOrder[a.severity] - severityOrder[b.severity];
});
}
async getExposureById(exposureId: string): Promise<ExposureResult | null> {
const exposure = await prisma.exposure.findUnique({
where: { id: exposureId },
include: { watchListItem: true },
});
if (!exposure) return null;
return {
dataSource: exposure.dataSource,
breachName: exposure.breachName,
exposedAt: exposure.exposedAt,
dataType: exposure.dataType,
severity: exposure.severity,
details: exposure.details || "",
};
}
countExposures(userId: string): Promise<number> {
return prisma.exposure.count({
where: {
watchListItem: { userId },
},
});
}
computeContentHash(dataSource: DataSource, breachName: string, watchListItemId: string): string {
const raw = `${dataSource}:${breachName}:${watchListItemId}`;
return createHash("sha256").update(raw).digest("hex");
}
}