feat(spec,middleware): event history, redelivery and delivery log for webhooks - #102
Open
ucekmez wants to merge 1 commit into
Open
feat(spec,middleware): event history, redelivery and delivery log for webhooks#102ucekmez wants to merge 1 commit into
ucekmez wants to merge 1 commit into
Conversation
… webhooks
SSE subscribers recover missed events with `Last-Event-ID` (§4.3, 24h
retention floor). Layer 3 clients recover with `system`/`replay` from a
`seq` (§6.3.1). Webhook subscribers had no equivalent at all.
That gap is load-bearing rather than theoretical. §10 moves a
subscription to `paused` after repeated delivery failures — precisely
when the subscriber's endpoint was down and it missed the most. Nothing
specified what happened to events emitted during the outage, so resuming
produced a silent hole: the subscription works again and the events are
simply gone.
`delivery_guarantees.md` §4 already mandated `GET
/eep/subscriptions/:id/delivery-log` with 30-day retention. The
normative spec never mentioned it and nothing implemented it, so a
subscriber could not distinguish "the publisher never sent it" from "my
endpoint rejected it" — unanswerable from its side.
Spec §5.1.2:
- `GET /eep/events` with `source` / `since` / `until` / `limit`, ordered
by emission, retention floor matching SSE replay. `next_cursor` is
present only while events remain, so its absence is how a subscriber
knows it is caught up.
- A `since` outside the retention window MUST produce `410 Gone` naming
the oldest retained id. Explicitly NOT an empty page: an empty page is
indistinguishable from "you are up to date", which would let a
subscriber believe it caught up while silently losing events.
- History is scoped to the caller and gates still apply, so catch-up
cannot be used to read events the caller could never have subscribed
to.
- `POST /eep/subscriptions/{id}/redeliver`. Redelivered events keep
their original `id` so an already-processed event is discarded by the
ordinary idempotency rule instead of being processed twice; they are
re-signed with a current timestamp and do not reset the failure
counter.
- The delivery log is promoted into the normative spec.
- §13's "Event stream history queries" quota now names a defined
endpoint. It previously metered nothing (audit finding A10).
Middleware: `EventStore` interface plus an in-memory implementation
covering history, redelivery lookup, and the delivery log with independent
retention windows; three new routes; and the dispatcher records every
attempt. Log writes are wrapped so observability can never break
delivery, and `undeliverable` is reserved for the final attempt of an
exhausted schedule — conflating it with an interim `failed` would make
the log read as though every retry were fatal.
Refs: EEP audit 2026-08 findings B1, A10
Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR 10 of a stacked series. Base is #101. Not for merge without review.
The three layers had wildly asymmetric reliability:
Last-Event-ID, ≥24h retention (§4.3)system/replayfromseq,historywith cursor pagination (§6.3)This is load-bearing rather than theoretical. §10 moves a subscription to
pausedafter repeated delivery failures — precisely when the subscriber's endpoint was down and it missed the most. Nothing specified what happened to events emitted during the outage, so resuming produced a silent hole: the subscription works again and the events are simply gone.Separately,
delivery_guarantees.md§4 already mandatedGET /eep/subscriptions/:id/delivery-logwith 30-day retention. The normative spec never mentioned it and nothing implemented it — so a subscriber could not distinguish "the publisher never sent it" from "my endpoint rejected it", which is unanswerable from its own side.What changed
Spec — new §5.1.2
GET /eep/eventswithsource/since/until/limit, ordered by emission, retention floor matching SSE replay.next_cursorpresent only while events remain, so its absence is how a subscriber knows it is caught up.sinceoutside retention MUST produce410 Gonenaming the oldest retained id. Explicitly not an empty page — an empty page is indistinguishable from "you are up to date", which would let a subscriber believe it caught up while silently losing events.POST /eep/subscriptions/{id}/redeliver. Redelivered events keep their originalid, so an already-processed event is discarded by the ordinary idempotency rule rather than processed twice. Re-signed with a current timestamp; does not reset the failure counter.Middleware
EventStoreinterface + in-memory implementation: history, redelivery lookup, delivery log, with independent retention windows for events (24h floor) and attempts (30d floor).try/catch— observability must never break delivery.undeliverableis reserved for the final attempt of an exhausted schedule; an interim failure isfailed. Conflating them would make the log read as though every retry were fatal.Scope
Checklist
eventStoreoption that defaults to an in-process store. Publishers that implement none of it will newly fail the §5.1.2 requirements, which is the point.Verification
@eep-dev/middlewaretests/@eep-dev/compliance-clicodegen-schema-types --checkNotes for reviewers
The in-memory store is a reference, not a recommendation. It is correct for a single process and for tests; a deployment that must survive a restart backs the
EventStoreinterface with the same storage its subscriptions live in. §5.1.2 requires the behaviour, not this class — the interface is the contract.Two deliberate omissions, flagged rather than skipped silently:
/eep/eventsyet. Meaningfully probing catch-up needs a publisher that has emitted events the runner can name, which the current probe flow does not set up.I did not add a
delivery-logresponse schema underschemas/v0.1/. The field list lives indelivery_guarantees.md§4 and I did not want to fork it into a second normative source in the same PR that promotes the endpoint. Worth doing — say the word and I'll add it with the codegen refresh.