- ScanScheduler: tier-based scheduling (BASIC=24h, PLUS=6h, PREMIUM=1h) - WebhookHandler: HMAC-verified webhook ingestion with SCAN_TRIGGER support - API routes: /scheduler and /webhooks endpoints under /api/v1/darkwatch - Jobs: scheduled scan checker + webhook retry processor via BullMQ - Schema: ScanSchedule, WebhookEvent models; ScanJob.scheduledBy field - Types: ScheduleStatus, WebhookEventType, WebhookTriggerInput - Tests: scheduler lifecycle + webhook signature/processing tests Co-Authored-By: Paperclip <noreply@paperclip.ing>
335 lines
8.2 KiB
Plaintext
335 lines
8.2 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum SubscriptionTier {
|
|
BASIC
|
|
PLUS
|
|
PREMIUM
|
|
}
|
|
|
|
enum IdentifierType {
|
|
EMAIL
|
|
PHONE
|
|
SSN
|
|
}
|
|
|
|
enum WatchListStatus {
|
|
ACTIVE
|
|
PAUSED
|
|
}
|
|
|
|
enum Severity {
|
|
INFO
|
|
WARNING
|
|
CRITICAL
|
|
}
|
|
|
|
enum AlertChannel {
|
|
EMAIL
|
|
PUSH
|
|
SMS
|
|
}
|
|
|
|
enum AlertStatus {
|
|
PENDING
|
|
SENT
|
|
READ
|
|
}
|
|
|
|
enum ScanJobStatus {
|
|
PENDING
|
|
RUNNING
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
enum DataSource {
|
|
HIBP
|
|
SECURITY_TRAILS
|
|
CENSYS
|
|
SHODAN
|
|
HONEYPOT
|
|
}
|
|
|
|
enum AnalysisJobStatus {
|
|
PENDING
|
|
RUNNING
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
enum AnalysisType {
|
|
SYNTHETIC_DETECTION
|
|
VOICE_MATCH
|
|
BATCH
|
|
}
|
|
|
|
enum DetectionVerdict {
|
|
NATURAL
|
|
SYNTHETIC
|
|
UNCERTAIN
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
email String @unique
|
|
name String?
|
|
subscriptionTier SubscriptionTier @default(BASIC)
|
|
familyGroupId String?
|
|
watchListItems WatchListItem[]
|
|
alerts Alert[]
|
|
scanJobs ScanJob[]
|
|
scanSchedules ScanSchedule[]
|
|
voiceEnrollments VoiceEnrollment[]
|
|
analysisJobs AnalysisJob[]
|
|
spamFeedback SpamFeedback[]
|
|
spamCallAnalyses SpamCallAnalysis[]
|
|
spamAuditLogs SpamAuditLog[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([email])
|
|
}
|
|
|
|
model WatchListItem {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
identifierType IdentifierType
|
|
identifierValue String
|
|
identifierHash String @unique
|
|
status WatchListStatus @default(ACTIVE)
|
|
exposures Exposure[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([identifierHash])
|
|
}
|
|
|
|
model Exposure {
|
|
id String @id @default(uuid())
|
|
watchListItemId String
|
|
watchListItem WatchListItem @relation(fields: [watchListItemId], references: [id], onDelete: Cascade)
|
|
dataSource DataSource
|
|
breachName String
|
|
exposedAt DateTime
|
|
dataType String[]
|
|
severity Severity
|
|
details String?
|
|
contentHash String @unique
|
|
alert Alert?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([watchListItemId])
|
|
@@index([contentHash])
|
|
@@index([dataSource])
|
|
}
|
|
|
|
model Alert {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
exposureId String @unique
|
|
exposure Exposure @relation(fields: [exposureId], references: [id], onDelete: Cascade)
|
|
severity Severity
|
|
channel AlertChannel
|
|
status AlertStatus @default(PENDING)
|
|
dedupKey String
|
|
sentAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId, status])
|
|
@@index([dedupKey])
|
|
}
|
|
|
|
model ScanJob {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
status ScanJobStatus @default(PENDING)
|
|
source DataSource?
|
|
resultCount Int @default(0)
|
|
errorMessage String?
|
|
scheduledBy String?
|
|
webhookEvents WebhookEvent[]
|
|
completedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId, status])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
enum ScheduleStatus {
|
|
ACTIVE
|
|
PAUSED
|
|
}
|
|
|
|
model ScanSchedule {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
intervalMinutes Int // minutes between scans
|
|
cronExpression String // cron expression for scheduling
|
|
status ScheduleStatus @default(ACTIVE)
|
|
lastScanAt DateTime?
|
|
nextScanAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([userId])
|
|
@@index([status])
|
|
}
|
|
|
|
enum WebhookEventType {
|
|
SCAN_TRIGGER
|
|
BREACH_DETECTED
|
|
SUBSCRIPTION_CHANGE
|
|
}
|
|
|
|
model WebhookEvent {
|
|
id String @id @default(uuid())
|
|
eventType WebhookEventType
|
|
payload String
|
|
source String?
|
|
signature String?
|
|
processed Boolean @default(false)
|
|
processedAt DateTime?
|
|
scanJobId String?
|
|
scanJob ScanJob? @relation(fields: [scanJobId], references: [id], onDelete: SetNull)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([eventType, processed])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model VoiceEnrollment {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
label String
|
|
embeddingVector Float[]
|
|
embeddingDim Int @default(192)
|
|
audioFilePath String?
|
|
sampleRate Int @default(16000)
|
|
durationSec Float?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([embeddingDim])
|
|
}
|
|
|
|
model AnalysisJob {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
analysisType AnalysisType
|
|
audioFilePath String
|
|
status AnalysisJobStatus @default(PENDING)
|
|
result AnalysisResult?
|
|
errorMessage String?
|
|
completedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId, status])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model AnalysisResult {
|
|
id String @id @default(uuid())
|
|
analysisJobId String @unique
|
|
analysisJob AnalysisJob @relation(fields: [analysisJobId], references: [id], onDelete: Cascade)
|
|
syntheticScore Float
|
|
verdict DetectionVerdict
|
|
matchedEnrollmentId String?
|
|
matchedSimilarity Float?
|
|
confidence Float
|
|
processingTimeMs Int
|
|
modelVersion String?
|
|
metadata String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([analysisJobId])
|
|
@@index([verdict])
|
|
}
|
|
|
|
enum SpamDecision {
|
|
BLOCK
|
|
FLAG
|
|
ALLOW
|
|
}
|
|
|
|
model SpamFeedback {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
phoneNumber String // AES-256 encrypted PII
|
|
phoneNumberHash String // SHA-256 hash for anonymized lookup
|
|
isSpam Boolean
|
|
label String?
|
|
metadata String? // Unbounded JSON
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([phoneNumberHash])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model SpamCallAnalysis {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
phoneNumber String
|
|
callTimestamp DateTime
|
|
hiyaReputationScore Float?
|
|
truecallerSpamScore Float?
|
|
decision SpamDecision
|
|
confidence Float
|
|
ruleMatches String[] // IDs of matched rules
|
|
auditLogs SpamAuditLog[]
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([phoneNumber])
|
|
@@index([callTimestamp])
|
|
}
|
|
|
|
model SpamRule {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
pattern String // Regex pattern - needs ReDoS validation
|
|
decision SpamDecision
|
|
description String?
|
|
isActive Boolean @default(true)
|
|
priority Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([isActive])
|
|
@@index([priority])
|
|
}
|
|
|
|
model SpamAuditLog {
|
|
id String @id @default(uuid())
|
|
analysisId String?
|
|
analysis SpamCallAnalysis? @relation(fields: [analysisId], references: [id], onDelete: SetNull)
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
phoneNumber String
|
|
decision SpamDecision
|
|
reason String
|
|
ruleId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([createdAt])
|
|
@@index([decision])
|
|
}
|