events: metadata rides as headers, not in the body (ADR 0047)

emit stamps the ADR 0047 headers — x-event-id, x-source, x-node, x-time,
content-type, and optional x-causation-id / x-schema — and publishes the
body as only the domain payload. on() reconstructs the Event from those
headers. Event gains id (the x-event-id a consumer dedups on) plus the
optional causation/schema. EventHeaders joins the contracts spine.

Supersedes the first cut that carried source/node/time in the body.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-04 00:25:55 +02:00
parent f335bfb9e7
commit 20f7bd2a7b
4 changed files with 95 additions and 22 deletions
+6 -1
View File
@@ -195,12 +195,17 @@ test("events: a module emits, a listener and the audit sink (#) both receive it,
assert.deepEqual(heard.map((e) => e.type), ["module.umami.site.created"]);
assert.deepEqual(audited.map((e) => e.type), ["module.umami.site.created", "module.plex.play.started"]);
// The metadata an audit trail needs is present.
// The metadata an audit trail needs is present — read back from the ADR 0047 headers, not the body.
const e = heard[0];
assert.equal(e.source, "umami");
assert.equal(e.node, "anchor");
assert.equal((e.body as { domain: string }).domain, "my-app");
assert.match(e.at, /^\d{4}-\d{2}-\d{2}T/);
// Every event carries a unique id (x-event-id) — the handle a consumer dedups on.
assert.ok(e.id, "event has an x-event-id");
assert.notEqual(audited[0].id, audited[1].id);
// The body is exactly the domain payload — provenance never leaks into it.
assert.deepEqual(Object.keys(e.body as object), ["domain"]);
delete process.env.MESH_MODULE;
delete process.env.MESH_NODE;