Проверка доставки
`verifyWebhookSignature`: сравнение за постоянное время, окно защиты от повторов и разобранное событие на выходе.
В обработчике запроса
URL вебхука публичен. Что угодно в интернете может отправить на него POST с JSON нужной формы, поэтому обработчик, который читает 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 или обычный объект, например req.headers из Node, а X-OpenEmail-Signature находится в любом регистре.
Две вещи, которые здесь учтены и которых обычно нет в самодельной проверке: MAC сравнивается за постоянное время, поэтому верный префикс нельзя восстановить по замерам времени, и доставка старше toleranceSeconds в любую сторону отклоняется — пять минут, если вы не указали иное, — чтобы перехваченный запрос нельзя было воспроизводить вечно. toleranceSeconds: 0 отключает проверку на повтор. Обе ошибки безмолвны. Обработчик с любой из них проходит все тесты, которые вам пришло бы в голову написать.
Он выбрасывает исключение при любом сбое: отсутствующий заголовок X-OpenEmail-Signature, заголовок не в форме t=<seconds>,v1=<hex>, отметка времени вне окна или несовпадающая подпись. При успехе он разрешается телом, разобранным как WebhookPayload, поэтому второго JSON.parse, в котором можно ошибиться, нет. Передайте аргумент типа, например EmailOpenedData, чтобы типизировать data.
Ему нужен globalThis.crypto.subtle, который предоставляют Node 20+, Bun, Deno и Cloudflare Workers.