Перевірка підпису
Перевіряйте початкове тіло й envelope webhook-протоколу v2.
Збирайте точні Buffer-байти до JSON middleware, обмежуючи читання 256 KiB. Відхиляйте повторні заголовки підпису за початковим списком HTTP; лише потім приводьте назви до нижнього регістру. Обирайте секрет із довіреної карти за X-Key-Id; невідомий ID відхиляйте. До перевірки ID — лише підказка пошуку. Зберігайте старі нескомпрометовані секрети для наявних доставок. Синхронізуйте годинник сервера.
Обов’язкові заголовки: X-Webhook-Version, X-Event-Id, X-Delivery-Id, X-Event-Type, X-Key-Id, X-Timestamp, X-Attempt, X-Idempotency-Key, X-Signature-SHA256. Namespace підпису залишається буквально vaultless.webhook. Підписується JSON.stringify упорядкованого масиву нижче: версія — число 2, інші значення envelope — рядки.
Функція передбачає, що HTTP-обробник уже відхилив повторні заголовки, обмежив розмір тіла й обрав довірений секрет.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyBramaPayWebhook(
rawBody: Buffer,
headers: Record<string, string | undefined>,
secret: string,
) {
const required = (name: string) => {
const value = headers[name.toLowerCase()];
if (!value) throw new Error(`Missing ${name}`);
return value;
};
const version = required("x-webhook-version");
const eventId = required("x-event-id");
const deliveryId = required("x-delivery-id");
const eventType = required("x-event-type");
const timestamp = required("x-timestamp");
const keyId = required("x-key-id");
const attempt = required("x-attempt");
const signature = required("x-signature-sha256");
if (version !== "2" || required("x-idempotency-key") !== eventId)
throw new Error("Invalid webhook envelope");
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300)
throw new Error("Stale webhook");
if (!/^[0-9a-f]{64}$/.test(signature) || !/^[1-9]\d*$/.test(attempt))
throw new Error("Invalid signature encoding or attempt");
const input = JSON.stringify([
"vaultless.webhook",
2,
eventId,
deliveryId,
eventType,
timestamp,
keyId,
attempt,
rawBody.toString("utf8"),
]);
const expected = Buffer.from(createHmac("sha256", secret).update(input).digest("hex"), "hex");
const actual = Buffer.from(signature, "hex");
if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
throw new Error("Invalid webhook signature");
return { eventId, eventType, keyId };
}Розбирайте JSON і перевіряйте схему й підписаний тип події лише після успіху перевірки. Самого підпису недостатньо для видачі: звіряйте сесію, проєкт, режим, замовлення, валюту й ціну з авторизованою PAID-відповіддю. Відхиляйте Test-подію для Live-замовлення. Тест зв’язку може отримати 2xx після перевірки, але не оплачує замовлення. За тимчасового збою читання/зберігання повертайте non-2xx для повтору. Див. дедуплікацію і сесії.