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,94 @@
import { z } from 'zod';
export const SubscriptionTier = {
FREE: 'free',
BASIC: 'basic',
PLUS: 'plus',
PREMIUM: 'premium',
} as const;
export type SubscriptionTier = typeof SubscriptionTier[keyof typeof SubscriptionTier];
export const BillingConfigSchema = z.object({
stripe: z.object({
apiKey: z.string().min(1, 'STRIPE_API_KEY required'),
webhookSecret: z.string().min(1, 'STRIPE_WEBHOOK_SECRET required'),
pricingTableId: z.string().optional(),
}),
tiers: z.object({
free: z.object({
priceId: z.string(),
monthlyPriceCents: z.number().default(0),
callMinutesLimit: z.number().default(100),
smsCountLimit: z.number().default(500),
darkWebScans: z.number().default(1),
}),
basic: z.object({
priceId: z.string(),
monthlyPriceCents: z.number().default(999),
callMinutesLimit: z.number().default(500),
smsCountLimit: z.number().default(2000),
darkWebScans: z.number().default(12),
}),
plus: z.object({
priceId: z.string(),
monthlyPriceCents: z.number().default(1999),
callMinutesLimit: z.number().default(2000),
smsCountLimit: z.number().default(10000),
darkWebScans: z.number().default(12),
voiceCloning: z.boolean().default(true),
}),
premium: z.object({
priceId: z.string(),
monthlyPriceCents: z.number().default(4999),
callMinutesLimit: z.number().default(10000),
smsCountLimit: z.number().default(50000),
darkWebScans: z.number().default(12),
voiceCloning: z.boolean().default(true),
homeTitleMonitor: z.boolean().default(true),
}),
}),
});
export type BillingConfig = z.infer<typeof BillingConfigSchema>;
export const loadBillingConfig = (): BillingConfig => ({
stripe: {
apiKey: process.env.STRIPE_API_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
pricingTableId: process.env.STRIPE_PRICING_TABLE_ID,
},
tiers: {
free: {
priceId: process.env.STRIPE_FREE_TIER_PRICE_ID || 'price_free',
monthlyPriceCents: 0,
callMinutesLimit: 100,
smsCountLimit: 500,
darkWebScans: 1,
},
basic: {
priceId: process.env.STRIPE_BASIC_TIER_PRICE_ID || 'price_basic',
monthlyPriceCents: 999,
callMinutesLimit: 500,
smsCountLimit: 2000,
darkWebScans: 12,
},
plus: {
priceId: process.env.STRIPE_PLUS_TIER_PRICE_ID!,
monthlyPriceCents: 1999,
callMinutesLimit: 2000,
smsCountLimit: 10000,
darkWebScans: 12,
voiceCloning: true,
},
premium: {
priceId: process.env.STRIPE_PREMIUM_TIER_PRICE_ID!,
monthlyPriceCents: 4999,
callMinutesLimit: 10000,
smsCountLimit: 50000,
darkWebScans: 12,
voiceCloning: true,
homeTitleMonitor: true,
},
},
});