Skip to content

fix(spec,middleware,compliance-cli): define one canonical subscription resource - #94

Merged
ucekmez merged 1 commit into
fix/spec-webhook-verification-examplefrom
fix/canonical-subscription-endpoints
Sep 2, 2026
Merged

fix(spec,middleware,compliance-cli): define one canonical subscription resource#94
ucekmez merged 1 commit into
fix/spec-webhook-verification-examplefrom
fix/canonical-subscription-endpoints

Conversation

@ucekmez

@ucekmez ucekmez commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 2 of a stacked series. Base is #93 — review that first; GitHub will retarget this to main when #93 merges. Not for merge without review.

The subscription resource is addressed five different ways across the repo, and two of those paths are load-bearing for conformance:

Source Path
spec §5.1 POST /eep/subscribe
spec §10 POST /subscribe
spec §10 POST /subscriptions/:id/resume
middleware GET|DELETE /eep/subscribe/:id
compliance-cli GET /eep/subscriptions
compliance-cli POST /eep/subscriptions/:id/test

The consequence

POST /eep/subscriptions/:id/test is how the Core-tier probe triggers a delivery in order to verify Standard Webhooks headers and HMAC correctness. It is specified nowhere and implemented nowhere — not in the middleware route table, not in either reference implementation.

And it fails silently. fetch rejects only on a transport error, so a 404 resolves normally, the runner sleeps 5s, receives nothing, and reports:

❌ Webhook delivery received: no webhook received within 5s

An implementer reads that as "my dispatcher is broken" and goes hunting for a bug that was never theirs. GET /eep/subscriptions (Standard-tier rate-limit probe) has the same shape — asserted against a 404.

The paths here are not new

docs/guides/how-to-subscribe.md has documented /eep/subscriptions/{id} with pause, resume, test and DELETE since v0.1, and delivery_guarantees.md references the same collection for delivery logs. The guide was right; the spec and middleware were the outliers. This PR makes the guide's API normative rather than inventing anything.

Creation stays on POST /eep/subscribe — that URL is what the manifest advertises as layers.layer2_webhook and what the rel="subscribe" Link header points at, so moving it would break every deployed publisher.

What changed

Spec

  • New §5.1.1: the subscription resource, its member operations, status codes and scopes; delivery_secret never re-exposed; 404-not-403 for another subscriber's id so the collection can't be enumerated; test-delivery semantics (202, signed exactly like production traffic, 409 when not active).
  • §10 lifecycle uses the canonical paths and now defines what a "failed delivery" is — a fully exhausted §5.4 retry schedule, counter reset on the next 2xx. That ambiguity (7 retry attempts vs. "5 consecutive failures") was previously resolvable only by reading the CHANGELOG.
  • §14.2 conformance line points at §5.1.1 instead of claiming a lifecycle the spec never wrote down.

Middleware

  • Serves list / pause / resume / test alongside status and unsubscribe, all under /eep/subscriptions; /eep/subscribe/:id kept as deprecated 0.1.x aliases.
  • WebhookDispatcher routes com.eep.subscription.test to the single subscription named in data.subscription_id instead of fanning out by event_types — a test delivery must reach a subscriber whose patterns would never match it, and must reach nobody else.

compliance-cli

  • The trigger reports its own outcome and names the missing endpoint on a 404.
  • Downstream signature probes SKIP rather than FAIL when nothing could be triggered: the publisher's signing is untested, not proven broken.

Scope

  • Spec / schema only
  • TypeScript package(s)
  • Python package(s)
  • Tests / CI
  • Docs / examples

Checklist

  • I read CONTRIBUTING.md and CODE_OF_CONDUCT.md.
  • Tests added or updated where appropriate.
  • Breaking change? Additive for publishers. Middleware consumers see new routes; the old member paths still work as deprecated aliases through 0.1.x. Publishers that never implemented /test will now see an explicit, actionable conformance failure where they previously saw a misleading one.
  • Documentation updated for user-visible behavior.

Verification

