Контракт доставки
| Часть | Контракт |
|---|---|
| События | booking.confirmed, booking.cancelled, booking.reschedule_requested, booking.rescheduled, booking.updated |
| Заголовки | X-Fenix-Event-Id, X-Fenix-Delivery-Id, X-Fenix-Timestamp, X-Fenix-Signature и Idempotency-Key |
| Имена совместимости | X-Fenix-*, data-fenix-* и FENIX_* — стабильные унаследованные имена протокола. Сохраняйте их без изменений в существующих интеграциях. |
| Подпись | v1=<HMAC-SHA256(timestamp + "." + raw_body)> с одноразовым секретом whsec_ |
| Актуальность | Принимайте только пятиминутное окно timestamp и отклоняйте некорректные version/type/reference конверта. |
| Повторные попытки | Пять ограниченных попыток с задержками 1m, 5m, 30m и 2h. 2xx завершает доставку; 3xx отклоняется без перехода. |
Минимальный поток проверки
js
import { createHmac, timingSafeEqual } from 'node:crypto';
const rawBody = await request.text();
const timestamp = request.headers.get('x-fenix-timestamp') || '';
const signature = request.headers.get('x-fenix-signature') || '';
const ageSeconds = Math.abs(Date.now() - Number(timestamp) * 1000) / 1000;
const expected = 'v1=' + createHmac('sha256', process.env.FENIX_WEBHOOK_SECRET)
.update(timestamp + '.' + rawBody)
.digest('hex');
const received = Buffer.from(signature);
const candidate = Buffer.from(expected);
if (!Number.isInteger(Number(timestamp)) || ageSeconds > 300 ||
received.length !== candidate.length || !timingSafeEqual(received, candidate)) {
return new Response(null, { status: 400 });
}
const event = JSON.parse(rawBody);
if (await events.has(event.id)) return new Response(null, { status: 204 });
await database.transaction(async () => {
await events.insert({ id: event.id });
await applyBookingEvent(event);
});
return new Response(null, { status: 204 });Политика обработки получателя
- Проверьте подпись по неизменённому необработанному телу до разбора JSON. Проверьте формат подписи v1, окно timestamp и ожидаемую схему события.
- Атомарно сохраните X-Fenix-Event-Id до побочных эффектов. X-Fenix-Delivery-Id определяет попытку; Idempotency-Key повторяет id события для устранения дубликатов.
- Возвращайте 2xx только после безопасной обработки события. Redirect отклоняется; неудачная доставка получает не более пяти попыток с задержками 1m, 5m, 30m и 2h.
- Считайте payload webhook операционными данными. Не передавайте их в телеметрию браузера и скрывайте в логах, заявках и отчётах.
Безопасность endpoint
- Регистрируйте только публичный HTTPS hostname. URL с учётными данными, fragment, query string или нестандартным портом отклоняются.
- Платформа определяет и проверяет DNS перед настройкой и каждой доставкой; loopback, частные, link-local, зарезервированные и смешанные адреса блокируются.
- Одноразовый секрет подписи whsec_ показывается владельцу только при создании или ротации. Храните его в серверном хранилище, ротируйте после утечки и не отправляйте поддержке.
- Приостановите endpoint во время инцидента. Повторяйте только неудачные или пропущенные доставки после готовности получателя устранять дубликаты по исходному id события.
Feny