Webhooks
@lumenflow/surfaces’s http/webhooks/ module (WU-3522, INIT-091 phase 3)
ships signed outbound webhooks with durable delivery history and idempotent
replay, so a service account can verify a delivery and safely replay one it
missed during an outage.
Signing and verification
Section titled “Signing and verification”Every outbound event is signed with HMAC-SHA256 over a canonical (key-sorted) JSON encoding of its payload:
Verification needs only the raw shared secret — never the mutable key
store — so a consumer can verify a delivery without holding any
server-side mutable state. Signature comparison uses timingSafeEqual, and
verification fails closed (returns false, never throws) on malformed
input, since this is a trust boundary.
Rotation and revocation
Section titled “Rotation and revocation”Every sign/verify lookup reads the store at call time, never a value captured once at startup — rotating in a new key or revoking an old one takes effect on the very next call, with no redeploy or process restart required.
Durable delivery history
Section titled “Durable delivery history”Delivery history is the one durable outbox for signed webhooks: signing produces the signature, this store is where the resulting delivery is persisted and queried, and replay re-drains from this same store rather than standing up a second queue.
Storage is append-only NDJSON — the same durable-outbox convention this
repo already uses elsewhere — so the store survives a process restart:
reopening it against the same file rebuilds identical in-memory state by
replaying the log from the start. status (pending / in_flight /
acked / abandoned) reuses the same ReplayArtifactStatus vocabulary
@lumenflow/control-plane-sdk already publishes, rather than a parallel
status enum.
Idempotent replay
Section titled “Idempotent replay”Replay never regenerates a payload, signature, or event id: it re-drains
the same durable record and hands it, byte for byte, to the injected
WebhookDeliverer. Because the event’s identity never changes across
attempts, a consumer that dedupes by eventId sees a replay as a no-op
retry of an event it may already have processed — exactly the
at-least-once, idempotent-consumer contract this repo’s other outbox uses.
The actual network call is injected via WebhookDeliverer (a port); this
module owns replay policy, not HTTP transport.
Related
Section titled “Related”- Contract spine — the error taxonomy webhook error classes map onto.