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; // Allowed return URL origins for Stripe customer portal (open redirect prevention) const allowedReturnUrlOrigins: string[] = (process.env.ALLOWED_RETURN_URL_ORIGINS || 'https://app.shieldai.com,shieldai://').split(',').map(s => s.trim()).filter(Boolean); export function isValidReturnUrl(url: string): boolean { try { const parsed = new URL(url); // Custom app schemes (e.g., shieldai://) have origin === 'null' // Use prefix matching for these since they don't have a standard origin if (parsed.origin === 'null') { return allowedReturnUrlOrigins.some(origin => url.startsWith(origin)); } // Standard web protocols: compare origins to prevent substring attacks return allowedReturnUrlOrigins.some(origin => { try { const originUrl = new URL(origin); return parsed.origin === originUrl.origin; } catch { // If the allowed origin can't be parsed as a URL, it's a custom scheme return false; } }); } catch { return false; } } export const loadBillingConfig = (): BillingConfig => { const rawConfig = { 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 || 'price_plus', monthlyPriceCents: 1999, callMinutesLimit: 2000, smsCountLimit: 10000, darkWebScans: 12, voiceCloning: true, }, premium: { priceId: process.env.STRIPE_PREMIUM_TIER_PRICE_ID || 'price_premium', monthlyPriceCents: 4999, callMinutesLimit: 10000, smsCountLimit: 50000, darkWebScans: 12, voiceCloning: true, homeTitleMonitor: true, }, }, }; return BillingConfigSchema.parse(rawConfig); };