import { Router, Request, Response } from 'express' import { syncEntitlement } from '../services/entitlement' import { RevenueCatEvent } from '../types' import { getEnv } from '../config/env' import * as crypto from 'crypto' const router = Router() /** * Verifies RevenueCat webhook signature using constant-time comparison. * Assumes validateEnv() was called at startup - throws if secret is unexpectedly missing. */ function verifyRevenueCatSecret(req: Request): boolean { const secret = getEnv('REVENUECAT_WEBHOOK_SECRET') const auth = req.headers['authorization'] ?? '' // Constant-time comparison to prevent timing attacks const authBuffer = Buffer.from(auth) const secretBuffer = Buffer.from(secret) // Use timingSafeEqual with fixed buffer length to prevent length leakage const maxLength = Math.max(authBuffer.length, secretBuffer.length) const paddedAuth = Buffer.alloc(maxLength) const paddedSecret = Buffer.alloc(maxLength) authBuffer.copy(paddedAuth) secretBuffer.copy(paddedSecret) return crypto.timingSafeEqual(paddedAuth, paddedSecret) } router.post('/revenuecat', async (req: Request, res: Response) => { if (!verifyRevenueCatSecret(req)) { res.status(401).json({ error: 'unauthorized' }) return } const body = req.body as RevenueCatEvent if (!body?.event?.type || !body?.event?.app_user_id) { res.status(400).json({ error: 'malformed payload' }) return } // Acknowledge immediately — RC retries if we don't respond within 10s res.status(200).json({ received: true }) try { await syncEntitlement(body) } catch (err) { console.error('[webhook] entitlement sync failed:', err) } }) export default router