From 8dbf41799808539a1529d9edda530177fe17c4c0 Mon Sep 17 00:00:00 2001 From: Ugur Cekmez Date: Wed, 26 Aug 2026 22:10:42 +0300 Subject: [PATCH] feat(spec,schemas): adopt the CloudEvents attributes EEP had skipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The envelope defined eight `eep_`-prefixed extensions while using none of the optional CloudEvents attributes that solve the same problems. `subject`, `dataschema` and `dataref` were all absent, as were the Distributed Tracing extension attributes — in a protocol whose whole premise is agents consuming events efficiently across multiple hops. - `subject` — which thing inside `source` changed. Without it a subscriber must parse `data` to decide whether it wanted the event at all, which is exactly the context bloat EEP claims to remove. - `dataschema` — the payload contract travels with the event instead of being documented out of band, so a subscriber can validate or typed-decode a payload it has never seen. - `dataref` — the Claim Check pattern. A publisher sends a reference and only subscribers that need the body pay for it. Notable by its absence in a project whose front-page demo is "2.2 KB instead of 46 KB". Retrieval is an ordinary Layer 1 request and stays subject to the entity's gates, so a claim check does not become a gate bypass. - `traceparent` / `tracestate` — EEP is multi-hop by design (agent → publisher → subscriber → downstream agent) and the causal chain broke at every boundary. `docs/ops/observability.md` said "use OpenTelemetry" but no wire field carried the context. Changes: - `event.envelope.json` gains all five, with a W3C Trace Context pattern on `traceparent`. Types regenerated. - New normative §7.1 covering when to set each, the rule that a `dataref` without `data` MUST be fetched, and the prohibition on sending `data` and a `dataref` that disagree. - `WebhookDispatcher` mirrors trace context into HTTP headers per the CloudEvents Distributed Tracing extension. Only well-formed values are forwarded: a malformed `traceparent` is worse than none, because it silently roots the subscriber's spans under a trace that never existed. - Conformance vectors for the standard attributes, a claim-check event, and a malformed `traceparent`. - §7 records the CloudEvents attribute-naming problem: the existing `eep_`-prefixed names contain underscores, which CloudEvents v1.0.2 excludes. Documented as an open issue rather than renamed — a rename is breaking and deserves its own decision. All additions are optional, so existing publishers stay conformant. Refs: EEP audit 2026-08 findings B5, B6 Signed-off-by: Ugur Cekmez --- docs/current/SPECIFICATION.md | 48 +++++++++++++++++ .../src/dispatcher/webhook-dispatcher.test.ts | 48 +++++++++++++++++ .../src/dispatcher/webhook-dispatcher.ts | 31 ++++++++++- schemas/v0.1/event.envelope.json | 51 ++++++++++++++++--- .../claim-check-dataref.expected.json | 4 ++ .../envelope/claim-check-dataref.input.json | 11 ++++ .../malformed-traceparent.expected.json | 4 ++ .../envelope/malformed-traceparent.input.json | 11 ++++ .../standard-attributes.expected.json | 4 ++ .../envelope/standard-attributes.input.json | 18 +++++++ tests/conformance-fixtures/manifest.json | 33 ++++++++++++ tests/types/eep-schemas.d.ts | 40 +++++++++++++++ 12 files changed, 296 insertions(+), 7 deletions(-) create mode 100644 tests/conformance-fixtures/envelope/claim-check-dataref.expected.json create mode 100644 tests/conformance-fixtures/envelope/claim-check-dataref.input.json create mode 100644 tests/conformance-fixtures/envelope/malformed-traceparent.expected.json create mode 100644 tests/conformance-fixtures/envelope/malformed-traceparent.input.json create mode 100644 tests/conformance-fixtures/envelope/standard-attributes.expected.json create mode 100644 tests/conformance-fixtures/envelope/standard-attributes.input.json diff --git a/docs/current/SPECIFICATION.md b/docs/current/SPECIFICATION.md index 9603e30..714bdce 100644 --- a/docs/current/SPECIFICATION.md +++ b/docs/current/SPECIFICATION.md @@ -581,6 +581,54 @@ All EEP events MUST be valid CloudEvents v1.0.2 envelopes with EEP-specific exte | `eep_reputation_score` | integer | MAY | On-chain ERC-8004 reputation score at event time (0–100) | | `eep_on_chain_did` | string | MAY | On-chain DID linked to the entity (e.g. via ERC-8004 NFT token) | +> **Attribute naming (open issue).** CloudEvents v1.0.2 restricts context +> attribute names to lowercase ASCII letters and digits — the underscore is +> excluded. The `eep_`-prefixed names above are what every deployed +> implementation emits, but they do not satisfy that rule. The divergence is +> harmless in structured mode and becomes load-bearing in binary content mode, +> where attributes are carried as `ce-`-prefixed HTTP headers. Tracked for +> resolution before v1.0; see the editor's note in +> [`draft-eep-protocol-core-00.md`](../standards/draft-eep-protocol-core-00.md). + +### 7.1 Standard CloudEvents attributes (normative) + +EEP previously defined eight `eep_`-prefixed extensions while using none of the +optional CloudEvents attributes that solve the same problems. Publishers SHOULD +populate the following, and subscribers MUST tolerate their presence: + +| Attribute | Type | Level | Purpose | +|-----------|------|-------|---------| +| `subject` | string | SHOULD | Which thing inside `source` changed. Lets a subscriber filter without parsing `data`. | +| `dataschema` | URI | SHOULD | Schema describing `data`, so a subscriber can validate or typed-decode before use. | +| `dataref` | URI | MAY | Claim Check: retrieve the payload from this URI instead of inlining it. | +| `traceparent` | string | SHOULD | W3C Trace Context, propagated across the delivery boundary. | +| `tracestate` | string | MAY | Vendor trace data accompanying `traceparent`. | + +**`subject`.** Set it whenever an event concerns one addressable thing. It is +the difference between a subscriber discarding an unwanted event after parsing +its body and a publisher never sending it (see the filtering rules in §5.1). + +**`dataschema`.** Without it, a subscriber cannot validate a payload it has not +seen before, and an agent must be told the payload contract out of band. With +it, the contract travels with the event. + +**`dataref` (Claim Check).** When `dataref` is present and `data` is absent, the +subscriber MUST fetch `dataref` to obtain the payload. This keeps a large +payload off the delivery path — the publisher sends a reference, and only +subscribers that need the body pay for it. Retrieval is an ordinary Layer 1 +request and is subject to the entity's gates (§3.4), so a claim check does not +bypass access control. Publishers MUST NOT send both `data` and a `dataref` +that resolve to different content. + +**`traceparent` / `tracestate`.** EEP is multi-hop by design: agent → publisher +→ subscriber → downstream agent. Without propagation the causal chain breaks at +every boundary and a misbehaving pipeline cannot be correlated back to the +originating event. Publishers SHOULD set `traceparent` from the context that +produced the event, and MUST NOT invent one that implies a trace they did not +participate in. Over HTTP these MUST also appear as the corresponding +`traceparent` / `tracestate` headers, per the CloudEvents Distributed Tracing +extension. + --- ## 8. Event type naming convention diff --git a/packages/@eep-dev/middleware/src/dispatcher/webhook-dispatcher.test.ts b/packages/@eep-dev/middleware/src/dispatcher/webhook-dispatcher.test.ts index 894a1d0..0e10596 100644 --- a/packages/@eep-dev/middleware/src/dispatcher/webhook-dispatcher.test.ts +++ b/packages/@eep-dev/middleware/src/dispatcher/webhook-dispatcher.test.ts @@ -59,6 +59,54 @@ describe("WebhookDispatcher", () => { expect(DEFAULT_RETRY_SCHEDULE_MS).toEqual([0, 5_000, 30_000, 120_000, 900_000, 3_600_000, 21_600_000]); }); + // SPECIFICATION.md §7.1 — W3C Trace Context is mirrored into HTTP headers + // per the CloudEvents Distributed Tracing extension. EEP is multi-hop by + // design, and without propagation the causal chain breaks at every + // publisher/subscriber boundary. + describe("trace context propagation (§7.1)", () => { + const TRACEPARENT = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + + const deliver = async (overrides: Partial) => { + const db = new InMemoryDBAdapter(); + await db.saveSubscription(subscription()); + const { client, calls } = mockClient([200]); + const dispatcher = new WebhookDispatcher({ db, httpClient: client, retryScheduleMs: NO_DELAY }); + await dispatcher.dispatch(event(overrides)); + return calls[0]!.headers; + }; + + it("forwards a well-formed traceparent as an HTTP header", async () => { + const headers = await deliver({ traceparent: TRACEPARENT } as Partial); + expect(headers.traceparent).toBe(TRACEPARENT); + }); + + it("forwards tracestate alongside traceparent", async () => { + const headers = await deliver({ + traceparent: TRACEPARENT, + tracestate: "vendor=abc123" + } as Partial); + expect(headers.tracestate).toBe("vendor=abc123"); + }); + + it("sends no trace headers when the event carries none", async () => { + const headers = await deliver({}); + expect(headers.traceparent).toBeUndefined(); + expect(headers.tracestate).toBeUndefined(); + }); + + // A malformed traceparent is worse than none: it silently roots the + // subscriber's spans under a trace that does not exist. + it("drops a malformed traceparent rather than forwarding it", async () => { + const headers = await deliver({ traceparent: "not-a-trace-context" } as Partial); + expect(headers.traceparent).toBeUndefined(); + }); + + it("drops tracestate when traceparent is absent or invalid", async () => { + const headers = await deliver({ tracestate: "vendor=abc123" } as Partial); + expect(headers.tracestate).toBeUndefined(); + }); + }); + // SPECIFICATION.md §10.2: an elapsed lease means no more deliveries. Checked // at delivery time rather than by a sweeper, so the rule holds in a // deployment with no background job — an unenforced lease is no lease. diff --git a/packages/@eep-dev/middleware/src/dispatcher/webhook-dispatcher.ts b/packages/@eep-dev/middleware/src/dispatcher/webhook-dispatcher.ts index 67b3de8..ba2649f 100644 --- a/packages/@eep-dev/middleware/src/dispatcher/webhook-dispatcher.ts +++ b/packages/@eep-dev/middleware/src/dispatcher/webhook-dispatcher.ts @@ -119,6 +119,29 @@ function sleep(ms: number): Promise { * promise so it never blocks the event bus. Deployments that need durable, * restart-surviving retries should back the event bus with a queue. */ +/** + * Mirror an event's trace context into HTTP headers. + * + * Only well-formed values are forwarded: a malformed `traceparent` is worse + * than none, because it silently roots the subscriber's spans under a trace + * that does not exist. + */ +const TRACEPARENT_PATTERN = /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/; + +function traceHeaders(event: CloudEvent): Record { + const headers: Record = {}; + const candidate = (event as { traceparent?: unknown }).traceparent; + if (typeof candidate === "string" && TRACEPARENT_PATTERN.test(candidate)) { + headers.traceparent = candidate; + const state = (event as { tracestate?: unknown }).tracestate; + // `tracestate` is meaningless without a `traceparent` to accompany. + if (typeof state === "string" && state.length > 0 && state.length <= 512) { + headers.tracestate = state; + } + } + return headers; +} + export class WebhookDispatcher { private readonly db: DBAdapter; private readonly fallbackSecret?: string; @@ -281,7 +304,13 @@ export class WebhookDispatcher { "content-type": "application/json", "webhook-id": webhookId, "webhook-timestamp": timestamp, - "webhook-signature": signature + "webhook-signature": signature, + // W3C Trace Context is mirrored into HTTP headers per the + // CloudEvents Distributed Tracing extension (SPECIFICATION.md + // §7.1). Without this the subscriber's spans are orphaned and a + // multi-hop agent workflow cannot be correlated back to the + // originating event. + ...traceHeaders(event) }, body, signal: controller.signal diff --git a/schemas/v0.1/event.envelope.json b/schemas/v0.1/event.envelope.json index bf0cb6e..8f2f0df 100644 --- a/schemas/v0.1/event.envelope.json +++ b/schemas/v0.1/event.envelope.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://eep.dev/schemas/v0.1/event.envelope.json", "title": "EEP Event Envelope", - "description": "Schema for a valid EEP/CloudEvents v1.0.2 event. All EEP events MUST conform to this schema. This is a superset of the CloudEvents v1.0.2 envelope with EEP-specific extensions. See SPECIFICATION.md §13 for the canonical event type registry.", + "description": "Schema for a valid EEP/CloudEvents v1.0.2 event. All EEP events MUST conform to this schema. This is a superset of the CloudEvents v1.0.2 envelope with EEP-specific extensions. See SPECIFICATION.md \u00a713 for the canonical event type registry.", "type": "object", "required": [ "specversion", @@ -38,7 +38,7 @@ }, "type": { "type": "string", - "description": "The event type in reverse-domain dot notation. Format: {reverse-domain}.{entity-type}.{action}. Well-known EEP event types are listed in eep_known_event_types below and documented normatively in SPECIFICATION.md §13.", + "description": "The event type in reverse-domain dot notation. Format: {reverse-domain}.{entity-type}.{action}. Well-known EEP event types are listed in eep_known_event_types below and documented normatively in SPECIFICATION.md \u00a713.", "pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9_]*)+$", "minLength": 5, "examples": [ @@ -62,9 +62,48 @@ "description": "MIME type of the data field. MUST be 'application/json' for EEP events.", "const": "application/json" }, + "subject": { + "type": "string", + "description": "CloudEvents v1.0.2 OPTIONAL context attribute. Identifies the subject of the event within the context of `source` \u2014 for example the specific resource that changed. Subscribers can filter on it without parsing `data`, which is why publishers SHOULD set it whenever an event concerns one addressable thing.", + "minLength": 1, + "maxLength": 256, + "examples": [ + "profile/bio", + "listing/8241" + ] + }, + "dataschema": { + "type": "string", + "description": "CloudEvents v1.0.2 OPTIONAL context attribute. Absolute URI of a schema describing `data`. Lets a subscriber validate or typed-decode the payload before touching it, and lets an agent discover the payload contract without out-of-band documentation.", + "format": "uri", + "examples": [ + "https://example.com/schemas/entity.updated/v2.json" + ] + }, + "dataref": { + "type": "string", + "description": "CloudEvents Claim Check extension. Absolute URI at which the full payload can be retrieved. Lets a publisher send a small reference instead of a large body; when present without `data`, the subscriber MUST fetch this URI to obtain the payload. Retrieval is subject to the entity's gates.", + "format": "uri", + "examples": [ + "https://api.example.com/eep/payloads/01HN3QK7GX" + ] + }, + "traceparent": { + "type": "string", + "description": "CloudEvents Distributed Tracing extension, carrying a W3C Trace Context `traceparent`. Propagating it across the publisher/subscriber boundary is what keeps an agent's causal chain intact through a multi-hop workflow.", + "pattern": "^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$", + "examples": [ + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ] + }, + "tracestate": { + "type": "string", + "description": "CloudEvents Distributed Tracing extension, carrying a W3C Trace Context `tracestate`. Vendor-specific trace data accompanying `traceparent`.", + "maxLength": 512 + }, "data": { "type": "object", - "description": "The event payload. Structure varies by event type. Refer to the Event Catalog in SPECIFICATION.md §13 for the normative schema of each event type." + "description": "The event payload. Structure varies by event type. Refer to the Event Catalog in SPECIFICATION.md \u00a713 for the normative schema of each event type." }, "eep_version": { "type": "string", @@ -125,9 +164,9 @@ ] }, "eep_known_event_types": { - "$comment": "NORMATIVE REFERENCE — not a data field. This enum lists all canonical EEP event type suffixes as defined in SPECIFICATION.md §13 (Event Type Registry). The full type value prefixes these with a reverse-domain (e.g., 'com.example.session.revoked'). Publishers MAY define custom event types but MUST NOT reuse these suffixes with different semantics.", + "$comment": "NORMATIVE REFERENCE \u2014 not a data field. This enum lists all canonical EEP event type suffixes as defined in SPECIFICATION.md \u00a713 (Event Type Registry). The full type value prefixes these with a reverse-domain (e.g., 'com.example.session.revoked'). Publishers MAY define custom event types but MUST NOT reuse these suffixes with different semantics.", "type": "string", - "description": "NORMATIVE EVENT TYPE REGISTRY (SPECIFICATION.md §13). The suffix portion of canonical EEP event types. Publishers use these as the action portion of their typed events.", + "description": "NORMATIVE EVENT TYPE REGISTRY (SPECIFICATION.md \u00a713). The suffix portion of canonical EEP event types. Publishers use these as the action portion of their typed events.", "enum": [ "entity.updated", "entity.state.changed", @@ -165,4 +204,4 @@ ] } } -} \ No newline at end of file +} diff --git a/tests/conformance-fixtures/envelope/claim-check-dataref.expected.json b/tests/conformance-fixtures/envelope/claim-check-dataref.expected.json new file mode 100644 index 0000000..f4a828b --- /dev/null +++ b/tests/conformance-fixtures/envelope/claim-check-dataref.expected.json @@ -0,0 +1,4 @@ +{ + "valid": true, + "reason": "dataref without data is the CloudEvents Claim Check pattern; the subscriber fetches the payload (SPECIFICATION.md \u00a77.1)" +} diff --git a/tests/conformance-fixtures/envelope/claim-check-dataref.input.json b/tests/conformance-fixtures/envelope/claim-check-dataref.input.json new file mode 100644 index 0000000..20c77e3 --- /dev/null +++ b/tests/conformance-fixtures/envelope/claim-check-dataref.input.json @@ -0,0 +1,11 @@ +{ + "specversion": "1.0", + "id": "01HN3QK7GX-1708123456000", + "source": "did:web:test.eep.dev:u:alice", + "type": "com.example.entity.updated", + "time": "2026-05-09T12:00:00Z", + "datacontenttype": "application/json", + "eep_version": "0.1", + "subject": "report/q4-2026", + "dataref": "https://api.example.com/eep/payloads/01HN3QK7GX" +} diff --git a/tests/conformance-fixtures/envelope/malformed-traceparent.expected.json b/tests/conformance-fixtures/envelope/malformed-traceparent.expected.json new file mode 100644 index 0000000..147967a --- /dev/null +++ b/tests/conformance-fixtures/envelope/malformed-traceparent.expected.json @@ -0,0 +1,4 @@ +{ + "valid": false, + "reason": "traceparent must match the W3C Trace Context format; a malformed value roots subscriber spans under a trace that does not exist" +} diff --git a/tests/conformance-fixtures/envelope/malformed-traceparent.input.json b/tests/conformance-fixtures/envelope/malformed-traceparent.input.json new file mode 100644 index 0000000..8f73c4d --- /dev/null +++ b/tests/conformance-fixtures/envelope/malformed-traceparent.input.json @@ -0,0 +1,11 @@ +{ + "specversion": "1.0", + "id": "01HN3QK7GX-1708123456000", + "source": "did:web:test.eep.dev:u:alice", + "type": "com.example.entity.updated", + "time": "2026-05-09T12:00:00Z", + "datacontenttype": "application/json", + "eep_version": "0.1", + "traceparent": "not-a-trace-context", + "data": {} +} diff --git a/tests/conformance-fixtures/envelope/standard-attributes.expected.json b/tests/conformance-fixtures/envelope/standard-attributes.expected.json new file mode 100644 index 0000000..98d52e7 --- /dev/null +++ b/tests/conformance-fixtures/envelope/standard-attributes.expected.json @@ -0,0 +1,4 @@ +{ + "valid": true, + "reason": "subject, dataschema, traceparent and tracestate are CloudEvents-defined attributes (SPECIFICATION.md \u00a77.1)" +} diff --git a/tests/conformance-fixtures/envelope/standard-attributes.input.json b/tests/conformance-fixtures/envelope/standard-attributes.input.json new file mode 100644 index 0000000..aabcb17 --- /dev/null +++ b/tests/conformance-fixtures/envelope/standard-attributes.input.json @@ -0,0 +1,18 @@ +{ + "specversion": "1.0", + "id": "01HN3QK7GX-1708123456000", + "source": "did:web:test.eep.dev:u:alice", + "type": "com.example.entity.updated", + "time": "2026-05-09T12:00:00Z", + "datacontenttype": "application/json", + "eep_version": "0.1", + "subject": "profile/bio", + "dataschema": "https://example.com/schemas/entity.updated/v2.json", + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "tracestate": "vendor=abc123", + "data": { + "field": "bio", + "previous": "Old bio", + "current": "New bio" + } +} diff --git a/tests/conformance-fixtures/manifest.json b/tests/conformance-fixtures/manifest.json index 7ca3f6d..a5045a8 100644 --- a/tests/conformance-fixtures/manifest.json +++ b/tests/conformance-fixtures/manifest.json @@ -49,6 +49,39 @@ "shape": "json-pair", "asserts_valid": false }, + { + "id": "envelope-standard-attributes", + "category": "envelope", + "tier": "Core", + "spec_section": "\u00a77.1 Standard CloudEvents attributes", + "schema": "schemas/v0.1/event.envelope.json", + "input": "envelope/standard-attributes.input.json", + "expected": "envelope/standard-attributes.expected.json", + "shape": "json-pair", + "asserts_valid": true + }, + { + "id": "envelope-claim-check-dataref", + "category": "envelope", + "tier": "Core", + "spec_section": "\u00a77.1 Claim Check (dataref)", + "schema": "schemas/v0.1/event.envelope.json", + "input": "envelope/claim-check-dataref.input.json", + "expected": "envelope/claim-check-dataref.expected.json", + "shape": "json-pair", + "asserts_valid": true + }, + { + "id": "envelope-malformed-traceparent", + "category": "envelope", + "tier": "Core", + "spec_section": "\u00a77.1 Distributed tracing", + "schema": "schemas/v0.1/event.envelope.json", + "input": "envelope/malformed-traceparent.input.json", + "expected": "envelope/malformed-traceparent.expected.json", + "shape": "json-pair", + "asserts_valid": false + }, { "id": "signature-valid-fresh", "category": "signature", diff --git a/tests/types/eep-schemas.d.ts b/tests/types/eep-schemas.d.ts index 67b36cb..176fe1e 100644 --- a/tests/types/eep-schemas.d.ts +++ b/tests/types/eep-schemas.d.ts @@ -763,6 +763,26 @@ export interface EEPEventEnvelope { * MIME type of the data field. MUST be 'application/json' for EEP events. */ datacontenttype: 'application/json'; + /** + * CloudEvents v1.0.2 OPTIONAL context attribute. Identifies the subject of the event within the context of `source` — for example the specific resource that changed. Subscribers can filter on it without parsing `data`, which is why publishers SHOULD set it whenever an event concerns one addressable thing. + */ + subject?: string; + /** + * CloudEvents v1.0.2 OPTIONAL context attribute. Absolute URI of a schema describing `data`. Lets a subscriber validate or typed-decode the payload before touching it, and lets an agent discover the payload contract without out-of-band documentation. + */ + dataschema?: string; + /** + * CloudEvents Claim Check extension. Absolute URI at which the full payload can be retrieved. Lets a publisher send a small reference instead of a large body; when present without `data`, the subscriber MUST fetch this URI to obtain the payload. Retrieval is subject to the entity's gates. + */ + dataref?: string; + /** + * CloudEvents Distributed Tracing extension, carrying a W3C Trace Context `traceparent`. Propagating it across the publisher/subscriber boundary is what keeps an agent's causal chain intact through a multi-hop workflow. + */ + traceparent?: string; + /** + * CloudEvents Distributed Tracing extension, carrying a W3C Trace Context `tracestate`. Vendor-specific trace data accompanying `traceparent`. + */ + tracestate?: string; /** * The event payload. Structure varies by event type. Refer to the Event Catalog in SPECIFICATION.md §13 for the normative schema of each event type. */ @@ -1291,6 +1311,26 @@ export interface EEPEventEnvelope { * MIME type of the data field. MUST be 'application/json' for EEP events. */ datacontenttype: 'application/json'; + /** + * CloudEvents v1.0.2 OPTIONAL context attribute. Identifies the subject of the event within the context of `source` — for example the specific resource that changed. Subscribers can filter on it without parsing `data`, which is why publishers SHOULD set it whenever an event concerns one addressable thing. + */ + subject?: string; + /** + * CloudEvents v1.0.2 OPTIONAL context attribute. Absolute URI of a schema describing `data`. Lets a subscriber validate or typed-decode the payload before touching it, and lets an agent discover the payload contract without out-of-band documentation. + */ + dataschema?: string; + /** + * CloudEvents Claim Check extension. Absolute URI at which the full payload can be retrieved. Lets a publisher send a small reference instead of a large body; when present without `data`, the subscriber MUST fetch this URI to obtain the payload. Retrieval is subject to the entity's gates. + */ + dataref?: string; + /** + * CloudEvents Distributed Tracing extension, carrying a W3C Trace Context `traceparent`. Propagating it across the publisher/subscriber boundary is what keeps an agent's causal chain intact through a multi-hop workflow. + */ + traceparent?: string; + /** + * CloudEvents Distributed Tracing extension, carrying a W3C Trace Context `tracestate`. Vendor-specific trace data accompanying `traceparent`. + */ + tracestate?: string; /** * The event payload. Structure varies by event type. Refer to the Event Catalog in SPECIFICATION.md §13 for the normative schema of each event type. */