openvibers/openvibe.contracts Commits

Event delivery signatures: document v2 (X-OpenVibe-Timestamp, X-OpenVibe-Signature-V2 = HMAC of "<t>.<raw body>"), the ±300 s replay window, no v1 fallback when v2 is present, and the rollout (Events sends both, consumers move to openvibe-sdk 0.4.0 then set requireV2, v1 dropped last); ADR-004 note. Docs only, no version change

OP OpenVibers committed 23 Sept 2026 · 0257570079 · 2 files, +22 −1


Changes

ddiff --git a/README.md b/README.md
iindex 67691607..9b57cb45 100644
---- a/README.md
++++ b/README.md
@@@ -80,6 +80,20 @@ Three `public` capabilities let a developer app (ADR-014) use OpenVibe.Events wi
`project_key` is `p` followed by the project's ULID in lowercase (`prj_01JAB…` → `p01jab…`), so it fits an event-type segment. An app event's `source` is `app-` followed by the app's ULID in lowercase (`app:app_01JAB…` → `app-01jab…`), which keeps `events.event-envelope@1` unchanged (its `source` pattern already allows it). OpenVibe.Events enforces the scope, the sandbox separation and per-project quotas.
++## Event delivery signatures
++
++OpenVibe.Events POSTs each webhook delivery as `{ "event": <events.event-envelope@1>, "seq": n }`, keyed with the subscription secret:
++
++| Header | Value |
++|---|---|
++| `X-OpenVibe-Signature` (v1) | `sha256=<hex HMAC-SHA256 of the raw body>` |
++| `X-OpenVibe-Timestamp` | `<unix seconds>` when this attempt was sent (every retry gets a fresh one) |
++| `X-OpenVibe-Signature-V2` | `t=<that timestamp>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">` |
++
++v1 covers only the body, so a captured delivery verifies forever (only `event_id` dedupe limits a replay). A consumer checking v2 refuses a timestamp more than **300 s** from its own clock in either direction, compares in constant time, and never falls back to v1 when a v2 header is present but wrong or stale. openvibe-sdk ≥ 0.4.0 does this in `parseDelivery(raw, headers, secret, { requireV2 })` and `verifyDeliveryV2()`; the `openvibe-events` package has `verifyDeliveryV2()`.
++
++Rollout: (1) Events sends v2 beside v1; (2) each consumer moves to SDK 0.4.0, which verifies v2 whenever it is present; (3) each consumer sets `requireV2: true`, so a v1-only (header-stripped) replay fails; (4) once every consumer requires v2, v1 may be dropped. See ADR-004.
++
## Versioning and compatibility
- A contract id is permanent. Minor versions only add optional fields.
ddiff --git a/docs/adr/ADR-004-durable-events.md b/docs/adr/ADR-004-durable-events.md
iindex 5747aa99..1fac4144 100644
---- a/docs/adr/ADR-004-durable-events.md
++++ b/docs/adr/ADR-004-durable-events.md
@@@ -13,6 +13,13 @@ Cross-service side effects are best-effort POSTs and HMAC webhooks; a consumer t
- Consumers get exactly-once *effects* through an inbox of idempotency receipts in their own database.
- Storage starts on SQLite (single host, see ADR-007); Redis is not durable truth and is not used.
++### Delivery signatures and replay window (2026-09-23)
++
++- Every delivery is HMAC-SHA256-signed with the subscription secret. v1, `X-OpenVibe-Signature: sha256=<hex HMAC of the raw body>`, has no time in it: a captured delivery can be replayed forever, and only the consumer's `event_id` inbox limits the damage.
++- v2 adds `X-OpenVibe-Timestamp: <unix seconds>` and `X-OpenVibe-Signature-V2: t=<ts>,v2=<hex HMAC of "<ts>.<raw body>">`. Events signs every attempt, retries and replays included, with the time it is sent.
++- Consumers accept a v2 signature only within ±300 s of their clock, compare in constant time, and never fall back to v1 when a v2 header is present but bad or stale. With `requireV2` they also refuse deliveries without v2, which closes the replay-by-stripping-the-header path.
++- Rollout is additive: Events sends v1 and v2 first; consumers then move to openvibe-sdk 0.4.0 (v2 checked whenever present) and set `requireV2: true` one by one; v1 stays on the wire until every consumer requires v2, and only then may it be removed. Rollback at any step is to unset `requireV2` (consumers) or stop sending v2 (Events, only while no consumer requires it).
++
## Alternatives considered
- Kafka/NATS now: rejected, operational weight far beyond current volume.
@@@ -28,4 +35,4 @@ Producers can switch back to their direct calls; events already stored stay repl
## Acceptance tests
--Events tests: persistence before delivery, retry/backoff, DLQ, replay, crash-and-replay yields one effect, outbox commit/rollback, SSE visibility and resume.
++Events tests: persistence before delivery, retry/backoff, DLQ, replay, crash-and-replay yields one effect, outbox commit/rollback, SSE visibility and resume. Signatures: v1 and v2 on every attempt, a fresh timestamp per retry, v2 refused outside ±300 s or with a changed body or timestamp, and (SDK) no v1 fallback when v2 is present, v1-only refused under `requireV2`.