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. * Fails closed: returns false if secret is missing or signature doesn't match. */ 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) if (authBuffer.length !== secretBuffer.length) { return false } return crypto.timingSafeEqual(authBuffer, secretBuffer) } 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