配信の検証
`verifyWebhookSignature`: 一定時間での比較、リプレイ許容ウィンドウ、そしてパース済みイベントの返却。
リクエストハンドラーでの使い方
Webhook の URL は公開されている。インターネット上の誰でも、正しい形の JSON をそこへ POST できる。したがって、署名を検証せずに payload.type を読むハンドラーは、誰でも書き込める API にほかならない。
import { verifyWebhookSignature } from '@openemail/sdk' export async function POST(request: Request) { try { const event = await verifyWebhookSignature({ payload: await request.text(), headers: request.headers, secret: process.env.OPENEMAIL_WEBHOOK_SECRET!, toleranceSeconds: 300, }) console.log(event.type, event.data) } catch { return new Response('bad signature', { status: 400 }) } return new Response(null, { status: 204 })}ボディは生のまま渡すこと。パースして再度シリアライズするとキーの順序や空白が変わり、署名が一致しなくなる。headers は Headers オブジェクト、または Node の req.headers のような素のオブジェクトを受け取り、X-OpenEmail-Signature は大文字小文字にかかわらず見つけられる。
自前の検証が取りこぼしがちな 2 点をこの関数は処理する。1 つは MAC を一定時間で比較することで、処理時間から正しい先頭部分を割り出せないようにする。もう 1 つは、前後いずれの方向でも toleranceSeconds(指定しなければ 5 分)を超えて古い配信を拒否することで、傍受されたリクエストが永久に再送可能にならないようにする。toleranceSeconds: 0 にするとリプレイ検査は無効になる。どちらの不具合も表に出ない。いずれかを抱えたハンドラーでも、思いつくかぎりのテストはすべて通ってしまう。
失敗した場合は必ず例外を送出する。X-OpenEmail-Signature ヘッダーがない、形式が t=<seconds>,v1=<hex> になっていない、タイムスタンプが許容範囲外である、署名が一致しない、のいずれの場合も同様である。成功した場合は、ボディを WebhookPayload としてパースした結果に解決するため、間違えやすい 2 度目の JSON.parse は不要である。data に型を付けるには、EmailOpenedData のような型引数を渡すこと。
globalThis.crypto.subtle を必要とするが、これは Node 20+、Bun、Deno、Cloudflare Workers のいずれも提供している。