SIGNED ENVELOPE · SANDBOX INBOX · EXTERNAL_CALLS=0

Webhook envelope и безопасный Inbox

Реализованный контур создаёт tenant-scoped sandbox Webhook Inbox и append-only delivery observations. Он не отправляет внешний HTTPS-запрос: external_calls=0 остаётся частью safety contract.

Граница выполнения
LOCAL in-process Inbox · no external HTTPS target
Signed sandbox Webhook Inbox
РЕАЛИЗОВАНО
Контракты и исходные файлы

OpenAPI, схемы и примеры для скачивания.

Доступная поверхность

Webhook API
Endpoint familyПоведениеExternal effect
/v1/checkout/webhooks/verifyПроверяет signed envelope и replay windowНет внешнего вызова
/v1/checkout/sandbox/webhook-subscriptionsСоздаёт и читает tenant-scoped Inbox subscriptionsТолько Local durable state
…/{subscription_id}/rotate | revokeРотирует или отзывает sandbox signing stateНет provider credential
…/test-events | deliveries | retryФиксирует synthetic attempts и bounded retryТолько in-process Inbox

Проверка exact bytes в TypeScript, Python и Go

  • Считайте request body один раз как exact bytes и передавайте их verifier до JSON normalization.
  • Выбирайте current или previous secret по X-CertaRail-Key-ID; после проверки сопоставьте header с verified key_id.
  • Не логируйте signing secret, raw restricted payload или bearer.
Verify before business handlingtypescript
import { createHash } from 'node:crypto';
import { verifyWebhook } from '@certrail/sdk';

const rawBody = new Uint8Array(await readRawRequestBytes(request));
const keyId = request.headers.get('X-CertaRail-Key-ID') ?? '';
const secret = await keyring.require(keyId); // keep current + previous key by key_id

const event = await verifyWebhook(rawBody, secret, {
  toleranceMs: 5 * 60 * 1_000,
});
if (event.keyId !== keyId) throw new Error('WEBHOOK_KEY_ID_MISMATCH');

// The event identity, digest binding, and business change commit together.
const digest = createHash('sha256').update(rawBody).digest('hex');
const result = await inbox.processOnceAtomic(
  event.eventId,
  event.bodySha256,
  digest,
  (tx) => handleVerifiedEvent(tx, event),
);
if (result === 'CONFLICTING_DIGEST') {
  await quarantine(event.eventId, 'WEBHOOK_REPLAY_CONFLICT');
  return new Response(null, { status: 409 });
}
return new Response(null, { status: 204 });

Timestamp tolerance

Verifier принимает occurred_at только в симметричном окне пяти минут относительно trusted UTC clock. Формат нормализован до UTC с точностью не более микросекунды; событие раньше или позже окна отклоняется как WEBHOOK_REPLAY_WINDOW до business handling.

Синхронизируйте часы receiver, измеряйте clock skew и не увеличивайте tolerance как способ скрыть проблемы времени.

Replay defence

  1. Verify first

    Проверьте exact raw bytes, body_sha256, HMAC и timestamp window до любой бизнес-операции.

  2. Reserve atomically

    В одной транзакции создайте UNIQUE(event_id), привяжите его к проверенному body_sha256 и зафиксируйте business change. При ошибке handler вся транзакция откатывается; race между workers имеет одного победителя.

  3. Acknowledge exact duplicate

    Тот же event_id с тем же body_sha256 возвращает 2xx без повторного handler; другой digest для того же event_id отклоняется и помещается в quarantine.

Test event generator

Каждый вызов фиксирует synthetic delivery observation внутри sandbox Webhook Inbox. Ни один сценарий не выполняет внешний HTTPS-вызов.

Детерминированные signature fixtures
signature_scenarioReceiver expectationПредусловие
VALID_CURRENT_KEYVALID · DELIVEREDACTIVE endpoint и current secret
INVALID_SIGNATUREWEBHOOK_SIGNATURE_INVALID · QUARANTINEDACTIVE endpoint; RETRYABLE_FAILURE запрещён
ROTATED_KEYVALID · DELIVEREDПосле rotate → verify → activate; session существовала при rotation, previous secret сохранён, событие внутри пяти минут
Generate current, invalid and rotated fixturesbash
# Valid event signed by the current key generation
curl -X POST "http://localhost:8080/v1/checkout/sandbox/webhook-subscriptions/${SUBSCRIPTION_ID}/test-events" \
  -H "Authorization: Bearer ${CERTARAIL_API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: webhook-current-000001" \
  --data '{"session_id":"'"${SESSION_ID}"'","simulate_outcome":"ACCEPTED","signature_scenario":"VALID_CURRENT_KEY"}'

# Expected receiver result: WEBHOOK_SIGNATURE_INVALID; stored as QUARANTINED
curl -X POST "http://localhost:8080/v1/checkout/sandbox/webhook-subscriptions/${SUBSCRIPTION_ID}/test-events" \
  -H "Authorization: Bearer ${CERTARAIL_API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: webhook-invalid-000001" \
  --data '{"session_id":"'"${SESSION_ID}"'","simulate_outcome":"ACCEPTED","signature_scenario":"INVALID_SIGNATURE"}'

# Run after rotate -> verify -> activate, inside the five-minute overlap
curl -X POST "http://localhost:8080/v1/checkout/sandbox/webhook-subscriptions/${SUBSCRIPTION_ID}/test-events" \
  -H "Authorization: Bearer ${CERTARAIL_API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: webhook-rotated-000001" \
  --data '{"session_id":"'"${SESSION_ID}"'","simulate_outcome":"ACCEPTED","signature_scenario":"ROTATED_KEY"}'

Граница production delivery

External HTTPS delivery ещё не реализована
До production нужны allowlisted HTTPS targets, production secret lifecycle, SSRF/egress controls, receiver authentication, UAT, monitoring и incident recovery. Sandbox delivery observation не является доказательством доставки партнёру.

Нашли неточность?

Участники private repository могут предложить правку через reviewed pull request. Остальные пользователи — отправить техническое сообщение без credentials и чувствительных данных.