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
Changes
@@@ -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.@@@ -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`.
@@ -80,6 +80,20 @@ Three `public` capabilities let a developer app (ADR-014) use OpenVibe.Events wi
@@ -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.
`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
## Versioning and compatibility
- A contract id is permanent. Minor versions only add optional fields.
- A contract id is permanent. Minor versions only add optional fields.
@@ -13,6 +13,13 @@ Cross-service side effects are best-effort POSTs and HMAC webhooks; a consumer t
@@ -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.
- 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.
- 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
## Alternatives considered
- Kafka/NATS now: rejected, operational weight far beyond current volume.
- 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
@@ -28,4 +35,4 @@ Producers can switch back to their direct calls; events already stored stay repl
## Acceptance tests
## 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`.