/** * Resend 웹훅 서명 검증(Svix 방식). 서명 대상은 "{svix-id}.{svix-timestamp}.{원문 본문}" 이고 * 비밀키는 "whsec_" 뒤의 base64 를 풀어 HMAC-SHA256 키로 씁니다. 헤더에는 "v1," 가 공백으로 여러 개 올 수 있습니다. */ import { createHmac, timingSafeEqual } from 'node:crypto'; export function verifySvix(secret, headers, rawBody, { toleranceSec = 300, nowMs = Date.now() } = {}) { const id = headers['svix-id'], ts = headers['svix-timestamp'], sig = headers['svix-signature']; if (!secret || !id || !ts || !sig) return { ok: false, error: 'missing_signature' }; const t = Number(ts); if (!Number.isFinite(t) || Math.abs(nowMs / 1000 - t) > toleranceSec) return { ok: false, error: 'timestamp_out_of_range' }; const key = Buffer.from(String(secret).replace(/^whsec_/, ''), 'base64'); const expected = createHmac('sha256', key).update(`${id}.${ts}.`).update(rawBody).digest(); for (const candidate of String(sig).split(/\s+/)) { const [version, value] = candidate.split(','); if (version !== 'v1' || !value) continue; const given = Buffer.from(value, 'base64'); if (given.length === expected.length && timingSafeEqual(given, expected)) return { ok: true }; } return { ok: false, error: 'bad_signature' }; } /** 테스트·문서용: 같은 규칙으로 서명을 만듭니다. */ export function signSvix(secret, id, timestamp, rawBody) { const key = Buffer.from(String(secret).replace(/^whsec_/, ''), 'base64'); return 'v1,' + createHmac('sha256', key).update(`${id}.${timestamp}.`).update(rawBody).digest('base64'); }