Files
AriWeb-Suite/modules/arimail/server/webhook.mjs
T

25 lines
1.6 KiB
JavaScript
Raw Normal View History

/**
* Resend 웹훅 서명 검증(Svix 방식). 서명 대상은 "{svix-id}.{svix-timestamp}.{원문 본문}" 이고
* 비밀키는 "whsec_" 뒤의 base64 를 풀어 HMAC-SHA256 키로 씁니다. 헤더에는 "v1,<base64>" 가 공백으로 여러 개 올 수 있습니다.
*/
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');
}