import { prisma, AlertType, AlertSeverity } from '@shieldai/db'; import { NotificationService, NotificationPriority, loadNotificationConfig, } from '@shieldsai/shared-notifications'; const ALERT_DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000; export class AlertPipeline { private notificationService: NotificationService; constructor() { this.notificationService = new NotificationService(loadNotificationConfig()); } async processNewExposures(exposureIds: string[]) { const exposures = await prisma.exposure.findMany({ where: { id: { in: exposureIds }, isFirstTime: true }, include: { subscription: { select: { id: true, userId: true, tier: true, }, }, watchlistItem: true, }, }); const alertsCreated: Awaited>[] = []; for (const exposure of exposures) { const dedupKey = `exposure:${exposure.subscriptionId}:${exposure.source}:${exposure.identifierHash}`; const recentAlert = await prisma.alert.findFirst({ where: { subscriptionId: exposure.subscriptionId, type: AlertType.exposure_detected, createdAt: { gte: new Date(Date.now() - ALERT_DEDUP_WINDOW_MS), }, }, orderBy: { createdAt: 'desc' }, }); if (recentAlert) { continue; } const alert = await prisma.alert.create({ data: { subscriptionId: exposure.subscriptionId, userId: exposure.subscription.userId, exposureId: exposure.id, type: AlertType.exposure_detected, title: this.buildTitle(exposure), message: this.buildMessage(exposure), severity: this.mapSeverity(exposure.severity), channel: this.getChannelsForTier(exposure.subscription.tier), }, }); alertsCreated.push(alert); await this.dispatchNotification(alert, exposure); } return alertsCreated; } async dispatchScanCompleteAlert( subscriptionId: string, userId: string, exposuresFound: number ) { const subscription = await prisma.subscription.findUnique({ where: { id: subscriptionId }, select: { tier: true }, }); if (!subscription) return; const alert = await prisma.alert.create({ data: { subscriptionId, userId, type: AlertType.scan_complete, title: 'DarkWatch Scan Complete', message: `Scan found ${exposuresFound} new exposure${exposuresFound === 1 ? '' : 's'}.`, severity: exposuresFound > 0 ? 'warning' : 'info', channel: this.getChannelsForTier(subscription.tier), }, }); await this.dispatchNotification(alert, { source: 'hibp', severity: 'info', identifier: '', dataType: 'email', } as any); return alert; } private async dispatchNotification( alert: { userId: string; channel: string[]; title: string; message: string; severity: AlertSeverity; }, exposure: { source: string; severity: string; identifier: string; dataType: string } ) { try { if (!this.notificationService.isFullyConfigured()) return; await this.notificationService.sendMultiChannelNotification( { userId: alert.userId, }, alert.channel as any, alert.title, `

${alert.message}

Source: ${exposure.source}

Severity: ${exposure.severity}

Type: ${exposure.dataType}

`, alert.severity === 'critical' ? NotificationPriority.HIGH : NotificationPriority.NORMAL ); } catch (error) { console.error('[AlertPipeline] Notification dispatch error:', error); } } private buildTitle(exposure: { source: string; dataType: string; severity: string; }): string { return `${exposure.severity.toUpperCase()}: ${exposure.dataType} exposure on ${exposure.source}`; } private buildMessage(exposure: { identifier: string; source: string; severity: string; dataType: string; }): string { const masked = exposure.identifier.includes('@') ? exposure.identifier.replace(/(?<=.{2}).*(?=@)/, '***') : exposure.identifier.slice(0, 3) + '***'; return `Your ${exposure.dataType} (${masked}) was found in a ${exposure.source} breach with ${exposure.severity} severity.`; } private mapSeverity(severity: string): AlertSeverity { return severity as AlertSeverity; } private getChannelsForTier(tier: string): string[] { const channelMap: Record = { basic: ['email'], plus: ['email', 'push'], premium: ['email', 'push', 'sms'], }; return channelMap[tier] || ['email']; } } export const alertPipeline = new AlertPipeline();