Suite Result
@eep-dev/middleware 109 passed (was 93)
@eep-dev/compliance-cli 57 passed
tests/ (vitest) 164 passed
npx tsc --noEmit (middleware, compliance-cli) clean
node scripts/codegen-schema-types.mjs --check no drift

Notes for reviewers

Design decision worth confirming: I aligned the spec and middleware to the guides, rather than aligning the guides to the middleware. The alternative — standardising on /eep/subscribe/:id — would have required rewriting how-to-subscribe.md and delivery_guarantees.md and would leave the CLI's existing paths wrong. Happy to flip it if you'd rather.

delivery_guarantees.md:92 also references GET /eep/subscriptions/:id/delivery-log, which is still unspecified. That belongs with the webhook backfill/redelivery work (audit finding B1) and is deliberately not in this PR.

Python middleware parity for the new handlers is not included here — flagging it rather than silently skipping it.

Copilot AI lite review requested due to automatic review settings August 26, 2026 18:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…n resource

The subscription resource was addressed five different ways across the
repository, and two of those paths were load-bearing for conformance:

  spec §5.1        POST   /eep/subscribe
  spec §10         POST   /subscribe
  spec §10         POST   /subscriptions/:id/resume
  middleware       GET|DELETE /eep/subscribe/:id
  compliance-cli   GET    /eep/subscriptions
  compliance-cli   POST   /eep/subscriptions/:id/test

`POST /eep/subscriptions/:id/test` is how the Core-tier probe triggers a
delivery in order to verify Standard Webhooks headers and the HMAC
signature. It was specified nowhere and implemented nowhere — not in the
middleware, not in either reference implementation. Worse, it failed
silently: `fetch` rejects only on a transport error, so a 404 resolved
normally, the runner slept 5s, received nothing, and reported "Webhook
delivery received: FAIL". Implementers saw a delivery failure and went
hunting in their own dispatcher for a bug that was never theirs.

`GET /eep/subscriptions` (the Standard-tier rate-limit probe) had the
same shape: asserted against a 404.

The paths chosen here are not new. `docs/guides/how-to-subscribe.md`
has documented `/eep/subscriptions/{id}` with `pause`, `resume`, `test`
and `DELETE` since v0.1, and `delivery_guarantees.md` references the
same collection. The guide was right; the spec and the middleware were
the outliers. Creation stays on `POST /eep/subscribe` because that is
what the manifest advertises as `layers.layer2_webhook` and what the
`rel="subscribe"` Link header points at.

Changes:
- Spec: new §5.1.1 making the subscription resource normative — the
  member operations, their status codes and scopes, the rule that
  `delivery_secret` is never re-exposed, 404-not-403 for another
  subscriber's id, and the semantics of a test delivery.
- Spec: §10 lifecycle now uses the canonical paths and defines what a
  "failed delivery" is (a fully exhausted §5.4 retry schedule); §14.2's
  conformance line points at §5.1.1 instead of claiming an unwritten
  lifecycle.
- Middleware: serve list / pause / resume / test alongside the existing
  status and unsubscribe handlers, all under `/eep/subscriptions`, with
  the pre-§5.1.1 `/eep/subscribe/:id` paths kept as deprecated aliases.
- Middleware: `WebhookDispatcher` routes `com.eep.subscription.test` to
  the single subscription in `data.subscription_id` rather than fanning
  out by `event_types` — a test delivery must reach a subscriber whose
  patterns would never match it, and must reach nobody else.
- compliance-cli: the trigger now reports its own outcome, naming the
  missing endpoint on a 404, and downstream signature probes SKIP rather
  than FAIL when no delivery could be triggered.

Refs: EEP audit 2026-08 findings A4, A9
Signed-off-by: Ugur Cekmez <ucekmez@gmail.com>
@ucekmez
ucekmez force-pushed the fix/canonical-subscription-endpoints branch from 6c7252e to 202e140 Compare August 26, 2026 18:46
@ucekmez
ucekmez merged commit bf3d293 into fix/spec-webhook-verification-example Sep 2, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants