import crypto from 'node:crypto' /** * Verify the HMAC-SHA256 signature Meta attaches to webhook POSTs. * * Meta signs the raw request body with your App Secret and sends the * result in the `x-hub-signature-256: sha256=` header. Without * verification, anyone who knows our webhook URL can POST fabricated * status updates and drift broadcast counts arbitrarily. * * Reference: * https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verify-payloads * * Rollout behavior: * If `META_APP_SECRET` is unset, we **fail open** with a loud warning * log. This avoids breaking existing deployments at the moment this * code ships — operators can set the env var on their next deploy. * Once the env var is set, verification is strict. */ export function verifyMetaWebhookSignature( rawBody: string, signatureHeader: string | null, ): boolean { const secret = process.env.META_APP_SECRET if (!secret) { console.warn( '[webhook] META_APP_SECRET not set — skipping signature verification. ' + 'Set the env var to harden against spoofed webhook POSTs.', ) return true } if (!signatureHeader) return false if (!signatureHeader.startsWith('sha256=')) return false const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex') const a = Buffer.from(signatureHeader) const b = Buffer.from(expected) // Bail if lengths differ — timingSafeEqual throws otherwise. if (a.length !== b.length) return false return crypto.timingSafeEqual(a, b) }