import { createHmac, timingSafeEqual } from 'node:crypto'; export function verifyWebhook(rawBody, signature, secrets, now = Date.now()) { if (!Buffer.isBuffer(rawBody) || rawBody.length > 65536) throw new Error('Invalid body.'); const match = /^t=(\d{1,12}),v1=([a-f0-9]{64})$/.exec(signature || ''); if (!match || Math.abs(now / 1000 - Number(match[1])) > 300) { throw new Error('Invalid or expired signature.'); } const provided = Buffer.from(match[2], 'hex'); const keys = Array.isArray(secrets) ? secrets : [secrets]; const valid = keys.filter(key => typeof key === 'string' && key.startsWith('whsec_')).some(key => { const expected = createHmac('sha256', key) .update(match[1] + '.').update(rawBody).digest(); return timingSafeEqual(provided, expected); }); if (!valid) throw new Error('Invalid signature.'); const event = JSON.parse(rawBody.toString('utf8')); const statuses = ['verified', 'expired', 'cancelled']; if (!event || typeof event.id !== 'string' || !event.id.startsWith('evt_') || !event.data || !statuses.includes(event.data.status) || event.type !== 'verification.' + event.data.status) { throw new Error('Invalid event.'); } // The receiver must still compare Veri-Event-ID to event.id, bind event.data // to its stored attempt, check live/mode/state/reference, and deduplicate in DB. return event; }