Add tier-based scan scheduler and webhook triggers (FRE-4498)

- 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>
This commit is contained in:
2026-04-30 10:57:56 -04:00
parent 76d431e1ec
commit 9fb5379b7a
43 changed files with 7819 additions and 93 deletions

View File

@@ -0,0 +1,59 @@
import { z } from 'zod';
export const NotificationConfigSchema = z.object({
resend: z.object({
apiKey: z.string().min(1, 'RESEND_API_KEY required'),
baseUrl: z.string().default('https://api.resend.com'),
}),
fcm: z.object({
privateKey: z.string().min(1, 'FCM_PRIVATE_KEY required'),
projectId: z.string().min(1, 'FCM_PROJECT_ID required'),
clientEmail: z.string().email(),
}),
apns: z.object({
key: z.string().min(1, 'APNS_KEY required'),
keyId: z.string().min(1, 'APNS_KEY_ID required'),
teamId: z.string().min(1, 'APNS_TEAM_ID required'),
bundleId: z.string().min(1, 'APNS_BUNDLE_ID required'),
}),
twilio: z.object({
accountSid: z.string().min(1, 'TWILIO_ACCOUNT_SID required'),
authToken: z.string().min(1, 'TWILIO_AUTH_TOKEN required'),
messagingServiceSid: z.string().min(1, 'TWILIO_MESSAGING_SERVICE_SID required'),
}),
rateLimits: z.object({
emailPerMinute: z.number().default(60),
smsPerMinute: z.number().default(30),
pushPerMinute: z.number().default(100),
}),
});
export type NotificationConfig = z.infer<typeof NotificationConfigSchema>;
export const loadNotificationConfig = (): NotificationConfig => ({
resend: {
apiKey: process.env.RESEND_API_KEY!,
baseUrl: process.env.RESEND_BASE_URL || 'https://api.resend.com',
},
fcm: {
privateKey: process.env.FCM_PRIVATE_KEY!,
projectId: process.env.FCM_PROJECT_ID!,
clientEmail: process.env.FCM_CLIENT_EMAIL!,
},
apns: {
key: process.env.APNS_KEY!,
keyId: process.env.APNS_KEY_ID!,
teamId: process.env.APNS_TEAM_ID!,
bundleId: process.env.APNS_BUNDLE_ID!,
},
twilio: {
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID!,
},
rateLimits: {
emailPerMinute: parseInt(process.env.EMAIL_RATE_LIMIT || '60', 10),
smsPerMinute: parseInt(process.env.SMS_RATE_LIMIT || '30', 10),
pushPerMinute: parseInt(process.env.PUSH_RATE_LIMIT || '100', 10),
},
});