Files
Kordant/packages/db/prisma/schema.prisma
Michael Freno 7410813f4e Fix review findings for info broker removal service FRE-5402
P0 fixes:
- Add CANCELLED status to RemovalStatus enum (types + Prisma schema)
- Use CANCELLED instead of REJECTED for user-initiated cancellations
- Add null guard for req.broker?.name in GET /request/:id
- Remove unsafe 'as any' casts in RemoveBrokersService.ts
- Add type-safe toPersonalInfo() validator for JSON deserialization
- Type RemovalRequestWithBroker properly in getRemovalStatus()
- Fix alert: any to NormalizedAlertInput in BrokerAlertPipeline

P1 fixes:
- Fix admin role check: remove non-existent 'admin', only check 'support'
- Fix BrokerDefinition.category type from string to BrokerCategory
- Add complete OpenAPI spec for all removebrokers routes and schemas
2026-05-17 02:30:00 -04:00

919 lines
22 KiB
Plaintext

// Prisma schema for ShieldAI
// All models for the multi-service SaaS platform
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================
// User & Authentication Models
// ============================================
model User {
id String @id @default(uuid())
email String @unique
emailVerified DateTime?
name String?
image String?
role UserRole @default(user)
// Relationships
accounts Account[]
sessions Session[]
deviceTokens DeviceToken[]
familyGroups FamilyGroupMember[]
familyGroupOwned FamilyGroup[] @relation("FamilyGroupOwner")
subscriptions Subscription[]
alerts Alert[]
voiceEnrollments VoiceEnrollment[]
voiceAnalyses VoiceAnalysis[]
spamFeedback SpamFeedback[]
spamRules SpamRule[]
normalizedAlerts NormalizedAlert[]
correlationGroups CorrelationGroup[]
securityReports SecurityReport[]
analysisJobs AnalysisJob[]
// Audit
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@index([role])
}
enum UserRole {
user
family_admin
family_member
support
}
enum DetectionVerdict {
NATURAL
SYNTHETIC
UNCERTAIN
}
enum AnalysisType {
SYNTHETIC_DETECTION
VOICE_MATCH
BATCH
}
enum AnalysisJobStatus {
PENDING
RUNNING
COMPLETED
FAILED
}
model Account {
id String @id @default(uuid())
userId String
provider String
providerAccountId String
access_token String?
refresh_token String?
expires_at Int?
token_type String?
scope String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, provider, providerAccountId])
@@index([userId])
}
model Session {
id String @id @default(uuid())
userId String
sessionToken String @unique
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sessionToken])
@@index([userId])
}
model DeviceToken {
id String @id @default(uuid())
userId String
deviceType DeviceType
token String @unique
platform Platform
appName String?
appVersion String?
osVersion String?
model String?
isActive Boolean @default(true)
lastUsedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([deviceType])
@@index([platform])
@@index([isActive])
}
enum DeviceType {
mobile
web
desktop
}
enum Platform {
ios
android
web
}
// ============================================
// Family & Subscription Models
// ============================================
model FamilyGroup {
id String @id @default(uuid())
name String
ownerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation("FamilyGroupOwner", fields: [ownerId], references: [id])
members FamilyGroupMember[]
subscriptions Subscription[]
@@index([ownerId])
@@index([name])
}
model FamilyGroupMember {
id String @id @default(uuid())
groupId String
userId String
role FamilyMemberRole @default(member)
joinedAt DateTime @default(now())
group FamilyGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([groupId, userId])
@@index([groupId])
@@index([userId])
}
enum FamilyMemberRole {
owner
admin
member
}
model Subscription {
id String @id @default(uuid())
userId String
familyGroupId String?
stripeId String? @unique
tier SubscriptionTier @default(basic)
status SubscriptionStatus @default(active)
currentPeriodStart DateTime
currentPeriodEnd DateTime
cancelAtPeriodEnd Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
familyGroup FamilyGroup? @relation(fields: [familyGroupId], references: [id])
watchlistItems WatchlistItem[]
exposures Exposure[]
alerts Alert[]
propertyWatchlistItems PropertyWatchlistItem[]
removalRequests RemovalRequest[]
brokerListings BrokerListing[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([familyGroupId])
@@index([stripeId])
@@index([tier])
}
enum SubscriptionTier {
basic
plus
premium
}
enum SubscriptionStatus {
active
past_due
canceled
unpaid
trialing
}
// ============================================
// DarkWatch Models (Dark Web Monitoring)
// ============================================
model WatchlistItem {
id String @id @default(uuid())
subscriptionId String
type WatchlistType
value String
hash String // SHA-256 hash for deduplication
isActive Boolean @default(true)
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
exposures Exposure[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([subscriptionId, type, hash])
@@index([subscriptionId])
@@index([type])
@@index([hash])
}
enum WatchlistType {
email
phoneNumber
ssn
address
domain
}
model Exposure {
id String @id @default(uuid())
subscriptionId String
watchlistItemId String?
source ExposureSource
dataType WatchlistType
identifier String
identifierHash String
severity ExposureSeverity @default(info)
metadata Json? // Additional source-specific data
isFirstTime Boolean @default(false)
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
watchlistItem WatchlistItem? @relation(fields: [watchlistItemId], references: [id])
alerts Alert[]
detectedAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([subscriptionId])
@@index([watchlistItemId])
@@index([source])
@@index([severity])
@@index([detectedAt])
}
enum ExposureSource {
hibp // Have I Been Pwned
securityTrails
censys
darkWebForum
shodan
honeypot
}
enum ExposureSeverity {
info
warning
critical
}
// ============================================
// Notification & Alert Models
// ============================================
model Alert {
id String @id @default(uuid())
subscriptionId String
userId String
exposureId String?
type AlertType
title String
message String
severity AlertSeverity @default(info)
isRead Boolean @default(false)
readAt DateTime?
channel AlertChannel[] // Array of notification channels
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
exposure Exposure? @relation(fields: [exposureId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([subscriptionId])
@@index([userId])
@@index([isRead])
@@index([createdAt])
}
enum AlertType {
exposure_detected
exposure_resolved
scan_complete
subscription_changed
system_warning
}
enum AlertSeverity {
info
warning
critical
}
enum AlertChannel {
email
push
sms
}
// ============================================
// VoicePrint Models (Voice Cloning Detection)
// ============================================
model VoiceEnrollment {
id String @id @default(uuid())
userId String
name String
voiceHash String // FAISS embedding hash
audioMetadata Json? // Sample rate, duration, etc.
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
analyses VoiceAnalysis[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([voiceHash])
}
model VoiceAnalysis {
id String @id @default(uuid())
enrollmentId String?
userId String
audioHash String // Content hash of audio file
isSynthetic Boolean
confidence Float // 0.0 to 1.0
analysisResult Json // Full ML analysis results
audioUrl String // S3 storage URL
enrollment VoiceEnrollment? @relation(fields: [enrollmentId], references: [id])
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
@@index([userId])
@@index([enrollmentId])
@@index([audioHash])
}
model AnalysisJob {
id String @id @default(uuid())
userId String
analysisType AnalysisType
audioFilePath String
status AnalysisJobStatus
errorMessage String?
completedAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
result AnalysisResult?
@@index([userId])
@@index([status])
@@index([createdAt])
}
model AnalysisResult {
id String @id @default(uuid())
analysisJobId String @unique
syntheticScore Float
verdict DetectionVerdict
confidence Float
processingTimeMs Int
matchedEnrollmentId String?
matchedSimilarity Float?
modelVersion String?
analysisJob AnalysisJob @relation(fields: [analysisJobId], references: [id])
createdAt DateTime @default(now())
@@index([analysisJobId])
@@index([syntheticScore])
@@index([verdict])
}
// ============================================
// SpamShield Models (Spam Detection)
// ============================================
model SpamFeedback {
id String @id @default(uuid())
userId String
phoneNumber String
phoneNumberHash String // SHA-256 hash
isSpam Boolean
confidence Float? // ML model confidence
feedbackType FeedbackType
metadata Json? // Call duration, time, etc.
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([phoneNumberHash])
@@index([isSpam])
}
enum FeedbackType {
initial_detection
user_confirmation
user_rejection
auto_learned
}
model SpamRule {
id String @id @default(uuid())
userId String?
isGlobal Boolean @default(false)
ruleType RuleType
pattern String
action RuleAction
priority Int @default(0)
isActive Boolean @default(true)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([isGlobal])
@@index([ruleType])
}
enum RuleType {
phoneNumber
areaCode
prefix
pattern
reputation
}
enum RuleAction {
block
flag
allow
challenge
}
// ============================================
// Audit & Analytics Models
// ============================================
model AuditLog {
id String @id @default(uuid())
userId String?
action String
resource String
resourceId String?
changes Json? // Before/after values
metadata Json?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([userId])
@@index([action])
@@index([resource])
@@index([createdAt])
}
model KPISnapshot {
id String @id @default(uuid())
date DateTime @unique
metricName String
metricValue Float
metadata Json?
createdAt DateTime @default(now())
@@index([metricName])
@@index([date])
}
// ============================================
// Cross-Service Alert Correlation Models
// ============================================
enum AlertSource {
DARKWATCH
SPAMSHIELD
VOICEPRINT
CALL_ANALYSIS
HOME_TITLE
INFO_BROKER
}
enum AlertCategory {
BREACH_EXPOSURE
SPAM_CALL
SPAM_SMS
SYNTHETIC_VOICE
VOICE_MISMATCH
CALL_ANOMALY
CALL_QUALITY
CALL_EVENT
HOME_TITLE
INFO_BROKER_LISTING
INFO_BROKER_REMOVAL
}
enum NormalizedAlertSeverity {
LOW
INFO
MEDIUM
WARNING
HIGH
CRITICAL
}
enum CorrelationStatus {
ACTIVE
RESOLVED
FALSE_POSITIVE
}
model NormalizedAlert {
id String @id @default(uuid())
source AlertSource
category AlertCategory
severity NormalizedAlertSeverity
userId String
title String
description String
entities Json
sourceAlertId String
groupId String?
payload Json?
createdAt DateTime
updatedAt DateTime @default(now()) @updatedAt
correlationGroup CorrelationGroup? @relation(fields: [groupId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([sourceAlertId])
@@index([userId])
@@index([groupId])
@@index([source])
@@index([severity])
@@index([createdAt])
@@index([userId, createdAt])
}
model CorrelationGroup {
id String @id @default(uuid())
userId String
entities Json
highestSeverity NormalizedAlertSeverity
status CorrelationStatus @default(ACTIVE)
alertCount Int @default(0)
summary String?
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
alerts NormalizedAlert[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([status])
@@index([userId, status])
@@index([createdAt])
}
// ============================================
// Report Generation Models
// ============================================
enum ReportType {
MONTHLY_PLUS
ANNUAL_PREMIUM
WEEKLY_DIGEST
}
enum ReportStatus {
PENDING
GENERATING
COMPLETED
FAILED
DELIVERED
}
model SecurityReport {
id String @id @default(uuid())
userId String
subscriptionId String
reportType ReportType
status ReportStatus @default(PENDING)
periodStart DateTime
periodEnd DateTime
title String
summary String?
htmlContent String?
pdfUrl String?
dataPayload Json?
error String?
scheduledFor DateTime?
deliveredAt DateTime?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@index([userId])
@@index([subscriptionId])
@@index([reportType])
@@index([status])
@@index([periodStart, periodEnd])
@@index([createdAt])
}
// ============================================
// Waitlist & Marketing Models
// ============================================
model WaitlistEntry {
id String @id @default(uuid())
email String
name String?
source String? // landing_page, blog, referral, social, paid_search
tier SubscriptionTier? // interest level
utmSource String?
utmMedium String?
utmCampaign String?
metadata Json? // Browser, device, location, etc.
// Conversion tracking
convertedAt DateTime?
convertedToUserId String?
convertedToSubscriptionId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@index([source])
@@index([createdAt])
}
model BlogPost {
id String @id @default(uuid())
slug String @unique
title String
excerpt String?
content String
authorName String?
coverImageUrl String?
tags String[] // Array of tag strings
published Boolean @default(false)
publishedAt DateTime?
viewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([slug])
@@index([published, publishedAt])
@@index([tags])
}
// ============================================
// Home Title Service Models
// ============================================
enum PropertyChangeType {
tax_change
deed_change
ownership_transfer
lien_filing
metadata_change
}
enum PropertyChangeSeverity {
info
warning
critical
}
model PropertyWatchlistItem {
id String @id @default(uuid())
subscriptionId String
address String
parcelId String?
ownerName String?
streetAddress String
city String? @default("")
state String? @default("")
zipCode String? @default("")
latitude Float?
longitude Float?
isActive Boolean @default(true)
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
snapshots PropertySnapshot[]
changes PropertyChange[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([subscriptionId, parcelId])
@@index([subscriptionId])
@@index([parcelId])
@@index([address])
}
model PropertySnapshot {
id String @id @default(uuid())
propertyWatchlistItemId String
subscriptionId String
capturedAt DateTime
ownerName String
address Json // { streetNumber, streetName, streetType, unit, city, state, zip, latitude?, longitude? }
deedDate String?
taxId String?
propertyType String @default("residential")
taxAmount Float?
lienCount Int @default(0)
propertyWatchlistItem PropertyWatchlistItem @relation(fields: [propertyWatchlistItemId], references: [id], onDelete: Cascade)
changes PropertyChange[] @relation("SnapshotChanges")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([propertyWatchlistItemId])
@@index([subscriptionId])
@@index([capturedAt])
}
model PropertyChange {
id String @id @default(uuid())
propertyWatchlistItemId String
snapshotId String?
changeType PropertyChangeType
severity PropertyChangeSeverity @default(info)
details Json?
detectedAt DateTime @default(now())
propertyWatchlistItem PropertyWatchlistItem @relation(fields: [propertyWatchlistItemId], references: [id], onDelete: Cascade)
snapshot PropertySnapshot? @relation("SnapshotChanges", fields: [snapshotId], references: [id])
@@index([propertyWatchlistItemId])
@@index([snapshotId])
@@index([changeType])
}
// ============================================
// Info Broker Removal Models
// ============================================
enum BrokerCategory {
PEOPLE_SEARCH
BACKGROUND_CHECK
PUBLIC_RECORDS
REVERSE_LOOKUP
SOCIAL_MEDIA
}
enum RemovalMethod {
AUTOMATED
MANUAL_FORM
EMAIL
PHONE
MAIL
NONE
}
enum RemovalStatus {
PENDING
SUBMITTED
IN_PROGRESS
COMPLETED
FAILED
REJECTED
CANCELLED
}
model InfoBroker {
id String @id @default(uuid())
name String
domain String @unique
category BrokerCategory
removalMethod RemovalMethod
removalUrl String?
requiresAccount Boolean @default(false)
requiresVerification Boolean @default(false)
estimatedDays Int @default(14)
isActive Boolean @default(true)
removalRequests RemovalRequest[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@index([category])
@@index([isActive])
@@index([removalMethod])
}
model RemovalRequest {
id String @id @default(uuid())
subscriptionId String
brokerId String
status RemovalStatus @default(PENDING)
personalInfo Json // { fullName, email?, phone?, address?, dob? }
method RemovalMethod
attempts Int @default(0)
nextRetryAt DateTime?
submittedAt DateTime?
completedAt DateTime?
error String?
notes String?
metadata Json? // Broker response data, tracking info
broker InfoBroker @relation(fields: [brokerId], references: [id])
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
brokerListings BrokerListing[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@index([subscriptionId])
@@index([brokerId])
@@index([status])
@@index([submittedAt])
@@index([subscriptionId, status])
}
model BrokerListing {
id String @id @default(uuid())
subscriptionId String
brokerId String
removalRequestId String?
url String
dataFound Json // Fields found on the listing
screenshotUrl String?
isRemoved Boolean @default(false)
removedAt DateTime?
removalRequest RemovalRequest? @relation(fields: [removalRequestId], references: [id])
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
scannedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@index([subscriptionId])
@@index([brokerId])
@@index([removalRequestId])
@@index([isRemoved])
@@index([subscriptionId, isRemoved])
}