From 62100405767a816cb52c92417a810df6276ff822 Mon Sep 17 00:00:00 2001 From: Ugur Cekmez Date: Wed, 26 Aug 2026 21:54:58 +0300 Subject: [PATCH] fix(examples): reference stacks read delivery_url, the field the schema defines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both reference implementations read `body.callback_url` on subscribe. `schemas/v0.1/subscription.request.json` defines `delivery_url` and has no `callback_url` property at all, so a conformant subscriber's delivery target was silently discarded: the subscription was created, returned 200, and could never receive a webhook. `callback_url` is the stacks' own internal column name. It was never part of the request body. Both `@eep-dev/middleware` and `eep-middleware` read `delivery_url` correctly; only the reference stacks — the code implementors are pointed at as the worked example — got it wrong. The existing tests passed because they posted `callback_url` themselves and never asserted on the stored value. `tests/cross-impl` does send the correct `delivery_url`, but exercises the gate publisher example rather than these stacks. Changes: - Read `delivery_url`, falling back to `callback_url` as a deprecated alias so existing demo scripts keep working. - Echo `delivery_url` and `created_at` in the subscribe response, so the recorded target is assertable rather than write-only. This is what makes the regression tests meaningful. - Return `201` for a created subscription, matching SPECIFICATION.md §5.1.1 and both middleware packages. The stacks returned `200`. - Regression tests in both stacks covering the wire field, the deprecated alias, and the status code. Note for maintainers: these suites are not run by any CI job — there is no `test-reference-implementation` in .github/workflows/test.yml — which is why this survived. `python/tests/test_app.py::test_combined_bundle_content` is also failing on main today, independently of this change. Both are addressed in a follow-up rather than folded in here. Refs: EEP audit 2026-08, follow-on from finding A1 Signed-off-by: Ugur Cekmez --- .../node/src/server.test.ts | 42 +++++++++++++++++-- .../node/src/server.ts | 19 ++++++++- .../python/eep_api_python/app.py | 19 ++++++++- .../python/tests/test_app.py | 35 +++++++++++++++- 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/examples/eep-reference-implementation/node/src/server.test.ts b/examples/eep-reference-implementation/node/src/server.test.ts index 2d4df76..cc73d3f 100644 --- a/examples/eep-reference-implementation/node/src/server.test.ts +++ b/examples/eep-reference-implementation/node/src/server.test.ts @@ -40,14 +40,50 @@ describe("EEP node reference", () => { body: JSON.stringify({ source_did: "did:web:test", delivery_method: "webhook", - callback_url: "https://example.com/hook", + event_types: ["com.example.entity.updated"], + delivery_url: "https://example.com/hook", }), }); - expect(res.status).toBe(200); + expect(res.status).toBe(201); const body = await res.json(); expect(body.status).toBe("pending_verification"); }); + // `delivery_url` is the field schemas/v0.1/subscription.request.json + // defines. Reading `callback_url` instead meant a conformant subscriber's + // delivery target was silently dropped: the subscription was created but + // could never receive a webhook. + it("records the delivery_url from the request body", async () => { + const res = await fetch(`${baseUrl}/eep/subscribe`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + source_did: "did:web:test", + delivery_method: "webhook", + event_types: ["com.example.entity.updated"], + delivery_url: "https://agent.example.com/hooks/eep", + }), + }); + expect(res.status).toBe(201); + const body = await res.json(); + expect(body.delivery_url).toBe("https://agent.example.com/hooks/eep"); + }); + + it("still accepts the deprecated callback_url alias", async () => { + const res = await fetch(`${baseUrl}/eep/subscribe`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + source_did: "did:web:test", + delivery_method: "webhook", + callback_url: "https://legacy.example.com/hooks/eep", + }), + }); + expect(res.status).toBe(201); + const body = await res.json(); + expect(body.delivery_url).toBe("https://legacy.example.com/hooks/eep"); + }); + it("resolves entity and link headers", async () => { const res = await fetch(`${baseUrl}/u/u/acme-corp`); expect(res.status).toBe(200); @@ -63,7 +99,7 @@ describe("EEP node reference", () => { headers: { "Content-Type": "application/json" }, body: "", }); - expect(sub.status).toBe(200); + expect(sub.status).toBe(201); const payload = await sub.json(); expect(payload.status).toBe("active"); expect(payload.source_did).toContain("did:web:api.eep.dev"); diff --git a/examples/eep-reference-implementation/node/src/server.ts b/examples/eep-reference-implementation/node/src/server.ts index 317049f..2f1071e 100644 --- a/examples/eep-reference-implementation/node/src/server.ts +++ b/examples/eep-reference-implementation/node/src/server.ts @@ -287,18 +287,33 @@ export function createEEPNodeServer(baseUrl = "http://localhost:3100") { const body = await readJson(req); const method = body.delivery_method === "webhook" ? "webhook" : "sse"; const subscription_id = `sub_ref_${Date.now()}`; + // The wire field is `delivery_url` — that is what + // schemas/v0.1/subscription.request.json defines and what every + // conformant subscriber sends. `callback_url` is this stack's own + // internal column name and was never part of the request body; + // reading it here silently discarded the delivery target. + const deliveryUrl = + typeof body.delivery_url === "string" + ? body.delivery_url + : typeof body.callback_url === "string" + ? body.callback_url // deprecated alias, accepted for older demos + : undefined; const entry: SubscriptionEntry = { subscription_id, source_did: String(body.source_did ?? "did:web:api.eep.dev:u:acme-corp"), delivery_method: method, - callback_url: typeof body.callback_url === "string" ? body.callback_url : undefined, + callback_url: deliveryUrl, created_at: new Date().toISOString(), }; await subscriptions.save(entry); - return sendJson(res, 200, { + return sendJson(res, 201, { subscription_id, status: method === "webhook" ? "pending_verification" : "active", source_did: entry.source_did, + // Echo the stored target so a subscriber (and the conformance + // suite) can confirm it was actually recorded. + delivery_url: entry.callback_url, + created_at: entry.created_at, }); } diff --git a/examples/eep-reference-implementation/python/eep_api_python/app.py b/examples/eep-reference-implementation/python/eep_api_python/app.py index 7e62fd9..0e7ea64 100644 --- a/examples/eep-reference-implementation/python/eep_api_python/app.py +++ b/examples/eep-reference-implementation/python/eep_api_python/app.py @@ -313,15 +313,26 @@ def gates() -> Dict[str, Any]: return serialize_gate_config(GATE_CONFIG) -@app.post("/eep/subscribe") +@app.post("/eep/subscribe", status_code=201) def subscribe(payload: Dict[str, Any]) -> Dict[str, Any]: method = "webhook" if payload.get("delivery_method") == "webhook" else "sse" subscription_id = f"sub_ref_{int(time.time() * 1000)}" + # The wire field is `delivery_url` — that is what + # schemas/v0.1/subscription.request.json defines and what every + # conformant subscriber sends. `callback_url` is this stack's own + # internal column name and was never part of the request body; reading + # it here silently discarded the delivery target. + delivery_url = payload.get("delivery_url") + if not isinstance(delivery_url, str): + # Deprecated alias, accepted for older demos. + delivery_url = payload.get("callback_url") + if not isinstance(delivery_url, str): + delivery_url = None entry = { "subscription_id": subscription_id, "source_did": payload.get("source_did", "did:web:api.eep.dev:u:acme-corp"), "delivery_method": method, - "callback_url": payload.get("callback_url"), + "callback_url": delivery_url, "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), } SUBSCRIPTIONS.save(entry) @@ -329,6 +340,10 @@ def subscribe(payload: Dict[str, Any]) -> Dict[str, Any]: "subscription_id": subscription_id, "status": "pending_verification" if method == "webhook" else "active", "source_did": entry["source_did"], + # Echo the stored target so a subscriber (and the conformance + # suite) can confirm it was actually recorded. + "delivery_url": entry["callback_url"], + "created_at": entry["created_at"], } diff --git a/examples/eep-reference-implementation/python/tests/test_app.py b/examples/eep-reference-implementation/python/tests/test_app.py index cfccb8c..f0f4708 100644 --- a/examples/eep-reference-implementation/python/tests/test_app.py +++ b/examples/eep-reference-implementation/python/tests/test_app.py @@ -51,10 +51,43 @@ def test_registry_manifest_has_economics(): def test_subscribe(): res = client.post("/eep/subscribe", json={"source_did": "did:web:test", "delivery_method": "webhook"}) - assert res.status_code == 200 + assert res.status_code == 201 assert res.json()["status"] == "pending_verification" +def test_subscribe_records_the_delivery_url_from_the_request_body(): + """`delivery_url` is the field subscription.request.json defines. + + Reading `callback_url` instead meant a conformant subscriber's delivery + target was silently dropped: the subscription was created but could + never receive a webhook. + """ + res = client.post( + "/eep/subscribe", + json={ + "source_did": "did:web:test", + "delivery_method": "webhook", + "event_types": ["com.example.entity.updated"], + "delivery_url": "https://agent.example.com/hooks/eep", + }, + ) + assert res.status_code == 201 + assert res.json()["delivery_url"] == "https://agent.example.com/hooks/eep" + + +def test_subscribe_still_accepts_the_deprecated_callback_url_alias(): + res = client.post( + "/eep/subscribe", + json={ + "source_did": "did:web:test", + "delivery_method": "webhook", + "callback_url": "https://legacy.example.com/hooks/eep", + }, + ) + assert res.status_code == 201 + assert res.json()["delivery_url"] == "https://legacy.example.com/hooks/eep" + + def test_entity_resolution_headers(): res = client.get("/u/u/acme-corp") assert res.status_code == 200