From e5daf06902a3eb50af7184d0713186aaaa2117d1 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 29 Oct 2026 06:21:50 +0100 Subject: [PATCH 01/28] feat(extensions): add Web of Things context extension Signed-off-by: Marc --- cloudevents/extensions/README.md | 1 + cloudevents/extensions/opcua.md | 91 +++++++- cloudevents/extensions/wot.md | 342 +++++++++++++++++++++++++++++++ 3 files changed, 431 insertions(+), 3 deletions(-) create mode 100644 cloudevents/extensions/wot.md diff --git a/cloudevents/extensions/README.md b/cloudevents/extensions/README.md index 0b66ea88d..8982d3699 100644 --- a/cloudevents/extensions/README.md +++ b/cloudevents/extensions/README.md @@ -53,3 +53,4 @@ for more information. - [Sampling](sampledrate.md) - [Sequence](sequence.md) - [Severity](severity.md) +- [Web of Things](wot.md) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 95954fb23..918584878 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -39,6 +39,31 @@ like a unified namespace path. MUST map to [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) field `MessageType`. +### WoT mapping + +When a CloudEvent uses both this extension and the +[WoT extension](./wot.md), the following mappings apply to data and event +messages: + +- `type` MUST follow the WoT extension's semantic event-type rules instead of + mapping to the native `MessageType`. +- `opcuamessagetype` MUST carry the native `MessageType`. +- `subject` MUST follow the WoT extension's affordance-pointer rules. +- `opcuadatasetwriterid` MUST carry the native `DataSetWriterId`; it MUST NOT + be appended to `subject`. +- For an event message, the `EventId` remains in the OPC UA event payload and + MUST NOT be appended to `subject`. +- `opcuapublisherid` MUST identify the native PublisherId. +- `opcuasequence` MUST carry the native `SequenceNumber`. The CloudEvents + `sequence` attribute, when present, MUST satisfy the independent ordering + requirements of the Sequence extension and MUST NOT be inferred from + `opcuasequence`. + +These conditional mappings replace only the conflicting OPC UA mappings above. +All other requirements in this extension remain applicable. A profile that +uses only part of this extension MUST NOT claim conformance to it unless the +profile satisfies every applicable requirement in this document. + ## Mapping of OPTIONAL Attributes ### datacontenttype @@ -57,6 +82,8 @@ those cases the attribute references the schema and is used for versioning. ### subject +When the WoT extension is not used, the following OPC UA mappings apply: + For metadata, event and data messages (type one of `ua-metadata`, `ua-keyframe`, `ua-deltaframe`, `ua-event`), `subject` MUST map to either [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) @@ -74,14 +101,17 @@ field `Timestamp`. ## Mapping for other extensions -The following well-known extensions attributes MUST be used for data messages and -event messages (type one of `ua-keyframe`, `ua-deltaframe`, `ua-event`). +For data and event messages, the following well-known extension attributes +apply when this extension is used without the WoT extension. When both +extensions are used, determine message type from `opcuamessagetype`. ### sequence Attribute as defined by [sequence extensions](./sequence.md) MUST map to [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) -field `SequenceNumber`. +field `SequenceNumber`, except when the WoT extension is also used. In that +case, `opcuasequence` maps to the native field and `sequence` has the +independent meaning defined by the Sequence extension. ### traceparent @@ -128,6 +158,61 @@ field `MetaDataVersion`. - REQUIRED if status is not _Good_ - MAY be omitted if status is _Good_ +### opcuapublisherid + +- Type: `String` +- Description: Identifies the OPC UA PubSub PublisherId. +- Constraints + - OPTIONAL + - If present, MUST use `:`. + - For `Byte`, `UInt16`, `UInt32` and `UInt64`, `` MUST be the + canonical unsigned decimal representation of the PublisherId. + - For `Guid`, `` MUST be the lowercase canonical UUID representation + of the PublisherId. + - For `String`, `` MUST be the unpadded base64url encoding of the + PublisherId's UTF-8 bytes. + - In combination with the WoT extension, REQUIRED for data and event + messages. + - MUST map to the Network Message Header `PublisherId`. + +### opcuadatasetwriterid + +- Type: `Integer` +- Description: Identifies the DataSetWriter that produced the DataSetMessage. +- Constraints + - OPTIONAL + - If present, MUST be in the range 0 to 65535, inclusive. + - In combination with the WoT extension, REQUIRED for data and event + messages. + - MUST map to the Data Set Message Header `DataSetWriterId`. + +### opcuamessagetype + +- Type: `String` +- Description: Carries the OPC UA PubSub DataSetMessage type when `type` + identifies a WoT semantic event type. +- Constraints + - OPTIONAL + - In combination with the WoT extension, REQUIRED for data and event + messages. + - MUST map to the Data Set Message Header `MessageType`. + - The value MUST be the native OPC UA message type, such as `ua-keyframe`, + `ua-deltaframe` or `ua-event`. + +### opcuasequence + +- Type: `Integer` +- Description: Carries the native DataSetMessage sequence counter without + assigning CloudEvents Sequence extension ordering semantics to it. +- Constraints + - OPTIONAL + - If present, MUST be in the range 0 to 65535, inclusive. + - In combination with the WoT extension, REQUIRED for keyframe, deltaframe + and event messages. + - MUST map to the Data Set Message Header `SequenceNumber`. + - MUST NOT be interpreted as a source-wide sequence unless it also + independently satisfies the Sequence extension's requirements. + ## General Constraints - OPC UA messages MUST use `binary-mode` of CloudEvents. diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md new file mode 100644 index 000000000..7c7e1308d --- /dev/null +++ b/cloudevents/extensions/wot.md @@ -0,0 +1,342 @@ +# Web of Things + +This extension identifies the [Web of Things Thing Description][TD] (TD) that +describes the interaction associated with a CloudEvent. It separates the +Thing's identity, the selected TD representation, its revision and +verification of its bytes. The selected representation can be the authored TD +or a resolved TD derived from it. + +The extension also defines a convention for event types derived from a +[Thing Model][TM] (TM), affordance subjects and the relationship between a TD +and the payload schema. It does not define a transport binding, provision a +Thing, or require a particular registry implementation. + +## Notational Conventions + +As with the main [CloudEvents specification](../spec.md), the key words "MUST", +"MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", +"RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as +described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +The scope of these key words is limited to events using this extension. An +attribute marked REQUIRED here is not REQUIRED in every CloudEvent. + +## Attributes + +### wotid + +- Type: `URI` +- Description: The stable WoT identity of the Thing described by the + referenced TD. It is not the location of a registry entry or the schema of + the CloudEvent's `data`. +- Constraints: + - REQUIRED + - MUST be a non-empty absolute URI. + - MUST equal the TD's `id` value when that member is present. + - A resolved TD MUST retain the authored TD's `id` when it is present. + - When the authored TD has no `id`, the Producer MUST use the stable WoT + identity assigned to it by its publication contract. A resolved TD for + that Thing MUST use the same identity. + - MUST NOT change merely because another revision of the same TD is + published. + +A URI used as an identity is not necessarily a document retrieval URL. A +Consumer MUST NOT assume that an arbitrary `wotid` can be fetched directly. +The deployment or publication profile defines the resolver and its lookup +scope. The identity can name either an authored TD or a resolved TD for the +same Thing; it does not select between those representations. + +### wottd + +- Type: `URI` +- Description: The location of the TD document view to use. It can identify + either the authored TD or a resolved TD. +- Constraints: + - OPTIONAL + - If present, MUST be a non-empty absolute URI. + - MUST identify the TD document, not registry metadata about the TD. + - The resolved document MUST have the `wotid` identity. + - If `wotversion` is also present, the selected document MUST be that exact + revision. + +`wottd` selects a representation; `wotid` identifies the Thing. A Producer +MAY set `wottd` to distinguish an authored TD from a resolved TD when both +are available under the same identity and revision. If it is absent, the +configured resolver selects the representation. A resolution scope that +matches more than one representation MUST be narrowed by the publication +profile or by `wottd`; a Consumer MUST NOT choose one arbitrarily. + +### wotversion + +- Type: `String` +- Description: The exact revision of the TD identified by `wotid`, within the + configured resolution scope and selected representation. This is a document + revision, not the CloudEvents `specversion`, the version of a WoT vocabulary + or the version of the wire-payload schema. +- Constraints: + - OPTIONAL + - If present, MUST be non-empty and MUST identify exactly one revision. + - MUST NOT be interpreted as a minimum acceptable version or a version range. + - MUST NOT be inferred from, or appended to, `wotid`. + - If absent, the Consumer MUST use the revision identified by `wottd`, when + present, or the default revision selected by the configured resolver. + - If `wottd` is present and the locator identifies a revision, that revision + MUST match `wotversion`. + +A publication profile MAY require `wotversion`. An exact revision is +RECOMMENDED when later interpretation of a stored event depends on the same +description. An unversioned reference can resolve differently after the +resolver's default changes; it does not pin the description that existed +when the event was produced. + +Version identifiers are opaque outside their governing publication contract. +A Consumer MUST NOT select the default by sorting identifiers or substitute +a different revision when the requested revision is unavailable. + +### wotdigest + +- Type: `String` +- Description: A digest of the selected authored or resolved TD document's + exact bytes, independent of its identity and retrieval location. +- Constraints: + - OPTIONAL + - If present, MUST have the form `sha-256:` followed by exactly 64 lowercase + hexadecimal characters. + - The hexadecimal value MUST be the SHA-256 digest of the TD document bytes + obtained after removing transport framing and content coding, but before + parsing or reserializing the document. + - When a Consumer uses the referenced TD to interpret the event, it MUST + verify a supplied digest before using that TD. + +The digest covers the selected TD representation, not the CloudEvent, its +payload, a registry metadata response or the documents referenced by the TD. +It does not require JSON-LD expansion or JSON canonicalization. Whitespace +changes can change the digest. + +When both `wotversion` and `wottd` are absent, `wotdigest` verifies the default +revision and representation selected by the resolver; it does not define a +search for some historical revision or representation having that digest. A +missing digest provides no byte-verification result. A matching digest is not +a signature and does not authenticate the Producer. + +## Resolving the Thing Description + +The Producer and Consumer MUST agree on the resolution scope out of band or +through their publication profile. That scope includes any registry, tenant +or Group selection needed to distinguish copies of the same WoT identity and, +where necessary, the authored and resolved representations. This extension +does not prescribe a discovery protocol. + +A Consumer using this extension to interpret `data` MUST resolve the selected +representation and revision without substituting a document with another +identity. It MUST retrieve the document view identified by `wottd` when that +attribute is present. Otherwise, it MUST use the configured resolver for +`wotid`, `wotversion` and the publication profile's representation selection. +An unavailable revision, ambiguous representation, mismatching TD `id`, +malformed digest or failed digest comparison MUST be treated as unresolved +description metadata. A Consumer MAY retain or forward the CloudEvent without +interpreting its payload; it MUST NOT report successful TD-based +interpretation using an unresolved reference. + +The [xRegistry WoT working draft][WoT Registry] describes one resolution +mechanism. It is a working draft, not a universal dependency of this extension. +In that draft: + +- `wotid` is the authored WoT identity, distinct from a registry Resource id. +- `wotversion`, when present, corresponds to the Resource's exact `versionid`. +- An unversioned Resource reference serves its configured default Version, + which is not necessarily the most recently created Version. +- The Consumer retrieves the TD document view, not the registry metadata view. + +The xRegistry WoT draft stores authored documents. A resolved TD can instead be +identified by `wottd` when a deployment publishes that representation as a +document view. A registry Resource URL MUST NOT be treated as the resolved TD +unless its document view actually returns that TD. + +The Consumer still needs a configured registry scope. A registry-wide identity +query can return more than one Resource; choosing the first match does not +establish which description the Producer meant. The same applies when a scope +can return both authored and resolved representations. + +## Mapping Core Context Attributes + +### type + +The Core [`type`](../spec.md#type) describes the semantic event type. A +Thing's identity in `wotid` is not its event type: two Things can produce +events described by the same model. + +For TM-derived publications, the Producer MUST select a governing TM and the +corresponding model-affordance path. When composition or derivation does not +identify one such mapping unambiguously, the publication configuration MUST +select it explicitly. A Producer MUST NOT infer that correspondence solely +from matching names or select the first TM link arbitrarily. + +The following naming convention constructs a reverse-DNS-prefixed type base +from an HTTPS TM identifier: + +1. Parse the absolute identifier without removing its path, query or fragment. + The authority MUST contain an ASCII DNS hostname with no empty labels or + trailing root dot, and MUST NOT contain user information. Other authorities, + including IP addresses, require an explicit base instead. +2. Lowercase the ASCII DNS hostname and reverse its labels. +3. Encode the identifier's exact suffix after the hostname, including any + explicit port, path, query and fragment, as one token using the encoding + below. +4. Append `.wot.` and that token to the reversed hostname. + +For other TM identifier schemes, or HTTPS identifiers not meeting these +conditions, the publication configuration MUST supply an explicit +reverse-DNS-prefixed base. That configuration MUST assign distinct bases to +distinct governing TMs in its type namespace. + +Append `.affordance.` and the encoded model-affordance path tokens to the +base. The model path is an [RFC 6901][JSON Pointer] JSON Pointer within the +TM that identifies an interaction affordance. Decode its JSON Pointer +escaping before encoding each token, and join the resulting tokens with `.`. + +For both uses of token encoding, encode the input's UTF-8 bytes. Leave only +ASCII letters, digits, `-` and `_` unescaped. Encode every other byte as `%` +followed by two uppercase hexadecimal digits. Represent an empty token as +`~`; a literal tilde is encoded as `%7E`. Dots and percent signs are escaped, +so a literal dot in a name cannot be confused with a path-token separator. +This encoding is part of the type String, not an instruction to dereference it. + +For example, TM `https://models.example.com/tm/pump` and model affordance +`/properties/processData` produce: + +```text +com.example.models.wot.%2Ftm%2Fpump.affordance.properties.processData +``` + +Changing the encoded TM suffix, governing base or model-affordance path +changes this derived type; changing only the hostname's letter case does not. +The Producer MUST NOT change that derivation merely because a different TD +instance or registry location is used. Versioning the payload schema is +separate from naming a semantic event type. + +A TD can be published without a governing TM. This extension does not invent +one: publications without a TM-derived type follow Core's producer-defined +type rules. A profile requiring TM-derived types MUST reject missing or +ambiguous model mappings rather than inventing a base. + +### subject + +The [`subject`](../spec.md#subject), when present, MUST be an RFC 6901 JSON +Pointer to the concrete interaction affordance within the referenced TD, +such as `/properties/processData` or `/events/alarm`. It is not a URI +fragment and MUST NOT be prefixed with `#`. + +The subject identifies an affordance, not an occurrence. Different events +from that affordance can have the same subject and different CloudEvents +`id` values. A binding-specific writer identifier or native occurrence +identifier is not appended to this pointer. + +The concrete TD path and the corresponding TM declaration path can differ. +This extension does not infer a declaration-lineage mapping between them. + +### dataschema and datacontenttype + +Core [`dataschema`](../spec.md#dataschema) identifies the schema that the +CloudEvent's actual `data` adheres to. +[`datacontenttype`](../spec.md#datacontenttype) identifies that data's media +type. Neither identifies the TD document's format. + +A TD describes interactions and can contain several DataSchemas. The +Producer MUST NOT use the whole TD as `dataschema` merely because the TD +contains a schema for the selected affordance. A TD's retrieval URL is a +valid `dataschema` only if the identified representation is itself the +schema applicable to `data`. + +The publication profile or protocol binding defines how the selected +affordance's DataSchema maps to the wire payload. An object containing +several property values, an event payload and a protocol envelope can need +different schemas. When `dataschema` is present, the Producer MUST ensure +that it identifies the schema of the representation actually sent, including +any encoding or envelope selected by the binding. + +An xRegistry Schema Resource's document-view URL can identify that external +schema. It is not the WoT Resource's metadata-view URL. A profile MAY require +an exact schema Version independently of `wotversion`. + +### Relationship to Other Extensions + +This extension does not redefine native message types, sequence counters, +writer identities or source identities. A profile that also uses the +[OPC UA extension](opcua.md) MUST specify how its TM-derived type and +affordance subject interact with that extension's existing type and subject +mappings. Merely adding `wotid` does not permit overriding them or establish +conformance to the OPC UA extension. + +## Example + +The following CloudEvents describe the same pump and affordance. The first +references the authored TD through the configured resolver. The second uses +`wottd` to select a resolved representation explicitly. Both retain the +authored TD's stable `wotid`. + +```json +{ + "specversion": "1.0", + "id": "sample-4711", + "source": "urn:example:plant:publisher", + "type": "com.example.models.wot.%2Ftm%2Fpump.affordance.properties.processData", + "subject": "/properties/processData", + "datacontenttype": "application/json", + "dataschema": "https://example.com/schemas/pump-process-data/versions/3", + "wotid": "urn:example:pump:7", + "wotversion": "3", + "data": { + "Speed": 1500, + "Pressure": 4.5 + } +} +``` + +For an authored TD selected by `wotid`, the Consumer resolves revision `3` +through its configured resolver. The subject selects that TD's `processData` +property. + +The resolved-view form adds a document-view URI: + +```json +{ + "specversion": "1.0", + "id": "sample-4712", + "source": "urn:example:plant:publisher", + "type": "com.example.models.wot.%2Ftm%2Fpump.affordance.properties.processData", + "subject": "/properties/processData", + "datacontenttype": "application/json", + "dataschema": "https://example.com/schemas/pump-process-data/versions/3", + "wotid": "urn:example:pump:7", + "wottd": "https://registry.example/tds/pump-7/resolved/3", + "wotversion": "3", + "data": { + "Speed": 1500, + "Pressure": 4.5 + } +} +``` + +In either form, the separate schema describes the JSON object containing +`Speed` and `Pressure`; it does not describe the TD document. Another pump +with the same model and model-affordance mapping can use the same type with a +different `wotid`. + +## Security Considerations + +Resolvers MUST apply their configured trust, authorization and retrieval +policies before accessing documents. An untrusted identity or schema URI +MUST NOT authorize arbitrary network access, redirects or credential +forwarding. Implementations SHOULD bound document sizes and dependency +resolution. + +The Producer controls these attributes unless another mechanism authenticates +them. A matching digest does not establish that a TD, its model lineage or a +schema is authorized for the Consumer's use. Exact revisions and digests do +not grant access to referenced documents or make their dependencies available. + +[TD]: https://www.w3.org/TR/wot-thing-description11/ +[TM]: https://www.w3.org/TR/wot-thing-description11/#thing-model +[JSON Pointer]: https://www.rfc-editor.org/rfc/rfc6901 +[WoT Registry]: https://github.com/xregistry/spec/blob/main/workingdrafts/models/wot/spec.md From 1418500faef5c576cbb8ff4a82dc3a0940704f29 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 29 Oct 2026 07:31:18 +0100 Subject: [PATCH 02/28] Align WoT extension identity with xRegistry Signed-off-by: Marc --- cloudevents/extensions/wot.md | 76 +++++++++++++---------------------- 1 file changed, 27 insertions(+), 49 deletions(-) diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 7c7e1308d..282ea6c24 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -46,26 +46,6 @@ The deployment or publication profile defines the resolver and its lookup scope. The identity can name either an authored TD or a resolved TD for the same Thing; it does not select between those representations. -### wottd - -- Type: `URI` -- Description: The location of the TD document view to use. It can identify - either the authored TD or a resolved TD. -- Constraints: - - OPTIONAL - - If present, MUST be a non-empty absolute URI. - - MUST identify the TD document, not registry metadata about the TD. - - The resolved document MUST have the `wotid` identity. - - If `wotversion` is also present, the selected document MUST be that exact - revision. - -`wottd` selects a representation; `wotid` identifies the Thing. A Producer -MAY set `wottd` to distinguish an authored TD from a resolved TD when both -are available under the same identity and revision. If it is absent, the -configured resolver selects the representation. A resolution scope that -matches more than one representation MUST be narrowed by the publication -profile or by `wottd`; a Consumer MUST NOT choose one arbitrarily. - ### wotversion - Type: `String` @@ -78,10 +58,8 @@ profile or by `wottd`; a Consumer MUST NOT choose one arbitrarily. - If present, MUST be non-empty and MUST identify exactly one revision. - MUST NOT be interpreted as a minimum acceptable version or a version range. - MUST NOT be inferred from, or appended to, `wotid`. - - If absent, the Consumer MUST use the revision identified by `wottd`, when - present, or the default revision selected by the configured resolver. - - If `wottd` is present and the locator identifies a revision, that revision - MUST match `wotversion`. + - If absent, the Consumer MUST use the default revision selected by the + configured resolver for the selected representation. A publication profile MAY require `wotversion`. An exact revision is RECOMMENDED when later interpretation of a stored event depends on the same @@ -113,11 +91,11 @@ payload, a registry metadata response or the documents referenced by the TD. It does not require JSON-LD expansion or JSON canonicalization. Whitespace changes can change the digest. -When both `wotversion` and `wottd` are absent, `wotdigest` verifies the default -revision and representation selected by the resolver; it does not define a -search for some historical revision or representation having that digest. A -missing digest provides no byte-verification result. A matching digest is not -a signature and does not authenticate the Producer. +When `wotversion` is absent, `wotdigest` verifies the default revision and +representation selected by the resolver; it does not define a search for some +historical revision or representation having that digest. A missing digest +provides no byte-verification result. A matching digest is not a signature +and does not authenticate the Producer. ## Resolving the Thing Description @@ -127,16 +105,17 @@ or Group selection needed to distinguish copies of the same WoT identity and, where necessary, the authored and resolved representations. This extension does not prescribe a discovery protocol. -A Consumer using this extension to interpret `data` MUST resolve the selected -representation and revision without substituting a document with another -identity. It MUST retrieve the document view identified by `wottd` when that -attribute is present. Otherwise, it MUST use the configured resolver for -`wotid`, `wotversion` and the publication profile's representation selection. -An unavailable revision, ambiguous representation, mismatching TD `id`, -malformed digest or failed digest comparison MUST be treated as unresolved -description metadata. A Consumer MAY retain or forward the CloudEvent without -interpreting its payload; it MUST NOT report successful TD-based -interpretation using an unresolved reference. +A Consumer using this extension to interpret `data` MUST resolve the +representation and revision selected by the publication profile without +substituting a document with another identity. The profile MUST select the +authored or resolved representation when the configured scope can return +both. The Consumer MUST use the configured resolver for `wotid`, +`wotversion` and that representation selection. An unavailable revision, +ambiguous representation, mismatching TD `id`, malformed digest or failed +digest comparison MUST be treated as unresolved description metadata. A +Consumer MAY retain or forward the CloudEvent without interpreting its +payload; it MUST NOT report successful TD-based interpretation using an +unresolved reference. The [xRegistry WoT working draft][WoT Registry] describes one resolution mechanism. It is a working draft, not a universal dependency of this extension. @@ -148,10 +127,10 @@ In that draft: which is not necessarily the most recently created Version. - The Consumer retrieves the TD document view, not the registry metadata view. -The xRegistry WoT draft stores authored documents. A resolved TD can instead be -identified by `wottd` when a deployment publishes that representation as a -document view. A registry Resource URL MUST NOT be treated as the resolved TD -unless its document view actually returns that TD. +The xRegistry WoT draft stores authored documents. A deployment that makes a +resolved TD available MUST define how its resolver selects that representation +for the same `wotid`. A registry Resource URL MUST NOT be treated as the +resolved TD unless its document view actually returns that TD. The Consumer still needs a configured registry scope. A registry-wide identity query can return more than one Resource; choosing the first match does not @@ -270,10 +249,9 @@ conformance to the OPC UA extension. ## Example -The following CloudEvents describe the same pump and affordance. The first -references the authored TD through the configured resolver. The second uses -`wottd` to select a resolved representation explicitly. Both retain the -authored TD's stable `wotid`. +The following CloudEvents describe the same pump and affordance. Both use the +authored TD's stable `wotid`; their publication profiles select the authored +and resolved representations, respectively. ```json { @@ -297,7 +275,8 @@ For an authored TD selected by `wotid`, the Consumer resolves revision `3` through its configured resolver. The subject selects that TD's `processData` property. -The resolved-view form adds a document-view URI: +The resolved-view form uses the same event attributes. Its publication profile +selects the resolved TD representation for `wotid`: ```json { @@ -309,7 +288,6 @@ The resolved-view form adds a document-view URI: "datacontenttype": "application/json", "dataschema": "https://example.com/schemas/pump-process-data/versions/3", "wotid": "urn:example:pump:7", - "wottd": "https://registry.example/tds/pump-7/resolved/3", "wotversion": "3", "data": { "Speed": 1500, From 0f0c219bdf9757c42eb5a49fe8df46e59b880018 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 29 Oct 2026 12:37:26 +0100 Subject: [PATCH 03/28] fix(wot): address identity, digest and event scope feedback Signed-off-by: Marc --- cloudevents/extensions/wot.md | 114 +++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 29 deletions(-) diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 282ea6c24..ac78707ea 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -25,14 +25,14 @@ attribute marked REQUIRED here is not REQUIRED in every CloudEvent. ### wotid -- Type: `URI` +- Type: `String` - Description: The stable WoT identity of the Thing described by the referenced TD. It is not the location of a registry entry or the schema of the CloudEvent's `data`. - Constraints: - REQUIRED - - MUST be a non-empty absolute URI. - - MUST equal the TD's `id` value when that member is present. + - MUST be a non-empty absolute IRI as defined in [RFC 3987][IRI]. + - MUST equal the TD's `id` value verbatim when that member is present. - A resolved TD MUST retain the authored TD's `id` when it is present. - When the authored TD has no `id`, the Producer MUST use the stable WoT identity assigned to it by its publication contract. A resolved TD for @@ -40,7 +40,12 @@ attribute marked REQUIRED here is not REQUIRED in every CloudEvent. - MUST NOT change merely because another revision of the same TD is published. -A URI used as an identity is not necessarily a document retrieval URL. A +The CloudEvents `URI` type uses RFC 3986 and does not include IRIs containing +non-ASCII characters. This attribute uses `String` to preserve those identifiers +without percent-encoding or otherwise rewriting the TD's `id`. Absolute URIs +are also valid absolute IRIs. + +An IRI used as an identity is not necessarily a document retrieval URL. A Consumer MUST NOT assume that an arbitrary `wotid` can be fetched directly. The deployment or publication profile defines the resolver and its lookup scope. The identity can name either an authored TD or a resolved TD for the @@ -68,14 +73,14 @@ resolver's default changes; it does not pin the description that existed when the event was produced. Version identifiers are opaque outside their governing publication contract. -A Consumer MUST NOT select the default by sorting identifiers or substitute -a different revision when the requested revision is unavailable. +A Consumer MUST NOT substitute a different revision when the requested +revision is unavailable. ### wotdigest - Type: `String` - Description: A digest of the selected authored or resolved TD document's - exact bytes, independent of its identity and retrieval location. + exact bytes. - Constraints: - OPTIONAL - If present, MUST have the form `sha-256:` followed by exactly 64 lowercase @@ -83,19 +88,21 @@ a different revision when the requested revision is unavailable. - The hexadecimal value MUST be the SHA-256 digest of the TD document bytes obtained after removing transport framing and content coding, but before parsing or reserializing the document. - - When a Consumer uses the referenced TD to interpret the event, it MUST + - When a Consumer uses the referenced TD to interpret the event, it SHOULD verify a supplied digest before using that TD. -The digest covers the selected TD representation, not the CloudEvent, its -payload, a registry metadata response or the documents referenced by the TD. -It does not require JSON-LD expansion or JSON canonicalization. Whitespace -changes can change the digest. +The digest covers the entire selected TD representation, including its `id` +member when present. No document members are excluded. It does not cover the +CloudEvent, its payload, a registry metadata response or the documents referenced +by the TD. It does not require JSON-LD expansion or JSON canonicalization. +Whitespace changes can change the digest. When `wotversion` is absent, `wotdigest` verifies the default revision and representation selected by the resolver; it does not define a search for some -historical revision or representation having that digest. A missing digest -provides no byte-verification result. A matching digest is not a signature -and does not authenticate the Producer. +historical revision or representation having that digest. A missing or unchecked +digest provides no byte-verification result. A Consumer that skips verification +MUST NOT report that the TD's bytes have been verified against `wotdigest`. +A matching digest is not a signature and does not authenticate the Producer. ## Resolving the Thing Description @@ -124,7 +131,8 @@ In that draft: - `wotid` is the authored WoT identity, distinct from a registry Resource id. - `wotversion`, when present, corresponds to the Resource's exact `versionid`. - An unversioned Resource reference serves its configured default Version, - which is not necessarily the most recently created Version. + which is not necessarily the most recently created Version. A Consumer + MUST NOT infer that default by sorting `versionid` values. - The Consumer retrieves the TD document view, not the registry metadata view. The xRegistry WoT draft stores authored documents. A deployment that makes a @@ -145,11 +153,13 @@ The Core [`type`](../spec.md#type) describes the semantic event type. A Thing's identity in `wotid` is not its event type: two Things can produce events described by the same model. -For TM-derived publications, the Producer MUST select a governing TM and the +For TM-derived publications, the Producer MUST select a governing TM. For a +publication describing one affordance, the Producer MUST also select the corresponding model-affordance path. When composition or derivation does not -identify one such mapping unambiguously, the publication configuration MUST -select it explicitly. A Producer MUST NOT infer that correspondence solely -from matching names or select the first TM link arbitrarily. +identify the governing TM or affordance mapping unambiguously, the publication +configuration MUST select it explicitly. A Producer MUST NOT infer that +correspondence solely from matching names or select the first TM link +arbitrarily. The following naming convention constructs a reverse-DNS-prefixed type base from an HTTPS TM identifier: @@ -162,17 +172,25 @@ from an HTTPS TM identifier: 3. Encode the identifier's exact suffix after the hostname, including any explicit port, path, query and fragment, as one token using the encoding below. -4. Append `.wot.` and that token to the reversed hostname. +4. Append `.` and that token to the reversed hostname. + +The type does not include a separate WoT marker; the presence of `wotid` +identifies use of this extension. For other TM identifier schemes, or HTTPS identifiers not meeting these conditions, the publication configuration MUST supply an explicit reverse-DNS-prefixed base. That configuration MUST assign distinct bases to distinct governing TMs in its type namespace. -Append `.affordance.` and the encoded model-affordance path tokens to the -base. The model path is an [RFC 6901][JSON Pointer] JSON Pointer within the -TM that identifies an interaction affordance. Decode its JSON Pointer -escaping before encoding each token, and join the resulting tokens with `.`. +For a publication describing one affordance, append `.affordance.` and the +encoded model-affordance path tokens to the base. The model path is an +[RFC 6901][JSON Pointer] JSON Pointer within the TM that identifies an +interaction affordance. Decode its JSON Pointer escaping before encoding +each token, and join the resulting tokens with `.`. + +For a publication describing the Thing as a whole, the Producer MUST use the +base without an affordance suffix. The publication profile defines how the +Thing's interaction data is aggregated into the payload. For both uses of token encoding, encode the input's UTF-8 bytes. Leave only ASCII letters, digits, `-` and `_` unescaped. Encode every other byte as `%` @@ -185,7 +203,7 @@ For example, TM `https://models.example.com/tm/pump` and model affordance `/properties/processData` produce: ```text -com.example.models.wot.%2Ftm%2Fpump.affordance.properties.processData +com.example.models.%2Ftm%2Fpump.affordance.properties.processData ``` Changing the encoded TM suffix, governing base or model-affordance path @@ -206,6 +224,9 @@ Pointer to the concrete interaction affordance within the referenced TD, such as `/properties/processData` or `/events/alarm`. It is not a URI fragment and MUST NOT be prefixed with `#`. +For a publication describing the Thing as a whole, the Producer MUST omit +`subject` rather than use an empty pointer or an arbitrary affordance. + The subject identifies an affordance, not an occurrence. Different events from that affordance can have the same subject and different CloudEvents `id` values. A binding-specific writer identifier or native occurrence @@ -247,7 +268,9 @@ affordance subject interact with that extension's existing type and subject mappings. Merely adding `wotid` does not permit overriding them or establish conformance to the OPC UA extension. -## Example +## Examples + +### Single Affordance The following CloudEvents describe the same pump and affordance. Both use the authored TD's stable `wotid`; their publication profiles select the authored @@ -258,7 +281,7 @@ and resolved representations, respectively. "specversion": "1.0", "id": "sample-4711", "source": "urn:example:plant:publisher", - "type": "com.example.models.wot.%2Ftm%2Fpump.affordance.properties.processData", + "type": "com.example.models.%2Ftm%2Fpump.affordance.properties.processData", "subject": "/properties/processData", "datacontenttype": "application/json", "dataschema": "https://example.com/schemas/pump-process-data/versions/3", @@ -283,7 +306,7 @@ selects the resolved TD representation for `wotid`: "specversion": "1.0", "id": "sample-4712", "source": "urn:example:plant:publisher", - "type": "com.example.models.wot.%2Ftm%2Fpump.affordance.properties.processData", + "type": "com.example.models.%2Ftm%2Fpump.affordance.properties.processData", "subject": "/properties/processData", "datacontenttype": "application/json", "dataschema": "https://example.com/schemas/pump-process-data/versions/3", @@ -301,6 +324,38 @@ In either form, the separate schema describes the JSON object containing with the same model and model-affordance mapping can use the same type with a different `wotid`. +### Whole Thing + +This CloudEvent carries aggregated data for the pump described by the same TD. +The publication profile selects the authored TD and defines a payload containing +the `processData` and `running` property values. The type uses the governing TM's +base without an affordance suffix, and `subject` is absent because the event is +not scoped to one affordance. + +```json +{ + "specversion": "1.0", + "id": "sample-4713", + "source": "urn:example:plant:publisher", + "type": "com.example.models.%2Ftm%2Fpump", + "datacontenttype": "application/json", + "dataschema": "https://example.com/schemas/pump-state/versions/3", + "wotid": "urn:example:pump:7", + "wotversion": "3", + "data": { + "processData": { + "Speed": 1500, + "Pressure": 4.5 + }, + "running": true + } +} +``` + +`wotid` and `wotversion` select the TD used to interpret these property values. +The TD document itself is not the payload. `dataschema` identifies the schema +of the aggregate JSON object, not the TD or either property's DataSchema alone. + ## Security Considerations Resolvers MUST apply their configured trust, authorization and retrieval @@ -316,5 +371,6 @@ not grant access to referenced documents or make their dependencies available. [TD]: https://www.w3.org/TR/wot-thing-description11/ [TM]: https://www.w3.org/TR/wot-thing-description11/#thing-model +[IRI]: https://www.rfc-editor.org/rfc/rfc3987 [JSON Pointer]: https://www.rfc-editor.org/rfc/rfc6901 [WoT Registry]: https://github.com/xregistry/spec/blob/main/workingdrafts/models/wot/spec.md From 83539d54633eefaa40cb1378237305dc23efb6ed Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 29 Oct 2026 12:41:42 +0100 Subject: [PATCH 04/28] docs(wot): clarify payload member and affordance naming Signed-off-by: Marc --- cloudevents/extensions/wot.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index ac78707ea..7b8228705 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -296,7 +296,9 @@ and resolved representations, respectively. For an authored TD selected by `wotid`, the Consumer resolves revision `3` through its configured resolver. The subject selects that TD's `processData` -property. +property. In the JSON event format, `data` is the CloudEvent's payload member; +it is not named after the affordance. Here, `data` directly contains that +property's object value without a `processData` wrapper. The resolved-view form uses the same event attributes. Its publication profile selects the resolved TD representation for `wotid`: From e6856c24dc8323ffcfcc191ef6b279231ad0dc1b Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 29 Oct 2026 12:48:34 +0100 Subject: [PATCH 05/28] ci: skip flaky RFC Editor link checks Signed-off-by: Marc --- tools/test_verify.py | 18 ++++++++++++++++++ tools/verify.py | 3 +++ 2 files changed, 21 insertions(+) diff --git a/tools/test_verify.py b/tools/test_verify.py index 3698e277b..061dc8280 100644 --- a/tools/test_verify.py +++ b/tools/test_verify.py @@ -14,6 +14,7 @@ _is_text_all_uppercase, _plain_text_issues, _render_markdown_to_html, + _uri_availability_issues, ) @@ -187,6 +188,23 @@ def test_rtl_unicode_must_be_rendered_in_the_id(): ) +@pytest.mark.asyncio +@pytest.mark.parametrize( + "uri", + ( + "https://rfc-editor.org/rfc/rfc3987", + "https://www.rfc-editor.org/info/rfc2119", + ), +) +async def test_rfc_editor_links_skip_network_check(monkeypatch, uri): + monkeypatch.setattr( + "verify.ClientSession", + lambda: pytest.fail("RFC Editor links must not be fetched"), + ) + + assert await _uri_availability_issues(uri, Settings(excluded_paths=set())) == [] + + @pytest.mark.asyncio async def test_app(monkeypatch): monkeypatch.chdir(str(Path(__file__).parent)) diff --git a/tools/verify.py b/tools/verify.py index f7f93f155..c25a7c968 100644 --- a/tools/verify.py +++ b/tools/verify.py @@ -8,6 +8,7 @@ from http import HTTPStatus from pathlib import Path from typing import Iterable, List, NewType, Optional, Sequence, Set, Tuple, TypeVar +from urllib.parse import urlsplit from aiohttp import ClientSession from bs4 import BeautifulSoup @@ -136,6 +137,8 @@ async def _uri_availability_issues(uri: HttpUri, settings: Settings) -> Sequence if "example.com" in uri: return [] if "ietf.org" in uri: return [] if "rfc-edit.org" in uri: return [] + if urlsplit(uri).hostname in {"rfc-editor.org", "www.rfc-editor.org"}: + return [] if "iso20022.org" in uri: return [] try: From 2cc6898cc8d17bd9cf268fa9f724d781b273cf31 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 29 Oct 2026 12:53:45 +0100 Subject: [PATCH 06/28] docs(opcua): clarify WoT writer and sequence mappings Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 48 ++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 918584878..4f4df1244 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -49,8 +49,11 @@ messages: mapping to the native `MessageType`. - `opcuamessagetype` MUST carry the native `MessageType`. - `subject` MUST follow the WoT extension's affordance-pointer rules. -- `opcuadatasetwriterid` MUST carry the native `DataSetWriterId`; it MUST NOT - be appended to `subject`. +- `opcuadatasetwriterid` and `opcuadatasetwritername`, when present, MUST + identify the native DataSetWriter; neither MAY be appended to `subject`. +- For data and event messages, at least one of `opcuadatasetwriterid` and + `opcuadatasetwritername` MUST be present. If both are present, they MUST + identify the same DataSetWriter. - For an event message, the `EventId` remains in the OPC UA event payload and MUST NOT be appended to `subject`. - `opcuapublisherid` MUST identify the native PublisherId. @@ -107,11 +110,21 @@ extensions are used, determine message type from `opcuamessagetype`. ### sequence -Attribute as defined by [sequence extensions](./sequence.md) MUST map to [Data -Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) -field `SequenceNumber`, except when the WoT extension is also used. In that -case, `opcuasequence` maps to the native field and `sequence` has the -independent meaning defined by the Sequence extension. +Without the WoT extension, a `sequence` attribute, when present, MUST map to +the native DataSetMessage `SequenceNumber`. The OPC UA Publisher assigns and +increments this counter for messages from a DataSetWriter, as defined in +[OPC UA Part 14, Section 7.2.3][UA SequenceNumber]. This is the existing +OPC UA mapping for the [Sequence extension](./sequence.md), but it does not by +itself establish an ordering across multiple DataSetWriters that share one +CloudEvents `source`. A Publisher using this mapping MUST scope `source` to the +DataSetWriter's ordering scope; otherwise, it MUST NOT present the per-writer +counter as the CloudEvents `sequence`. + +When the WoT extension is also used, `subject` identifies a TD affordance, not +the DataSetWriter. `opcuasequence` therefore carries the native OPC UA counter; +a CloudEvents `sequence`, if present, MUST satisfy the Sequence extension's +ordering requirements independently and MUST NOT be inferred from +`opcuasequence`. ### traceparent @@ -182,10 +195,24 @@ field `MetaDataVersion`. - Constraints - OPTIONAL - If present, MUST be in the range 0 to 65535, inclusive. - - In combination with the WoT extension, REQUIRED for data and event - messages. - MUST map to the Data Set Message Header `DataSetWriterId`. +### opcuadatasetwritername + +- Type: `String` +- Description: Carries the human-readable name of the DataSetWriter that + produced the DataSetMessage. +- Constraints + - OPTIONAL + - If present, MUST be non-empty. + - MUST match the DataSetWriter's configured `DataSetWriterName`. For JSON + message mapping, this is the [`DataSetWriterName` field][UA DataSetWriterName] + when present in the DataSetMessage; otherwise the publisher obtains it + from configuration. + - In combination with the WoT extension, if used without + `opcuadatasetwriterid`, it MUST uniquely identify the DataSetWriter within + the configured `opcuapublisherid` scope. + ### opcuamessagetype - Type: `String` @@ -419,3 +446,6 @@ recordedtime: 2024-03-28T23:59:59Z ----------------------------------------------- ``` + +[UA DataSetWriterName]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table185 +[UA SequenceNumber]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.3 From a06d52327cce755c71e02e07bc086aa0be9091e8 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 29 Oct 2026 12:55:46 +0100 Subject: [PATCH 07/28] docs(opcua): add combined WoT telemetry example Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 4f4df1244..6a1f3b909 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -447,5 +447,37 @@ recordedtime: 2024-03-28T23:59:59Z ----------------------------------------------- ``` +### WoT and OPC UA telemetry message + +This keyframe uses the WoT event type and affordance subject while retaining +the native OPC UA message type, writer identity and sequence number. The +human-readable DataSetWriter name is used without a numeric writer ID. The +example assumes that `PumpProcessData` uniquely identifies a writer within +publisher `UInt16:12`. The CloudEvents `sequence` attribute is absent because +this example does not define a source-wide ordering across DataSetWriters. + +```json +{ + "specversion": "1.0", + "id": "ua-message-25460", + "source": "urn:example:plant:publisher", + "type": "com.example.models.%2Ftm%2Fpump.affordance.properties.processData", + "subject": "/properties/processData", + "time": "2026-10-29T12:00:00Z", + "datacontenttype": "application/json", + "dataschema": "https://example.com/schemas/pump-process-data/versions/3", + "wotid": "urn:example:pump:7", + "wotversion": "3", + "opcuapublisherid": "UInt16:12", + "opcuadatasetwritername": "PumpProcessData", + "opcuamessagetype": "ua-keyframe", + "opcuasequence": 25460, + "data": { + "Speed": 1500, + "Pressure": 4.5 + } +} +``` + [UA DataSetWriterName]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table185 [UA SequenceNumber]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.3 From fb77f3a88ffc00970eea61831a3fde062e1c8158 Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 31 Oct 2026 05:30:24 +0100 Subject: [PATCH 08/28] docs(wot): align CloudEvents Thing source mapping Signed-off-by: Marc --- cloudevents/extensions/README.md | 2 +- cloudevents/extensions/opcua.md | 122 ++++---- cloudevents/extensions/wot.md | 471 ++++++++++++++----------------- tools/test_wot_examples.py | 85 ++++++ 4 files changed, 372 insertions(+), 308 deletions(-) create mode 100644 tools/test_wot_examples.py diff --git a/cloudevents/extensions/README.md b/cloudevents/extensions/README.md index 8982d3699..88968c440 100644 --- a/cloudevents/extensions/README.md +++ b/cloudevents/extensions/README.md @@ -53,4 +53,4 @@ for more information. - [Sampling](sampledrate.md) - [Sequence](sequence.md) - [Severity](severity.md) -- [Web of Things](wot.md) +- [Web of Things](wot.md) (Core attribute mapping profile) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 6a1f3b909..b630f8073 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -1,10 +1,9 @@ # OPC UA -This extension defines the mapping of [OPC UA](https://reference.opcfoundation.org/Core/Part1/v105/docs/) -[PubSub](https://reference.opcfoundation.org/Core/Part14/v105/docs/) dataset to -CloudEvents to allow seamless routing of OPC UA dataset messages via different -protocols, it therefore provides a recommendation to map known REQUIRED and -OPTIONAL attributes using other extensions as well as defines its own extension +This extension maps [OPC UA](https://reference.opcfoundation.org/Core/Part1/v105/docs/) +[PubSub](https://reference.opcfoundation.org/Core/Part14/v105/docs/) +DataSetMessages to CloudEvents for routing over supported protocols. It defines +mappings for Core and other extension attributes and specifies OPC UA extension attributes. ## Notational Conventions @@ -29,26 +28,31 @@ field `MessageId`. ### source -MUST either map to [Application -Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) field -`applicationUri` of the OPC UA server or to a customer configured identifier -like a unified namespace path. +Without the WoT profile, MUST either map to [Application +Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) +field `applicationUri` of the OPC UA server or to a customer configured +identifier like a unified namespace path. + +With the WoT profile, MUST follow its `source` mapping. ### type -MUST map to [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) -field `MessageType`. +Without the WoT profile, MUST map to +[Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) +field `MessageType`. With the WoT profile, MUST follow its semantic event-type +rules. ### WoT mapping When a CloudEvent uses both this extension and the -[WoT extension](./wot.md), the following mappings apply to data and event +[WoT profile](./wot.md), the following mappings apply to data and event messages: -- `type` MUST follow the WoT extension's semantic event-type rules instead of +- `source` MUST follow the WoT profile's Thing identity and version rules. +- `type` MUST follow the WoT profile's semantic event-type rules instead of mapping to the native `MessageType`. - `opcuamessagetype` MUST carry the native `MessageType`. -- `subject` MUST follow the WoT extension's affordance-pointer rules. +- `subject` MUST follow the WoT profile's resolved-TD pointer rules. - `opcuadatasetwriterid` and `opcuadatasetwritername`, when present, MUST identify the native DataSetWriter; neither MAY be appended to `subject`. - For data and event messages, at least one of `opcuadatasetwriterid` and @@ -65,7 +69,10 @@ messages: These conditional mappings replace only the conflicting OPC UA mappings above. All other requirements in this extension remain applicable. A profile that uses only part of this extension MUST NOT claim conformance to it unless the -profile satisfies every applicable requirement in this document. +profile satisfies every applicable requirement in this document. Producers +sharing a WoT `source` MUST assign distinct native `MessageId` values to +distinct events within that source to satisfy Core's `source` and `id` +uniqueness requirement. ## Mapping of OPTIONAL Attributes @@ -85,7 +92,7 @@ those cases the attribute references the schema and is used for versioning. ### subject -When the WoT extension is not used, the following OPC UA mappings apply: +When the WoT profile is not used, the following OPC UA mappings apply: For metadata, event and data messages (type one of `ua-metadata`, `ua-keyframe`, `ua-deltaframe`, `ua-event`), `subject` MUST map to either [Data Set Message @@ -105,12 +112,12 @@ field `Timestamp`. ## Mapping for other extensions For data and event messages, the following well-known extension attributes -apply when this extension is used without the WoT extension. When both +apply when this extension is used without the WoT profile. When both extensions are used, determine message type from `opcuamessagetype`. ### sequence -Without the WoT extension, a `sequence` attribute, when present, MUST map to +Without the WoT profile, a `sequence` attribute, when present, MUST map to the native DataSetMessage `SequenceNumber`. The OPC UA Publisher assigns and increments this counter for messages from a DataSetWriter, as defined in [OPC UA Part 14, Section 7.2.3][UA SequenceNumber]. This is the existing @@ -120,7 +127,7 @@ CloudEvents `source`. A Publisher using this mapping MUST scope `source` to the DataSetWriter's ordering scope; otherwise, it MUST NOT present the per-writer counter as the CloudEvents `sequence`. -When the WoT extension is also used, `subject` identifies a TD affordance, not +When the WoT profile is also used, `subject` identifies a TD or affordance, not the DataSetWriter. `opcuasequence` therefore carries the native OPC UA counter; a CloudEvents `sequence`, if present, MUST satisfy the Sequence extension's ordering requirements independently and MUST NOT be inferred from @@ -171,6 +178,10 @@ field `MetaDataVersion`. - REQUIRED if status is not _Good_ - MAY be omitted if status is _Good_ +CloudEvents `Integer` is limited to the signed 32-bit range. If a native +status value cannot be represented, the Producer MUST report an error rather +than omit the status, wrap the value, or report it as _Good_. + ### opcuapublisherid - Type: `String` @@ -184,7 +195,7 @@ field `MetaDataVersion`. of the PublisherId. - For `String`, `` MUST be the unpadded base64url encoding of the PublisherId's UTF-8 bytes. - - In combination with the WoT extension, REQUIRED for data and event + - In combination with the WoT profile, REQUIRED for data and event messages. - MUST map to the Network Message Header `PublisherId`. @@ -209,9 +220,10 @@ field `MetaDataVersion`. message mapping, this is the [`DataSetWriterName` field][UA DataSetWriterName] when present in the DataSetMessage; otherwise the publisher obtains it from configuration. - - In combination with the WoT extension, if used without - `opcuadatasetwriterid`, it MUST uniquely identify the DataSetWriter within - the configured `opcuapublisherid` scope. + - In combination with the WoT profile, MUST uniquely identify the + DataSetWriter within the configured `opcuapublisherid` scope. + - In combination with the WoT profile, names MUST be compared exactly, + without case folding or Unicode normalization. ### opcuamessagetype @@ -220,7 +232,7 @@ field `MetaDataVersion`. identifies a WoT semantic event type. - Constraints - OPTIONAL - - In combination with the WoT extension, REQUIRED for data and event + - In combination with the WoT profile, REQUIRED for data and event messages. - MUST map to the Data Set Message Header `MessageType`. - The value MUST be the native OPC UA message type, such as `ua-keyframe`, @@ -228,15 +240,18 @@ field `MetaDataVersion`. ### opcuasequence -- Type: `Integer` +- Type: `String` - Description: Carries the native DataSetMessage sequence counter without assigning CloudEvents Sequence extension ordering semantics to it. - Constraints - OPTIONAL - - If present, MUST be in the range 0 to 65535, inclusive. - - In combination with the WoT extension, REQUIRED for keyframe, deltaframe + - If present, MUST be a canonical unsigned decimal representation in the + range `0` to `4294967295`, inclusive, with no leading zero except for `0`. + - In combination with the WoT profile, REQUIRED for keyframe, deltaframe and event messages. - - MUST map to the Data Set Message Header `SequenceNumber`. + - MUST map to the Data Set Message Header `SequenceNumber`. The + [OPC UA JSON mapping][UA DataSetWriterName] defines this field as `UInt32`; + the String representation retains the full range of that value. - MUST NOT be interpreted as a source-wide sequence unless it also independently satisfies the Sequence extension's requirements. @@ -449,34 +464,41 @@ recordedtime: 2024-03-28T23:59:59Z ### WoT and OPC UA telemetry message -This keyframe uses the WoT event type and affordance subject while retaining -the native OPC UA message type, writer identity and sequence number. The -human-readable DataSetWriter name is used without a numeric writer ID. The +This binary-mode keyframe uses the WoT event type and affordance subject while +retaining the native OPC UA message type, writer identity, and sequence +number. The DataSetWriter name is used without a numeric writer ID. The example assumes that `PumpProcessData` uniquely identifies a writer within publisher `UInt16:12`. The CloudEvents `sequence` attribute is absent because this example does not define a source-wide ordering across DataSetWriters. -```json +```text +------------------ PUBLISH ------------------- + +Topic Name: opcua/json/DataSetMessage/pump +Content Type: application/json + +------------- User Properties ---------------- + +specversion: 1.0 +id: ua-message-25460 +source: wot-td://factory/line1?wotid=urn:example:pump:7;wotversion=3.0.0 +type: com.example.models.tm.pump.properties.processData +subject: #/properties/processData +time: 2026-10-29T12:00:00Z +dataschema: https://example.com/schemas/pump-process-data/versions/3 +opcuapublisherid: UInt16:12 +opcuadatasetwritername: PumpProcessData +opcuamessagetype: ua-keyframe +opcuasequence: 4294967295 + +------------------ payload ------------------- + { - "specversion": "1.0", - "id": "ua-message-25460", - "source": "urn:example:plant:publisher", - "type": "com.example.models.%2Ftm%2Fpump.affordance.properties.processData", - "subject": "/properties/processData", - "time": "2026-10-29T12:00:00Z", - "datacontenttype": "application/json", - "dataschema": "https://example.com/schemas/pump-process-data/versions/3", - "wotid": "urn:example:pump:7", - "wotversion": "3", - "opcuapublisherid": "UInt16:12", - "opcuadatasetwritername": "PumpProcessData", - "opcuamessagetype": "ua-keyframe", - "opcuasequence": 25460, - "data": { - "Speed": 1500, - "Pressure": 4.5 - } + "Speed": 1500, + "Pressure": 4.5 } + +----------------------------------------------- ``` [UA DataSetWriterName]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table185 diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 7b8228705..714caa955 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -1,15 +1,13 @@ # Web of Things -This extension identifies the [Web of Things Thing Description][TD] (TD) that -describes the interaction associated with a CloudEvent. It separates the -Thing's identity, the selected TD representation, its revision and -verification of its bytes. The selected representation can be the authored TD -or a resolved TD derived from it. +This profile maps a [Web of Things Thing Description][TD] (TD) to CloudEvents +Core context attributes. The `source` identifies the Thing and, when provided, +a version of its resolved TD. The `subject` identifies the part of that TD +associated with the event, and the `type` names the event contract. -The extension also defines a convention for event types derived from a -[Thing Model][TM] (TM), affordance subjects and the relationship between a TD -and the payload schema. It does not define a transport binding, provision a -Thing, or require a particular registry implementation. +The profile defines no CloudEvents extension attributes. Its use is identified +by the `wot-td` scheme in `source`. It does not define a transport binding, +provision a Thing, or require a particular registry implementation. ## Notational Conventions @@ -18,222 +16,214 @@ As with the main [CloudEvents specification](../spec.md), the key words "MUST", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). -The scope of these key words is limited to events using this extension. An +The scope of these key words is limited to events using this profile. An attribute marked REQUIRED here is not REQUIRED in every CloudEvent. -## Attributes - -### wotid - -- Type: `String` -- Description: The stable WoT identity of the Thing described by the - referenced TD. It is not the location of a registry entry or the schema of - the CloudEvent's `data`. -- Constraints: - - REQUIRED - - MUST be a non-empty absolute IRI as defined in [RFC 3987][IRI]. - - MUST equal the TD's `id` value verbatim when that member is present. - - A resolved TD MUST retain the authored TD's `id` when it is present. - - When the authored TD has no `id`, the Producer MUST use the stable WoT - identity assigned to it by its publication contract. A resolved TD for - that Thing MUST use the same identity. - - MUST NOT change merely because another revision of the same TD is - published. - -The CloudEvents `URI` type uses RFC 3986 and does not include IRIs containing -non-ASCII characters. This attribute uses `String` to preserve those identifiers -without percent-encoding or otherwise rewriting the TD's `id`. Absolute URIs -are also valid absolute IRIs. - -An IRI used as an identity is not necessarily a document retrieval URL. A -Consumer MUST NOT assume that an arbitrary `wotid` can be fetched directly. -The deployment or publication profile defines the resolver and its lookup -scope. The identity can name either an authored TD or a resolved TD for the -same Thing; it does not select between those representations. - -### wotversion - -- Type: `String` -- Description: The exact revision of the TD identified by `wotid`, within the - configured resolution scope and selected representation. This is a document - revision, not the CloudEvents `specversion`, the version of a WoT vocabulary - or the version of the wire-payload schema. -- Constraints: - - OPTIONAL - - If present, MUST be non-empty and MUST identify exactly one revision. - - MUST NOT be interpreted as a minimum acceptable version or a version range. - - MUST NOT be inferred from, or appended to, `wotid`. - - If absent, the Consumer MUST use the default revision selected by the - configured resolver for the selected representation. - -A publication profile MAY require `wotversion`. An exact revision is -RECOMMENDED when later interpretation of a stored event depends on the same -description. An unversioned reference can resolve differently after the -resolver's default changes; it does not pin the description that existed -when the event was produced. - -Version identifiers are opaque outside their governing publication contract. -A Consumer MUST NOT substitute a different revision when the requested -revision is unavailable. - -### wotdigest - -- Type: `String` -- Description: A digest of the selected authored or resolved TD document's - exact bytes. -- Constraints: - - OPTIONAL - - If present, MUST have the form `sha-256:` followed by exactly 64 lowercase - hexadecimal characters. - - The hexadecimal value MUST be the SHA-256 digest of the TD document bytes - obtained after removing transport framing and content coding, but before - parsing or reserializing the document. - - When a Consumer uses the referenced TD to interpret the event, it SHOULD - verify a supplied digest before using that TD. - -The digest covers the entire selected TD representation, including its `id` -member when present. No document members are excluded. It does not cover the -CloudEvent, its payload, a registry metadata response or the documents referenced -by the TD. It does not require JSON-LD expansion or JSON canonicalization. -Whitespace changes can change the digest. - -When `wotversion` is absent, `wotdigest` verifies the default revision and -representation selected by the resolver; it does not define a search for some -historical revision or representation having that digest. A missing or unchecked -digest provides no byte-verification result. A Consumer that skips verification -MUST NOT report that the TD's bytes have been verified against `wotdigest`. -A matching digest is not a signature and does not authenticate the Producer. +## Source + +The Core [`source`](../spec.md#source) identifies a Thing within a configured +logical topology. It uses this absolute URI form: + +```text +wot-td://?wotid=[;wotversion=] +``` + +For example: + +```text +wot-td://factory/line1?wotid=urn:example:pump:7;wotversion=3.0.0 +``` + +`wotid` and `wotversion` are query parameters, not CloudEvents attributes. +The `wot-td` scheme is proposed and is not currently registered with IANA. +The URI is an identifier for a configured resolver; it is not an HTTP registry +URL and does not itself specify where to retrieve a TD. + +### Topology + +The Producer configures the topology. It consists of a non-empty authority +followed by an OPTIONAL hierarchical path. Both MUST conform to +[RFC 3986][URI]. The authority MUST contain a non-empty host and MUST NOT +contain user information. The host need not be a resolvable DNS name. +Implementations MUST apply RFC 3986 host case rules and MUST NOT change the +case of the path when interpreting the topology. + +The topology can represent, for example, a site and production line. It is +not the Thing's identity or a registry Resource identifier. + +### Query syntax and encoding + +The query contains `wotid` followed by an OPTIONAL `wotversion`. The literal +parameter names are case-sensitive, and `;` separates the parameters. Each +parameter consists of its name, one `=`, and a non-empty value. No other +parameters are defined. + +The value's unescaped characters are the RFC 3986 `unreserved` characters +and `!`, `$`, `'`, `(`, `)`, `*`, `,`, `:`, `@`, `/`, and `?`. A Producer +MUST leave these characters unescaped. It MUST percent-encode the UTF-8 bytes +of all other characters using uppercase hexadecimal digits. +In particular, a literal `%`, `;`, `=`, `&`, `+`, or `#` in a value MUST be +encoded. A Producer MUST NOT use form-query encoding or encode a space as `+`. + +A Consumer MUST split the query at `;` before decoding the values. It MUST +reject an unknown or duplicate parameter, a missing or empty `wotid`, an empty +`wotversion`, an invalid percent escape, invalid UTF-8, an out-of-order +parameter, an unescaped character outside the permitted set, or a URI with an +outer fragment. It MUST percent-decode each value exactly once and MUST NOT +interpret `+` as a space. + +For example, an identifier containing U+00E9 followed by the literal characters +`%2F` encodes that portion as `%C3%A9%252F`. One decoding pass recovers the +original characters, including the literal `%2F`; it does not turn them into +a slash. + +### Thing identity and version + +The decoded `wotid` MUST be a non-empty absolute IRI as defined in +[RFC 3987][IRI]. When the authored TD has an `id`, `wotid` MUST equal that +value verbatim and the resolved TD MUST retain it. When the authored TD has +no `id`, the publication contract MUST assign a stable absolute WoT identity +for the Thing. Publishing another version MUST NOT by itself change that +identity. + +The OPTIONAL `wotversion` selects one exact version of the resolved TD within +the configured resolution scope. It is not a version range, the CloudEvents +`specversion`, a WoT vocabulary version, or a payload-schema version. Its +meaning is defined by the publication contract; a Consumer MUST NOT substitute +a different version when the requested version is unavailable. Selecting that +version does not by itself establish the compatibility of its dependencies. + +An explicit version identifies a stable resolved view. A Producer MUST NOT +change the logical view identified by a published version. It MUST publish a +new version when the view changes, including changes introduced by its source +closure. A serialization or formatting change alone does not require a new +version. + +When `wotversion` is absent, the Consumer resolves the current default view. +That default can change, so a delayed event can resolve to a different view +than the one available when the event was produced. A publication profile +that requires repeatable replay MUST require `wotversion`. + +Core requires `source` and `id` together to identify each distinct event. +Changing the topology or version changes the `source` and therefore its event +identity and [Sequence extension](sequence.md) scope. Returning to an earlier +`source` returns to its existing scope: Producers MUST NOT reuse event +identities or restart a claimed source-wide sequence within that scope. ## Resolving the Thing Description -The Producer and Consumer MUST agree on the resolution scope out of band or -through their publication profile. That scope includes any registry, tenant -or Group selection needed to distinguish copies of the same WoT identity and, -where necessary, the authored and resolved representations. This extension -does not prescribe a discovery protocol. - -A Consumer using this extension to interpret `data` MUST resolve the -representation and revision selected by the publication profile without -substituting a document with another identity. The profile MUST select the -authored or resolved representation when the configured scope can return -both. The Consumer MUST use the configured resolver for `wotid`, -`wotversion` and that representation selection. An unavailable revision, -ambiguous representation, mismatching TD `id`, malformed digest or failed -digest comparison MUST be treated as unresolved description metadata. A -Consumer MAY retain or forward the CloudEvent without interpreting its -payload; it MUST NOT report successful TD-based interpretation using an -unresolved reference. +The Producer and Consumer MUST agree on a resolver and its scope out of band +or through their publication profile. The scope selects a registry and Group, +tenant, or equivalent boundary in which the Thing identity is resolved. This +profile does not prescribe a discovery protocol. + +The resolver MUST find exactly one Resource or equivalent publication for +`wotid` within that scope. It MUST select the exact version named by +`wotversion`, or the current default when the parameter is absent. A missing +or ambiguous match is a resolution failure; selecting the first match does not +establish which Thing the Producer meant. + +The Consumer MUST obtain the resolved TD selected by that publication. It MAY +retrieve a published resolved view or reconstruct the same logical view from +a complete, pinned source closure and the applicable resolution rules. The +closure and rules MUST establish one unambiguous view. The JSON serialization +does not need to be byte-identical. + +If the selected view cannot be retrieved or reconstructed, the Consumer MUST +report a resolution failure and leave the event uncorrelated. It MAY retain +or forward the event without interpreting its payload. The Consumer MUST NOT +claim successful TD-based interpretation when the resolved view or its +identity cannot be established. The [xRegistry WoT working draft][WoT Registry] describes one resolution -mechanism. It is a working draft, not a universal dependency of this extension. -In that draft: - -- `wotid` is the authored WoT identity, distinct from a registry Resource id. -- `wotversion`, when present, corresponds to the Resource's exact `versionid`. -- An unversioned Resource reference serves its configured default Version, - which is not necessarily the most recently created Version. A Consumer - MUST NOT infer that default by sorting `versionid` values. -- The Consumer retrieves the TD document view, not the registry metadata view. - -The xRegistry WoT draft stores authored documents. A deployment that makes a -resolved TD available MUST define how its resolver selects that representation -for the same `wotid`. A registry Resource URL MUST NOT be treated as the -resolved TD unless its document view actually returns that TD. - -The Consumer still needs a configured registry scope. A registry-wide identity -query can return more than one Resource; choosing the first match does not -establish which description the Producer meant. The same applies when a scope -can return both authored and resolved representations. +mechanism. It is a working draft, not a universal dependency of this profile. +In that draft, `wotid` is the WoT identity rather than the Resource ID, and +`wotversion` corresponds to the Resource's exact `versionid`. The default +Version is not necessarily the most recently created Version and MUST NOT be +inferred by sorting version identifiers. + +The draft stores authored documents and describes a publication closure. +Some closure reference mappings remain unresolved; the closure schema alone +does not establish that every resolved view can be reconstructed. Where the +closure and applicable rules cannot establish the view, the publication +contract MUST provide the resolved view for this profile to use it. + +The event carries no digest of the TD. An exact version or a successful +resolution does not by itself prove that retrieved bytes are unchanged. ## Mapping Core Context Attributes ### type -The Core [`type`](../spec.md#type) describes the semantic event type. A -Thing's identity in `wotid` is not its event type: two Things can produce -events described by the same model. +The Core [`type`](../spec.md#type) identifies the event contract. Two Things +can produce events of the same type when their interactions follow the same +model. This profile uses a dot-separated symbol so a Consumer can map the type +to a code-generation symbol. -For TM-derived publications, the Producer MUST select a governing TM. For a -publication describing one affordance, the Producer MUST also select the +The type MUST contain one or more segments separated by single dots. Each +segment MUST start with an ASCII letter or underscore and contain only ASCII +letters, digits, or underscores thereafter. These character restrictions apply +only to `type`; they do not change the Thing identifier, TD member names, +`source`, `subject`, or payload. A code generator remains responsible for +language-specific keywords. + +A Producer MAY configure the complete type. Otherwise, it derives the type +from a governing [Thing Model][TM] (TM) and, for an affordance event, the corresponding model-affordance path. When composition or derivation does not -identify the governing TM or affordance mapping unambiguously, the publication -configuration MUST select it explicitly. A Producer MUST NOT infer that -correspondence solely from matching names or select the first TM link -arbitrarily. - -The following naming convention constructs a reverse-DNS-prefixed type base -from an HTTPS TM identifier: - -1. Parse the absolute identifier without removing its path, query or fragment. - The authority MUST contain an ASCII DNS hostname with no empty labels or - trailing root dot, and MUST NOT contain user information. Other authorities, - including IP addresses, require an explicit base instead. -2. Lowercase the ASCII DNS hostname and reverse its labels. -3. Encode the identifier's exact suffix after the hostname, including any - explicit port, path, query and fragment, as one token using the encoding - below. -4. Append `.` and that token to the reversed hostname. - -The type does not include a separate WoT marker; the presence of `wotid` -identifies use of this extension. - -For other TM identifier schemes, or HTTPS identifiers not meeting these -conditions, the publication configuration MUST supply an explicit -reverse-DNS-prefixed base. That configuration MUST assign distinct bases to -distinct governing TMs in its type namespace. - -For a publication describing one affordance, append `.affordance.` and the -encoded model-affordance path tokens to the base. The model path is an -[RFC 6901][JSON Pointer] JSON Pointer within the TM that identifies an -interaction affordance. Decode its JSON Pointer escaping before encoding -each token, and join the resulting tokens with `.`. - -For a publication describing the Thing as a whole, the Producer MUST use the -base without an affordance suffix. The publication profile defines how the -Thing's interaction data is aggregated into the payload. - -For both uses of token encoding, encode the input's UTF-8 bytes. Leave only -ASCII letters, digits, `-` and `_` unescaped. Encode every other byte as `%` -followed by two uppercase hexadecimal digits. Represent an empty token as -`~`; a literal tilde is encoded as `%7E`. Dots and percent signs are escaped, -so a literal dot in a name cannot be confused with a path-token separator. -This encoding is part of the type String, not an instruction to dereference it. +identify that mapping unambiguously, the publication configuration MUST +select it explicitly. A Producer MUST NOT infer the mapping solely from +matching names or select the first TM link. + +For an eligible HTTPS TM identifier, derive the type as follows: + +1. Require an ASCII DNS hostname without empty labels or a trailing root dot. + The identifier cannot contain user information, a port, a query, or a + fragment for this derivation. +2. Lowercase the hostname, reverse its labels, and join them with dots. +3. Append the TM identifier's path, replacing each `/` with `.`. +4. For an affordance event, append the model-affordance JSON Pointer path. + Decode its [RFC 6901][JSON Pointer] token escaping, then replace each `/` + with `.`. For example, TM `https://models.example.com/tm/pump` and model affordance `/properties/processData` produce: ```text -com.example.models.%2Ftm%2Fpump.affordance.properties.processData +com.example.models.tm.pump.properties.processData ``` -Changing the encoded TM suffix, governing base or model-affordance path -changes this derived type; changing only the hostname's letter case does not. -The Producer MUST NOT change that derivation merely because a different TD -instance or registry location is used. Versioning the payload schema is -separate from naming a semantic event type. +For a whole-Thing event, use the TM-derived type without an affordance path. +The publication profile defines how interaction data is aggregated into the +payload. -A TD can be published without a governing TM. This extension does not invent -one: publications without a TM-derived type follow Core's producer-defined -type rules. A profile requiring TM-derived types MUST reject missing or -ambiguous model mappings rather than inventing a base. +The Producer MUST configure a valid complete type when the derivation is +unavailable, produces an invalid symbol, or collides with a different event +contract in the Producer's type namespace. A slash-to-dot conversion is not +reversible: `/tm/pump.v1` and `/tm/pump/v1` produce the same suffix. The +Producer MUST disambiguate such contracts by configuration rather than +silently rewriting identifiers or member names. ### subject -The [`subject`](../spec.md#subject), when present, MUST be an RFC 6901 JSON -Pointer to the concrete interaction affordance within the referenced TD, -such as `/properties/processData` or `/events/alarm`. It is not a URI -fragment and MUST NOT be prefixed with `#`. +The [`subject`](../spec.md#subject) is REQUIRED. It MUST be an +[RFC 6901][JSON Pointer] URI-fragment pointer evaluated against the resolved +TD. For an event describing the whole Thing, the Producer MUST use `#`. For +an affordance event, the pointer MUST identify the concrete affordance in the +resolved TD, for example `#/properties/processData` or `#/events/alarm`. -For a publication describing the Thing as a whole, the Producer MUST omit -`subject` rather than use an empty pointer or an arbitrary affordance. +The Producer MUST escape JSON Pointer tokens before applying URI-fragment +encoding. For example, the property name `pressure/bar` is addressed by +`#/properties/pressure~1bar`. The Consumer MUST report a failure and leave +the event uncorrelated if the pointer is invalid or does not select the +intended member of the resolved view. -The subject identifies an affordance, not an occurrence. Different events -from that affordance can have the same subject and different CloudEvents -`id` values. A binding-specific writer identifier or native occurrence -identifier is not appended to this pointer. +The subject identifies the Thing or affordance, not an occurrence. Different +events can have the same subject and different CloudEvents `id` values. A +binding-specific writer identifier or native occurrence identifier is not +appended to this pointer. -The concrete TD path and the corresponding TM declaration path can differ. -This extension does not infer a declaration-lineage mapping between them. +The concrete TD path and corresponding TM declaration path can differ. This +profile does not infer a declaration-lineage mapping between them. ### dataschema and datacontenttype @@ -257,63 +247,30 @@ any encoding or envelope selected by the binding. An xRegistry Schema Resource's document-view URL can identify that external schema. It is not the WoT Resource's metadata-view URL. A profile MAY require -an exact schema Version independently of `wotversion`. +an exact schema Version independently of the `wotversion` query parameter. ### Relationship to Other Extensions -This extension does not redefine native message types, sequence counters, -writer identities or source identities. A profile that also uses the -[OPC UA extension](opcua.md) MUST specify how its TM-derived type and -affordance subject interact with that extension's existing type and subject -mappings. Merely adding `wotid` does not permit overriding them or establish -conformance to the OPC UA extension. +This profile does not redefine native message types, sequence counters, or +writer identities. An event that also uses the [OPC UA extension](opcua.md) +MUST follow that extension's conditional WoT mapping. ## Examples ### Single Affordance -The following CloudEvents describe the same pump and affordance. Both use the -authored TD's stable `wotid`; their publication profiles select the authored -and resolved representations, respectively. +This CloudEvent identifies a pump and its `processData` property. The +configured resolver selects version `3.0.0` of the resolved TD. ```json { "specversion": "1.0", "id": "sample-4711", - "source": "urn:example:plant:publisher", - "type": "com.example.models.%2Ftm%2Fpump.affordance.properties.processData", - "subject": "/properties/processData", - "datacontenttype": "application/json", - "dataschema": "https://example.com/schemas/pump-process-data/versions/3", - "wotid": "urn:example:pump:7", - "wotversion": "3", - "data": { - "Speed": 1500, - "Pressure": 4.5 - } -} -``` - -For an authored TD selected by `wotid`, the Consumer resolves revision `3` -through its configured resolver. The subject selects that TD's `processData` -property. In the JSON event format, `data` is the CloudEvent's payload member; -it is not named after the affordance. Here, `data` directly contains that -property's object value without a `processData` wrapper. - -The resolved-view form uses the same event attributes. Its publication profile -selects the resolved TD representation for `wotid`: - -```json -{ - "specversion": "1.0", - "id": "sample-4712", - "source": "urn:example:plant:publisher", - "type": "com.example.models.%2Ftm%2Fpump.affordance.properties.processData", - "subject": "/properties/processData", + "source": "wot-td://factory/line1?wotid=urn:example:pump:7;wotversion=3.0.0", + "type": "com.example.models.tm.pump.properties.processData", + "subject": "#/properties/processData", "datacontenttype": "application/json", "dataschema": "https://example.com/schemas/pump-process-data/versions/3", - "wotid": "urn:example:pump:7", - "wotversion": "3", "data": { "Speed": 1500, "Pressure": 4.5 @@ -321,29 +278,27 @@ selects the resolved TD representation for `wotid`: } ``` -In either form, the separate schema describes the JSON object containing -`Speed` and `Pressure`; it does not describe the TD document. Another pump -with the same model and model-affordance mapping can use the same type with a -different `wotid`. +The subject selects the property in that resolved view. The payload directly +contains the property's object value without a `processData` wrapper. The +separate schema describes that object, not the TD document. Another pump can +use the same event type with a different Thing identity in its `source`. ### Whole Thing -This CloudEvent carries aggregated data for the pump described by the same TD. -The publication profile selects the authored TD and defines a payload containing -the `processData` and `running` property values. The type uses the governing TM's -base without an affordance suffix, and `subject` is absent because the event is -not scoped to one affordance. +This CloudEvent carries aggregated data for the same pump. Its unversioned +source selects the current default resolved TD, which this example assumes is +version `3.0.0`. The publication profile defines a payload containing the +`processData` and `running` property values. The subject selects the whole TD. ```json { "specversion": "1.0", "id": "sample-4713", - "source": "urn:example:plant:publisher", - "type": "com.example.models.%2Ftm%2Fpump", + "source": "wot-td://factory/line1?wotid=urn:example:pump:7", + "type": "com.example.models.tm.pump", + "subject": "#", "datacontenttype": "application/json", "dataschema": "https://example.com/schemas/pump-state/versions/3", - "wotid": "urn:example:pump:7", - "wotversion": "3", "data": { "processData": { "Speed": 1500, @@ -354,7 +309,8 @@ not scoped to one affordance. } ``` -`wotid` and `wotversion` select the TD used to interpret these property values. +The source selects the TD used to interpret these property values. If the +default changes, a later resolution of this event can select another version. The TD document itself is not the payload. `dataschema` identifies the schema of the aggregate JSON object, not the TD or either property's DataSchema alone. @@ -366,13 +322,14 @@ MUST NOT authorize arbitrary network access, redirects or credential forwarding. Implementations SHOULD bound document sizes and dependency resolution. -The Producer controls these attributes unless another mechanism authenticates -them. A matching digest does not establish that a TD, its model lineage or a -schema is authorized for the Consumer's use. Exact revisions and digests do -not grant access to referenced documents or make their dependencies available. +The Producer controls the context attributes unless another mechanism +authenticates them. A version does not establish that a TD, its model lineage, +or a schema is authorized for the Consumer's use. It does not grant access to +referenced documents or make their dependencies available. [TD]: https://www.w3.org/TR/wot-thing-description11/ [TM]: https://www.w3.org/TR/wot-thing-description11/#thing-model [IRI]: https://www.rfc-editor.org/rfc/rfc3987 +[URI]: https://www.rfc-editor.org/rfc/rfc3986 [JSON Pointer]: https://www.rfc-editor.org/rfc/rfc6901 [WoT Registry]: https://github.com/xregistry/spec/blob/main/workingdrafts/models/wot/spec.md diff --git a/tools/test_wot_examples.py b/tools/test_wot_examples.py new file mode 100644 index 000000000..03cc1a8f3 --- /dev/null +++ b/tools/test_wot_examples.py @@ -0,0 +1,85 @@ +import json +import re +from pathlib import Path +from urllib.parse import quote, unquote_to_bytes, urlsplit + + +EXTENSIONS = Path(__file__).resolve().parents[1] / "cloudevents" / "extensions" +TYPE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*") +SAFE_VALUE = "!$'()*,:@/?" + + +def _source_parameters(source): + uri = urlsplit(source) + assert uri.scheme == "wot-td" + assert uri.hostname + assert uri.username is None and uri.password is None + assert "#" not in source + parts = uri.query.split(";") + assert len(parts) in (1, 2) + values = {} + for part in parts: + name, separator, encoded = part.partition("=") + assert separator and encoded and "=" not in encoded + assert name not in values + assert not re.search(r"%(?![0-9A-Fa-f]{2})", encoded) + value = unquote_to_bytes(encoded).decode("utf-8", errors="strict") + assert quote(value, safe=SAFE_VALUE) == encoded + values[name] = value + assert list(values) in (["wotid"], ["wotid", "wotversion"]) + return values + + +def _assert_wot_context(event): + assert event["specversion"] == "1.0" + assert event["id"] + assert TYPE.fullmatch(event["type"]) + assert event["subject"] == "#" or event["subject"].startswith("#/") + assert not {"wotid", "wotversion", "wotdigest"} & event.keys() + return _source_parameters(event["source"]) + + +def test_wot_json_examples_use_canonical_core_attributes(): + text = (EXTENSIONS / "wot.md").read_text(encoding="utf-8") + examples = [ + json.loads(block) + for block in re.findall(r"```json\s*\n(.*?)\n```", text, re.DOTALL) + ] + assert len(examples) == 2 + affordance, whole_thing = examples + assert _assert_wot_context(affordance) == { + "wotid": "urn:example:pump:7", + "wotversion": "3.0.0", + } + assert _assert_wot_context(whole_thing) == {"wotid": "urn:example:pump:7"} + assert affordance["subject"] == "#/properties/processData" + assert affordance["type"] == "com.example.models.tm.pump.properties.processData" + assert affordance["data"] == {"Speed": 1500, "Pressure": 4.5} + assert whole_thing["subject"] == "#" + assert whole_thing["type"] == "com.example.models.tm.pump" + assert whole_thing["data"]["processData"] == affordance["data"] + + +def test_opcua_binary_example_retains_native_writer_and_uint32_sequence(): + text = (EXTENSIONS / "opcua.md").read_text(encoding="utf-8") + section = text.split("### WoT and OPC UA telemetry message", 1)[1] + example = section.split("```text", 1)[1].split("```", 1)[0] + properties = example.split("------------- User Properties ----------------", 1)[1] + properties, payload = properties.split("------------------ payload -------------------", 1) + event = dict( + line.split(": ", 1) for line in properties.strip().splitlines() if line.strip() + ) + assert _assert_wot_context(event) == { + "wotid": "urn:example:pump:7", + "wotversion": "3.0.0", + } + assert event["type"] == "com.example.models.tm.pump.properties.processData" + assert event["subject"] == "#/properties/processData" + assert event["opcuapublisherid"] == "UInt16:12" + assert event["opcuadatasetwritername"] == "PumpProcessData" + assert "opcuadatasetwriterid" not in event + assert event["opcuamessagetype"] == "ua-keyframe" + assert event["opcuasequence"] == "4294967295" + assert "sequence" not in event + payload_json = payload.split("-----------------------------------------------", 1)[0] + assert json.loads(payload_json) == {"Speed": 1500, "Pressure": 4.5} From 6842f78f530dbfb7309ec9bf8c576e354841228a Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 31 Oct 2026 05:32:41 +0100 Subject: [PATCH 09/28] docs(opcua): preserve sequence string ordering Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 13 ++++++++++--- tools/test_wot_examples.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index b630f8073..d1571ca5b 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -127,6 +127,13 @@ CloudEvents `source`. A Publisher using this mapping MUST scope `source` to the DataSetWriter's ordering scope; otherwise, it MUST NOT present the per-writer counter as the CloudEvents `sequence`. +The Sequence extension compares String values lexicographically. The Producer +MUST format the mapped counter so that string comparison preserves the event +order; padding the unsigned decimal value to ten digits is one way to do so. +A reset or rollover can reverse that order within the same source scope. The +Producer MUST NOT use the counter for `sequence` when it cannot preserve the +ordering defined by the Sequence extension. + When the WoT profile is also used, `subject` identifies a TD or affordance, not the DataSetWriter. `opcuasequence` therefore carries the native OPC UA counter; a CloudEvents `sequence`, if present, MUST satisfy the Sequence extension's @@ -331,7 +338,7 @@ id: 1235-1235-1235 source: urn:factory:aggregationserver:opcua datacontenttype: application/json; charset=utf-8 subject: energy-consumption-asset -sequence: 7 +sequence: 0000000007 traceparent: 4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00000011 recordedtime: 2024-03-28T21:56:43Z opcuametadatamajorversion: 672338910 @@ -406,7 +413,7 @@ id: 1236-1237-1238 source: urn:factory:aggregationserver:opcua datacontenttype: application/json; charset=utf-8 subject: energy-consumption-asset/444321 -sequence: 18 +sequence: 0000000018 traceparent: caffef3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00000011 recordedtime: 2024-03-28T21:57:01Z opcuametadatamajorversion: 672338910 @@ -449,7 +456,7 @@ source: bottling-company/amsterdam/FillingArea1/FillingLine9/Cell1/Conveyor datacontenttype: application/avro subject: energy-consumption-asset dataschema: http://example.com/schemas/energy-consumption-asset/v1.8 -sequence: 3141 +sequence: 0000003141 traceparent: 22222f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00000011 recordedtime: 2024-03-28T23:59:59Z .... further attributes ... diff --git a/tools/test_wot_examples.py b/tools/test_wot_examples.py index 03cc1a8f3..283913fc9 100644 --- a/tools/test_wot_examples.py +++ b/tools/test_wot_examples.py @@ -83,3 +83,27 @@ def test_opcua_binary_example_retains_native_writer_and_uint32_sequence(): assert "sequence" not in event payload_json = payload.split("-----------------------------------------------", 1)[0] assert json.loads(payload_json) == {"Speed": 1500, "Pressure": 4.5} + + +def test_ua_only_sequence_examples_preserve_lexicographic_order(): + text = (EXTENSIONS / "opcua.md").read_text(encoding="utf-8") + sections = [ + "### Telemetry message\n", + "### Event message\n", + "### Telemetry message with different Encoding\n", + ] + examples = [ + text.split(heading, 1)[1].split("\n### ", 1)[0] for heading in sections + ] + sequences = [ + re.search(r"^sequence: (\S+)$", example, re.MULTILINE).group(1) + for example in examples + ] + sources = [ + re.search(r"^source: (\S+)$", example, re.MULTILINE).group(1) + for example in examples + ] + assert sequences == ["0000000007", "0000000018", "0000003141"] + assert [int(sequence) for sequence in sequences] == [7, 18, 3141] + assert sources[0] == sources[1] + assert sequences[0] < sequences[1] From b3784a2f646dbd066073ab9d58d4b7fe7ea193c2 Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 31 Oct 2026 09:30:42 +0100 Subject: [PATCH 10/28] docs(wot): generalize source URI mapping Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 2 +- cloudevents/extensions/wot.md | 99 +++++++++++++++++---------------- tools/test_wot_examples.py | 74 +++++++++++++++++++++--- 3 files changed, 120 insertions(+), 55 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index d1571ca5b..f9e7a967b 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -488,7 +488,7 @@ Content Type: application/json specversion: 1.0 id: ua-message-25460 -source: wot-td://factory/line1?wotid=urn:example:pump:7;wotversion=3.0.0 +source: https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site=north type: com.example.models.tm.pump.properties.processData subject: #/properties/processData time: 2026-10-29T12:00:00Z diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 714caa955..f7117668b 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -5,9 +5,10 @@ Core context attributes. The `source` identifies the Thing and, when provided, a version of its resolved TD. The `subject` identifies the part of that TD associated with the event, and the `type` names the event contract. -The profile defines no CloudEvents extension attributes. Its use is identified -by the `wot-td` scheme in `source`. It does not define a transport binding, -provision a Thing, or require a particular registry implementation. +The profile defines no CloudEvents extension attributes or URI scheme. +Producers and Consumers agree on its use through a publication profile or out +of band. It does not define a transport binding, provision a Thing, or require +a particular registry implementation. ## Notational Conventions @@ -21,56 +22,59 @@ attribute marked REQUIRED here is not REQUIRED in every CloudEvent. ## Source -The Core [`source`](../spec.md#source) identifies a Thing within a configured -logical topology. It uses this absolute URI form: +The Core [`source`](../spec.md#source) identifies a Thing within the Producer's +chosen context. It MUST contain the `wotid` query parameter and MAY contain +`wotversion` and additional parameters. This profile does not prescribe the URI +scheme or the rest of the URI-reference. Core permits relative URI-references +and recommends an absolute URI. + +For example: ```text -wot-td://?wotid=[;wotversion=] +https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site=north ``` -For example: +The `site` parameter is defined by the Producer, not this profile. An HTTPS +source is an identifier and is not necessarily a retrieval URL for a TD. +Another possible form is: ```text wot-td://factory/line1?wotid=urn:example:pump:7;wotversion=3.0.0 ``` -`wotid` and `wotversion` are query parameters, not CloudEvents attributes. -The `wot-td` scheme is proposed and is not currently registered with IANA. -The URI is an identifier for a configured resolver; it is not an HTTP registry -URL and does not itself specify where to retrieve a TD. - -### Topology - -The Producer configures the topology. It consists of a non-empty authority -followed by an OPTIONAL hierarchical path. Both MUST conform to -[RFC 3986][URI]. The authority MUST contain a non-empty host and MUST NOT -contain user information. The host need not be a resolvable DNS name. -Implementations MUST apply RFC 3986 host case rules and MUST NOT change the -case of the path when interpreting the topology. - -The topology can represent, for example, a site and production line. It is -not the Thing's identity or a registry Resource identifier. +This profile does not define the `wot-td` scheme or propose its registration. +The part of `source` outside the standardized parameters can identify a +topology, publisher, or another Producer-defined context. This profile does +not prescribe its structure or derive a TD retrieval location from it. ### Query syntax and encoding -The query contains `wotid` followed by an OPTIONAL `wotversion`. The literal -parameter names are case-sensitive, and `;` separates the parameters. Each -parameter consists of its name, one `=`, and a non-empty value. No other -parameters are defined. - -The value's unescaped characters are the RFC 3986 `unreserved` characters -and `!`, `$`, `'`, `(`, `)`, `*`, `,`, `:`, `@`, `/`, and `?`. A Producer -MUST leave these characters unescaped. It MUST percent-encode the UTF-8 bytes -of all other characters using uppercase hexadecimal digits. -In particular, a literal `%`, `;`, `=`, `&`, `+`, or `#` in a value MUST be -encoded. A Producer MUST NOT use form-query encoding or encode a space as `+`. - -A Consumer MUST split the query at `;` before decoding the values. It MUST -reject an unknown or duplicate parameter, a missing or empty `wotid`, an empty -`wotversion`, an invalid percent escape, invalid UTF-8, an out-of-order -parameter, an unescaped character outside the permitted set, or a URI with an -outer fragment. It MUST percent-decode each value exactly once and MUST NOT -interpret `+` as a space. +The `source` query uses `;` as the parameter separator. This profile +standardizes two query parameters, not CloudEvents attributes: `wotid` is +REQUIRED and appears first; `wotversion` is OPTIONAL and, when present, +immediately follows `wotid`. Their names are case-sensitive and MUST appear +literally. Each MUST use `name=value` with a non-empty value. `wotid` MUST +appear exactly once, and `wotversion` MUST NOT appear more than once. + +A Producer MAY include additional parameters after the standardized parameters. +This profile does not assign them a meaning; a publication contract MAY define +their semantics. A Consumer MUST NOT reject a source solely because it contains +parameters not defined by this profile. + +For the standardized values, the unescaped characters are the +[RFC 3986][URI] `unreserved` characters and `!`, `$`, `'`, `(`, `)`, `*`, +`,`, `:`, `@`, `/`, and `?`. A Producer MUST leave these characters +unescaped. It MUST percent-encode the UTF-8 bytes of all other characters using +uppercase hexadecimal digits. In particular, a literal `%`, `;`, `=`, `&`, +`+`, or `#` in a standardized value MUST be encoded. A Producer MUST NOT use +form-query encoding or encode a space as `+` for those values. + +A Consumer MUST split the query at `;` before decoding the standardized values. +It MUST reject a missing or empty `wotid`, an empty `wotversion`, a duplicate +standardized parameter, or a standardized parameter in the wrong position. For +a standardized value, it MUST reject an invalid percent escape, invalid UTF-8, +or an unescaped character outside the permitted set. It MUST percent-decode +each standardized value exactly once and MUST NOT interpret `+` as a space. For example, an identifier containing U+00E9 followed by the literal characters `%2F` encodes that portion as `%C3%A9%252F`. One decoding pass recovers the @@ -105,10 +109,11 @@ than the one available when the event was produced. A publication profile that requires repeatable replay MUST require `wotversion`. Core requires `source` and `id` together to identify each distinct event. -Changing the topology or version changes the `source` and therefore its event -identity and [Sequence extension](sequence.md) scope. Returning to an earlier -`source` returns to its existing scope: Producers MUST NOT reuse event -identities or restart a claimed source-wide sequence within that scope. +Changing the `source`, including its context, version, or other parameters, +changes the event identity and [Sequence extension](sequence.md) scope. +Returning to an earlier `source` returns to its existing scope: Producers MUST +NOT reuse event identities or restart a claimed source-wide sequence within +that scope. ## Resolving the Thing Description @@ -266,7 +271,7 @@ configured resolver selects version `3.0.0` of the resolved TD. { "specversion": "1.0", "id": "sample-4711", - "source": "wot-td://factory/line1?wotid=urn:example:pump:7;wotversion=3.0.0", + "source": "https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site=north", "type": "com.example.models.tm.pump.properties.processData", "subject": "#/properties/processData", "datacontenttype": "application/json", @@ -294,7 +299,7 @@ version `3.0.0`. The publication profile defines a payload containing the { "specversion": "1.0", "id": "sample-4713", - "source": "wot-td://factory/line1?wotid=urn:example:pump:7", + "source": "https://factory.example.com/line1?wotid=urn:example:pump:7;site=north", "type": "com.example.models.tm.pump", "subject": "#", "datacontenttype": "application/json", diff --git a/tools/test_wot_examples.py b/tools/test_wot_examples.py index 283913fc9..705fe3390 100644 --- a/tools/test_wot_examples.py +++ b/tools/test_wot_examples.py @@ -3,33 +3,90 @@ from pathlib import Path from urllib.parse import quote, unquote_to_bytes, urlsplit +import pytest + EXTENSIONS = Path(__file__).resolve().parents[1] / "cloudevents" / "extensions" TYPE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*") SAFE_VALUE = "!$'()*,:@/?" +STANDARD_PARAMETERS = {"wotid", "wotversion"} def _source_parameters(source): + assert source uri = urlsplit(source) - assert uri.scheme == "wot-td" - assert uri.hostname - assert uri.username is None and uri.password is None - assert "#" not in source parts = uri.query.split(";") - assert len(parts) in (1, 2) values = {} - for part in parts: + for index, part in enumerate(parts): name, separator, encoded = part.partition("=") + if name not in STANDARD_PARAMETERS: + continue + assert (name == "wotid" and index == 0) or ( + name == "wotversion" and index == 1 and "wotid" in values + ) assert separator and encoded and "=" not in encoded assert name not in values assert not re.search(r"%(?![0-9A-Fa-f]{2})", encoded) value = unquote_to_bytes(encoded).decode("utf-8", errors="strict") assert quote(value, safe=SAFE_VALUE) == encoded values[name] = value - assert list(values) in (["wotid"], ["wotid", "wotversion"]) + assert "wotid" in values return values +@pytest.mark.parametrize( + "source, expected", + ( + ( + "https://factory.example.com/line1?wotid=urn:example:pump:7" + ";wotversion=3.0.0;site=north", + {"wotid": "urn:example:pump:7", "wotversion": "3.0.0"}, + ), + ( + "wot-td://factory/line1?wotid=urn:example:pump:7;" + "wotversion=3.0.0;site=north", + {"wotid": "urn:example:pump:7", "wotversion": "3.0.0"}, + ), + ( + "line1?wotid=urn:example:pump:7;site=north;site=south#details", + {"wotid": "urn:example:pump:7"}, + ), + ( + "custom:line1?wotid=urn:example:pump:7;site=north", + {"wotid": "urn:example:pump:7"}, + ), + ( + "?wotid=urn:example:caf%C3%A9%252F;other=a=b;flag", + {"wotid": "urn:example:caf\u00e9%2F"}, + ), + ), +) +def test_source_parameters_allow_other_context_and_parameters(source, expected): + assert _source_parameters(source) == expected + + +@pytest.mark.parametrize( + "query, error", + ( + ("site=north", AssertionError), + ("site=north;wotid=urn:example:pump:7", AssertionError), + ("wotid=", AssertionError), + ("wotid=urn:example:pump:7;wotversion=", AssertionError), + ("wotid=urn:example:pump:7;wotid=urn:example:pump:8", AssertionError), + ("wotid=urn:example:pump:7;wotversion=1;wotversion=2", AssertionError), + ("wotversion=1;wotid=urn:example:pump:7", AssertionError), + ("wotid=urn:example:pump:7;site=north;wotversion=1", AssertionError), + ("wotid=urn:example:pump:7&wotversion=1", AssertionError), + ("wotid=urn:example:pump%2", AssertionError), + ("wotid=urn:example:pump%FF", UnicodeDecodeError), + ("wotid=urn:example:pump+7", AssertionError), + ), +) +def test_source_parameters_reject_invalid_standard_parameters(query, error): + with pytest.raises(error): + _source_parameters(f"https://factory.example.com/line1?{query}") + + def _assert_wot_context(event): assert event["specversion"] == "1.0" assert event["id"] @@ -52,6 +109,8 @@ def test_wot_json_examples_use_canonical_core_attributes(): "wotversion": "3.0.0", } assert _assert_wot_context(whole_thing) == {"wotid": "urn:example:pump:7"} + assert urlsplit(affordance["source"]).query.endswith(";site=north") + assert urlsplit(whole_thing["source"]).query.endswith(";site=north") assert affordance["subject"] == "#/properties/processData" assert affordance["type"] == "com.example.models.tm.pump.properties.processData" assert affordance["data"] == {"Speed": 1500, "Pressure": 4.5} @@ -73,6 +132,7 @@ def test_opcua_binary_example_retains_native_writer_and_uint32_sequence(): "wotid": "urn:example:pump:7", "wotversion": "3.0.0", } + assert urlsplit(event["source"]).query.endswith(";site=north") assert event["type"] == "com.example.models.tm.pump.properties.processData" assert event["subject"] == "#/properties/processData" assert event["opcuapublisherid"] == "UInt16:12" From 8552ac425f29ac4ddf411e35fbe077edf3816fad Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 31 Oct 2026 09:30:42 +0100 Subject: [PATCH 11/28] docs: remove dead UNCTAD spreadsheet link Signed-off-by: Marc --- cloudevents/extensions/data-classification.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cloudevents/extensions/data-classification.md b/cloudevents/extensions/data-classification.md index e1d10a13e..087c5e61e 100644 --- a/cloudevents/extensions/data-classification.md +++ b/cloudevents/extensions/data-classification.md @@ -96,11 +96,10 @@ Examples where data classification of events can be useful are: ## Appendix: Data Protection and Privacy Regulations -For reference purposes, a catalog of common data protection and privacy -regulation and abbreviations is available from [UNCTAD +For reference purposes, information on data protection and privacy legislation +worldwide is available from [UNCTAD (United Nations Conference on Trade and Development)](https://unctad.org/page/data-protection-and-privacy-legislation-worldwide). -Others might exist. Some examples include: - `GDPR` - General Data Protection Regulation, Europe From 67f61982328a39659f8587f2cb1ad71a4df28cea Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 31 Oct 2026 09:35:09 +0100 Subject: [PATCH 12/28] docs(wot): add MQTT source example Signed-off-by: Marc --- cloudevents/extensions/wot.md | 7 +++++++ tools/test_wot_examples.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index f7117668b..89c79cc06 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -36,6 +36,13 @@ https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site The `site` parameter is defined by the Producer, not this profile. An HTTPS source is an identifier and is not necessarily a retrieval URL for a TD. + +An MQTT source identifier can use the same query parameters: + +```text +mqtt://site/line/cell/device/sensor?wotid=urn:example:sensor:7;wotversion=3.0.0 +``` + Another possible form is: ```text diff --git a/tools/test_wot_examples.py b/tools/test_wot_examples.py index 705fe3390..7f8982a6f 100644 --- a/tools/test_wot_examples.py +++ b/tools/test_wot_examples.py @@ -65,6 +65,24 @@ def test_source_parameters_allow_other_context_and_parameters(source, expected): assert _source_parameters(source) == expected +def test_mqtt_source_example_uses_standard_parameters(): + text = (EXTENSIONS / "wot.md").read_text(encoding="utf-8") + section = text.split("## Source\n", 1)[1].split("### Query syntax", 1)[0] + examples = re.findall(r"```text\s*\n(mqtt://[^\n]+)\n```", section) + assert len(examples) == 1 + source = examples[0] + uri = urlsplit(source) + assert (uri.scheme, uri.netloc, uri.path) == ( + "mqtt", + "site", + "/line/cell/device/sensor", + ) + assert _source_parameters(source) == { + "wotid": "urn:example:sensor:7", + "wotversion": "3.0.0", + } + + @pytest.mark.parametrize( "query, error", ( From d9c65271c55509e0999a2ced2fa3e7fb9c8013e0 Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 31 Oct 2026 09:38:42 +0100 Subject: [PATCH 13/28] Revert unrelated classification and tool changes Signed-off-by: Marc --- cloudevents/extensions/data-classification.md | 9 +- tools/test_verify.py | 18 -- tools/test_wot_examples.py | 187 ------------------ tools/verify.py | 3 - 4 files changed, 6 insertions(+), 211 deletions(-) delete mode 100644 tools/test_wot_examples.py diff --git a/cloudevents/extensions/data-classification.md b/cloudevents/extensions/data-classification.md index 087c5e61e..37c7a9b4a 100644 --- a/cloudevents/extensions/data-classification.md +++ b/cloudevents/extensions/data-classification.md @@ -96,10 +96,13 @@ Examples where data classification of events can be useful are: ## Appendix: Data Protection and Privacy Regulations -For reference purposes, information on data protection and privacy legislation -worldwide is available from [UNCTAD +For reference purposes, a catalog of common data protection and privacy +regulation and abbreviations is availble from [UNCTAD (United Nations Conference on Trade and -Development)](https://unctad.org/page/data-protection-and-privacy-legislation-worldwide). +Development)](https://unctad.org/page/data-protection-and-privacy-legislation-worldwide), +under the `DOWNLOAD FULL DATA` button ([direct +link](https://unctad.org/system/files/information-document/DP.xlsx)). Others +might exist. Some examples include: - `GDPR` - General Data Protection Regulation, Europe diff --git a/tools/test_verify.py b/tools/test_verify.py index 061dc8280..3698e277b 100644 --- a/tools/test_verify.py +++ b/tools/test_verify.py @@ -14,7 +14,6 @@ _is_text_all_uppercase, _plain_text_issues, _render_markdown_to_html, - _uri_availability_issues, ) @@ -188,23 +187,6 @@ def test_rtl_unicode_must_be_rendered_in_the_id(): ) -@pytest.mark.asyncio -@pytest.mark.parametrize( - "uri", - ( - "https://rfc-editor.org/rfc/rfc3987", - "https://www.rfc-editor.org/info/rfc2119", - ), -) -async def test_rfc_editor_links_skip_network_check(monkeypatch, uri): - monkeypatch.setattr( - "verify.ClientSession", - lambda: pytest.fail("RFC Editor links must not be fetched"), - ) - - assert await _uri_availability_issues(uri, Settings(excluded_paths=set())) == [] - - @pytest.mark.asyncio async def test_app(monkeypatch): monkeypatch.chdir(str(Path(__file__).parent)) diff --git a/tools/test_wot_examples.py b/tools/test_wot_examples.py deleted file mode 100644 index 7f8982a6f..000000000 --- a/tools/test_wot_examples.py +++ /dev/null @@ -1,187 +0,0 @@ -import json -import re -from pathlib import Path -from urllib.parse import quote, unquote_to_bytes, urlsplit - -import pytest - - -EXTENSIONS = Path(__file__).resolve().parents[1] / "cloudevents" / "extensions" -TYPE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*") -SAFE_VALUE = "!$'()*,:@/?" -STANDARD_PARAMETERS = {"wotid", "wotversion"} - - -def _source_parameters(source): - assert source - uri = urlsplit(source) - parts = uri.query.split(";") - values = {} - for index, part in enumerate(parts): - name, separator, encoded = part.partition("=") - if name not in STANDARD_PARAMETERS: - continue - assert (name == "wotid" and index == 0) or ( - name == "wotversion" and index == 1 and "wotid" in values - ) - assert separator and encoded and "=" not in encoded - assert name not in values - assert not re.search(r"%(?![0-9A-Fa-f]{2})", encoded) - value = unquote_to_bytes(encoded).decode("utf-8", errors="strict") - assert quote(value, safe=SAFE_VALUE) == encoded - values[name] = value - assert "wotid" in values - return values - - -@pytest.mark.parametrize( - "source, expected", - ( - ( - "https://factory.example.com/line1?wotid=urn:example:pump:7" - ";wotversion=3.0.0;site=north", - {"wotid": "urn:example:pump:7", "wotversion": "3.0.0"}, - ), - ( - "wot-td://factory/line1?wotid=urn:example:pump:7;" - "wotversion=3.0.0;site=north", - {"wotid": "urn:example:pump:7", "wotversion": "3.0.0"}, - ), - ( - "line1?wotid=urn:example:pump:7;site=north;site=south#details", - {"wotid": "urn:example:pump:7"}, - ), - ( - "custom:line1?wotid=urn:example:pump:7;site=north", - {"wotid": "urn:example:pump:7"}, - ), - ( - "?wotid=urn:example:caf%C3%A9%252F;other=a=b;flag", - {"wotid": "urn:example:caf\u00e9%2F"}, - ), - ), -) -def test_source_parameters_allow_other_context_and_parameters(source, expected): - assert _source_parameters(source) == expected - - -def test_mqtt_source_example_uses_standard_parameters(): - text = (EXTENSIONS / "wot.md").read_text(encoding="utf-8") - section = text.split("## Source\n", 1)[1].split("### Query syntax", 1)[0] - examples = re.findall(r"```text\s*\n(mqtt://[^\n]+)\n```", section) - assert len(examples) == 1 - source = examples[0] - uri = urlsplit(source) - assert (uri.scheme, uri.netloc, uri.path) == ( - "mqtt", - "site", - "/line/cell/device/sensor", - ) - assert _source_parameters(source) == { - "wotid": "urn:example:sensor:7", - "wotversion": "3.0.0", - } - - -@pytest.mark.parametrize( - "query, error", - ( - ("site=north", AssertionError), - ("site=north;wotid=urn:example:pump:7", AssertionError), - ("wotid=", AssertionError), - ("wotid=urn:example:pump:7;wotversion=", AssertionError), - ("wotid=urn:example:pump:7;wotid=urn:example:pump:8", AssertionError), - ("wotid=urn:example:pump:7;wotversion=1;wotversion=2", AssertionError), - ("wotversion=1;wotid=urn:example:pump:7", AssertionError), - ("wotid=urn:example:pump:7;site=north;wotversion=1", AssertionError), - ("wotid=urn:example:pump:7&wotversion=1", AssertionError), - ("wotid=urn:example:pump%2", AssertionError), - ("wotid=urn:example:pump%FF", UnicodeDecodeError), - ("wotid=urn:example:pump+7", AssertionError), - ), -) -def test_source_parameters_reject_invalid_standard_parameters(query, error): - with pytest.raises(error): - _source_parameters(f"https://factory.example.com/line1?{query}") - - -def _assert_wot_context(event): - assert event["specversion"] == "1.0" - assert event["id"] - assert TYPE.fullmatch(event["type"]) - assert event["subject"] == "#" or event["subject"].startswith("#/") - assert not {"wotid", "wotversion", "wotdigest"} & event.keys() - return _source_parameters(event["source"]) - - -def test_wot_json_examples_use_canonical_core_attributes(): - text = (EXTENSIONS / "wot.md").read_text(encoding="utf-8") - examples = [ - json.loads(block) - for block in re.findall(r"```json\s*\n(.*?)\n```", text, re.DOTALL) - ] - assert len(examples) == 2 - affordance, whole_thing = examples - assert _assert_wot_context(affordance) == { - "wotid": "urn:example:pump:7", - "wotversion": "3.0.0", - } - assert _assert_wot_context(whole_thing) == {"wotid": "urn:example:pump:7"} - assert urlsplit(affordance["source"]).query.endswith(";site=north") - assert urlsplit(whole_thing["source"]).query.endswith(";site=north") - assert affordance["subject"] == "#/properties/processData" - assert affordance["type"] == "com.example.models.tm.pump.properties.processData" - assert affordance["data"] == {"Speed": 1500, "Pressure": 4.5} - assert whole_thing["subject"] == "#" - assert whole_thing["type"] == "com.example.models.tm.pump" - assert whole_thing["data"]["processData"] == affordance["data"] - - -def test_opcua_binary_example_retains_native_writer_and_uint32_sequence(): - text = (EXTENSIONS / "opcua.md").read_text(encoding="utf-8") - section = text.split("### WoT and OPC UA telemetry message", 1)[1] - example = section.split("```text", 1)[1].split("```", 1)[0] - properties = example.split("------------- User Properties ----------------", 1)[1] - properties, payload = properties.split("------------------ payload -------------------", 1) - event = dict( - line.split(": ", 1) for line in properties.strip().splitlines() if line.strip() - ) - assert _assert_wot_context(event) == { - "wotid": "urn:example:pump:7", - "wotversion": "3.0.0", - } - assert urlsplit(event["source"]).query.endswith(";site=north") - assert event["type"] == "com.example.models.tm.pump.properties.processData" - assert event["subject"] == "#/properties/processData" - assert event["opcuapublisherid"] == "UInt16:12" - assert event["opcuadatasetwritername"] == "PumpProcessData" - assert "opcuadatasetwriterid" not in event - assert event["opcuamessagetype"] == "ua-keyframe" - assert event["opcuasequence"] == "4294967295" - assert "sequence" not in event - payload_json = payload.split("-----------------------------------------------", 1)[0] - assert json.loads(payload_json) == {"Speed": 1500, "Pressure": 4.5} - - -def test_ua_only_sequence_examples_preserve_lexicographic_order(): - text = (EXTENSIONS / "opcua.md").read_text(encoding="utf-8") - sections = [ - "### Telemetry message\n", - "### Event message\n", - "### Telemetry message with different Encoding\n", - ] - examples = [ - text.split(heading, 1)[1].split("\n### ", 1)[0] for heading in sections - ] - sequences = [ - re.search(r"^sequence: (\S+)$", example, re.MULTILINE).group(1) - for example in examples - ] - sources = [ - re.search(r"^source: (\S+)$", example, re.MULTILINE).group(1) - for example in examples - ] - assert sequences == ["0000000007", "0000000018", "0000003141"] - assert [int(sequence) for sequence in sequences] == [7, 18, 3141] - assert sources[0] == sources[1] - assert sequences[0] < sequences[1] diff --git a/tools/verify.py b/tools/verify.py index c25a7c968..f7f93f155 100644 --- a/tools/verify.py +++ b/tools/verify.py @@ -8,7 +8,6 @@ from http import HTTPStatus from pathlib import Path from typing import Iterable, List, NewType, Optional, Sequence, Set, Tuple, TypeVar -from urllib.parse import urlsplit from aiohttp import ClientSession from bs4 import BeautifulSoup @@ -137,8 +136,6 @@ async def _uri_availability_issues(uri: HttpUri, settings: Settings) -> Sequence if "example.com" in uri: return [] if "ietf.org" in uri: return [] if "rfc-edit.org" in uri: return [] - if urlsplit(uri).hostname in {"rfc-editor.org", "www.rfc-editor.org"}: - return [] if "iso20022.org" in uri: return [] try: From 376091f0a5079b6f791573799472d2181f1dbb41 Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 31 Oct 2026 09:50:34 +0100 Subject: [PATCH 14/28] docs: update UNCTAD legislation reference Signed-off-by: Marc --- cloudevents/extensions/data-classification.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cloudevents/extensions/data-classification.md b/cloudevents/extensions/data-classification.md index 37c7a9b4a..53e52c697 100644 --- a/cloudevents/extensions/data-classification.md +++ b/cloudevents/extensions/data-classification.md @@ -96,13 +96,10 @@ Examples where data classification of events can be useful are: ## Appendix: Data Protection and Privacy Regulations -For reference purposes, a catalog of common data protection and privacy -regulation and abbreviations is availble from [UNCTAD -(United Nations Conference on Trade and -Development)](https://unctad.org/page/data-protection-and-privacy-legislation-worldwide), -under the `DOWNLOAD FULL DATA` button ([direct -link](https://unctad.org/system/files/information-document/DP.xlsx)). Others -might exist. +[UNCTAD (United Nations Conference on Trade and +Development)](https://unctad.org/page/data-protection-and-privacy-legislation-worldwide) +provides a [catalog of legislation by country](https://storage.unctad.org/2026-cyberlaw_tracker/assets/data/document_links.json), +including data protection and privacy regulations. Some examples include: - `GDPR` - General Data Protection Regulation, Europe From 47ad4382fc9374c35b1061f8d55f1a0087aae5b5 Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 31 Oct 2026 11:17:03 +0100 Subject: [PATCH 15/28] docs(wot): version event contracts in types Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 2 +- cloudevents/extensions/wot.md | 45 ++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index f9e7a967b..54110bdf9 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -489,7 +489,7 @@ Content Type: application/json specversion: 1.0 id: ua-message-25460 source: https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site=north -type: com.example.models.tm.pump.properties.processData +type: com.example.models.tm.pump.properties.processData.v1 subject: #/properties/processData time: 2026-10-29T12:00:00Z dataschema: https://example.com/schemas/pump-process-data/versions/3 diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 89c79cc06..0025b8ba1 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -167,17 +167,20 @@ resolution does not by itself prove that retrieved bytes are unchanged. ### type -The Core [`type`](../spec.md#type) identifies the event contract. Two Things -can produce events of the same type when their interactions follow the same -model. This profile uses a dot-separated symbol so a Consumer can map the type -to a code-generation symbol. +The Core [`type`](../spec.md#type) identifies the event contract. The contract +is the payload representation defined by the publication profile or protocol +binding, including its members, member types, and semantics. Two Things can +produce events of the same type when their event contracts are the same. This +profile uses a dot-separated symbol so a Consumer can map the type to a +code-generation symbol. The type MUST contain one or more segments separated by single dots. Each segment MUST start with an ASCII letter or underscore and contain only ASCII letters, digits, or underscores thereafter. These character restrictions apply only to `type`; they do not change the Thing identifier, TD member names, `source`, `subject`, or payload. A code generator remains responsible for -language-specific keywords. +language-specific keywords. The final segment MUST identify the event-contract +revision as `vN`, where `N` is a positive decimal integer without leading zeros. A Producer MAY configure the complete type. Otherwise, it derives the type from a governing [Thing Model][TM] (TM) and, for an affordance event, the @@ -196,17 +199,35 @@ For an eligible HTTPS TM identifier, derive the type as follows: 4. For an affordance event, append the model-affordance JSON Pointer path. Decode its [RFC 6901][JSON Pointer] token escaping, then replace each `/` with `.`. +5. Append the event-contract revision as the final segment. For example, TM `https://models.example.com/tm/pump` and model affordance -`/properties/processData` produce: +`/properties/processData` with event-contract revision 1 produce: ```text -com.example.models.tm.pump.properties.processData +com.example.models.tm.pump.properties.processData.v1 ``` -For a whole-Thing event, use the TM-derived type without an affordance path. -The publication profile defines how interaction data is aggregated into the -payload. +For a whole-Thing event, use the TM-derived type without an affordance path, +then append the revision. The publication profile defines how interaction data +is aggregated into the payload. + +The publication profile MUST assign the same revision to the same event +contract. A Producer MUST use a new revision when the contract changes, +including when a member is added or removed, a member's type or meaning +changes, or the payload envelope changes. This applies even when the change is +backwardly compatible. A Producer MUST NOT reuse a revision for a different +contract with the same base type. + +For example, adding a `quality` member to +`com.example.models.tm.pump.properties.processData.v1` requires +`com.example.models.tm.pump.properties.processData.v2`, even if Consumers can +ignore the new member. + +A change to an individual value or the presence of a member already defined as +OPTIONAL does not change the contract revision. A change to `wotversion` or +`dataschema` alone also does not change the revision; if it changes the payload +contract, the Producer MUST use a new revision. The Producer MUST configure a valid complete type when the derivation is unavailable, produces an invalid symbol, or collides with a different event @@ -279,7 +300,7 @@ configured resolver selects version `3.0.0` of the resolved TD. "specversion": "1.0", "id": "sample-4711", "source": "https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site=north", - "type": "com.example.models.tm.pump.properties.processData", + "type": "com.example.models.tm.pump.properties.processData.v1", "subject": "#/properties/processData", "datacontenttype": "application/json", "dataschema": "https://example.com/schemas/pump-process-data/versions/3", @@ -307,7 +328,7 @@ version `3.0.0`. The publication profile defines a payload containing the "specversion": "1.0", "id": "sample-4713", "source": "https://factory.example.com/line1?wotid=urn:example:pump:7;site=north", - "type": "com.example.models.tm.pump", + "type": "com.example.models.tm.pump.v1", "subject": "#", "datacontenttype": "application/json", "dataschema": "https://example.com/schemas/pump-state/versions/3", From 36411e0988c731844df4ad3640fed19aaa63852d Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 11:44:56 +0100 Subject: [PATCH 16/28] docs: align WoT and OPC UA with xRegistry Event Context Signed-off-by: Marc --- cloudevents/extensions/README.md | 1 + cloudevents/extensions/context.md | 210 ++++++++++++++++++++++++++++++ cloudevents/extensions/opcua.md | 137 +++++++++++++++++-- cloudevents/extensions/wot.md | 101 +++++++++++--- 4 files changed, 420 insertions(+), 29 deletions(-) create mode 100644 cloudevents/extensions/context.md diff --git a/cloudevents/extensions/README.md b/cloudevents/extensions/README.md index 88968c440..971bdbd4c 100644 --- a/cloudevents/extensions/README.md +++ b/cloudevents/extensions/README.md @@ -46,6 +46,7 @@ for more information. - [Dataref (Claim Check Pattern)](dataref.md) - [Deprecation](deprecation.md) - [Distributed Tracing](distributed-tracing.md) +- [Event Context](context.md) - [Expiry Time](expirytime.md) - [OPC UA](opcua.md) - [Partitioning](partitioning.md) diff --git a/cloudevents/extensions/context.md b/cloudevents/extensions/context.md new file mode 100644 index 000000000..10463ca9c --- /dev/null +++ b/cloudevents/extensions/context.md @@ -0,0 +1,210 @@ +# Event Context + +The `contexturl` extension attribute identifies shared information that a +Consumer needs to interpret an event. Instead of repeating a Message definition +URL, schema URL, and emitter description URL in every event, a Producer names +one Context that references those representations by Role. + +This proposal aligns with the [xRegistry Event Context working draft][Context +Registry] in [xregistry/spec#796][Context PR]. The attribute name and the +resolved-view media type remain provisional. A Context is a metadata-only +xRegistry Resource, not a document attached to that Resource. This extension +does not require a Producer to host or query a Registry. + +A Publisher creates Contexts and their referenced representations. A Producer +emits events, and a Consumer processes them. + +## Notational Conventions + +As with the main [CloudEvents specification](../spec.md), the key words "MUST", +"MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", +"RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as +described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +The scope of these key words is limited to events using this extension. + +## Attributes + +### contexturl + +- Type: `URI` +- Description: The absolute URL of the Context that applies to this event. +- Constraints: + - OPTIONAL. + - If present, MUST identify a Context Resource or Context Version and MUST + NOT contain a fragment. + - SHOULD identify a Context Version. + - Consumers MUST use the value unchanged as the request URL and as the URL + component of the cache key. Cache selection MUST also account for the + selected representation and authorization context. + - Consumers MUST NOT derive other URLs from the value. In particular, they + MUST NOT append `$details`, a view path, or a query parameter, or remove + `/versions/`. + - When absent, this extension supplies no Context for the event. + +In the JSON event format, `contexturl` is a top-level string member. In HTTP +binary content mode, it is the `ce-contexturl` header, encoded and decoded +according to the [HTTP binding](../bindings/http-protocol-binding.md). +Other bindings use their normal extension-attribute mapping. + +## Context Representation + +An ordinary `GET` of `contexturl` with `Accept: application/json` returns +Context metadata directly. The xRegistry Context model sets `hasdocument` to +`false`; no `$details` request or separate Context document is needed. + +The REQUIRED `references` map contains at least one entry. Each key is a Role, +and each value is an object with a REQUIRED `url` member. That member is an +absolute URL without a fragment. Consumers MUST use each referenced URL +unchanged when retrieving its representation. + +The working draft defines these Roles. + +| Role | Referenced representation | +| --- | --- | +| `message` | Message definition metadata describing the event. | +| `schema` | Schema document for the event's `data`. | +| `emitter` | Description of the emitting entity, such as a WoT Thing Description Version document. | +| `target` | Model that the event concerns, such as an OpenUSD asset. | + +Roles follow the working draft's map-key rules. Unprefixed names are reserved +for that draft. Other Roles MUST use an identifying prefix followed by `:`, +such as `acme:policy`. Consumers MUST ignore Roles they do not use and Reference +attributes they do not understand. + +Contexts and References permit extension attributes. A profile can use those +attributes for metadata shared by a message stream. Such metadata is not +automatically a CloudEvents attribute: the profile MUST define its meaning and +how it relates to attributes carried in the event. + +## Processing and Versioning + +When including `contexturl`, a Producer MUST name a Context whose references +apply to the event. A Publisher MUST make referenced representations available +before configuring Producers with the Context URL. Producers SHOULD reuse a +configured Context Version URL for events with the same shared information. +A Context with a `message` Reference applies to one event type; Producers +that emit several types use a separate Context for each type in that case. + +A Publisher MUST NOT change a Context Version's `references` after any +Producer is configured with its URL. Changing references requires a new +Version and reconfigured Producers. Profiles that store shared metadata in +Context extension attributes MUST define equivalent stability rules for those +values. + +A Context Resource URL serves the current default Version. That default can +change without changing `contexturl`, so Consumers SHOULD bound the lifetime +of cached Resource representations. A Context Version fixes its references, +not the bytes of referenced representations. Publishers SHOULD reference +explicit Target Versions when replay requires reproducible interpretation and +SHOULD retain Contexts and referenced representations for the replay period. + +A Consumer retrieves the Context from the unchanged URL or uses a valid cache +entry for that URL, representation, and authorization context. It MUST check +the response status, media type, and Context representation against the +working draft. A failed response, unsupported representation, or missing or +empty `references` map makes the Context unavailable. For a valid Context, the +Consumer retrieves the representations for the Roles it needs, subject to its +access and content-trust policies. A missing Role that the Consumer needs +makes the Context incomplete. + +When retrieval fails or the Context is invalid or incomplete, the Consumer +MUST NOT derive replacement URLs from `contexturl`. It SHOULD record the +condition. Local policy determines whether to defer, reject, or process the +event without that information, unless another profile requires a particular +outcome. + +### Resolved Context view + +A supporting server can return the Context and all directly referenced +representations in one response. A Consumer requests that view at the same +unchanged URL with: + +```http +Accept: application/vnd.xregistry.context-view+json, application/json;q=0.5 +``` + +The Consumer MUST inspect `Content-Type` rather than assume that the server +supports the view. Ordinary `application/json` returns Context metadata; +`application/vnd.xregistry.context-view+json` returns `context` and `documents` +as defined by the working draft. The view includes every direct Reference or +returns an error. It does not recursively retrieve their dependencies or +establish a snapshot across Registries. + +## Relationship to Other Attributes + +Core attributes retain their meaning. In particular, `contexturl` does not +replace REQUIRED `id`, `source`, or `type`, and does not locate the event's +`data` as the [Dataref extension](dataref.md) does. + +A Consumer selects the first applicable schema source in this order: + +1. The event's `dataschema`. +2. The Context's `schema` Reference. +3. The schema declared by the Context's `message` representation. + +Producers SHOULD omit `dataschema` when the Context supplies the schema. +Publishers and Producers SHOULD supply only one schema source, or identify +the same schema when several are supplied. Consumers apply the precedence +without comparing the sources. + +An `emitter` Reference does not authenticate the event or redefine `source`. +The [WoT profile](wot.md) defines how it selects a Thing Description (TD) when +this extension is used. The +[OPC UA extension](opcua.md#shared-stream-metadata-in-a-context) +defines which stream metadata can be supplied by a Context. Neither rule +applies to generic events merely because they contain `contexturl`. + +## Example + +This event references shared emitter and payload-schema information. + +```json +{ + "specversion": "1.0", + "id": "sample-4711", + "source": "mqtt://site/line/cell/device/sensor", + "type": "com.example.models.tm.pump.properties.processData.v1", + "subject": "#/properties/processData", + "datacontenttype": "application/json", + "contexturl": "https://registry.example/contextgroups/line1/contexts/pump-process-data/versions/1", + "data": { + "Speed": 1500, + "Pressure": 4.5 + } +} +``` + +The Context Version has the following metadata. Other Core metadata is omitted +from this excerpt. + +```json +{ + "contextid": "pump-process-data", + "versionid": "1", + "references": { + "emitter": { + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3.0.0" + }, + "schema": { + "url": "https://example.com/schemas/pump-process-data/versions/3" + } + } +} +``` + +## Security Considerations + +Consumers and servers that assemble resolved views MUST apply authorization, +URL, redirect, and content-trust policies before retrieving or using a Context +or any referenced representation. An event-provided URL MUST NOT authorize +arbitrary network access or credential forwarding. Implementations SHOULD +bound response sizes, retrieval times, and dependency processing. + +Caches MUST NOT share representations across authorization contexts unless +the applicable access policy permits it. A resolved view does not bypass the +Consumer's trust checks. Context Versions and Target Versions do not by +themselves authenticate content or prove that its bytes are unchanged. + +[Context PR]: https://github.com/xregistry/spec/pull/796 +[Context Registry]: https://github.com/xregistry/spec/blob/e46561facacb672ed3d7603be1574a75b1a11789/workingdrafts/models/context/spec.md diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 54110bdf9..3fba9dd3d 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -6,6 +6,13 @@ DataSetMessages to CloudEvents for routing over supported protocols. It defines mappings for Core and other extension attributes and specifies OPC UA extension attributes. +When used with the [Event Context extension](context.md) and the xRegistry +Context model, metadata that stays constant for a message stream is preferably +stored in the Context rather than repeated in each event. The existing OPC UA +attributes remain defined and usable. The +[shared metadata rules](#shared-stream-metadata-in-a-context) describe how a +Consumer obtains omitted values from the Context. + ## Notational Conventions As with the main [CloudEvents specification](../spec.md), the key words "MUST", @@ -66,6 +73,10 @@ messages: requirements of the Sequence extension and MUST NOT be inferred from `opcuasequence`. +The Context rules below allow stable OPC UA values to be supplied by a +Context instead of a per-event attribute. They do not relocate `source`, +`type`, or `subject`, and they do not change how those Core attributes map. + These conditional mappings replace only the conflicting OPC UA mappings above. All other requirements in this extension remain applicable. A profile that uses only part of this extension MUST NOT claim conformance to it unless the @@ -90,6 +101,11 @@ separate format like [JSON schema](https://json-schema.org/specification) or [Avro schema](https://avro.apache.org/docs/1.11.1/specification/) or others. For those cases the attribute references the schema and is used for versioning. +When the Event Context extension is used, its `schema` Reference or `message` +representation can supply this schema instead. The Consumer follows that +extension's schema precedence, and the Producer SHOULD omit `dataschema` when +the Context supplies the schema. + ### subject When the WoT profile is not used, the following OPC UA mappings apply: @@ -267,11 +283,84 @@ than omit the status, wrap the value, or report it as _Good_. - OPC UA messages MUST use `binary-mode` of CloudEvents. - OPC UA PubSub JSON messages MUST be encoded using non-reversible encoding as the decoding information is contained in metadata messages or by schema -referenced via `dataschema` attribute. +referenced via `dataschema` attribute or selected through Event Context. - Payload of OPC UA PubSub JSON messages MUST NOT contain Network Message Header and Data Set Header as that information is mapped into CloudEvents attributes. - OPC UA PubSub JSON messages MUST contain exactly one dataset message. +## Shared Stream Metadata in a Context + +A message stream here is a configured flow from one Publisher and +DataSetWriter with a defined payload contract. It is not necessarily a broker +topic or a CloudEvents `source`; either can carry several streams. + +When Producers and Consumers agree to use this extension with the +[Event Context extension](context.md), the Context Publisher SHOULD store +metadata that is constant for the stream as Context Version extension +attributes. Each event then carries `contexturl`. The attributes defined in +[Attributes](#attributes) remain valid in events, but storing stream-constant +values once in the Context is preferable because events no longer repeat them, +which keeps messages small. The Context has no document body: these +values are members of its metadata object, alongside `references`. + +The following values can be supplied by the Context when they remain constant +for every event using that Context Version. When a Context supplies any of +these values, `contexturl` MUST identify a Context Version. A Context Resource +URL can select a different default Version when a delayed or replayed event is +processed. + +| Context attribute | Meaning and conditions | +| --- | --- | +| `opcuapublisherid` | The stream's native PublisherId. | +| `opcuadatasetwriterid` | The stream's native DataSetWriterId, when used. | +| `opcuadatasetwritername` | The stream's configured DataSetWriterName, when used. | +| `opcuametadatamajorversion` | The MajorVersion of the applicable native metadata, when no payload schema is selected. | +| `opcuametadataminorversion` | The MinorVersion of the applicable native metadata, when no payload schema is selected. | +| `opcuamessagetype` | The native MessageType, only for a stream restricted to one message type. Streams that mix keyframes, deltaframes, or events retain it in each event. | + +These members MUST use the CloudEvents types, value constraints, and native +field mappings defined in [Attributes](#attributes). For example, a Context's +`opcuadatasetwriterid` is a JSON integer, not a string. The Context's `schema` +Reference, rather than a new OPC UA attribute, identifies an external payload +schema. Native metadata-version attributes MUST NOT be supplied in the event +or Context when a schema is selected through `dataschema` or the Context. + +For these listed attributes, a value supplied by an applicable Context +satisfies this extension's presence requirements, including those of the +combined WoT mapping. A Producer MAY omit the corresponding event attribute. +All existing attribute definitions remain unchanged for events that do not +use this Context mapping. + +The Consumer uses an event attribute when it is present, otherwise the value +in the Context. If both are present, they MUST have the same typed value. If +they disagree, the Consumer MUST report a metadata conflict and MUST NOT +interpret the payload using that Context. If a REQUIRED value is absent from +both, the Consumer MUST report missing metadata and MUST NOT interpret the +payload as a conforming OPC UA message. A Consumer that cannot retrieve the +Context MAY process the event using only its explicit attributes if they +satisfy every applicable requirement, including the WoT resolution rules when +that profile is used. + +The Context Publisher MUST NOT change these values in a Context Version after +any Producer is configured with its URL. A change requires a new Context Version +and reconfigured Producers. Consumers MUST NOT retain values from a previous +Context when processing an event that selects another Context. A changed +payload contract also requires a changed WoT `type` revision when that profile +is used; changing only `contexturl` is not sufficient. + +Values that vary by occurrence remain in each event. In particular, +`opcuasequence`, `opcuastatus` when REQUIRED, `id`, and `time` MUST NOT be +supplied by the Context as replacements for their event attributes. Tracing, +recording times, and a CloudEvents `sequence`, when used, also remain in the +event. A Consumer MUST NOT use Context members with those names as event +values. Core attributes retain their normal placement and meaning even when +their values happen to be constant for a stream. + +Without a `contexturl`, no Context supplies missing OPC UA values. This +preserves the existing mapping for Consumers that do not support indirection. +Producers SHOULD use omission only when they know that the intended Consumers +support this mapping; retaining explicit OPC UA attributes remains valid. + ## Examples ### Metadata message @@ -471,10 +560,11 @@ recordedtime: 2024-03-28T23:59:59Z ### WoT and OPC UA telemetry message -This binary-mode keyframe uses the WoT event type and affordance subject while -retaining the native OPC UA message type, writer identity, and sequence -number. The DataSetWriter name is used without a numeric writer ID. The -example assumes that `PumpProcessData` uniquely identifies a writer within +This binary-mode keyframe uses the WoT event type and affordance subject. +Its Context supplies the resolved TD, payload schema, PublisherId, and +DataSetWriterName. The native OPC UA message type and sequence remain in the +event because they can vary within this stream. The example assumes that +`PumpProcessData` uniquely identifies a writer within publisher `UInt16:12`. The CloudEvents `sequence` attribute is absent because this example does not define a source-wide ordering across DataSetWriters. @@ -488,13 +578,11 @@ Content Type: application/json specversion: 1.0 id: ua-message-25460 -source: https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site=north +source: mqtt://site/line/cell/device/sensor type: com.example.models.tm.pump.properties.processData.v1 subject: #/properties/processData time: 2026-10-29T12:00:00Z -dataschema: https://example.com/schemas/pump-process-data/versions/3 -opcuapublisherid: UInt16:12 -opcuadatasetwritername: PumpProcessData +contexturl: https://registry.example/contextgroups/line1/contexts/opcua-pump-process-data/versions/1 opcuamessagetype: ua-keyframe opcuasequence: 4294967295 @@ -508,5 +596,36 @@ opcuasequence: 4294967295 ----------------------------------------------- ``` +The referenced Context Version has the following metadata. Other Core +metadata is omitted from this excerpt. + +```json +{ + "contextid": "opcua-pump-process-data", + "versionid": "1", + "references": { + "emitter": { + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3.0.0" + }, + "schema": { + "url": "https://example.com/schemas/pump-process-data/versions/3" + } + }, + "opcuapublisherid": "UInt16:12", + "opcuadatasetwritername": "PumpProcessData" +} +``` + +The Consumer uses the Context's PublisherId and writer name together with the +event's `opcuamessagetype` and `opcuasequence`. It evaluates `subject` against +the TD selected by `emitter` and uses the schema selected by `schema`. + +## Context Retrieval Security + +The Event Context extension's retrieval, authorization, and cache rules apply +to this mapping. Stable writer metadata in a Context does not authenticate +the Publisher or authorize access to the referenced TD or schema. A Consumer +MUST establish trust in the Context before using it as OPC UA metadata. + [UA DataSetWriterName]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table185 [UA SequenceNumber]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.3 diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 0025b8ba1..55b4d2b09 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -1,14 +1,17 @@ # Web of Things This profile maps a [Web of Things Thing Description][TD] (TD) to CloudEvents -Core context attributes. The `source` identifies the Thing and, when provided, -a version of its resolved TD. The `subject` identifies the part of that TD -associated with the event, and the `type` names the event contract. +Core context attributes and the [Event Context extension](context.md). +The `contexturl` identifies shared metadata whose `emitter` Reference selects +the resolved TD. The `subject` identifies the part of that TD associated with +the event, and the `type` names the event contract. Without `contexturl`, the +`source` query parameters select the Thing and its resolved TD. The profile defines no CloudEvents extension attributes or URI scheme. Producers and Consumers agree on its use through a publication profile or out of band. It does not define a transport binding, provision a Thing, or require -a particular registry implementation. +a particular registry implementation. Events with `contexturl` use the +xRegistry Context representation. ## Notational Conventions @@ -23,10 +26,14 @@ attribute marked REQUIRED here is not REQUIRED in every CloudEvent. ## Source The Core [`source`](../spec.md#source) identifies a Thing within the Producer's -chosen context. It MUST contain the `wotid` query parameter and MAY contain -`wotversion` and additional parameters. This profile does not prescribe the URI -scheme or the rest of the URI-reference. Core permits relative URI-references -and recommends an absolute URI. +chosen context. With `contexturl`, it follows Core's URI-reference rules and +does not need `wotid` or `wotversion`. The Context's `emitter` Reference, not +the `source`, selects the resolved TD. + +Without `contexturl`, `source` MUST contain the `wotid` query parameter and MAY +contain `wotversion` and additional parameters. This profile does not prescribe +the URI scheme or the rest of the URI-reference. Core permits relative +URI-references and recommends an absolute URI. For example: @@ -56,6 +63,8 @@ not prescribe its structure or derive a TD retrieval location from it. ### Query syntax and encoding +This subsection applies when the event has no `contexturl`. + The `source` query uses `;` as the parameter separator. This profile standardizes two query parameters, not CloudEvents attributes: `wotid` is REQUIRED and appears first; `wotversion` is OPTIONAL and, when present, @@ -90,6 +99,9 @@ a slash. ### Thing identity and version +This subsection applies to selection through `source` when the event has no +`contexturl`. + The decoded `wotid` MUST be a non-empty absolute IRI as defined in [RFC 3987][IRI]. When the authored TD has an `id`, `wotid` MUST equal that value verbatim and the resolved TD MUST retain it. When the authored TD has @@ -115,6 +127,8 @@ That default can change, so a delayed event can resolve to a different view than the one available when the event was produced. A publication profile that requires repeatable replay MUST require `wotversion`. +### Event identity + Core requires `source` and `id` together to identify each distinct event. Changing the `source`, including its context, version, or other parameters, changes the event identity and [Sequence extension](sequence.md) scope. @@ -122,8 +136,43 @@ Returning to an earlier `source` returns to its existing scope: Producers MUST NOT reuse event identities or restart a claimed source-wide sequence within that scope. +Changing `contexturl` alone does not change event identity or sequence scope. +A Producer MUST NOT reuse an `id` or restart a source-wide sequence merely +because it publishes a new Context Version. + ## Resolving the Thing Description +### With Event Context + +When `contexturl` is present, the Producer and Consumer MUST use the +[Event Context extension](context.md). The Context MUST contain an `emitter` +Reference whose `url` identifies the resolved TD document, not its Registry +metadata. The Context Publisher SHOULD reference an explicit TD Version and +publish the resolved TD view at that URL. The Consumer retrieves the document +directly or uses the corresponding document in an accepted resolved Context +view. It MUST NOT derive a TD URL from `contexturl` or from the `emitter` URL. + +The Context's `emitter` is authoritative for TD selection. The Consumer MUST +evaluate `subject` against that TD, even if an interpretation of `source`, +including `wotid` or `wotversion` query parameters, would select another TD. +Those parameters do not select a TD in this mode. This rule does not change +Core's meaning of `source` or authenticate the emitting entity. + +If the Context cannot be retrieved or is invalid, or its `emitter` Reference +is missing or invalid, or the referenced TD is unavailable or not resolved, +the Consumer MUST report a resolution failure and leave the event uncorrelated. +It MUST NOT fall back to selection through `source`. It MAY retain or forward +the event without TD-based interpretation. + +The Context Publisher MUST make the TD available before configuring Producers +with the Context Version URL. Changes to its Reference require a new Context +Version. A change to the TD's resolved logical view requires a new TD Version; +the Context Publisher MUST NOT change that view at a previously published +Version URL. Context versioning alone does not make an unversioned TD URL +stable. + +### Without Event Context + The Producer and Consumer MUST agree on a resolver and its scope out of band or through their publication profile. The scope selects a registry and Group, tenant, or equivalent boundary in which the Thing identity is resolved. This @@ -225,9 +274,10 @@ For example, adding a `quality` member to ignore the new member. A change to an individual value or the presence of a member already defined as -OPTIONAL does not change the contract revision. A change to `wotversion` or -`dataschema` alone also does not change the revision; if it changes the payload -contract, the Producer MUST use a new revision. +OPTIONAL does not change the contract revision. A change to `contexturl`, its +`emitter` Reference, `wotversion`, or the selected schema alone also does not +change the revision; if it changes the payload contract, the Producer MUST use +a new revision. The Producer MUST configure a valid complete type when the derivation is unavailable, produces an invalid symbol, or collides with a different event @@ -265,6 +315,11 @@ CloudEvent's actual `data` adheres to. [`datacontenttype`](../spec.md#datacontenttype) identifies that data's media type. Neither identifies the TD document's format. +When `contexturl` is present, the Consumer follows the Event Context +extension's schema precedence: `dataschema`, then the Context's `schema` +Reference, then the schema declared by its `message` representation. The +Producer SHOULD omit `dataschema` when the Context supplies the schema. + A TD describes interactions and can contain several DataSchemas. The Producer MUST NOT use the whole TD as `dataschema` merely because the TD contains a schema for the selected affordance. A TD's retrieval URL is a @@ -277,6 +332,7 @@ several property values, an event payload and a protocol envelope can need different schemas. When `dataschema` is present, the Producer MUST ensure that it identifies the schema of the representation actually sent, including any encoding or envelope selected by the binding. +The same requirement applies to a schema supplied by the Context. An xRegistry Schema Resource's document-view URL can identify that external schema. It is not the WoT Resource's metadata-view URL. A profile MAY require @@ -292,18 +348,18 @@ MUST follow that extension's conditional WoT mapping. ### Single Affordance -This CloudEvent identifies a pump and its `processData` property. The -configured resolver selects version `3.0.0` of the resolved TD. +This CloudEvent identifies a pump and its `processData` property. Its Context +selects version `3.0.0` of the resolved TD and supplies the payload schema. ```json { "specversion": "1.0", "id": "sample-4711", - "source": "https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site=north", + "source": "mqtt://site/line/cell/device/sensor", "type": "com.example.models.tm.pump.properties.processData.v1", "subject": "#/properties/processData", "datacontenttype": "application/json", - "dataschema": "https://example.com/schemas/pump-process-data/versions/3", + "contexturl": "https://registry.example/contextgroups/line1/contexts/pump-process-data/versions/1", "data": { "Speed": 1500, "Pressure": 4.5 @@ -311,17 +367,22 @@ configured resolver selects version `3.0.0` of the resolved TD. } ``` +The Context metadata is shown in the +[Event Context example](context.md#example). Its `emitter` selects the TD, +and its `schema` selects the payload schema. The subject selects the property in that resolved view. The payload directly contains the property's object value without a `processData` wrapper. The separate schema describes that object, not the TD document. Another pump can -use the same event type with a different Thing identity in its `source`. +use the same event type with a different Thing identity in its `source` and a +Context whose `emitter` describes that pump. ### Whole Thing -This CloudEvent carries aggregated data for the same pump. Its unversioned -source selects the current default resolved TD, which this example assumes is -version `3.0.0`. The publication profile defines a payload containing the -`processData` and `running` property values. The subject selects the whole TD. +This CloudEvent carries aggregated data for the same pump without +`contexturl`. Its unversioned source selects the current default resolved TD, +which this example assumes is version `3.0.0`. The publication profile defines +a payload containing the `processData` and `running` property values. The +subject selects the whole TD. ```json { From 6f3f097d548a71bcf7befd06fdd8b6c7af2fac2c Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 12:24:33 +0100 Subject: [PATCH 17/28] docs: require WoT event context Signed-off-by: Marc --- cloudevents/extensions/context.md | 4 +- cloudevents/extensions/data-classification.md | 9 +- cloudevents/extensions/opcua.md | 281 +++++++++++------- cloudevents/extensions/wot.md | 253 +++++----------- 4 files changed, 241 insertions(+), 306 deletions(-) diff --git a/cloudevents/extensions/context.md b/cloudevents/extensions/context.md index 10463ca9c..b01fa463f 100644 --- a/cloudevents/extensions/context.md +++ b/cloudevents/extensions/context.md @@ -149,8 +149,8 @@ the same schema when several are supplied. Consumers apply the precedence without comparing the sources. An `emitter` Reference does not authenticate the event or redefine `source`. -The [WoT profile](wot.md) defines how it selects a Thing Description (TD) when -this extension is used. The +The [WoT profile](wot.md) requires this extension and defines how `emitter` +selects a Thing Description (TD). The [OPC UA extension](opcua.md#shared-stream-metadata-in-a-context) defines which stream metadata can be supplied by a Context. Neither rule applies to generic events merely because they contain `contexturl`. diff --git a/cloudevents/extensions/data-classification.md b/cloudevents/extensions/data-classification.md index 53e52c697..e1d10a13e 100644 --- a/cloudevents/extensions/data-classification.md +++ b/cloudevents/extensions/data-classification.md @@ -96,10 +96,11 @@ Examples where data classification of events can be useful are: ## Appendix: Data Protection and Privacy Regulations -[UNCTAD (United Nations Conference on Trade and -Development)](https://unctad.org/page/data-protection-and-privacy-legislation-worldwide) -provides a [catalog of legislation by country](https://storage.unctad.org/2026-cyberlaw_tracker/assets/data/document_links.json), -including data protection and privacy regulations. +For reference purposes, a catalog of common data protection and privacy +regulation and abbreviations is available from [UNCTAD +(United Nations Conference on Trade and +Development)](https://unctad.org/page/data-protection-and-privacy-legislation-worldwide). +Others might exist. Some examples include: - `GDPR` - General Data Protection Regulation, Europe diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 3fba9dd3d..e6daeb4d4 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -9,9 +9,10 @@ attributes. When used with the [Event Context extension](context.md) and the xRegistry Context model, metadata that stays constant for a message stream is preferably stored in the Context rather than repeated in each event. The existing OPC UA -attributes remain defined and usable. The -[shared metadata rules](#shared-stream-metadata-in-a-context) describe how a -Consumer obtains omitted values from the Context. +attributes remain defined and usable. Publisher and writer metadata can +instead be supplied in the `source` query when the Context does not provide +it. The [shared metadata rules](#shared-stream-metadata-in-a-context) describe +how a Consumer obtains these values. ## Notational Conventions @@ -35,19 +36,19 @@ field `MessageId`. ### source -Without the WoT profile, MUST either map to [Application -Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) -field `applicationUri` of the OPC UA server or to a customer configured -identifier like a unified namespace path. - -With the WoT profile, MUST follow its `source` mapping. +An OPC UA source can identify the OPC UA server using its +[Application Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) +field `applicationUri` or a customer configured identifier like a unified +namespace path. It can include the publisher and writer query parameters +defined in this extension or follow the WoT profile's `source` mapping. ### type -Without the WoT profile, MUST map to +An OPC UA `type` can map to the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) -field `MessageType`. With the WoT profile, MUST follow its semantic event-type -rules. +field `MessageType` or follow the WoT profile's semantic event-type rules. +When the `type` identifies a WoT event contract, `opcuamessagetype` can carry +the native `MessageType`. ### WoT mapping @@ -55,13 +56,15 @@ When a CloudEvent uses both this extension and the [WoT profile](./wot.md), the following mappings apply to data and event messages: -- `source` MUST follow the WoT profile's Thing identity and version rules. +- `source` MUST follow the WoT profile's source mapping. - `type` MUST follow the WoT profile's semantic event-type rules instead of mapping to the native `MessageType`. -- `opcuamessagetype` MUST carry the native `MessageType`. +- The native `MessageType` MUST be supplied as `opcuamessagetype` in the event + or Context. - `subject` MUST follow the WoT profile's resolved-TD pointer rules. -- `opcuadatasetwriterid` and `opcuadatasetwritername`, when present, MUST - identify the native DataSetWriter; neither MAY be appended to `subject`. +- Publisher and writer metadata supplied in `source` or the Context MUST + identify the native Publisher and DataSetWriter. Writer identifiers MUST NOT + be appended to `subject`. - For data and event messages, at least one of `opcuadatasetwriterid` and `opcuadatasetwritername` MUST be present. If both are present, they MUST identify the same DataSetWriter. @@ -73,9 +76,10 @@ messages: requirements of the Sequence extension and MUST NOT be inferred from `opcuasequence`. -The Context rules below allow stable OPC UA values to be supplied by a -Context instead of a per-event attribute. They do not relocate `source`, -`type`, or `subject`, and they do not change how those Core attributes map. +The [publisher and writer metadata rules](#publisher-and-writer-metadata) +define the source-query and Context representations of these identifiers. +They do not relocate `source`, `type`, or `subject`, and they do not change +how those Core attributes map. These conditional mappings replace only the conflicting OPC UA mappings above. All other requirements in this extension remain applicable. A profile that @@ -108,15 +112,14 @@ the Context supplies the schema. ### subject -When the WoT profile is not used, the following OPC UA mappings apply: - -For metadata, event and data messages (type one of `ua-metadata`, `ua-keyframe`, -`ua-deltaframe`, `ua-event`), `subject` MUST map to either [Data Set Message +For metadata and data messages, an OPC UA-specific mapping can use +`DataSetWriterId` or `DataSetWriterName` from the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) -field `DataSetWriterId` or `DataSetWriterName`. +as `subject`. A publisher using the WoT profile can instead use its +resolved-TD pointer mapping. -For event messages (type equals to `ua-event`) `subject` MUST be appended with -"/" and [Base Event Type](https://reference.opcfoundation.org/Core/Part5/v104/docs/6.4.2) +For an OPC UA event message (`type` equals `ua-event`), `subject` can be +appended with "/" and the [Base Event Type](https://reference.opcfoundation.org/Core/Part5/v104/docs/6.4.2) field `EventId`. ### time @@ -205,49 +208,6 @@ CloudEvents `Integer` is limited to the signed 32-bit range. If a native status value cannot be represented, the Producer MUST report an error rather than omit the status, wrap the value, or report it as _Good_. -### opcuapublisherid - -- Type: `String` -- Description: Identifies the OPC UA PubSub PublisherId. -- Constraints - - OPTIONAL - - If present, MUST use `:`. - - For `Byte`, `UInt16`, `UInt32` and `UInt64`, `` MUST be the - canonical unsigned decimal representation of the PublisherId. - - For `Guid`, `` MUST be the lowercase canonical UUID representation - of the PublisherId. - - For `String`, `` MUST be the unpadded base64url encoding of the - PublisherId's UTF-8 bytes. - - In combination with the WoT profile, REQUIRED for data and event - messages. - - MUST map to the Network Message Header `PublisherId`. - -### opcuadatasetwriterid - -- Type: `Integer` -- Description: Identifies the DataSetWriter that produced the DataSetMessage. -- Constraints - - OPTIONAL - - If present, MUST be in the range 0 to 65535, inclusive. - - MUST map to the Data Set Message Header `DataSetWriterId`. - -### opcuadatasetwritername - -- Type: `String` -- Description: Carries the human-readable name of the DataSetWriter that - produced the DataSetMessage. -- Constraints - - OPTIONAL - - If present, MUST be non-empty. - - MUST match the DataSetWriter's configured `DataSetWriterName`. For JSON - message mapping, this is the [`DataSetWriterName` field][UA DataSetWriterName] - when present in the DataSetMessage; otherwise the publisher obtains it - from configuration. - - In combination with the WoT profile, MUST uniquely identify the - DataSetWriter within the configured `opcuapublisherid` scope. - - In combination with the WoT profile, names MUST be compared exactly, - without case folding or Unicode normalization. - ### opcuamessagetype - Type: `String` @@ -255,8 +215,8 @@ than omit the status, wrap the value, or report it as _Good_. identifies a WoT semantic event type. - Constraints - OPTIONAL - - In combination with the WoT profile, REQUIRED for data and event - messages. + - In combination with the WoT profile, the value is REQUIRED for data and + event messages, in the event or Context. - MUST map to the Data Set Message Header `MessageType`. - The value MUST be the native OPC UA message type, such as `ua-keyframe`, `ua-deltaframe` or `ua-event`. @@ -278,6 +238,91 @@ than omit the status, wrap the value, or report it as _Good_. - MUST NOT be interpreted as a source-wide sequence unless it also independently satisfies the Sequence extension's requirements. +## Publisher and Writer Metadata + +The following values identify the OPC UA Publisher and DataSetWriter. They +are not CloudEvents extension attributes. A Producer MAY supply them as +query parameters in `source`, as extension attributes of the applicable +Context Version, or in both places. The names are the same in both +representations. + +| Name | Value | +| --- | --- | +| `opcuapublisherid` | The native PublisherId, represented as a string. | +| `opcuadatasetwriterid` | The native DataSetWriterId, represented as an integer. | +| `opcuadatasetwritername` | The configured DataSetWriterName, represented as a string. | + +`opcuapublisherid` MUST use `:`. The type MUST be `Byte`, +`UInt16`, `UInt32`, `UInt64`, or `String`, as defined for the native +[PublisherId][UA PublisherId]. For an unsigned integer type, the value MUST be +the nonzero unsigned decimal representation within that type's range, without +a sign or leading zeros. For `String`, the value MUST be the unpadded base64url +encoding of the non-empty PublisherId's UTF-8 bytes. If the message encoding +does not preserve the PublisherId's type, the Producer obtains it from +configuration. If the type cannot be determined, the Producer MUST report an +error rather than assign one. + +`opcuadatasetwriterid` MUST map to the native DataSetWriterId. It MUST be in +the range 1 to 65535, inclusive; [OPC UA reserves zero][UA DataSetWriterId] +as a null value. +`opcuadatasetwritername` MUST be non-empty and match the DataSetWriter's +configured name. For JSON message mapping, this is the +[`DataSetWriterName` field][UA DataSetWriterName] when present in the +DataSetMessage; otherwise the Producer obtains it from configuration. + +In combination with the WoT profile, `opcuapublisherid` and at least one of +`opcuadatasetwriterid` and `opcuadatasetwritername` are REQUIRED for data and +event messages. If both writer values are supplied, they MUST identify the +same DataSetWriter. A writer name MUST uniquely identify the DataSetWriter +within the PublisherId scope. Consumers MUST compare names exactly, without +case folding or Unicode normalization. + +A Consumer MUST report invalid metadata and MUST NOT interpret the payload as +a conforming OPC UA message if a supplied publisher or writer value violates +these representation or range constraints. + +### Source query syntax + +When these values are supplied in `source`, the query parameters carry +stream-constant metadata. The Producer SHOULD omit a query parameter when the +Context supplies its value. If the Context does not supply a value REQUIRED by +the mapping, the Producer MUST supply it in `source`. + +The Producer MUST use `name=value` for each of these parameters, with a +non-empty value. It MUST separate them from each other and from preceding +query content with `;`, and place the query before any URI fragment. The three +names are case-sensitive and MUST appear literally, at most once each. Other +parameters MAY appear in any order; a Consumer MUST NOT reject the source +solely because it contains parameters not defined by this extension. + +For these parameter values, the unescaped characters are the [RFC 3986][URI] +`unreserved` characters and `!`, `$`, `'`, `(`, `)`, `*`, `,`, `:`, `@`, +`/`, and `?`. A Producer MUST leave these characters unescaped and +percent-encode the UTF-8 bytes of all other characters using uppercase +hexadecimal digits. In particular, a literal `%`, `;`, `=`, `&`, `+`, or `#` +MUST be encoded. A Producer MUST NOT use form-query encoding or encode a space +as `+`. + +A Consumer MUST split the query at `;` before decoding these values. It MUST +reject an empty or duplicate recognized parameter, an invalid percent escape, +invalid UTF-8, or an unescaped character outside the permitted set. It MUST +percent-decode each value exactly once and MUST NOT interpret `+` as a space. +The writer ID uses unsigned decimal notation without a sign or leading zeros +in the query and an integer in the Context; the other two values use strings +in both places. + +For example, the following source supplies the PublisherId and writer name +when the Context does not supply them. The writer name is `Pump&Process`. + +```text +mqtt://site/line/cell/device/sensor?opcuapublisherid=UInt16:12;opcuadatasetwritername=Pump%26Process;site=north +``` + +Changing `source`, including its query parameters, changes the CloudEvents +event-identity and [Sequence extension](sequence.md) scope. A Producer MUST +NOT reuse event identities or restart a claimed source-wide sequence when it +returns to a previously used `source`. + ## General Constraints - OPC UA messages MUST use `binary-mode` of CloudEvents. @@ -285,7 +330,8 @@ than omit the status, wrap the value, or report it as _Good_. the decoding information is contained in metadata messages or by schema referenced via `dataschema` attribute or selected through Event Context. - Payload of OPC UA PubSub JSON messages MUST NOT contain Network Message Header -and Data Set Header as that information is mapped into CloudEvents attributes. +and Data Set Header as that information is mapped into CloudEvents attributes, +`source`, or the Context. - OPC UA PubSub JSON messages MUST contain exactly one dataset message. ## Shared Stream Metadata in a Context @@ -297,11 +343,10 @@ topic or a CloudEvents `source`; either can carry several streams. When Producers and Consumers agree to use this extension with the [Event Context extension](context.md), the Context Publisher SHOULD store metadata that is constant for the stream as Context Version extension -attributes. Each event then carries `contexturl`. The attributes defined in -[Attributes](#attributes) remain valid in events, but storing stream-constant -values once in the Context is preferable because events no longer repeat them, -which keeps messages small. The Context has no document body: these -values are members of its metadata object, alongside `references`. +attributes. Each event then carries `contexturl`. Storing stream-constant +values once in the Context avoids repeating them in event attributes or the +`source` query. The Context has no document body: these values are members +of its metadata object, alongside `references`. The following values can be supplied by the Context when they remain constant for every event using that Context Version. When a Context supplies any of @@ -318,28 +363,32 @@ processed. | `opcuametadataminorversion` | The MinorVersion of the applicable native metadata, when no payload schema is selected. | | `opcuamessagetype` | The native MessageType, only for a stream restricted to one message type. Streams that mix keyframes, deltaframes, or events retain it in each event. | -These members MUST use the CloudEvents types, value constraints, and native -field mappings defined in [Attributes](#attributes). For example, a Context's -`opcuadatasetwriterid` is a JSON integer, not a string. The Context's `schema` -Reference, rather than a new OPC UA attribute, identifies an external payload -schema. Native metadata-version attributes MUST NOT be supplied in the event -or Context when a schema is selected through `dataschema` or the Context. - -For these listed attributes, a value supplied by an applicable Context -satisfies this extension's presence requirements, including those of the -combined WoT mapping. A Producer MAY omit the corresponding event attribute. -All existing attribute definitions remain unchanged for events that do not -use this Context mapping. - -The Consumer uses an event attribute when it is present, otherwise the value -in the Context. If both are present, they MUST have the same typed value. If -they disagree, the Consumer MUST report a metadata conflict and MUST NOT -interpret the payload using that Context. If a REQUIRED value is absent from -both, the Consumer MUST report missing metadata and MUST NOT interpret the -payload as a conforming OPC UA message. A Consumer that cannot retrieve the -Context MAY process the event using only its explicit attributes if they -satisfy every applicable requirement, including the WoT resolution rules when -that profile is used. +These members MUST use the value constraints and native field mappings +defined in [Attributes](#attributes) and +[Publisher and Writer Metadata](#publisher-and-writer-metadata). For example, +a Context's `opcuadatasetwriterid` is a JSON integer, not a string. The +Context's `schema` Reference identifies an external payload schema. Native +metadata-version attributes MUST NOT be supplied in the event or Context +when a schema is selected through `dataschema` or the Context. + +A value supplied by an applicable Context satisfies this extension's presence +requirements, including those of the combined WoT mapping. A Producer MAY +omit an event attribute or source query parameter when the Context supplies +the corresponding value. The existing OPC UA event attributes remain valid. + +If a publisher or writer value appears in both the `source` query and the +Context, the values MUST agree after decoding and type conversion. If an event +attribute and the Context both supply a metadata version or message type, +their typed values MUST agree. On a conflict, the Consumer MUST report a +metadata conflict and MUST NOT interpret the payload using that Context. If a +REQUIRED value is absent from all applicable locations, the Consumer MUST +report missing metadata and MUST NOT interpret the payload as a conforming +OPC UA message. + +A Consumer that cannot retrieve the Context MAY process the event using +its source query and explicit event attributes if they satisfy every applicable +requirement. When the WoT profile is used, the Consumer also needs the Context +to resolve the TD and MUST follow the WoT profile's failure rules. The Context Publisher MUST NOT change these values in a Context Version after any Producer is configured with its URL. A change requires a new Context Version @@ -357,9 +406,9 @@ values. Core attributes retain their normal placement and meaning even when their values happen to be constant for a stream. Without a `contexturl`, no Context supplies missing OPC UA values. This -preserves the existing mapping for Consumers that do not support indirection. -Producers SHOULD use omission only when they know that the intended Consumers -support this mapping; retaining explicit OPC UA attributes remains valid. +preserves the existing event attributes and lets a Producer supply publisher +and writer metadata in the `source` query. Producers SHOULD use Context +metadata only when they know that the intended Consumers support this mapping. ## Examples @@ -561,12 +610,13 @@ recordedtime: 2024-03-28T23:59:59Z ### WoT and OPC UA telemetry message This binary-mode keyframe uses the WoT event type and affordance subject. -Its Context supplies the resolved TD, payload schema, PublisherId, and -DataSetWriterName. The native OPC UA message type and sequence remain in the -event because they can vary within this stream. The example assumes that -`PumpProcessData` uniquely identifies a writer within -publisher `UInt16:12`. The CloudEvents `sequence` attribute is absent because -this example does not define a source-wide ordering across DataSetWriters. +Its Context supplies the resolved TD, payload schema, and PublisherId. The +`source` query supplies the DataSetWriterId because the Context does not +identify the writer. The native OPC UA message type and sequence remain in the +event because they can vary within this stream. The example uses writer ID +`17` within publisher `UInt16:12`. The CloudEvents `sequence` attribute is +absent because this example does not define a source-wide ordering across +DataSetWriters. ```text ------------------ PUBLISH ------------------- @@ -578,7 +628,7 @@ Content Type: application/json specversion: 1.0 id: ua-message-25460 -source: mqtt://site/line/cell/device/sensor +source: mqtt://site/line/cell/device/sensor?opcuadatasetwriterid=17;site=north type: com.example.models.tm.pump.properties.processData.v1 subject: #/properties/processData time: 2026-10-29T12:00:00Z @@ -611,14 +661,14 @@ metadata is omitted from this excerpt. "url": "https://example.com/schemas/pump-process-data/versions/3" } }, - "opcuapublisherid": "UInt16:12", - "opcuadatasetwritername": "PumpProcessData" + "opcuapublisherid": "UInt16:12" } ``` -The Consumer uses the Context's PublisherId and writer name together with the -event's `opcuamessagetype` and `opcuasequence`. It evaluates `subject` against -the TD selected by `emitter` and uses the schema selected by `schema`. +The Consumer uses the writer ID from `source` and the PublisherId from the +Context with the event's `opcuamessagetype` and `opcuasequence`. It evaluates +`subject` against the TD selected by `emitter` and uses the schema selected by +`schema`. ## Context Retrieval Security @@ -628,4 +678,7 @@ the Publisher or authorize access to the referenced TD or schema. A Consumer MUST establish trust in the Context before using it as OPC UA metadata. [UA DataSetWriterName]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table185 +[UA DataSetWriterId]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.4.1 +[UA PublisherId]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.7.1 [UA SequenceNumber]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.3 +[URI]: https://www.rfc-editor.org/rfc/rfc3986 diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 55b4d2b09..4ddf22740 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -2,16 +2,15 @@ This profile maps a [Web of Things Thing Description][TD] (TD) to CloudEvents Core context attributes and the [Event Context extension](context.md). -The `contexturl` identifies shared metadata whose `emitter` Reference selects -the resolved TD. The `subject` identifies the part of that TD associated with -the event, and the `type` names the event contract. Without `contexturl`, the -`source` query parameters select the Thing and its resolved TD. +The REQUIRED `contexturl` identifies shared metadata whose `emitter` Reference +selects the resolved TD. The `subject` identifies the part of that TD associated +with the event, and the `type` names the event contract. The profile defines no CloudEvents extension attributes or URI scheme. Producers and Consumers agree on its use through a publication profile or out of band. It does not define a transport binding, provision a Thing, or require -a particular registry implementation. Events with `contexturl` use the -xRegistry Context representation. +a particular registry implementation. Events use the xRegistry Context +representation defined by the Event Context extension. ## Notational Conventions @@ -25,113 +24,23 @@ attribute marked REQUIRED here is not REQUIRED in every CloudEvent. ## Source -The Core [`source`](../spec.md#source) identifies a Thing within the Producer's -chosen context. With `contexturl`, it follows Core's URI-reference rules and -does not need `wotid` or `wotversion`. The Context's `emitter` Reference, not -the `source`, selects the resolved TD. +The Core [`source`](../spec.md#source) identifies the context in which the +event occurred and follows Core's URI-reference rules. It can identify a Thing, +topology, publisher, or another Producer-defined context. This profile does not +prescribe a URI scheme or query parameters. The Context's `emitter` Reference, +not `source`, selects the resolved TD. -Without `contexturl`, `source` MUST contain the `wotid` query parameter and MAY -contain `wotversion` and additional parameters. This profile does not prescribe -the URI scheme or the rest of the URI-reference. Core permits relative -URI-references and recommends an absolute URI. - -For example: - -```text -https://factory.example.com/line1?wotid=urn:example:pump:7;wotversion=3.0.0;site=north -``` - -The `site` parameter is defined by the Producer, not this profile. An HTTPS -source is an identifier and is not necessarily a retrieval URL for a TD. - -An MQTT source identifier can use the same query parameters: - -```text -mqtt://site/line/cell/device/sensor?wotid=urn:example:sensor:7;wotversion=3.0.0 -``` - -Another possible form is: - -```text -wot-td://factory/line1?wotid=urn:example:pump:7;wotversion=3.0.0 -``` - -This profile does not define the `wot-td` scheme or propose its registration. -The part of `source` outside the standardized parameters can identify a -topology, publisher, or another Producer-defined context. This profile does -not prescribe its structure or derive a TD retrieval location from it. - -### Query syntax and encoding - -This subsection applies when the event has no `contexturl`. - -The `source` query uses `;` as the parameter separator. This profile -standardizes two query parameters, not CloudEvents attributes: `wotid` is -REQUIRED and appears first; `wotversion` is OPTIONAL and, when present, -immediately follows `wotid`. Their names are case-sensitive and MUST appear -literally. Each MUST use `name=value` with a non-empty value. `wotid` MUST -appear exactly once, and `wotversion` MUST NOT appear more than once. - -A Producer MAY include additional parameters after the standardized parameters. -This profile does not assign them a meaning; a publication contract MAY define -their semantics. A Consumer MUST NOT reject a source solely because it contains -parameters not defined by this profile. - -For the standardized values, the unescaped characters are the -[RFC 3986][URI] `unreserved` characters and `!`, `$`, `'`, `(`, `)`, `*`, -`,`, `:`, `@`, `/`, and `?`. A Producer MUST leave these characters -unescaped. It MUST percent-encode the UTF-8 bytes of all other characters using -uppercase hexadecimal digits. In particular, a literal `%`, `;`, `=`, `&`, -`+`, or `#` in a standardized value MUST be encoded. A Producer MUST NOT use -form-query encoding or encode a space as `+` for those values. - -A Consumer MUST split the query at `;` before decoding the standardized values. -It MUST reject a missing or empty `wotid`, an empty `wotversion`, a duplicate -standardized parameter, or a standardized parameter in the wrong position. For -a standardized value, it MUST reject an invalid percent escape, invalid UTF-8, -or an unescaped character outside the permitted set. It MUST percent-decode -each standardized value exactly once and MUST NOT interpret `+` as a space. - -For example, an identifier containing U+00E9 followed by the literal characters -`%2F` encodes that portion as `%C3%A9%252F`. One decoding pass recovers the -original characters, including the literal `%2F`; it does not turn them into -a slash. - -### Thing identity and version - -This subsection applies to selection through `source` when the event has no -`contexturl`. - -The decoded `wotid` MUST be a non-empty absolute IRI as defined in -[RFC 3987][IRI]. When the authored TD has an `id`, `wotid` MUST equal that -value verbatim and the resolved TD MUST retain it. When the authored TD has -no `id`, the publication contract MUST assign a stable absolute WoT identity -for the Thing. Publishing another version MUST NOT by itself change that -identity. - -The OPTIONAL `wotversion` selects one exact version of the resolved TD within -the configured resolution scope. It is not a version range, the CloudEvents -`specversion`, a WoT vocabulary version, or a payload-schema version. Its -meaning is defined by the publication contract; a Consumer MUST NOT substitute -a different version when the requested version is unavailable. Selecting that -version does not by itself establish the compatibility of its dependencies. - -An explicit version identifies a stable resolved view. A Producer MUST NOT -change the logical view identified by a published version. It MUST publish a -new version when the view changes, including changes introduced by its source -closure. A serialization or formatting change alone does not require a new -version. - -When `wotversion` is absent, the Consumer resolves the current default view. -That default can change, so a delayed event can resolve to a different view -than the one available when the event was produced. A publication profile -that requires repeatable replay MUST require `wotversion`. +For example, a Producer can use `https://factory.example.com/line1/pump7` or +`mqtt://site/line/cell/device/sensor` as a source. Another possible form is +`wot-td://factory/line1`; this profile does not define that scheme or propose +its registration. A source is an identifier and is not necessarily a retrieval +URL for a TD. ### Event identity Core requires `source` and `id` together to identify each distinct event. -Changing the `source`, including its context, version, or other parameters, -changes the event identity and [Sequence extension](sequence.md) scope. +Changing the `source`, including any query parameters, changes the event +identity and [Sequence extension](sequence.md) scope. Returning to an earlier `source` returns to its existing scope: Producers MUST NOT reuse event identities or restart a claimed source-wide sequence within that scope. @@ -142,27 +51,25 @@ because it publishes a new Context Version. ## Resolving the Thing Description -### With Event Context - -When `contexturl` is present, the Producer and Consumer MUST use the -[Event Context extension](context.md). The Context MUST contain an `emitter` -Reference whose `url` identifies the resolved TD document, not its Registry -metadata. The Context Publisher SHOULD reference an explicit TD Version and -publish the resolved TD view at that URL. The Consumer retrieves the document -directly or uses the corresponding document in an accepted resolved Context -view. It MUST NOT derive a TD URL from `contexturl` or from the `emitter` URL. +Every event using this profile MUST include `contexturl`. The Producer and +Consumer MUST implement the [Event Context extension](context.md). The Context +MUST contain an `emitter` Reference whose `url` identifies the resolved TD +document, not its Registry metadata. The Context Publisher SHOULD reference an +explicit TD Version and publish the resolved TD view at that URL. The Consumer +retrieves the document directly or uses the corresponding document in an +accepted resolved Context view. It MUST NOT derive a TD URL from `contexturl` +or from the `emitter` URL. The Context's `emitter` is authoritative for TD selection. The Consumer MUST -evaluate `subject` against that TD, even if an interpretation of `source`, -including `wotid` or `wotversion` query parameters, would select another TD. -Those parameters do not select a TD in this mode. This rule does not change -Core's meaning of `source` or authenticate the emitting entity. +evaluate `subject` against that TD and MUST NOT select another TD from `source`. +This rule does not change Core's meaning of `source` or authenticate the +emitting entity. -If the Context cannot be retrieved or is invalid, or its `emitter` Reference -is missing or invalid, or the referenced TD is unavailable or not resolved, -the Consumer MUST report a resolution failure and leave the event uncorrelated. -It MUST NOT fall back to selection through `source`. It MAY retain or forward -the event without TD-based interpretation. +If `contexturl` is missing, the Context cannot be retrieved or is invalid, its +`emitter` Reference is missing or invalid, or the referenced TD is unavailable +or not resolved, the Consumer MUST report a resolution failure and leave the +event uncorrelated. It MUST NOT fall back to selection through `source`. It +MAY retain or forward the event without TD-based interpretation. The Context Publisher MUST make the TD available before configuring Producers with the Context Version URL. Changes to its Reference require a new Context @@ -171,44 +78,6 @@ the Context Publisher MUST NOT change that view at a previously published Version URL. Context versioning alone does not make an unversioned TD URL stable. -### Without Event Context - -The Producer and Consumer MUST agree on a resolver and its scope out of band -or through their publication profile. The scope selects a registry and Group, -tenant, or equivalent boundary in which the Thing identity is resolved. This -profile does not prescribe a discovery protocol. - -The resolver MUST find exactly one Resource or equivalent publication for -`wotid` within that scope. It MUST select the exact version named by -`wotversion`, or the current default when the parameter is absent. A missing -or ambiguous match is a resolution failure; selecting the first match does not -establish which Thing the Producer meant. - -The Consumer MUST obtain the resolved TD selected by that publication. It MAY -retrieve a published resolved view or reconstruct the same logical view from -a complete, pinned source closure and the applicable resolution rules. The -closure and rules MUST establish one unambiguous view. The JSON serialization -does not need to be byte-identical. - -If the selected view cannot be retrieved or reconstructed, the Consumer MUST -report a resolution failure and leave the event uncorrelated. It MAY retain -or forward the event without interpreting its payload. The Consumer MUST NOT -claim successful TD-based interpretation when the resolved view or its -identity cannot be established. - -The [xRegistry WoT working draft][WoT Registry] describes one resolution -mechanism. It is a working draft, not a universal dependency of this profile. -In that draft, `wotid` is the WoT identity rather than the Resource ID, and -`wotversion` corresponds to the Resource's exact `versionid`. The default -Version is not necessarily the most recently created Version and MUST NOT be -inferred by sorting version identifiers. - -The draft stores authored documents and describes a publication closure. -Some closure reference mappings remain unresolved; the closure schema alone -does not establish that every resolved view can be reconstructed. Where the -closure and applicable rules cannot establish the view, the publication -contract MUST provide the resolved view for this profile to use it. - The event carries no digest of the TD. An exact version or a successful resolution does not by itself prove that retrieved bytes are unchanged. @@ -275,9 +144,9 @@ ignore the new member. A change to an individual value or the presence of a member already defined as OPTIONAL does not change the contract revision. A change to `contexturl`, its -`emitter` Reference, `wotversion`, or the selected schema alone also does not -change the revision; if it changes the payload contract, the Producer MUST use -a new revision. +`emitter` Reference, or the selected schema alone also does not change the +revision; if it changes the payload contract, the Producer MUST use a new +revision. The Producer MUST configure a valid complete type when the derivation is unavailable, produces an invalid symbol, or collides with a different event @@ -315,10 +184,10 @@ CloudEvent's actual `data` adheres to. [`datacontenttype`](../spec.md#datacontenttype) identifies that data's media type. Neither identifies the TD document's format. -When `contexturl` is present, the Consumer follows the Event Context -extension's schema precedence: `dataschema`, then the Context's `schema` -Reference, then the schema declared by its `message` representation. The -Producer SHOULD omit `dataschema` when the Context supplies the schema. +The Consumer follows the Event Context extension's schema precedence: +`dataschema`, then the Context's `schema` Reference, then the schema declared +by its `message` representation. The Producer SHOULD omit `dataschema` when +the Context supplies the schema. A TD describes interactions and can contain several DataSchemas. The Producer MUST NOT use the whole TD as `dataschema` merely because the TD @@ -336,7 +205,7 @@ The same requirement applies to a schema supplied by the Context. An xRegistry Schema Resource's document-view URL can identify that external schema. It is not the WoT Resource's metadata-view URL. A profile MAY require -an exact schema Version independently of the `wotversion` query parameter. +an exact schema Version independently of the selected TD Version. ### Relationship to Other Extensions @@ -378,21 +247,20 @@ Context whose `emitter` describes that pump. ### Whole Thing -This CloudEvent carries aggregated data for the same pump without -`contexturl`. Its unversioned source selects the current default resolved TD, -which this example assumes is version `3.0.0`. The publication profile defines -a payload containing the `processData` and `running` property values. The -subject selects the whole TD. +This CloudEvent carries aggregated data for the same pump. Its Context +selects version `3.0.0` of the resolved TD and the aggregate payload schema. +The publication profile defines a payload containing the `processData` and +`running` property values. The subject selects the whole TD. ```json { "specversion": "1.0", "id": "sample-4713", - "source": "https://factory.example.com/line1?wotid=urn:example:pump:7;site=north", + "source": "https://factory.example.com/line1/pump7", "type": "com.example.models.tm.pump.v1", "subject": "#", "datacontenttype": "application/json", - "dataschema": "https://example.com/schemas/pump-state/versions/3", + "contexturl": "https://registry.example/contextgroups/line1/contexts/pump-state/versions/1", "data": { "processData": { "Speed": 1500, @@ -403,10 +271,26 @@ subject selects the whole TD. } ``` -The source selects the TD used to interpret these property values. If the -default changes, a later resolution of this event can select another version. -The TD document itself is not the payload. `dataschema` identifies the schema -of the aggregate JSON object, not the TD or either property's DataSchema alone. +The referenced Context Version contains the following metadata. + +```json +{ + "contextid": "pump-state", + "versionid": "1", + "references": { + "emitter": { + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3.0.0" + }, + "schema": { + "url": "https://example.com/schemas/pump-state/versions/3" + } + } +} +``` + +The `emitter` selects the TD used to interpret these property values. The TD +document itself is not the payload. The `schema` identifies the aggregate JSON +object, not the TD or either property's DataSchema alone. ## Security Considerations @@ -423,7 +307,4 @@ referenced documents or make their dependencies available. [TD]: https://www.w3.org/TR/wot-thing-description11/ [TM]: https://www.w3.org/TR/wot-thing-description11/#thing-model -[IRI]: https://www.rfc-editor.org/rfc/rfc3987 -[URI]: https://www.rfc-editor.org/rfc/rfc3986 [JSON Pointer]: https://www.rfc-editor.org/rfc/rfc6901 -[WoT Registry]: https://github.com/xregistry/spec/blob/main/workingdrafts/models/wot/spec.md From 88e6152fd9c5ccb447c803498638cba562499e77 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 12:27:55 +0100 Subject: [PATCH 18/28] docs(opcua): make mapping choices normative Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index e6daeb4d4..5a7501090 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -36,19 +36,21 @@ field `MessageId`. ### source -An OPC UA source can identify the OPC UA server using its +An OPC UA Producer MAY identify the server using its [Application Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) -field `applicationUri` or a customer configured identifier like a unified -namespace path. It can include the publisher and writer query parameters -defined in this extension or follow the WoT profile's `source` mapping. +field `applicationUri` or a customer-configured identifier such as a unified +namespace path. A Producer MAY include the publisher and writer query +parameters defined in this extension. A Producer using the WoT profile MAY +instead follow its `source` mapping. ### type -An OPC UA `type` can map to the +An OPC UA Producer MAY map `type` to the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) -field `MessageType` or follow the WoT profile's semantic event-type rules. -When the `type` identifies a WoT event contract, `opcuamessagetype` can carry -the native `MessageType`. +field `MessageType`. A Producer using the WoT profile MUST follow its semantic +event-type rules. In that case, the Producer MUST supply the native +`MessageType` as `opcuamessagetype` in the event or Context, as specified in +the WoT mapping section. ### WoT mapping @@ -112,14 +114,15 @@ the Context supplies the schema. ### subject -For metadata and data messages, an OPC UA-specific mapping can use +For metadata and data messages, a Producer MAY map `subject` to `DataSetWriterId` or `DataSetWriterName` from the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) -as `subject`. A publisher using the WoT profile can instead use its +header. A Producer using the WoT profile MUST instead follow its resolved-TD pointer mapping. -For an OPC UA event message (`type` equals `ua-event`), `subject` can be -appended with "/" and the [Base Event Type](https://reference.opcfoundation.org/Core/Part5/v104/docs/6.4.2) +For an OPC UA event message (`type` equals `ua-event`), a Producer using the +OPC UA-specific `subject` mapping MUST append "/" and the +[Base Event Type](https://reference.opcfoundation.org/Core/Part5/v104/docs/6.4.2) field `EventId`. ### time From 4ad755ab4b43d2dfe47bf88835ef3ed465456222 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 12:30:05 +0100 Subject: [PATCH 19/28] docs(opcua): recommend WoT message type mapping Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 5a7501090..e52648110 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -48,9 +48,10 @@ instead follow its `source` mapping. An OPC UA Producer MAY map `type` to the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) field `MessageType`. A Producer using the WoT profile MUST follow its semantic -event-type rules. In that case, the Producer MUST supply the native -`MessageType` as `opcuamessagetype` in the event or Context, as specified in -the WoT mapping section. +event-type rules. When the `type` identifies a WoT event contract, a Producer +SHOULD carry the native `MessageType` as `opcuamessagetype` in the event or +Context. The [WoT mapping section](#wot-mapping) requires it when both +profiles are used. ### WoT mapping From d7e4c23918ad4d4c0b0fc2e60ab0953a5c0bb9be Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 12:37:25 +0100 Subject: [PATCH 20/28] docs(opcua): keep message type in each event Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index e52648110..d411e3f26 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -49,8 +49,8 @@ An OPC UA Producer MAY map `type` to the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) field `MessageType`. A Producer using the WoT profile MUST follow its semantic event-type rules. When the `type` identifies a WoT event contract, a Producer -SHOULD carry the native `MessageType` as `opcuamessagetype` in the event or -Context. The [WoT mapping section](#wot-mapping) requires it when both +SHOULD carry the native `MessageType` as `opcuamessagetype` in the event. The +[WoT mapping section](#wot-mapping) requires it when both profiles are used. ### WoT mapping @@ -62,8 +62,7 @@ messages: - `source` MUST follow the WoT profile's source mapping. - `type` MUST follow the WoT profile's semantic event-type rules instead of mapping to the native `MessageType`. -- The native `MessageType` MUST be supplied as `opcuamessagetype` in the event - or Context. +- The native `MessageType` MUST be supplied as `opcuamessagetype` in the event. - `subject` MUST follow the WoT profile's resolved-TD pointer rules. - Publisher and writer metadata supplied in `source` or the Context MUST identify the native Publisher and DataSetWriter. Writer identifiers MUST NOT @@ -220,7 +219,7 @@ than omit the status, wrap the value, or report it as _Good_. - Constraints - OPTIONAL - In combination with the WoT profile, the value is REQUIRED for data and - event messages, in the event or Context. + event messages. - MUST map to the Data Set Message Header `MessageType`. - The value MUST be the native OPC UA message type, such as `ua-keyframe`, `ua-deltaframe` or `ua-event`. @@ -365,7 +364,6 @@ processed. | `opcuadatasetwritername` | The stream's configured DataSetWriterName, when used. | | `opcuametadatamajorversion` | The MajorVersion of the applicable native metadata, when no payload schema is selected. | | `opcuametadataminorversion` | The MinorVersion of the applicable native metadata, when no payload schema is selected. | -| `opcuamessagetype` | The native MessageType, only for a stream restricted to one message type. Streams that mix keyframes, deltaframes, or events retain it in each event. | These members MUST use the value constraints and native field mappings defined in [Attributes](#attributes) and @@ -382,8 +380,8 @@ the corresponding value. The existing OPC UA event attributes remain valid. If a publisher or writer value appears in both the `source` query and the Context, the values MUST agree after decoding and type conversion. If an event -attribute and the Context both supply a metadata version or message type, -their typed values MUST agree. On a conflict, the Consumer MUST report a +attribute and the Context both supply a metadata version, their typed values +MUST agree. On a conflict, the Consumer MUST report a metadata conflict and MUST NOT interpret the payload using that Context. If a REQUIRED value is absent from all applicable locations, the Consumer MUST report missing metadata and MUST NOT interpret the payload as a conforming @@ -402,12 +400,12 @@ payload contract also requires a changed WoT `type` revision when that profile is used; changing only `contexturl` is not sufficient. Values that vary by occurrence remain in each event. In particular, -`opcuasequence`, `opcuastatus` when REQUIRED, `id`, and `time` MUST NOT be -supplied by the Context as replacements for their event attributes. Tracing, -recording times, and a CloudEvents `sequence`, when used, also remain in the -event. A Consumer MUST NOT use Context members with those names as event -values. Core attributes retain their normal placement and meaning even when -their values happen to be constant for a stream. +`opcuamessagetype`, `opcuasequence`, `opcuastatus` when REQUIRED, `id`, and +`time` MUST NOT be supplied by the Context as replacements for their event +attributes. Tracing, recording times, and a CloudEvents `sequence`, when used, +also remain in the event. A Consumer MUST NOT use Context members with those +names as event values. Core attributes retain their normal placement and +meaning even when their values happen to be constant for a stream. Without a `contexturl`, no Context supplies missing OPC UA values. This preserves the existing event attributes and lets a Producer supply publisher From a51945bda3bb239b5311da8534cba50869b8488f Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 12:56:46 +0100 Subject: [PATCH 21/28] docs(opcua): require source metadata when absent elsewhere Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index d411e3f26..428883b1c 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -39,9 +39,11 @@ field `MessageId`. An OPC UA Producer MAY identify the server using its [Application Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) field `applicationUri` or a customer-configured identifier such as a unified -namespace path. A Producer MAY include the publisher and writer query -parameters defined in this extension. A Producer using the WoT profile MAY -instead follow its `source` mapping. +namespace path. A Producer using the WoT profile MAY instead follow its +`source` mapping. For WoT data and event messages, a Producer MUST include +each publisher or writer query parameter REQUIRED by the mapping unless the +full OPC UA Part 14 NetworkMessage envelope or the Context supplies its +corresponding value. ### type @@ -288,8 +290,9 @@ these representation or range constraints. When these values are supplied in `source`, the query parameters carry stream-constant metadata. The Producer SHOULD omit a query parameter when the -Context supplies its value. If the Context does not supply a value REQUIRED by -the mapping, the Producer MUST supply it in `source`. +full OPC UA Part 14 NetworkMessage envelope or the Context supplies its value. +For each value REQUIRED by the WoT mapping that neither supplies, the Producer +MUST include the corresponding parameter in `source`. The Producer MUST use `name=value` for each of these parameters, with a non-empty value. It MUST separate them from each other and from preceding From d997746638cf7cf7cc01d40a4b460547e6320da8 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 12:58:58 +0100 Subject: [PATCH 22/28] docs(opcua): require WoT message type mapping Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 428883b1c..9d4162893 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -51,7 +51,7 @@ An OPC UA Producer MAY map `type` to the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) field `MessageType`. A Producer using the WoT profile MUST follow its semantic event-type rules. When the `type` identifies a WoT event contract, a Producer -SHOULD carry the native `MessageType` as `opcuamessagetype` in the event. The +MUST carry the native `MessageType` as `opcuamessagetype` in the event. The [WoT mapping section](#wot-mapping) requires it when both profiles are used. From b56c2728742b1a5d3fc7c1d4f11dabc00df50ef6 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 13:33:29 +0100 Subject: [PATCH 23/28] docs: address local review feedback on WoT and OPC UA Reorder the WoT profile so Core attribute mappings come first and Context resolution has its own contexturl section. Remove obsolete source-query and custom-scheme wording. Require OPC UA publisher and writer identity for all data and event messages, accept identity from a retained Part 14 NetworkMessage envelope, the source query, or the Context, and define handling for missing or conflicting values. Add a full-envelope example. Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 215 +++++++++++++++++++++++--------- cloudevents/extensions/wot.md | 107 ++++++++-------- 2 files changed, 204 insertions(+), 118 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 9d4162893..457946a2c 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -9,10 +9,11 @@ attributes. When used with the [Event Context extension](context.md) and the xRegistry Context model, metadata that stays constant for a message stream is preferably stored in the Context rather than repeated in each event. The existing OPC UA -attributes remain defined and usable. Publisher and writer metadata can -instead be supplied in the `source` query when the Context does not provide -it. The [shared metadata rules](#shared-stream-metadata-in-a-context) describe -how a Consumer obtains these values. +attributes remain defined and usable. A Producer MUST supply publisher and +writer metadata REQUIRED by this mapping in the `source` query when neither +the full Part 14 NetworkMessage envelope nor the Context provides it. The +[shared metadata rules](#shared-stream-metadata-in-a-context) describe how a +Consumer obtains these values. ## Notational Conventions @@ -39,8 +40,8 @@ field `MessageId`. An OPC UA Producer MAY identify the server using its [Application Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) field `applicationUri` or a customer-configured identifier such as a unified -namespace path. A Producer using the WoT profile MAY instead follow its -`source` mapping. For WoT data and event messages, a Producer MUST include +namespace path. A Producer using the WoT profile MUST instead follow its +`source` mapping. For data and event messages, a Producer MUST include each publisher or writer query parameter REQUIRED by the mapping unless the full OPC UA Part 14 NetworkMessage envelope or the Context supplies its corresponding value. @@ -52,8 +53,7 @@ An OPC UA Producer MAY map `type` to the field `MessageType`. A Producer using the WoT profile MUST follow its semantic event-type rules. When the `type` identifies a WoT event contract, a Producer MUST carry the native `MessageType` as `opcuamessagetype` in the event. The -[WoT mapping section](#wot-mapping) requires it when both -profiles are used. +[WoT mapping section](#wot-mapping) specifies the combined mapping. ### WoT mapping @@ -64,17 +64,13 @@ messages: - `source` MUST follow the WoT profile's source mapping. - `type` MUST follow the WoT profile's semantic event-type rules instead of mapping to the native `MessageType`. -- The native `MessageType` MUST be supplied as `opcuamessagetype` in the event. - `subject` MUST follow the WoT profile's resolved-TD pointer rules. -- Publisher and writer metadata supplied in `source` or the Context MUST - identify the native Publisher and DataSetWriter. Writer identifiers MUST NOT - be appended to `subject`. -- For data and event messages, at least one of `opcuadatasetwriterid` and - `opcuadatasetwritername` MUST be present. If both are present, they MUST - identify the same DataSetWriter. +- The native PublisherId and at least one DataSetWriter identifier MUST be + available from the full Part 14 NetworkMessage envelope, `source`, or the + Context, as defined in [Publisher and Writer Metadata](#publisher-and-writer-metadata). + Writer identifiers MUST NOT be appended to `subject`. - For an event message, the `EventId` remains in the OPC UA event payload and MUST NOT be appended to `subject`. -- `opcuapublisherid` MUST identify the native PublisherId. - `opcuasequence` MUST carry the native `SequenceNumber`. The CloudEvents `sequence` attribute, when present, MUST satisfy the independent ordering requirements of the Sequence extension and MUST NOT be inferred from @@ -84,6 +80,8 @@ The [publisher and writer metadata rules](#publisher-and-writer-metadata) define the source-query and Context representations of these identifiers. They do not relocate `source`, `type`, or `subject`, and they do not change how those Core attributes map. +Native identifiers retained in a full NetworkMessage envelope satisfy the +same presence requirements. These conditional mappings replace only the conflicting OPC UA mappings above. All other requirements in this extension remain applicable. A profile that @@ -109,17 +107,19 @@ separate format like [JSON schema](https://json-schema.org/specification) or [Avro schema](https://avro.apache.org/docs/1.11.1/specification/) or others. For those cases the attribute references the schema and is used for versioning. -When the Event Context extension is used, its `schema` Reference or `message` -representation can supply this schema instead. The Consumer follows that -extension's schema precedence, and the Producer SHOULD omit `dataschema` when -the Context supplies the schema. +When the Event Context extension is used, a Producer MAY reference a Context +whose `schema` Reference or `message` representation supplies this schema +instead. The Consumer follows that extension's schema precedence, and the +Producer SHOULD omit `dataschema` when the Context supplies the schema. The +selected schema MUST describe the actual `data`, including the full +NetworkMessage envelope when one is retained. ### subject -For metadata and data messages, a Producer MAY map `subject` to +For metadata, data, and event messages, a Producer MAY map `subject` to `DataSetWriterId` or `DataSetWriterName` from the [Data Set Message -Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) -header. A Producer using the WoT profile MUST instead follow its +Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164). +A Producer using the WoT profile MUST instead follow its resolved-TD pointer mapping. For an OPC UA event message (`type` equals `ua-event`), a Producer using the @@ -135,9 +135,9 @@ field `Timestamp`. ## Mapping for other extensions -For data and event messages, the following well-known extension attributes -apply when this extension is used without the WoT profile. When both -extensions are used, determine message type from `opcuamessagetype`. +The following well-known extension attributes apply to data and event +messages. When the WoT profile is used, determine the native message type +from `opcuamessagetype`. ### sequence @@ -246,9 +246,10 @@ than omit the status, wrap the value, or report it as _Good_. ## Publisher and Writer Metadata The following values identify the OPC UA Publisher and DataSetWriter. They -are not CloudEvents extension attributes. A Producer MAY supply them as -query parameters in `source`, as extension attributes of the applicable -Context Version, or in both places. The names are the same in both +are not CloudEvents extension attributes. A Producer MUST supply each +REQUIRED value as a query parameter in `source` or an extension attribute of +the applicable Context Version unless the full Part 14 NetworkMessage +envelope supplies it. The names are the same in the query and Context representations. | Name | Value | @@ -257,6 +258,12 @@ representations. | `opcuadatasetwriterid` | The native DataSetWriterId, represented as an integer. | | `opcuadatasetwritername` | The configured DataSetWriterName, represented as a string. | +In a full [Part 14 NetworkMessage][UA NetworkMessage], the Consumer obtains +the PublisherId from the NetworkMessage header and the DataSetWriterId or +DataSetWriterName from its single DataSetMessage header. Those fields retain +their native names and encodings. The following representation rules apply +to their equivalents in `source` and the Context. + `opcuapublisherid` MUST use `:`. The type MUST be `Byte`, `UInt16`, `UInt32`, `UInt64`, or `String`, as defined for the native [PublisherId][UA PublisherId]. For an unsigned integer type, the value MUST be @@ -275,12 +282,22 @@ configured name. For JSON message mapping, this is the [`DataSetWriterName` field][UA DataSetWriterName] when present in the DataSetMessage; otherwise the Producer obtains it from configuration. -In combination with the WoT profile, `opcuapublisherid` and at least one of -`opcuadatasetwriterid` and `opcuadatasetwritername` are REQUIRED for data and -event messages. If both writer values are supplied, they MUST identify the -same DataSetWriter. A writer name MUST uniquely identify the DataSetWriter -within the PublisherId scope. Consumers MUST compare names exactly, without -case folding or Unicode normalization. +The native PublisherId and at least one of DataSetWriterId and DataSetWriterName +are REQUIRED for all data and event messages, whether or not the WoT profile +is used. Each value MUST be supplied by the envelope or by the corresponding +query parameter or Context attribute. A full envelope that omits an identifier +does not satisfy that identifier's presence requirement. If both writer values +are supplied, they MUST identify the same DataSetWriter. A writer name MUST +uniquely identify the DataSetWriter within the PublisherId scope. Consumers +MUST compare names exactly, without case folding or Unicode normalization. + +When an identity is supplied in more than one place, the representations MUST +identify the same native Publisher or DataSetWriter. To compare a query or +Context value with an envelope field, the Consumer MUST decode that value and +apply the field's native Part 14 encoding. For example, `UInt16:12` is encoded +as `"12"` in a JSON NetworkMessage. The Consumer MUST report a metadata +conflict and MUST NOT interpret the payload as a conforming OPC UA message if +the values disagree. A Consumer MUST report invalid metadata and MUST NOT interpret the payload as a conforming OPC UA message if a supplied publisher or writer value violates @@ -291,8 +308,8 @@ these representation or range constraints. When these values are supplied in `source`, the query parameters carry stream-constant metadata. The Producer SHOULD omit a query parameter when the full OPC UA Part 14 NetworkMessage envelope or the Context supplies its value. -For each value REQUIRED by the WoT mapping that neither supplies, the Producer -MUST include the corresponding parameter in `source`. +For each REQUIRED publisher or writer value that neither supplies, the +Producer MUST include the corresponding parameter in `source`. The Producer MUST use `name=value` for each of these parameters, with a non-empty value. It MUST separate them from each other and from preceding @@ -318,7 +335,8 @@ in the query and an integer in the Context; the other two values use strings in both places. For example, the following source supplies the PublisherId and writer name -when the Context does not supply them. The writer name is `Pump&Process`. +when neither the envelope nor the Context supplies them. The writer name is +`Pump&Process`. ```text mqtt://site/line/cell/device/sensor?opcuapublisherid=UInt16:12;opcuadatasetwritername=Pump%26Process;site=north @@ -335,11 +353,20 @@ returns to a previously used `source`. - OPC UA PubSub JSON messages MUST be encoded using non-reversible encoding as the decoding information is contained in metadata messages or by schema referenced via `dataschema` attribute or selected through Event Context. -- Payload of OPC UA PubSub JSON messages MUST NOT contain Network Message Header -and Data Set Header as that information is mapped into CloudEvents attributes, -`source`, or the Context. +- A Producer MAY retain the full Part 14 NetworkMessage envelope in `data`. + Otherwise, the payload MUST omit the NetworkMessage and DataSetMessage + headers and supply their applicable information through the mappings in + this extension. - OPC UA PubSub JSON messages MUST contain exactly one dataset message. +A retained envelope does not replace the REQUIRED CloudEvents attributes or +the per-event `opcuamessagetype` and `opcuasequence` attributes when the WoT +mapping applies. `opcuamessagetype` identifies the DataSetMessage type, not +the enclosing NetworkMessage's `ua-data` type. Duplicated values MUST agree +with their native fields after applying the specified mappings. The Consumer +MUST report a conflict and MUST NOT interpret a message as conforming to this +extension when they disagree. + ## Shared Stream Metadata in a Context A message stream here is a configured flow from one Publisher and @@ -354,11 +381,11 @@ values once in the Context avoids repeating them in event attributes or the `source` query. The Context has no document body: these values are members of its metadata object, alongside `references`. -The following values can be supplied by the Context when they remain constant -for every event using that Context Version. When a Context supplies any of -these values, `contexturl` MUST identify a Context Version. A Context Resource -URL can select a different default Version when a delayed or replayed event is -processed. +The Context Publisher MAY store the following values in the Context when they +remain constant for every event using that Context Version. When a Context +supplies any of these values, `contexturl` MUST identify a Context Version. +A Context Resource URL can select a different default Version when a delayed +or replayed event is processed. | Context attribute | Meaning and conditions | | --- | --- | @@ -376,24 +403,26 @@ Context's `schema` Reference identifies an external payload schema. Native metadata-version attributes MUST NOT be supplied in the event or Context when a schema is selected through `dataschema` or the Context. -A value supplied by an applicable Context satisfies this extension's presence -requirements, including those of the combined WoT mapping. A Producer MAY -omit an event attribute or source query parameter when the Context supplies -the corresponding value. The existing OPC UA event attributes remain valid. +For the listed values, an applicable Context satisfies this extension's +presence requirements, including those of the combined WoT mapping. A Producer +MAY omit the corresponding event attribute or source query parameter. Other +event attributes, including `opcuamessagetype`, MUST NOT be replaced by Context +members. If a publisher or writer value appears in both the `source` query and the Context, the values MUST agree after decoding and type conversion. If an event attribute and the Context both supply a metadata version, their typed values -MUST agree. On a conflict, the Consumer MUST report a -metadata conflict and MUST NOT interpret the payload using that Context. If a -REQUIRED value is absent from all applicable locations, the Consumer MUST -report missing metadata and MUST NOT interpret the payload as a conforming -OPC UA message. +MUST agree. The same consistency requirement applies to publisher and writer +identities retained in the envelope. On a conflict, the Consumer MUST report a +metadata conflict and MUST NOT interpret the payload as a conforming OPC UA +message. If a REQUIRED value is absent from all applicable locations, the +Consumer MUST report missing metadata and MUST NOT interpret the payload as a +conforming OPC UA message. A Consumer that cannot retrieve the Context MAY process the event using -its source query and explicit event attributes if they satisfy every applicable -requirement. When the WoT profile is used, the Consumer also needs the Context -to resolve the TD and MUST follow the WoT profile's failure rules. +its envelope, source query, and explicit event attributes if they satisfy every +applicable requirement. When the WoT profile is used, the Consumer also needs +the Context to resolve the TD and MUST follow the WoT profile's failure rules. The Context Publisher MUST NOT change these values in a Context Version after any Producer is configured with its URL. A change requires a new Context Version @@ -417,6 +446,10 @@ metadata only when they know that the intended Consumers support this mapping. ## Examples +The examples without a Context use publisher `UInt16:12` and writer name +`energy-consumption-asset`. Their payloads omit the native headers, so their +`source` queries supply both identifiers. + ### Metadata message The metadata message helps Cloud applications to understand the semantics and @@ -434,7 +467,7 @@ specversion: 1.0 type: ua-metadata time: 2024-03-28T21:56:24Z id: 1234-1234-1234 -source: urn:factory:aggregationserver:opcua +source: https://factory.example.com/aggregationserver/opcua?opcuapublisherid=UInt16:12;opcuadatasetwritername=energy-consumption-asset datacontenttype: application/json; charset=utf-8 subject: energy-consumption-asset .... further attributes ... @@ -478,7 +511,7 @@ specversion: 1.0 type: ua-deltaframe time: 2024-03-28T21:56:42Z id: 1235-1235-1235 -source: urn:factory:aggregationserver:opcua +source: https://factory.example.com/aggregationserver/opcua?opcuapublisherid=UInt16:12;opcuadatasetwritername=energy-consumption-asset datacontenttype: application/json; charset=utf-8 subject: energy-consumption-asset sequence: 0000000007 @@ -553,7 +586,7 @@ specversion: 1.0 type: ua-event time: 2024-03-28T21:57:01Z id: 1236-1237-1238 -source: urn:factory:aggregationserver:opcua +source: https://factory.example.com/aggregationserver/opcua?opcuapublisherid=UInt16:12;opcuadatasetwritername=energy-consumption-asset datacontenttype: application/json; charset=utf-8 subject: energy-consumption-asset/444321 sequence: 0000000018 @@ -595,7 +628,7 @@ specversion: 1.0 type: ua-keyframe time: 2024-03-28T23:59:59Z id: 6235-7235-8235 -source: bottling-company/amsterdam/FillingArea1/FillingLine9/Cell1/Conveyor +source: bottling-company/amsterdam/FillingArea1/FillingLine9/Cell1/Conveyor?opcuapublisherid=UInt16:12;opcuadatasetwritername=energy-consumption-asset datacontenttype: application/avro subject: energy-consumption-asset dataschema: http://example.com/schemas/energy-consumption-asset/v1.8 @@ -675,6 +708,63 @@ Context with the event's `opcuamessagetype` and `opcuasequence`. It evaluates `subject` against the TD selected by `emitter` and uses the schema selected by `schema`. +#### Retaining the NetworkMessage envelope + +This event retains the full Part 14 NetworkMessage envelope. Its PublisherId +and DataSetWriterId satisfy the identity requirements, so `source` needs no +publisher or writer query parameters. Part 14 JSON encodes the configured +PublisherId `UInt16:12` as the string `"12"`. + +Context Version `2` retains the same `emitter` Reference and points its `schema` +Reference to `https://example.com/schemas/pump-process-data-envelope/versions/1`. +It contains no publisher or writer attributes. The event type uses revision +`v2` because retaining the envelope changes the payload contract. + +```text +------------------ PUBLISH ------------------- + +Topic Name: opcua/json/DataSetMessage/pump +Content Type: application/json + +------------- User Properties ---------------- + +specversion: 1.0 +id: ua-message-25461 +source: mqtt://site/line/cell/device/sensor?site=north +type: com.example.models.tm.pump.properties.processData.v2 +subject: #/properties/processData +time: 2026-10-29T12:00:01Z +contexturl: https://registry.example/contextgroups/line1/contexts/opcua-pump-process-data/versions/2 +opcuamessagetype: ua-keyframe +opcuasequence: 0 + +------------------ payload ------------------- + +{ + "MessageId": "ua-message-25461", + "MessageType": "ua-data", + "PublisherId": "12", + "Messages": [ + { + "DataSetWriterId": 17, + "MessageType": "ua-keyframe", + "SequenceNumber": 0, + "Timestamp": "2026-10-29T12:00:01Z", + "Payload": { + "Speed": 1500, + "Pressure": 4.5 + } + } + ] +} + +----------------------------------------------- +``` + +The native sequence has rolled over from `4294967295` to `0`; it remains +`opcuasequence`, not a CloudEvents `sequence`. The event still uses `contexturl` +to select its TD and schema, and carries `opcuamessagetype` in the event. + ## Context Retrieval Security The Event Context extension's retrieval, authorization, and cache rules apply @@ -683,6 +773,7 @@ the Publisher or authorize access to the referenced TD or schema. A Consumer MUST establish trust in the Context before using it as OPC UA metadata. [UA DataSetWriterName]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table185 +[UA NetworkMessage]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/7.2.5.3 [UA DataSetWriterId]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.4.1 [UA PublisherId]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.7.1 [UA SequenceNumber]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.3 diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 4ddf22740..0d1be6947 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -6,10 +6,10 @@ The REQUIRED `contexturl` identifies shared metadata whose `emitter` Reference selects the resolved TD. The `subject` identifies the part of that TD associated with the event, and the `type` names the event contract. -The profile defines no CloudEvents extension attributes or URI scheme. -Producers and Consumers agree on its use through a publication profile or out -of band. It does not define a transport binding, provision a Thing, or require -a particular registry implementation. Events use the xRegistry Context +The profile defines no CloudEvents extension attributes. Producers and +Consumers agree on its use through a publication profile or out of band. +It does not define a transport binding, provision a Thing, or require a +particular registry implementation. Events use the xRegistry Context representation defined by the Event Context extension. ## Notational Conventions @@ -22,67 +22,26 @@ described in [RFC 2119](https://tools.ietf.org/html/rfc2119). The scope of these key words is limited to events using this profile. An attribute marked REQUIRED here is not REQUIRED in every CloudEvent. -## Source +## Mapping Core Context Attributes + +### source The Core [`source`](../spec.md#source) identifies the context in which the -event occurred and follows Core's URI-reference rules. It can identify a Thing, -topology, publisher, or another Producer-defined context. This profile does not -prescribe a URI scheme or query parameters. The Context's `emitter` Reference, -not `source`, selects the resolved TD. +event occurred and follows Core's URI-reference rules. This profile leaves +the content of `source` undefined beyond those Core requirements. -For example, a Producer can use `https://factory.example.com/line1/pump7` or -`mqtt://site/line/cell/device/sensor` as a source. Another possible form is -`wot-td://factory/line1`; this profile does not define that scheme or propose -its registration. A source is an identifier and is not necessarily a retrieval -URL for a TD. +For example, `https://factory.example.com/line1/pump7` and +`mqtt://site/line/cell/device/sensor` are valid sources. -### Event identity +#### Event identity Core requires `source` and `id` together to identify each distinct event. -Changing the `source`, including any query parameters, changes the event -identity and [Sequence extension](sequence.md) scope. +Changing the `source` changes the event identity and +[Sequence extension](sequence.md) scope. Returning to an earlier `source` returns to its existing scope: Producers MUST NOT reuse event identities or restart a claimed source-wide sequence within that scope. -Changing `contexturl` alone does not change event identity or sequence scope. -A Producer MUST NOT reuse an `id` or restart a source-wide sequence merely -because it publishes a new Context Version. - -## Resolving the Thing Description - -Every event using this profile MUST include `contexturl`. The Producer and -Consumer MUST implement the [Event Context extension](context.md). The Context -MUST contain an `emitter` Reference whose `url` identifies the resolved TD -document, not its Registry metadata. The Context Publisher SHOULD reference an -explicit TD Version and publish the resolved TD view at that URL. The Consumer -retrieves the document directly or uses the corresponding document in an -accepted resolved Context view. It MUST NOT derive a TD URL from `contexturl` -or from the `emitter` URL. - -The Context's `emitter` is authoritative for TD selection. The Consumer MUST -evaluate `subject` against that TD and MUST NOT select another TD from `source`. -This rule does not change Core's meaning of `source` or authenticate the -emitting entity. - -If `contexturl` is missing, the Context cannot be retrieved or is invalid, its -`emitter` Reference is missing or invalid, or the referenced TD is unavailable -or not resolved, the Consumer MUST report a resolution failure and leave the -event uncorrelated. It MUST NOT fall back to selection through `source`. It -MAY retain or forward the event without TD-based interpretation. - -The Context Publisher MUST make the TD available before configuring Producers -with the Context Version URL. Changes to its Reference require a new Context -Version. A change to the TD's resolved logical view requires a new TD Version; -the Context Publisher MUST NOT change that view at a previously published -Version URL. Context versioning alone does not make an unversioned TD URL -stable. - -The event carries no digest of the TD. An exact version or a successful -resolution does not by itself prove that retrieved bytes are unchanged. - -## Mapping Core Context Attributes - ### type The Core [`type`](../spec.md#type) identifies the event contract. The contract @@ -207,7 +166,43 @@ An xRegistry Schema Resource's document-view URL can identify that external schema. It is not the WoT Resource's metadata-view URL. A profile MAY require an exact schema Version independently of the selected TD Version. -### Relationship to Other Extensions +## contexturl + +Every event using this profile MUST include `contexturl`. The Producer and +Consumer MUST implement the [Event Context extension](context.md). The Context +MUST contain an `emitter` Reference whose `url` identifies the resolved TD +document, not its Registry metadata. The Context Publisher SHOULD reference an +explicit TD Version and publish the resolved TD view at that URL. The Consumer +retrieves the document directly or uses the corresponding document in an +accepted resolved Context view. It MUST NOT derive a TD URL from `contexturl` +or from the `emitter` URL. + +The Context's `emitter` is authoritative for TD selection. The Consumer MUST +evaluate `subject` against that TD and MUST NOT select another TD from `source`. +This rule does not change Core's meaning of `source` or authenticate the +emitting entity. + +If `contexturl` is missing, the Context cannot be retrieved or is invalid, its +`emitter` Reference is missing or invalid, or the referenced TD is unavailable +or not resolved, the Consumer MUST report a resolution failure and leave the +event uncorrelated. It MUST NOT fall back to selection through `source`. It +MAY retain or forward the event without TD-based interpretation. + +The Context Publisher MUST make the TD available before configuring Producers +with the Context Version URL. Changes to its Reference require a new Context +Version. A change to the TD's resolved logical view requires a new TD Version; +the Context Publisher MUST NOT change that view at a previously published +Version URL. Context versioning alone does not make an unversioned TD URL +stable. + +Changing `contexturl` alone does not change event identity or sequence scope. +A Producer MUST NOT reuse an `id` or restart a source-wide sequence merely +because it publishes a new Context Version. + +The event carries no digest of the TD. An exact version or a successful +resolution does not by itself prove that retrieved bytes are unchanged. + +## Relationship to Other Extensions This profile does not redefine native message types, sequence counters, or writer identities. An event that also uses the [OPC UA extension](opcua.md) From a1a988334cba52396cb32c1f56eb4c69b62644a5 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 13:58:21 +0100 Subject: [PATCH 24/28] docs(opcua): address remaining review feedback Remove redundant WoT source and identity rules from the OPC UA extension, clarify subject and writer metadata mapping, and omit opcuamessagetype when the retained NetworkMessage carries it. Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 61 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 457946a2c..6206366aa 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -40,10 +40,9 @@ field `MessageId`. An OPC UA Producer MAY identify the server using its [Application Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) field `applicationUri` or a customer-configured identifier such as a unified -namespace path. A Producer using the WoT profile MUST instead follow its -`source` mapping. For data and event messages, a Producer MUST include -each publisher or writer query parameter REQUIRED by the mapping unless the -full OPC UA Part 14 NetworkMessage envelope or the Context supplies its +namespace path. For data and event messages, a Producer MUST include each +publisher or writer query parameter REQUIRED by the mapping unless the full +OPC UA Part 14 NetworkMessage envelope or the Context supplies its corresponding value. ### type @@ -52,8 +51,11 @@ An OPC UA Producer MAY map `type` to the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164) field `MessageType`. A Producer using the WoT profile MUST follow its semantic event-type rules. When the `type` identifies a WoT event contract, a Producer -MUST carry the native `MessageType` as `opcuamessagetype` in the event. The -[WoT mapping section](#wot-mapping) specifies the combined mapping. +MUST carry the native `MessageType` as `opcuamessagetype` when `data` omits +that field from the DataSetMessage header. When a retained NetworkMessage +envelope includes the field, the Producer MUST NOT repeat it as an event +attribute. The [WoT mapping section](#wot-mapping) specifies the combined +mapping. ### WoT mapping @@ -61,35 +63,27 @@ When a CloudEvent uses both this extension and the [WoT profile](./wot.md), the following mappings apply to data and event messages: -- `source` MUST follow the WoT profile's source mapping. - `type` MUST follow the WoT profile's semantic event-type rules instead of mapping to the native `MessageType`. - `subject` MUST follow the WoT profile's resolved-TD pointer rules. - The native PublisherId and at least one DataSetWriter identifier MUST be available from the full Part 14 NetworkMessage envelope, `source`, or the Context, as defined in [Publisher and Writer Metadata](#publisher-and-writer-metadata). - Writer identifiers MUST NOT be appended to `subject`. -- For an event message, the `EventId` remains in the OPC UA event payload and - MUST NOT be appended to `subject`. +- Nothing MUST be appended to `subject`. For an event message, the `EventId` + remains in the OPC UA event payload. - `opcuasequence` MUST carry the native `SequenceNumber`. The CloudEvents `sequence` attribute, when present, MUST satisfy the independent ordering requirements of the Sequence extension and MUST NOT be inferred from `opcuasequence`. The [publisher and writer metadata rules](#publisher-and-writer-metadata) -define the source-query and Context representations of these identifiers. -They do not relocate `source`, `type`, or `subject`, and they do not change -how those Core attributes map. -Native identifiers retained in a full NetworkMessage envelope satisfy the -same presence requirements. +define how OPC UA publisher and writer identifiers are supplied. The Core +attribute mappings above remain unchanged. These conditional mappings replace only the conflicting OPC UA mappings above. All other requirements in this extension remain applicable. A profile that uses only part of this extension MUST NOT claim conformance to it unless the -profile satisfies every applicable requirement in this document. Producers -sharing a WoT `source` MUST assign distinct native `MessageId` values to -distinct events within that source to satisfy Core's `source` and `id` -uniqueness requirement. +profile satisfies every applicable requirement in this document. ## Mapping of OPTIONAL Attributes @@ -216,12 +210,14 @@ than omit the status, wrap the value, or report it as _Good_. ### opcuamessagetype - Type: `String` -- Description: Carries the OPC UA PubSub DataSetMessage type when `type` - identifies a WoT semantic event type. +- Description: Carries the OPC UA PubSub DataSetMessage type when the + DataSetMessage header is omitted from `data`. - Constraints - OPTIONAL - In combination with the WoT profile, the value is REQUIRED for data and - event messages. + event messages when `data` omits the DataSetMessage header. + - MUST NOT be present when `data` retains a full NetworkMessage envelope + that includes the DataSetMessage `MessageType`. - MUST map to the Data Set Message Header `MessageType`. - The value MUST be the native OPC UA message type, such as `ua-keyframe`, `ua-deltaframe` or `ua-event`. @@ -357,15 +353,17 @@ referenced via `dataschema` attribute or selected through Event Context. Otherwise, the payload MUST omit the NetworkMessage and DataSetMessage headers and supply their applicable information through the mappings in this extension. -- OPC UA PubSub JSON messages MUST contain exactly one dataset message. +- OPC UA PubSub JSON messages MUST contain exactly one DataSetMessage, + including when a full NetworkMessage envelope is retained. A retained envelope does not replace the REQUIRED CloudEvents attributes or -the per-event `opcuamessagetype` and `opcuasequence` attributes when the WoT -mapping applies. `opcuamessagetype` identifies the DataSetMessage type, not -the enclosing NetworkMessage's `ua-data` type. Duplicated values MUST agree -with their native fields after applying the specified mappings. The Consumer -MUST report a conflict and MUST NOT interpret a message as conforming to this -extension when they disagree. +the per-event `opcuasequence` attribute when the WoT mapping applies. +`opcuamessagetype` is REQUIRED only when `data` omits the DataSetMessage +header; when the envelope includes that field, the attribute MUST be absent. +The NetworkMessage's `ua-data` type is not the DataSetMessage type. Any +duplicated native values MUST agree after applying the specified mappings. +The Consumer MUST report a conflict and MUST NOT interpret a message as +conforming to this extension when they disagree. ## Shared Stream Metadata in a Context @@ -735,7 +733,6 @@ type: com.example.models.tm.pump.properties.processData.v2 subject: #/properties/processData time: 2026-10-29T12:00:01Z contexturl: https://registry.example/contextgroups/line1/contexts/opcua-pump-process-data/versions/2 -opcuamessagetype: ua-keyframe opcuasequence: 0 ------------------ payload ------------------- @@ -763,7 +760,9 @@ opcuasequence: 0 The native sequence has rolled over from `4294967295` to `0`; it remains `opcuasequence`, not a CloudEvents `sequence`. The event still uses `contexturl` -to select its TD and schema, and carries `opcuamessagetype` in the event. +to select its TD and schema. The DataSetMessage's `MessageType` remains in the +retained NetworkMessage envelope instead of being repeated as +`opcuamessagetype`. ## Context Retrieval Security From 3fb6540266bd08ee7dcf68bd4a3c37fd3b691efc Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 12 Nov 2026 14:17:08 +0100 Subject: [PATCH 25/28] docs(opcua): read message type from retained envelope when present Signed-off-by: Marc --- cloudevents/extensions/opcua.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 6206366aa..65e7f30b4 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -131,7 +131,8 @@ field `Timestamp`. The following well-known extension attributes apply to data and event messages. When the WoT profile is used, determine the native message type -from `opcuamessagetype`. +from `opcuamessagetype` when present or from the retained NetworkMessage's +DataSetMessage header when it contains `MessageType`. ### sequence From 0f6537f8192a88d51b8a2979b1492d41f20f75f7 Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 14 Nov 2026 15:39:41 +0100 Subject: [PATCH 26/28] Define Descriptor Reference and direct TD subject mapping Signed-off-by: Marc --- cloudevents/extensions/README.md | 2 +- cloudevents/extensions/context.md | 210 --------------- cloudevents/extensions/describedby.md | 372 ++++++++++++++++++++++++++ cloudevents/extensions/opcua.md | 352 ++++++++++++++---------- cloudevents/extensions/wot.md | 372 +++++++++++++++++++------- 5 files changed, 851 insertions(+), 457 deletions(-) delete mode 100644 cloudevents/extensions/context.md create mode 100644 cloudevents/extensions/describedby.md diff --git a/cloudevents/extensions/README.md b/cloudevents/extensions/README.md index 971bdbd4c..7fdfb70f4 100644 --- a/cloudevents/extensions/README.md +++ b/cloudevents/extensions/README.md @@ -45,8 +45,8 @@ for more information. - [Data Classification](data-classification.md) - [Dataref (Claim Check Pattern)](dataref.md) - [Deprecation](deprecation.md) +- [Descriptor Reference](describedby.md) - [Distributed Tracing](distributed-tracing.md) -- [Event Context](context.md) - [Expiry Time](expirytime.md) - [OPC UA](opcua.md) - [Partitioning](partitioning.md) diff --git a/cloudevents/extensions/context.md b/cloudevents/extensions/context.md deleted file mode 100644 index b01fa463f..000000000 --- a/cloudevents/extensions/context.md +++ /dev/null @@ -1,210 +0,0 @@ -# Event Context - -The `contexturl` extension attribute identifies shared information that a -Consumer needs to interpret an event. Instead of repeating a Message definition -URL, schema URL, and emitter description URL in every event, a Producer names -one Context that references those representations by Role. - -This proposal aligns with the [xRegistry Event Context working draft][Context -Registry] in [xregistry/spec#796][Context PR]. The attribute name and the -resolved-view media type remain provisional. A Context is a metadata-only -xRegistry Resource, not a document attached to that Resource. This extension -does not require a Producer to host or query a Registry. - -A Publisher creates Contexts and their referenced representations. A Producer -emits events, and a Consumer processes them. - -## Notational Conventions - -As with the main [CloudEvents specification](../spec.md), the key words "MUST", -"MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", -"RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as -described in [RFC 2119](https://tools.ietf.org/html/rfc2119). - -The scope of these key words is limited to events using this extension. - -## Attributes - -### contexturl - -- Type: `URI` -- Description: The absolute URL of the Context that applies to this event. -- Constraints: - - OPTIONAL. - - If present, MUST identify a Context Resource or Context Version and MUST - NOT contain a fragment. - - SHOULD identify a Context Version. - - Consumers MUST use the value unchanged as the request URL and as the URL - component of the cache key. Cache selection MUST also account for the - selected representation and authorization context. - - Consumers MUST NOT derive other URLs from the value. In particular, they - MUST NOT append `$details`, a view path, or a query parameter, or remove - `/versions/`. - - When absent, this extension supplies no Context for the event. - -In the JSON event format, `contexturl` is a top-level string member. In HTTP -binary content mode, it is the `ce-contexturl` header, encoded and decoded -according to the [HTTP binding](../bindings/http-protocol-binding.md). -Other bindings use their normal extension-attribute mapping. - -## Context Representation - -An ordinary `GET` of `contexturl` with `Accept: application/json` returns -Context metadata directly. The xRegistry Context model sets `hasdocument` to -`false`; no `$details` request or separate Context document is needed. - -The REQUIRED `references` map contains at least one entry. Each key is a Role, -and each value is an object with a REQUIRED `url` member. That member is an -absolute URL without a fragment. Consumers MUST use each referenced URL -unchanged when retrieving its representation. - -The working draft defines these Roles. - -| Role | Referenced representation | -| --- | --- | -| `message` | Message definition metadata describing the event. | -| `schema` | Schema document for the event's `data`. | -| `emitter` | Description of the emitting entity, such as a WoT Thing Description Version document. | -| `target` | Model that the event concerns, such as an OpenUSD asset. | - -Roles follow the working draft's map-key rules. Unprefixed names are reserved -for that draft. Other Roles MUST use an identifying prefix followed by `:`, -such as `acme:policy`. Consumers MUST ignore Roles they do not use and Reference -attributes they do not understand. - -Contexts and References permit extension attributes. A profile can use those -attributes for metadata shared by a message stream. Such metadata is not -automatically a CloudEvents attribute: the profile MUST define its meaning and -how it relates to attributes carried in the event. - -## Processing and Versioning - -When including `contexturl`, a Producer MUST name a Context whose references -apply to the event. A Publisher MUST make referenced representations available -before configuring Producers with the Context URL. Producers SHOULD reuse a -configured Context Version URL for events with the same shared information. -A Context with a `message` Reference applies to one event type; Producers -that emit several types use a separate Context for each type in that case. - -A Publisher MUST NOT change a Context Version's `references` after any -Producer is configured with its URL. Changing references requires a new -Version and reconfigured Producers. Profiles that store shared metadata in -Context extension attributes MUST define equivalent stability rules for those -values. - -A Context Resource URL serves the current default Version. That default can -change without changing `contexturl`, so Consumers SHOULD bound the lifetime -of cached Resource representations. A Context Version fixes its references, -not the bytes of referenced representations. Publishers SHOULD reference -explicit Target Versions when replay requires reproducible interpretation and -SHOULD retain Contexts and referenced representations for the replay period. - -A Consumer retrieves the Context from the unchanged URL or uses a valid cache -entry for that URL, representation, and authorization context. It MUST check -the response status, media type, and Context representation against the -working draft. A failed response, unsupported representation, or missing or -empty `references` map makes the Context unavailable. For a valid Context, the -Consumer retrieves the representations for the Roles it needs, subject to its -access and content-trust policies. A missing Role that the Consumer needs -makes the Context incomplete. - -When retrieval fails or the Context is invalid or incomplete, the Consumer -MUST NOT derive replacement URLs from `contexturl`. It SHOULD record the -condition. Local policy determines whether to defer, reject, or process the -event without that information, unless another profile requires a particular -outcome. - -### Resolved Context view - -A supporting server can return the Context and all directly referenced -representations in one response. A Consumer requests that view at the same -unchanged URL with: - -```http -Accept: application/vnd.xregistry.context-view+json, application/json;q=0.5 -``` - -The Consumer MUST inspect `Content-Type` rather than assume that the server -supports the view. Ordinary `application/json` returns Context metadata; -`application/vnd.xregistry.context-view+json` returns `context` and `documents` -as defined by the working draft. The view includes every direct Reference or -returns an error. It does not recursively retrieve their dependencies or -establish a snapshot across Registries. - -## Relationship to Other Attributes - -Core attributes retain their meaning. In particular, `contexturl` does not -replace REQUIRED `id`, `source`, or `type`, and does not locate the event's -`data` as the [Dataref extension](dataref.md) does. - -A Consumer selects the first applicable schema source in this order: - -1. The event's `dataschema`. -2. The Context's `schema` Reference. -3. The schema declared by the Context's `message` representation. - -Producers SHOULD omit `dataschema` when the Context supplies the schema. -Publishers and Producers SHOULD supply only one schema source, or identify -the same schema when several are supplied. Consumers apply the precedence -without comparing the sources. - -An `emitter` Reference does not authenticate the event or redefine `source`. -The [WoT profile](wot.md) requires this extension and defines how `emitter` -selects a Thing Description (TD). The -[OPC UA extension](opcua.md#shared-stream-metadata-in-a-context) -defines which stream metadata can be supplied by a Context. Neither rule -applies to generic events merely because they contain `contexturl`. - -## Example - -This event references shared emitter and payload-schema information. - -```json -{ - "specversion": "1.0", - "id": "sample-4711", - "source": "mqtt://site/line/cell/device/sensor", - "type": "com.example.models.tm.pump.properties.processData.v1", - "subject": "#/properties/processData", - "datacontenttype": "application/json", - "contexturl": "https://registry.example/contextgroups/line1/contexts/pump-process-data/versions/1", - "data": { - "Speed": 1500, - "Pressure": 4.5 - } -} -``` - -The Context Version has the following metadata. Other Core metadata is omitted -from this excerpt. - -```json -{ - "contextid": "pump-process-data", - "versionid": "1", - "references": { - "emitter": { - "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3.0.0" - }, - "schema": { - "url": "https://example.com/schemas/pump-process-data/versions/3" - } - } -} -``` - -## Security Considerations - -Consumers and servers that assemble resolved views MUST apply authorization, -URL, redirect, and content-trust policies before retrieving or using a Context -or any referenced representation. An event-provided URL MUST NOT authorize -arbitrary network access or credential forwarding. Implementations SHOULD -bound response sizes, retrieval times, and dependency processing. - -Caches MUST NOT share representations across authorization contexts unless -the applicable access policy permits it. A resolved view does not bypass the -Consumer's trust checks. Context Versions and Target Versions do not by -themselves authenticate content or prove that its bytes are unchanged. - -[Context PR]: https://github.com/xregistry/spec/pull/796 -[Context Registry]: https://github.com/xregistry/spec/blob/e46561facacb672ed3d7603be1574a75b1a11789/workingdrafts/models/context/spec.md diff --git a/cloudevents/extensions/describedby.md b/cloudevents/extensions/describedby.md new file mode 100644 index 000000000..27bedc731 --- /dev/null +++ b/cloudevents/extensions/describedby.md @@ -0,0 +1,372 @@ +# Descriptor Reference + +The OPTIONAL `describedby` extension attribute identifies a Descriptor +containing shared information for interpreting an event. A Descriptor can +reference schemas, Message definitions, WoT Thing Descriptions or other +representations, and can contain typed metadata such as a publisher identity. It +describes information relevant to the event, not necessarily only the event's +source or subject. + +This proposal is developed with the Descriptor Registry revision of +[xregistry/spec#796][Descriptor Registry]. That domain defines the metadata-only +Descriptor Resource, its Asset, Actor and Context specializations, named +References, selectors and OPTIONAL bundle. These are drafts, not released +standards. This document defines their CloudEvents binding; it does not require +a Producer to host or query a Registry. + +A Publisher publishes Descriptors and their referenced representations. A +Producer emits events, and a Consumer processes them. A publication profile is +an agreement defining message interpretation and REQUIRED inputs. Producers and +Consumers establish that agreement through a specification or configuration, not +by trusting a profile asserted in untrusted metadata. + +## Notational Conventions + +As with the main [CloudEvents specification](../spec.md), the key words "MUST", +"MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", +"RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as +described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +The scope of these key words is limited to events using this extension or a +profile that explicitly uses its description-selection rules. + +## Attributes + +### describedby + +- Type: `URI` +- Description: The absolute URL of the Descriptor selected for interpreting + this event. +- Constraints: + - OPTIONAL. + - If present, MUST identify a Descriptor Resource or Descriptor Version + and MUST NOT contain a fragment. + - SHOULD identify a Descriptor Version. Profiles MAY require one. + - Consumers MUST use the value unchanged as the request URL and as the URL + component of the cache key. Cache selection MUST also account for the + selected representation and authorization context. + - Consumers MUST NOT derive other URLs from the value. In particular, they + MUST NOT append `$details`, a view path or query parameter, or remove + `/versions/`. + - An empty string or otherwise invalid set value is invalid, not absent. + +In the JSON event format, `describedby` is a top-level string member. In HTTP +binary content mode, it is the `ce-describedby` header, encoded and decoded +according to the [HTTP binding](../bindings/http-protocol-binding.md). Other +bindings use their normal extension-attribute mapping. + +Consumers MUST apply the event format's decoding rules before testing attribute +presence. In particular, the [JSON +format](../formats/json-format.md#22-type-system-mapping) treats `"describedby": +null` as unset, equivalent to omission. That is not a failed explicit reference +and does not prohibit a profile-defined route for events without `describedby`. +The same presence rule applies to the migration check for simultaneous old and +new attributes. + +The name expresses a descriptive relationship, consistent with the +[`describedby` Web link relation][Link Relations]; it does not define an HTTP +`Link` header or a list of links. + +## Core Attributes and Description Selection + +Core attributes retain their meaning: + +| Attribute | Meaning | +| --- | --- | +| `source` | Identifies the context in which the event occurred. It is not necessarily one physical Producer. | +| `subject` | Identifies what the event concerns within that originating context. It is a String, not inherently a retrieval URL. | +| `dataschema` | Identifies the schema that the event's `data` adheres to. | +| `describedby` | Identifies additional shared information for interpreting the event. It does not replace any of the above. | + +A Producer SHOULD omit `describedby` when the event and agreed profile already +supply all REQUIRED inputs. If only an external payload schema is needed, +`dataschema` supplies that relationship without a Descriptor. + +### Profile-defined source and subject retrieval + +A profile MAY use a Descriptor URL as `source` when that URL also identifies the +originating context. It MAY define a subject that identifies a Thing or other +entity and permits retrieval of its description. Such a profile MUST define the +selected representation, any fragment processing, REQUIRED inputs and failure +behavior. An arbitrary processing metadata URL MUST NOT replace `subject` if it +no longer identifies what the event concerns under that profile. + +Generic Consumers MUST NOT infer retrieval from an HTTPS scheme, a URL path +containing `descriptors`, or the absence of `describedby`. They MUST NOT resolve +arbitrary subject strings against `source`. Core URNs and relative source +references remain valid; a profile needing direct retrieval MUST define an +appropriate retrievable value or use `describedby`. + +The [WoT profile](wot.md#description-selection) defines both Descriptor-relative +subjects and self-contained TD URL subjects. The latter can select a TD without +any Descriptor. This extension does not define a universal syntax for following +references from an entity record into another document. + +### One selected processing description + +| Condition | Processing | +| --- | --- | +| `describedby` is present and valid | Select that Descriptor, whether or not source or subject descriptions are also available. | +| `describedby` is present but invalid, unavailable or incomplete | Report the condition. Do not fall back to source or subject descriptions. | +| `describedby` is absent and the agreed profile defines source or subject retrieval | Use the profile's selected route. | +| `describedby` is absent and no retrieval mapping applies | This extension supplies no description. Do not probe source or subject. | +| `source` and `describedby` have the same URL | One retrieval per representation and authorization context can serve both uses; event identity is unchanged. | + +When present, `describedby` MUST select a Descriptor supplying all Descriptor +inputs REQUIRED by the profile. It need not duplicate information already +supplied by the event. A Consumer MUST NOT merge missing fields from a source or +subject description, including when both descriptions identify the same entity. +The Source Descriptor and Processing Descriptor can describe different entities, +such as an actor and a device; differing entity identities alone are not a +metadata conflict. + +When `describedby` is absent, a profile MUST distinguish its retrieval routes +before access. There is no general "try source, then subject" algorithm. A +failure in the selected route MUST NOT cause an implicit switch to another +route. OPTIONAL descriptive lookups for display do not supply missing processing +inputs. + +For example, a source description might contain PublisherId. If an explicit +Processing Descriptor and the event omit that REQUIRED OPC UA value, the +Consumer reports missing metadata rather than borrowing it from the source +description. Conversely, an unavailable source description does not prevent +processing when the explicit Descriptor and event supply all REQUIRED inputs. + +## Descriptor Representation and Processing + +An ordinary `GET` of the selected Descriptor URL with `Accept: application/json` +returns Descriptor metadata directly. Its `hasdocument` model aspect is `false`; +no `$details` request or separate Descriptor document is needed. + +The [Descriptor Registry][Descriptor Registry] defines these members: + +| Member | Use | +| --- | --- | +| `references` | REQUIRED map of local Reference names to objects containing `class` and `url`. An empty map is valid. | +| `entityid`, `entitytype` | Describe a primary entity when one exists; not replacements for event source identity or authenticated principals. | +| `messageref`, `schemaref` | Select named Message and payload-schema References. | +| Profile attributes | Typed shared metadata whose meaning and relation to event fields are defined by the applicable profile. | + +Representation classes include `message`, `schema`, `thingdescription`, +`thingmodel` and `usdasset`. Vendor classes use URI identifiers. Multiple +References can have the same class. Reference names do not establish emitter, +target, authority or execution roles. + +A Consumer MUST check response status, media type and Descriptor validity +against that domain. A missing or incorrectly typed `references` map is invalid. +An empty map can be valid but insufficient for a profile requiring a TD or +schema. Explicit selectors MUST name an existing Reference of the expected +class, even when the event supplies `dataschema`. + +For a valid Descriptor, the Consumer retrieves only the representations it +needs, subject to its access and content-trust policies. It MUST use their URLs +unchanged. A Reference's `class` identifies a representation, not a requirement +to retrieve it or permission to execute anything it describes. An unknown class +can be ignored if the profile does not need it; an unsupported REQUIRED input +makes interpretation unavailable. Entity identity and class URIs are not +automatic retrieval instructions. + +### Message and schema selection + +The Consumer uses the Descriptor Registry's selection rule: an explicit selector +names one Reference; without a selector, a unique Reference of the requested +class is selected. Zero candidates means absence; multiple candidates mean +ambiguity, not permission to select the first entry. + +The Consumer selects the first applicable payload schema source: + +1. The event's `dataschema`. +2. The Processing Descriptor's selected `schema` Reference. +3. The schema declared by its selected `message` representation. + +Only absence permits proceeding to the next source. Ambiguity, an invalid +selector, unavailable selected content or an unsupported selected format MUST +NOT cause fallback to a different schema. With no Processing Descriptor, the +second and third candidates are absent; the Consumer MUST NOT synthesize one +from the TD or other retrieved content. + +When `dataschema` is present, otherwise unselected schema inventory is +irrelevant to precedence. Explicit Descriptor selector validity is still +checked. Publishers and Producers SHOULD supply one applicable schema source, or +identify the same schema when several are supplied. Consumers apply the +precedence without comparing schema documents. + +The Producer MUST ensure that the selected schema describes the actual `data` +representation, including any protocol envelope. A TD containing several +DataSchemas is not automatically the payload schema. The selected Message +definition applies only according to the publication profile; Consumers MUST NOT +guess it by matching `type` or by inspecting names when selection is ambiguous. + +### Failure outcomes + +| Condition | Result | +| --- | --- | +| REQUIRED inputs obtained and selected | Processing context available; this does not itself validate the payload or authorize an operation. | +| Missing, ambiguous, unavailable or unsupported REQUIRED input | Processing context unavailable. | +| Invalid shape, selector or profile value | Invalid metadata. | +| Repeated profile-defined values disagree after REQUIRED decoding | Metadata conflict. | +| Applicable validation completed | Payload valid or invalid, according to that validator. | +| Validation not performed or could not complete | Payload not checked; not a successful validation. | + +These are processing outcomes, not new event attributes or HTTP error codes. The +Consumer SHOULD record a failure. Unless a profile requires an outcome, local +policy determines whether to defer, reject, or retain/forward the event without +that interpretation. It MUST NOT claim successful interpretation or validation +when the REQUIRED inputs are missing. Retaining an uninterpreted event does not +permit bypassing a failed explicit `describedby`. + +## Publication, Identity and Replay + +The Descriptor Registry defines publication and Version stability for all +processing-significant fields, including References, selectors and typed +metadata. A Publisher MUST make the representations REQUIRED by the profile +available before configuring Producers to emit the selected Descriptor URL. A +Producer MUST select information applicable to the event and SHOULD reuse +configured Version URLs when that information is unchanged. Producers are not +REQUIRED to perform runtime validation of each event by this extension. + +A Descriptor Resource URL selects its current default Version, which can change. +Consumers SHOULD bound the freshness of cached Resource responses. A Version +fixes its published metadata, not the bytes at referenced URLs. Publishers +SHOULD pin REQUIRED representation Versions and retain them with the Descriptor +for the replay period. There is no lookup of the Version intended at event +`time`, and an ETag does not reconstruct that history. + +`source` plus `id` retains its Core uniqueness rule. Changing `source`, +including a Version segment or query, changes the identity and source-wide +Sequence scope. Changing only `describedby` does not. Producers MUST NOT reuse +event identities or restart a claimed source-wide sequence when returning to an +earlier source. + +Changing `subject` can change subscription filtering and correlation; it does +not change the source-plus-id identity scope. A profile using a versioned +description URL as subject MUST account for that coupling. A stable subject with +an independently versioned `describedby` avoids it. + +## OPTIONAL Descriptor Bundle + +A supporting endpoint can return the Descriptor and its direct referenced +representations at the same URL: + +```http +Accept: application/vnd.xregistry.descriptor-bundle+json, application/json;q=0.5 +``` + +The Consumer MUST inspect `Content-Type`. Ordinary `application/json` returns +metadata; the bundle media type returns `descriptor` and `documents` as defined +by the [Descriptor Registry][Descriptor Registry]. Each document entry uses the +same Reference name and records its exact URL, content type, content or base64 +bytes, and redirect base URL when applicable. + +The bundle covers each direct `references.*.url` or returns an error. It does +not fetch `planurl`, entity/class identifiers or transitive dependencies, and is +not a snapshot across Registries. An empty Reference map has an empty document +map. Relative dependencies use the target's recorded retrieval base, not the +Descriptor's URL. + +This behavior is additional to Core metadata serving. Content negotiation alone +does not collect referenced URLs. A Publisher can prebuild a bundle for a +suitably configured endpoint, but the same completeness, access, freshness and +representation requirements apply. + +A Consumer MAY retry ordinary JSON at the same unchanged Descriptor URL after a +bundle error and retrieve only its REQUIRED representations. This is a +representation retry, not fallback to another Descriptor or source. Consumers +and caches MUST distinguish negotiated representations and authorization +contexts, following the bundle's `Vary` and freshness rules. + +## Examples + +### Actor and processing description + +An application profile reports an actor's maintenance decision about Pump 7. It +defines `pump7` as the subject identifier within that actor's context; this +generic example does not assert the WoT or OPC UA profile. + +```json +{ + "specversion": "1.0", + "id": "decision-47", + "source": "https://registry.example/descriptorgroups/actors/descriptors/planner7", + "subject": "pump7", + "type": "com.example.maintenance.decision.v1", + "describedby": "https://registry.example/descriptorgroups/streams/descriptors/pump7-maintenance/versions/1", + "datacontenttype": "application/json", + "data": { + "inspectionNeeded": true + } +} +``` + +The Processing Descriptor contains the following metadata; Core bookkeeping is +omitted. Its schema describes the decision payload, not the TD document. + +```json +{ + "descriptorid": "pump7-maintenance", + "versionid": "1", + "schemaref": "payload", + "references": { + "pump7": { + "class": "thingdescription", + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3" + }, + "payload": { + "class": "schema", + "url": "https://schemas.example/maintenance-decision/versions/1" + } + } +} +``` + +The actor's independently retrieved description can have `references: {}` and +actor identity metadata. It is not merged with the Processing Descriptor. +Neither description is an authorization to inspect or operate the pump. + +### Without an additional link + +For the same application profile, if the event and an explicit `dataschema` +already supply every REQUIRED input, no `describedby` is needed. A profile can +also select a sufficient Descriptor through `source`, or retrieve a subject +description through a defined mapping. Neither case is inferred by a generic +Consumer. + +The [WoT examples](wot.md#examples) show a concrete direct TD subject without a +Descriptor and a source-selected Descriptor. The [OPC UA +mapping](opcua.md#shared-stream-metadata-in-a-descriptor) defines when a +selected Descriptor Version supplies native stream metadata. + +## Migration from Event Context + +This revision replaces the draft `contexturl` extension and Role-keyed Context +representation. Existing events retain their original attribute and profile +semantics; Consumers MUST NOT silently reinterpret them as the new binding. +Deployments retaining such events need the old reader or an explicit conversion +policy. A profile using this revision MUST reject an event with both +`contexturl` and `describedby` unless a separate migration agreement defines +that combination. + +Do not rewrite historical sources merely to adopt registry-based identity. Names +discussed only during design, such as `contracturl`, `refset` and `contextref`, +are not aliases defined by this extension. + +## Security Considerations + +Consumers and bundle endpoints MUST apply authorization, URL, redirect and +content-trust policies before retrieving or using Descriptors or their +representations. An event-provided URL MUST NOT authorize arbitrary network +access or credential forwarding. Implementations SHOULD bound response sizes, +decompression, retrieval time, redirects and dependency processing. + +Read access to a Descriptor does not grant access to its targets. Caches MUST +NOT share representations across authorization contexts unless the applicable +access policy permits it. A bundle does not bypass trust checks. Versions do not +authenticate content or prove unchanged bytes. + +Descriptive entity IDs, TD security declarations and references to actions are +not authenticated principals, execution grants or current readiness. Device +admission and operation outcomes remain governed by their own protocols and +trusted authorities. + +[Descriptor Registry]: https://github.com/xregistry/spec/pull/796 +[Link Relations]: https://www.iana.org/assignments/link-relations/ diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index 65e7f30b4..e4db2b9da 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -6,14 +6,16 @@ DataSetMessages to CloudEvents for routing over supported protocols. It defines mappings for Core and other extension attributes and specifies OPC UA extension attributes. -When used with the [Event Context extension](context.md) and the xRegistry -Context model, metadata that stays constant for a message stream is preferably -stored in the Context rather than repeated in each event. The existing OPC UA -attributes remain defined and usable. A Producer MUST supply publisher and -writer metadata REQUIRED by this mapping in the `source` query when neither -the full Part 14 NetworkMessage envelope nor the Context provides it. The -[shared metadata rules](#shared-stream-metadata-in-a-context) describe how a -Consumer obtains these values. +When used with the [Descriptor Reference extension](describedby.md) and the +xRegistry Descriptor model, metadata that stays constant for a message stream is +preferably stored in the selected Descriptor rather than repeated in each event. +The existing OPC UA attributes remain defined and usable. A Producer MUST supply +publisher and writer metadata REQUIRED by this mapping in the `source` query +when neither the full Part 14 NetworkMessage envelope nor the selected +Descriptor supplies it. The [shared metadata +rules](#shared-stream-metadata-in-a-descriptor) describe how a Consumer obtains +these values. A direct WoT TD subject can identify its description without a +Descriptor, but does not supply these native identities. ## Notational Conventions @@ -37,13 +39,12 @@ field `MessageId`. ### source -An OPC UA Producer MAY identify the server using its -[Application Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) -field `applicationUri` or a customer-configured identifier such as a unified -namespace path. For data and event messages, a Producer MUST include each -publisher or writer query parameter REQUIRED by the mapping unless the full -OPC UA Part 14 NetworkMessage envelope or the Context supplies its -corresponding value. +An OPC UA Producer MAY identify the server using its [Application +Description](https://reference.opcfoundation.org/Core/Part4/v104/docs/7.1) field +`applicationUri` or a customer-configured identifier such as a unified namespace +path. For data and event messages, a Producer MUST include each publisher or +writer query parameter REQUIRED by the mapping unless the full OPC UA Part 14 +NetworkMessage envelope or the Descriptor supplies its corresponding value. ### type @@ -65,10 +66,12 @@ messages: - `type` MUST follow the WoT profile's semantic event-type rules instead of mapping to the native `MessageType`. -- `subject` MUST follow the WoT profile's resolved-TD pointer rules. +- `subject` MUST follow the WoT profile's Descriptor-relative or direct-TD + subject rules. - The native PublisherId and at least one DataSetWriter identifier MUST be available from the full Part 14 NetworkMessage envelope, `source`, or the - Context, as defined in [Publisher and Writer Metadata](#publisher-and-writer-metadata). + selected Descriptor, as defined in [Publisher and Writer + Metadata](#publisher-and-writer-metadata). - Nothing MUST be appended to `subject`. For an event message, the `EventId` remains in the OPC UA event payload. - `opcuasequence` MUST carry the native `SequenceNumber`. The CloudEvents @@ -101,20 +104,21 @@ separate format like [JSON schema](https://json-schema.org/specification) or [Avro schema](https://avro.apache.org/docs/1.11.1/specification/) or others. For those cases the attribute references the schema and is used for versioning. -When the Event Context extension is used, a Producer MAY reference a Context -whose `schema` Reference or `message` representation supplies this schema -instead. The Consumer follows that extension's schema precedence, and the -Producer SHOULD omit `dataschema` when the Context supplies the schema. The -selected schema MUST describe the actual `data`, including the full -NetworkMessage envelope when one is retained. +When the Descriptor Reference selection rules are used, a Producer MAY select a +Descriptor whose schema Reference or Message definition supplies this schema +instead. The Consumer follows that extension's [schema +precedence](describedby.md#message-and-schema-selection), including its selector +and ambiguity rules. The Producer SHOULD omit `dataschema` when the selected +Descriptor supplies the schema. The selected schema MUST describe the actual +`data`, including the full NetworkMessage envelope when one is retained. ### subject For metadata, data, and event messages, a Producer MAY map `subject` to `DataSetWriterId` or `DataSetWriterName` from the [Data Set Message Header](https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table164). -A Producer using the WoT profile MUST instead follow its -resolved-TD pointer mapping. +A Producer using the WoT profile MUST instead follow its Descriptor-relative or +direct-TD subject mapping. For an OPC UA event message (`type` equals `ua-event`), a Producer using the OPC UA-specific `subject` mapping MUST append "/" and the @@ -242,12 +246,11 @@ than omit the status, wrap the value, or report it as _Good_. ## Publisher and Writer Metadata -The following values identify the OPC UA Publisher and DataSetWriter. They -are not CloudEvents extension attributes. A Producer MUST supply each -REQUIRED value as a query parameter in `source` or an extension attribute of -the applicable Context Version unless the full Part 14 NetworkMessage -envelope supplies it. The names are the same in the query and Context -representations. +The following values identify the OPC UA Publisher and DataSetWriter. They are +not CloudEvents extension attributes. A Producer MUST supply each REQUIRED value +as a query parameter in `source` or an extension attribute of the applicable +Descriptor Version unless the full Part 14 NetworkMessage envelope supplies it. +The names are the same in the query and Descriptor representations. | Name | Value | | --- | --- | @@ -255,11 +258,11 @@ representations. | `opcuadatasetwriterid` | The native DataSetWriterId, represented as an integer. | | `opcuadatasetwritername` | The configured DataSetWriterName, represented as a string. | -In a full [Part 14 NetworkMessage][UA NetworkMessage], the Consumer obtains -the PublisherId from the NetworkMessage header and the DataSetWriterId or +In a full [Part 14 NetworkMessage][UA NetworkMessage], the Consumer obtains the +PublisherId from the NetworkMessage header and the DataSetWriterId or DataSetWriterName from its single DataSetMessage header. Those fields retain -their native names and encodings. The following representation rules apply -to their equivalents in `source` and the Context. +their native names and encodings. The following representation rules apply to +their equivalents in `source` and the Descriptor. `opcuapublisherid` MUST use `:`. The type MUST be `Byte`, `UInt16`, `UInt32`, `UInt64`, or `String`, as defined for the native @@ -280,21 +283,21 @@ configured name. For JSON message mapping, this is the DataSetMessage; otherwise the Producer obtains it from configuration. The native PublisherId and at least one of DataSetWriterId and DataSetWriterName -are REQUIRED for all data and event messages, whether or not the WoT profile -is used. Each value MUST be supplied by the envelope or by the corresponding -query parameter or Context attribute. A full envelope that omits an identifier -does not satisfy that identifier's presence requirement. If both writer values -are supplied, they MUST identify the same DataSetWriter. A writer name MUST -uniquely identify the DataSetWriter within the PublisherId scope. Consumers -MUST compare names exactly, without case folding or Unicode normalization. +are REQUIRED for all data and event messages, whether or not the WoT profile is +used. Each value MUST be supplied by the envelope or by the corresponding query +parameter or Descriptor attribute. A full envelope that omits an identifier does +not satisfy that identifier's presence requirement. If both writer values are +supplied, they MUST identify the same DataSetWriter. A writer name MUST uniquely +identify the DataSetWriter within the PublisherId scope. Consumers MUST compare +names exactly, without case folding or Unicode normalization. When an identity is supplied in more than one place, the representations MUST identify the same native Publisher or DataSetWriter. To compare a query or -Context value with an envelope field, the Consumer MUST decode that value and +Descriptor value with an envelope field, the Consumer MUST decode that value and apply the field's native Part 14 encoding. For example, `UInt16:12` is encoded -as `"12"` in a JSON NetworkMessage. The Consumer MUST report a metadata -conflict and MUST NOT interpret the payload as a conforming OPC UA message if -the values disagree. +as `"12"` in a JSON NetworkMessage. The Consumer MUST report a metadata conflict +and MUST NOT interpret the payload as a conforming OPC UA message if the values +disagree. A Consumer MUST report invalid metadata and MUST NOT interpret the payload as a conforming OPC UA message if a supplied publisher or writer value violates @@ -304,9 +307,9 @@ these representation or range constraints. When these values are supplied in `source`, the query parameters carry stream-constant metadata. The Producer SHOULD omit a query parameter when the -full OPC UA Part 14 NetworkMessage envelope or the Context supplies its value. -For each REQUIRED publisher or writer value that neither supplies, the -Producer MUST include the corresponding parameter in `source`. +full OPC UA Part 14 NetworkMessage envelope or the selected Descriptor supplies +its value. For each REQUIRED publisher or writer value that neither supplies, +the Producer MUST include the corresponding parameter in `source`. The Producer MUST use `name=value` for each of these parameters, with a non-empty value. It MUST separate them from each other and from preceding @@ -327,12 +330,12 @@ A Consumer MUST split the query at `;` before decoding these values. It MUST reject an empty or duplicate recognized parameter, an invalid percent escape, invalid UTF-8, or an unescaped character outside the permitted set. It MUST percent-decode each value exactly once and MUST NOT interpret `+` as a space. -The writer ID uses unsigned decimal notation without a sign or leading zeros -in the query and an integer in the Context; the other two values use strings -in both places. +The writer ID uses unsigned decimal notation without a sign or leading zeros in +the query and an integer in the Descriptor; the other two values use strings in +both places. -For example, the following source supplies the PublisherId and writer name -when neither the envelope nor the Context supplies them. The writer name is +For example, the following source supplies the PublisherId and writer name when +neither the envelope nor the Descriptor supplies them. The writer name is `Pump&Process`. ```text @@ -349,7 +352,7 @@ returns to a previously used `source`. - OPC UA messages MUST use `binary-mode` of CloudEvents. - OPC UA PubSub JSON messages MUST be encoded using non-reversible encoding as the decoding information is contained in metadata messages or by schema -referenced via `dataschema` attribute or selected through Event Context. +referenced via `dataschema` attribute or selected through a Descriptor. - A Producer MAY retain the full Part 14 NetworkMessage envelope in `data`. Otherwise, the payload MUST omit the NetworkMessage and DataSetMessage headers and supply their applicable information through the mappings in @@ -366,86 +369,118 @@ duplicated native values MUST agree after applying the specified mappings. The Consumer MUST report a conflict and MUST NOT interpret a message as conforming to this extension when they disagree. -## Shared Stream Metadata in a Context +## Shared Stream Metadata in a Descriptor A message stream here is a configured flow from one Publisher and DataSetWriter with a defined payload contract. It is not necessarily a broker topic or a CloudEvents `source`; either can carry several streams. -When Producers and Consumers agree to use this extension with the -[Event Context extension](context.md), the Context Publisher SHOULD store -metadata that is constant for the stream as Context Version extension -attributes. Each event then carries `contexturl`. Storing stream-constant -values once in the Context avoids repeating them in event attributes or the -`source` query. The Context has no document body: these values are members -of its metadata object, alongside `references`. - -The Context Publisher MAY store the following values in the Context when they -remain constant for every event using that Context Version. When a Context -supplies any of these values, `contexturl` MUST identify a Context Version. -A Context Resource URL can select a different default Version when a delayed -or replayed event is processed. - -| Context attribute | Meaning and conditions | +When Producers and Consumers agree to use this extension with the [Descriptor +Reference selection rules](describedby.md#one-selected-processing-description), +the Publisher SHOULD store metadata that is constant for the stream as +Descriptor Version extension attributes. The agreed binding selects the +Descriptor through `describedby` or a profile-defined source or subject mapping. +Storing stream-constant values once avoids repeating them in event attributes or +the `source` query. The Descriptor has no document body: these values are +members of its metadata object, alongside `references`. + +The Publisher MAY store the following values in the Descriptor when they remain +constant for every event using that Version. When a Descriptor supplies any of +these values, its selected URL MUST identify a Descriptor Version, whether +selected explicitly or through a profile-defined mapping. A Resource URL can +select a different default Version when a delayed or replayed event is +processed. + +| Descriptor attribute | Meaning and conditions | | --- | --- | | `opcuapublisherid` | The stream's native PublisherId. | | `opcuadatasetwriterid` | The stream's native DataSetWriterId, when used. | | `opcuadatasetwritername` | The stream's configured DataSetWriterName, when used. | +| `opcuadatasetname` | The non-empty `Name` of the applicable DataSet metadata; descriptive, not a writer identifier. | | `opcuametadatamajorversion` | The MajorVersion of the applicable native metadata, when no payload schema is selected. | | `opcuametadataminorversion` | The MinorVersion of the applicable native metadata, when no payload schema is selected. | -These members MUST use the value constraints and native field mappings -defined in [Attributes](#attributes) and -[Publisher and Writer Metadata](#publisher-and-writer-metadata). For example, -a Context's `opcuadatasetwriterid` is a JSON integer, not a string. The -Context's `schema` Reference identifies an external payload schema. Native -metadata-version attributes MUST NOT be supplied in the event or Context -when a schema is selected through `dataschema` or the Context. - -For the listed values, an applicable Context satisfies this extension's -presence requirements, including those of the combined WoT mapping. A Producer -MAY omit the corresponding event attribute or source query parameter. Other -event attributes, including `opcuamessagetype`, MUST NOT be replaced by Context -members. +Publisher, writer and metadata-version members MUST use the constraints and +mappings defined in [Attributes](#attributes) and [Publisher and Writer +Metadata](#publisher-and-writer-metadata). For example, a Descriptor's +`opcuadatasetwriterid` is a JSON integer, not a string. The Descriptor's +selected schema Reference identifies an external payload schema. Native +metadata-version attributes MUST NOT be supplied in the event or Descriptor when +a schema is selected through `dataschema` or the Descriptor. + +`opcuadatasetname` is an OPTIONAL Descriptor attribute, not a new CloudEvents +attribute or source query parameter. If supplied, it MUST be a non-empty String +equal to `DataSetMetaDataType.Name` in the applicable [DataSet metadata][UA +DataSet Metadata], obtained from that metadata or its approved configuration. A +Producer MUST NOT infer it from a writer name, topic or TD affordance name. It +need not be unique and MUST NOT substitute for a REQUIRED PublisherId or +DataSetWriter identity. Repeated dataset-name representations, when the profile +supplies them, MUST compare exactly without case folding or Unicode +normalization. A mismatch is a metadata conflict. This attribute does not permit +removal of native fields REQUIRED by the selected encoding. + +For the listed values, an applicable Descriptor satisfies this extension's +presence requirements for the corresponding field, including those of the +combined WoT mapping. The dataset name satisfies no publisher or writer identity +requirement. A Producer MAY omit the corresponding event attribute or source +query parameter. Other event attributes, including `opcuamessagetype`, MUST NOT +be replaced by Descriptor members. If a publisher or writer value appears in both the `source` query and the -Context, the values MUST agree after decoding and type conversion. If an event -attribute and the Context both supply a metadata version, their typed values -MUST agree. The same consistency requirement applies to publisher and writer -identities retained in the envelope. On a conflict, the Consumer MUST report a -metadata conflict and MUST NOT interpret the payload as a conforming OPC UA -message. If a REQUIRED value is absent from all applicable locations, the +Descriptor, the values MUST agree after decoding and type conversion. If an +event attribute and the Descriptor both supply a metadata version, their typed +values MUST agree. The same consistency requirement applies to publisher and +writer identities retained in the envelope. On a conflict, the Consumer MUST +report a metadata conflict and MUST NOT interpret the payload as a conforming +OPC UA message. If a REQUIRED value is absent from all applicable locations, the Consumer MUST report missing metadata and MUST NOT interpret the payload as a conforming OPC UA message. -A Consumer that cannot retrieve the Context MAY process the event using -its envelope, source query, and explicit event attributes if they satisfy every -applicable requirement. When the WoT profile is used, the Consumer also needs -the Context to resolve the TD and MUST follow the WoT profile's failure rules. - -The Context Publisher MUST NOT change these values in a Context Version after -any Producer is configured with its URL. A change requires a new Context Version -and reconfigured Producers. Consumers MUST NOT retain values from a previous -Context when processing an event that selects another Context. A changed -payload contract also requires a changed WoT `type` revision when that profile -is used; changing only `contexturl` is not sufficient. +A Consumer MUST use only the Descriptor selected by the agreed binding. If +`describedby` is present, missing values MUST NOT be taken from a source or +subject description. If its Descriptor cannot be retrieved, the Consumer MAY +interpret the native payload using the envelope, source query and explicit event +attributes when they supply every native requirement, but MUST NOT report +successful Descriptor-based interpretation or WoT correlation. The WoT profile's +explicit-link failure rules still apply. + +Without `describedby`, the revised WoT profile can select a direct TD subject +without a Descriptor. Native publisher and writer requirements then have to be +satisfied by the retained envelope or the existing source query. A direct TD +MUST NOT be treated as a source of missing OPC UA metadata. Consumers MUST NOT +retrieve a source Descriptor as an implicit supplement. + +The Descriptor Publisher MUST NOT change these values in a Descriptor Version +after any Producer is configured with its URL. A change requires a new +Descriptor Version and reconfigured Producers. Consumers MUST NOT retain values +from a previous Descriptor when processing an event that selects another +Descriptor. A changed payload contract also requires a changed WoT `type` +revision when that profile is used; changing only `describedby` is not +sufficient. Values that vary by occurrence remain in each event. In particular, `opcuamessagetype`, `opcuasequence`, `opcuastatus` when REQUIRED, `id`, and -`time` MUST NOT be supplied by the Context as replacements for their event +`time` MUST NOT be supplied by the Descriptor as replacements for their event attributes. Tracing, recording times, and a CloudEvents `sequence`, when used, -also remain in the event. A Consumer MUST NOT use Context members with those -names as event values. Core attributes retain their normal placement and -meaning even when their values happen to be constant for a stream. - -Without a `contexturl`, no Context supplies missing OPC UA values. This -preserves the existing event attributes and lets a Producer supply publisher -and writer metadata in the `source` query. Producers SHOULD use Context -metadata only when they know that the intended Consumers support this mapping. +also remain in the event. A Consumer MUST NOT use Descriptor members with those +names as event values. Core attributes retain their normal placement and meaning +even when their values happen to be constant for a stream. + +When the agreed route selects no Descriptor, the Producer MUST supply all +REQUIRED OPC UA values through the other locations defined above. An absent +`describedby` alone does not determine whether a Descriptor was selected. +Producers SHOULD use Descriptor metadata only when the intended Consumers +support the agreed mapping. + +When `source` is itself a Descriptor URL, Producers SHOULD supply shared +identity metadata in the selected versioned Descriptor instead of appending +query parameters to that URL. Appending a query changes the event's source +identity and can change the retrieved resource. Consumers MUST NOT strip a query +to make source-based retrieval work. ## Examples -The examples without a Context use publisher `UInt16:12` and writer name +The examples preceding the WoT section use publisher `UInt16:12` and writer name `energy-consumption-asset`. Their payloads omit the native headers, so their `source` queries supply both identifiers. @@ -646,14 +681,13 @@ recordedtime: 2024-03-28T23:59:59Z ### WoT and OPC UA telemetry message -This binary-mode keyframe uses the WoT event type and affordance subject. -Its Context supplies the resolved TD, payload schema, and PublisherId. The -`source` query supplies the DataSetWriterId because the Context does not -identify the writer. The native OPC UA message type and sequence remain in the -event because they can vary within this stream. The example uses writer ID -`17` within publisher `UInt16:12`. The CloudEvents `sequence` attribute is -absent because this example does not define a source-wide ordering across -DataSetWriters. +This binary-mode keyframe uses the WoT event type and named affordance subject. +Its Descriptor supplies the resolved TD, payload schema, PublisherId, +DataSetWriter identity and dataset name. The native OPC UA message type and +sequence remain in the event because they can vary within this stream. The +example uses writer ID `17` within publisher `UInt16:12`. The CloudEvents +`sequence` attribute is absent because this example does not define a +source-wide ordering across DataSetWriters. ```text ------------------ PUBLISH ------------------- @@ -665,11 +699,11 @@ Content Type: application/json specversion: 1.0 id: ua-message-25460 -source: mqtt://site/line/cell/device/sensor?opcuadatasetwriterid=17;site=north +source: https://registry.example/descriptorgroups/devices/descriptors/pump7 type: com.example.models.tm.pump.properties.processData.v1 -subject: #/properties/processData +subject: pump7#/properties/processData time: 2026-10-29T12:00:00Z -contexturl: https://registry.example/contextgroups/line1/contexts/opcua-pump-process-data/versions/1 +describedby: https://registry.example/descriptorgroups/streams/descriptors/pump7-telemetry/versions/1 opcuamessagetype: ua-keyframe opcuasequence: 4294967295 @@ -683,41 +717,61 @@ opcuasequence: 4294967295 ----------------------------------------------- ``` -The referenced Context Version has the following metadata. Other Core +The referenced Descriptor Version has the following metadata. Other Core metadata is omitted from this excerpt. ```json { - "contextid": "opcua-pump-process-data", + "descriptorid": "pump7-telemetry", "versionid": "1", + "schemaref": "payload", "references": { - "emitter": { - "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3.0.0" + "pump7": { + "class": "thingdescription", + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3", + "planurl": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7-plan/versions/3" }, - "schema": { - "url": "https://example.com/schemas/pump-process-data/versions/3" + "gateway1": { + "class": "thingdescription", + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/gateway1/versions/1" + }, + "payload": { + "class": "schema", + "url": "https://schemas.example/pump-process-data/versions/3" + }, + "geometry": { + "class": "usdasset", + "url": "https://assets.example/usdassetgroups/line1/usdassets/pump7/versions/2" } }, - "opcuapublisherid": "UInt16:12" + "opcuapublisherid": "UInt16:12", + "opcuadatasetwriterid": 17, + "opcuadatasetwritername": "pump7-process", + "opcuadatasetname": "PumpProcessData" } ``` -The Consumer uses the writer ID from `source` and the PublisherId from the -Context with the event's `opcuamessagetype` and `opcuasequence`. It evaluates -`subject` against the TD selected by `emitter` and uses the schema selected by -`schema`. +The Consumer uses the selected Descriptor's PublisherId and writer identity with +the event's `opcuamessagetype` and `opcuasequence`. The subject's `pump7` prefix +selects the TD among two candidates; `schemaref` selects the payload schema. The +dataset name and writer name deliberately differ. + +With an agreed source-Descriptor binding, a Producer can instead use the +stream's Descriptor Version URL as `source` and omit `describedby`. This changes +the source identity; it is not an identity-preserving rewrite of already +published events. -#### Retaining the NetworkMessage envelope +#### Direct TD subject with the NetworkMessage envelope This event retains the full Part 14 NetworkMessage envelope. Its PublisherId and DataSetWriterId satisfy the identity requirements, so `source` needs no publisher or writer query parameters. Part 14 JSON encodes the configured PublisherId `UInt16:12` as the string `"12"`. -Context Version `2` retains the same `emitter` Reference and points its `schema` -Reference to `https://example.com/schemas/pump-process-data-envelope/versions/1`. -It contains no publisher or writer attributes. The event type uses revision -`v2` because retaining the envelope changes the payload contract. +The direct subject supplies the TD URL and affordance pointer; `dataschema` +supplies the envelope's schema. No Descriptor is REQUIRED, so `describedby` is +omitted. The event type uses revision `v2` because retaining the envelope +changes the payload contract. ```text ------------------ PUBLISH ------------------- @@ -731,9 +785,9 @@ specversion: 1.0 id: ua-message-25461 source: mqtt://site/line/cell/device/sensor?site=north type: com.example.models.tm.pump.properties.processData.v2 -subject: #/properties/processData +subject: https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3#/properties/processData time: 2026-10-29T12:00:01Z -contexturl: https://registry.example/contextgroups/line1/contexts/opcua-pump-process-data/versions/2 +dataschema: https://schemas.example/pump-process-data-envelope/versions/1 opcuasequence: 0 ------------------ payload ------------------- @@ -760,21 +814,23 @@ opcuasequence: 0 ``` The native sequence has rolled over from `4294967295` to `0`; it remains -`opcuasequence`, not a CloudEvents `sequence`. The event still uses `contexturl` -to select its TD and schema. The DataSetMessage's `MessageType` remains in the -retained NetworkMessage envelope instead of being repeated as +`opcuasequence`, not a CloudEvents `sequence`. The subject selects its TD +directly without a source lookup. The DataSetMessage's `MessageType` remains in +the retained NetworkMessage envelope instead of being repeated as `opcuamessagetype`. -## Context Retrieval Security +## Descriptor Retrieval Security -The Event Context extension's retrieval, authorization, and cache rules apply -to this mapping. Stable writer metadata in a Context does not authenticate -the Publisher or authorize access to the referenced TD or schema. A Consumer -MUST establish trust in the Context before using it as OPC UA metadata. +The Descriptor Reference extension's retrieval, authorization and cache rules +apply to this mapping. Stable writer metadata in a Descriptor does not +authenticate the Publisher or authorize access to the referenced TD or schema. A +Consumer MUST establish trust in the Descriptor before using it as OPC UA +metadata. [UA DataSetWriterName]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.5.4#Table185 [UA NetworkMessage]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/7.2.5.3 [UA DataSetWriterId]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.4.1 [UA PublisherId]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.7.1 [UA SequenceNumber]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.3 +[UA DataSet Metadata]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.3.2.3 [URI]: https://www.rfc-editor.org/rfc/rfc3986 diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 0d1be6947..cdd0748ff 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -1,16 +1,17 @@ # Web of Things This profile maps a [Web of Things Thing Description][TD] (TD) to CloudEvents -Core context attributes and the [Event Context extension](context.md). -The REQUIRED `contexturl` identifies shared metadata whose `emitter` Reference -selects the resolved TD. The `subject` identifies the part of that TD associated -with the event, and the `type` names the event contract. - -The profile defines no CloudEvents extension attributes. Producers and -Consumers agree on its use through a publication profile or out of band. -It does not define a transport binding, provision a Thing, or require a -particular registry implementation. Events use the xRegistry Context -representation defined by the Event Context extension. +Core context attributes and optionally the [Descriptor Reference +extension](describedby.md). The REQUIRED `subject` identifies a Thing or +affordance, either through a selected Descriptor or through a self-contained TD +document URL. The `type` names the event contract. + +The profile defines no new CloudEvents extension attributes. Producers and +Consumers agree on its use through a publication profile or configuration. It +does not define a transport binding, provision a Thing, or require a particular +registry implementation. A direct TD subject can supply the REQUIRED description +without a Descriptor; an explicit `describedby` can supply additional shared +information when needed. ## Notational Conventions @@ -26,13 +27,20 @@ attribute marked REQUIRED here is not REQUIRED in every CloudEvent. ### source -The Core [`source`](../spec.md#source) identifies the context in which the -event occurred and follows Core's URI-reference rules. This profile leaves -the content of `source` undefined beyond those Core requirements. +The Core [`source`](../spec.md#source) identifies the context in which the event +occurred and follows Core's URI-reference rules. It does not have to identify +the Thing selected by `subject`; an actor can emit an event concerning a +different device. For example, `https://factory.example.com/line1/pump7` and `mqtt://site/line/cell/device/sensor` are valid sources. +An agreed source-Descriptor binding MAY use an absolute, fragment-free +Descriptor URL as `source` when it identifies the originating context. Consumers +MUST NOT infer that binding merely from URL-like spelling. The +[description-selection rules](#description-selection) determine whether the +source is retrieved; a direct TD subject does not require that lookup. + #### Event identity Core requires `source` and `id` together to identify each distinct event. @@ -102,10 +110,9 @@ For example, adding a `quality` member to ignore the new member. A change to an individual value or the presence of a member already defined as -OPTIONAL does not change the contract revision. A change to `contexturl`, its -`emitter` Reference, or the selected schema alone also does not change the -revision; if it changes the payload contract, the Producer MUST use a new -revision. +OPTIONAL does not change the contract revision. A change to `describedby`, the +selected TD URL or the selected schema alone also does not change the revision; +if it changes the payload contract, the Producer MUST use a new revision. The Producer MUST configure a valid complete type when the derivation is unavailable, produces an invalid symbol, or collides with a different event @@ -116,17 +123,39 @@ silently rewriting identifiers or member names. ### subject -The [`subject`](../spec.md#subject) is REQUIRED. It MUST be an -[RFC 6901][JSON Pointer] URI-fragment pointer evaluated against the resolved -TD. For an event describing the whole Thing, the Producer MUST use `#`. For -an affordance event, the pointer MUST identify the concrete affordance in the -resolved TD, for example `#/properties/processData` or `#/events/alarm`. +The [`subject`](../spec.md#subject) is REQUIRED. It MUST be an [RFC 6901][JSON +Pointer] URI-fragment pointer with one of the prefixes defined below. The +pointer is evaluated against the selected ordinary, resolved TD, not Descriptor +metadata or an unresolved projection plan. + +| Subject form | Meaning | +| --- | --- | +| `#` or `#/properties/processData` | Select the unique TD Reference in the selected Descriptor, then the whole Thing or affordance. | +| `pump7#/properties/processData` | Select the Descriptor's named `thingdescription` Reference `pump7`, then its affordance. | +| `https://example.com/td/pump7/versions/3#/properties/processData` | Identify the Thing's affordance through a TD document URL; description selection follows the rules below. | + +The Consumer MUST split the value at the first literal `#`. A missing delimiter +is invalid. It recognizes a direct-URL candidate by an `http://` or `https://` +prefix, case-insensitively, and MUST validate that prefix as a complete absolute +HTTP(S) URL. Otherwise, the prefix MUST be empty or a Descriptor Reference name. +Reference names follow the Descriptor Registry's map-key rules and cannot +contain `/`; `http:pump7` can therefore be a name, but is not an HTTP URL. +Invalid input MUST NOT be retried through another form or resolved against +`source`. + +The suffix including `#` MUST be a valid JSON Pointer URI fragment. For the +whole Thing, the Producer MUST use an empty fragment, such as `#`, `pump7#` or +`https://example.com/td/pump7/versions/3#`. For an affordance event, the pointer +MUST identify the concrete affordance in that TD, such as +`#/properties/processData`, `#/actions/setSetpoint` or `#/events/alarm`. The Producer MUST escape JSON Pointer tokens before applying URI-fragment -encoding. For example, the property name `pressure/bar` is addressed by -`#/properties/pressure~1bar`. The Consumer MUST report a failure and leave -the event uncorrelated if the pointer is invalid or does not select the -intended member of the resolved view. +encoding. For example, property `pressure/bar` is addressed by +`#/properties/pressure~1bar`; a literal `#` in a name is percent-encoded. The +Consumer MUST apply URI-fragment decoding once, then JSON Pointer token decoding +as specified by RFC 6901. It MUST report a correlation failure if the encoding +or pointer is invalid or the pointer does not select the intended Thing or +affordance. The subject identifies the Thing or affordance, not an occurrence. Different events can have the same subject and different CloudEvents `id` values. A @@ -136,6 +165,11 @@ appended to this pointer. The concrete TD path and corresponding TM declaration path can differ. This profile does not infer a declaration-lineage mapping between them. +Changing a direct subject's TD Version URL changes the subject string used for +filtering and correlation, not the source-plus-id event identity. A +Descriptor-relative subject with an independently versioned `describedby` can +avoid that coupling. + ### dataschema and datacontenttype Core [`dataschema`](../spec.md#dataschema) identifies the schema that the @@ -143,10 +177,12 @@ CloudEvent's actual `data` adheres to. [`datacontenttype`](../spec.md#datacontenttype) identifies that data's media type. Neither identifies the TD document's format. -The Consumer follows the Event Context extension's schema precedence: -`dataschema`, then the Context's `schema` Reference, then the schema declared -by its `message` representation. The Producer SHOULD omit `dataschema` when -the Context supplies the schema. +The Consumer follows the Descriptor Reference extension's [schema +precedence](describedby.md#message-and-schema-selection): `dataschema`, then the +selected Descriptor's schema, then the schema declared by its selected Message +definition. Without a Descriptor, the second and third sources are absent. The +Producer SHOULD omit `dataschema` when a selected Descriptor already supplies +the schema. A TD describes interactions and can contain several DataSchemas. The Producer MUST NOT use the whole TD as `dataschema` merely because the TD @@ -155,52 +191,83 @@ valid `dataschema` only if the identified representation is itself the schema applicable to `data`. The publication profile or protocol binding defines how the selected -affordance's DataSchema maps to the wire payload. An object containing -several property values, an event payload and a protocol envelope can need -different schemas. When `dataschema` is present, the Producer MUST ensure -that it identifies the schema of the representation actually sent, including -any encoding or envelope selected by the binding. -The same requirement applies to a schema supplied by the Context. +affordance's DataSchema maps to the wire payload. An object containing several +property values, an event payload and a protocol envelope can need different +schemas. When `dataschema` is present, the Producer MUST ensure that it +identifies the schema of the representation actually sent, including any +encoding or envelope selected by the binding. The same requirement applies to a +schema supplied by a Descriptor. An xRegistry Schema Resource's document-view URL can identify that external schema. It is not the WoT Resource's metadata-view URL. A profile MAY require an exact schema Version independently of the selected TD Version. -## contexturl - -Every event using this profile MUST include `contexturl`. The Producer and -Consumer MUST implement the [Event Context extension](context.md). The Context -MUST contain an `emitter` Reference whose `url` identifies the resolved TD -document, not its Registry metadata. The Context Publisher SHOULD reference an -explicit TD Version and publish the resolved TD view at that URL. The Consumer -retrieves the document directly or uses the corresponding document in an -accepted resolved Context view. It MUST NOT derive a TD URL from `contexturl` -or from the `emitter` URL. - -The Context's `emitter` is authoritative for TD selection. The Consumer MUST -evaluate `subject` against that TD and MUST NOT select another TD from `source`. -This rule does not change Core's meaning of `source` or authenticate the -emitting entity. - -If `contexturl` is missing, the Context cannot be retrieved or is invalid, its -`emitter` Reference is missing or invalid, or the referenced TD is unavailable -or not resolved, the Consumer MUST report a resolution failure and leave the -event uncorrelated. It MUST NOT fall back to selection through `source`. It -MAY retain or forward the event without TD-based interpretation. - -The Context Publisher MUST make the TD available before configuring Producers -with the Context Version URL. Changes to its Reference require a new Context -Version. A change to the TD's resolved logical view requires a new TD Version; -the Context Publisher MUST NOT change that view at a previously published -Version URL. Context versioning alone does not make an unversioned TD URL -stable. - -Changing `contexturl` alone does not change event identity or sequence scope. -A Producer MUST NOT reuse an `id` or restart a source-wide sequence merely -because it publishes a new Context Version. - -The event carries no digest of the TD. An exact version or a successful -resolution does not by itself prove that retrieved bytes are unchanged. +## Description Selection + +This profile uses the [Descriptor Reference selection +rules](describedby.md#one-selected-processing-description), including when +`describedby` is absent. Apply event-format decoding before testing presence; +JSON `null` means unset. Selection proceeds as follows: + +1. If `describedby` is present, retrieve and validate its Descriptor. It is + authoritative; a failure MUST NOT cause fallback to source or direct TD + subject retrieval. +2. Otherwise, if `subject` has a direct HTTP(S) TD URL prefix, retrieve that + document. Use the prefix unchanged, including its query; the fragment is + evaluated locally and is not sent in HTTP. Do not retrieve a source + Descriptor or construct a synthetic Descriptor from the TD. +3. Otherwise, use the agreed source-Descriptor binding. Retrieve the exact + `source` URL, which MUST be an absolute URL without a fragment for this + binding. If no such binding was agreed, report a correlation failure. + +When a Descriptor is selected, select its TD Reference as follows: + +- For an empty subject prefix, the Descriptor MUST have exactly one + `thingdescription` Reference. +- For a Reference-name prefix, that entry MUST exist and its `class` MUST be + `thingdescription`. +- For a direct URL prefix, exactly one `thingdescription` Reference MUST + have a `url` equal to that prefix. Consumers MUST compare the strings exactly, + without URI normalization. Missing or duplicate matches are correlation + failures. A named subject can disambiguate duplicate URLs. + +The selected `url` identifies the ordinary TD document, not its Registry +metadata. The Consumer uses that representation directly or from an accepted +Descriptor bundle. It MUST NOT construct another URL from the Descriptor URL or +Reference. Redirect destinations, TD `id` and equal `entityid` values MUST NOT +be used to repair a subject-to-Reference mismatch. + +A `thingmodel` Reference cannot replace an instance TD. A document marked as an +unresolved [WoT projection plan][WoT Projections] MUST NOT be used as an +ordinary TD or re-resolved to replace the published result. OPTIONAL `planurl` +provenance follows the Descriptor Registry's class rules. Failure to retrieve +OPTIONAL provenance does not invalidate an otherwise usable TD, unless the +agreed profile requires that provenance. A detected invalid plan/view pairing is +invalid metadata. + +The Producer MUST supply all REQUIRED inputs through the selected route, the +event and the agreed publication profile. A direct TD subject without a +Descriptor does not supply missing Message definitions or native publisher +metadata. A Producer needing those shared inputs can use a complete Descriptor +with `describedby`. A Consumer MUST NOT merge them from source descriptions. + +If selection, retrieval or correlation fails, the Consumer MUST report the +failure and leave the event uncorrelated. It MAY retain or forward the event +without TD-based interpretation, but MUST NOT switch routes or substitute +another TD. This does not authenticate the originating entity or authorize an +action described by the TD. + +The Publisher MUST make the selected TD available before configuring Producers +with its URL or a Descriptor that references it. The Publisher SHOULD publish an +explicit TD Version. A change to the resolved logical view requires a new TD +Version; the Publisher MUST NOT change that view at a previously published +Version URL. Descriptor publication and Version stability follow the Descriptor +Registry rules. + +Changing only `describedby` does not change identity or sequence scope. A +Producer MUST NOT reuse an `id` or restart a source-wide sequence merely because +a description changes. An exact Version or successful resolution does not itself +prove that retrieved bytes are unchanged. ## Relationship to Other Extensions @@ -212,18 +279,126 @@ MUST follow that extension's conditional WoT mapping. ### Single Affordance -This CloudEvent identifies a pump and its `processData` property. Its Context -selects version `3.0.0` of the resolved TD and supplies the payload schema. +This CloudEvent identifies a pump and its `processData` property. Its Descriptor +selects version `3` of the resolved TD and supplies the payload schema. Another +TD in the same Descriptor describes a gateway. ```json { "specversion": "1.0", "id": "sample-4711", - "source": "mqtt://site/line/cell/device/sensor", + "source": "https://registry.example/descriptorgroups/devices/descriptors/pump7", + "type": "com.example.models.tm.pump.properties.processData.v1", + "subject": "pump7#/properties/processData", + "datacontenttype": "application/json", + "describedby": "https://registry.example/descriptorgroups/streams/descriptors/pump7-telemetry/versions/1", + "data": { + "Speed": 1500, + "Pressure": 4.5 + } +} +``` + +The Descriptor has the following metadata. Core bookkeeping and any +profile-specific shared metadata are omitted from this excerpt. + +```json +{ + "descriptorid": "pump7-telemetry", + "versionid": "1", + "schemaref": "payload", + "references": { + "pump7": { + "class": "thingdescription", + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3", + "planurl": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7-plan/versions/3" + }, + "gateway1": { + "class": "thingdescription", + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/gateway1/versions/1" + }, + "payload": { + "class": "schema", + "url": "https://schemas.example/pump-process-data/versions/3" + }, + "geometry": { + "class": "usdasset", + "url": "https://assets.example/usdassetgroups/line1/usdassets/pump7/versions/2" + } + } +} +``` + +The subject prefix selects `pump7`, not the first TD in the map. The suffix +selects its property. The payload directly contains the property's object value +without a `processData` wrapper. The selected schema describes that object, not +the TD document. The OPTIONAL plan and USD asset do not have to be retrieved for +this correlation. + +### Direct TD Subject + +This event needs only the TD and an external payload schema, so it omits +`describedby`. The source identifies an actor's originating context rather than +the pump; it is not retrieved. + +```json +{ + "specversion": "1.0", + "id": "sample-4712", + "source": "urn:example:actor:telemetry-service", + "type": "com.example.models.tm.pump.properties.processData.v1", + "subject": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3#/properties/processData", + "dataschema": "https://schemas.example/pump-process-data/versions/3", + "datacontenttype": "application/json", + "data": { + "Speed": 1500, + "Pressure": 4.5 + } +} +``` + +The Consumer retrieves the subject's fragment-free TD URL and evaluates +`#/properties/processData`. The URI fragment is not sent in HTTP. + +### Direct Subject with a Descriptor + +The preceding event can instead select the same telemetry Descriptor: + +```json +{ + "specversion": "1.0", + "id": "sample-4714", + "source": "urn:example:actor:telemetry-service", + "type": "com.example.models.tm.pump.properties.processData.v1", + "subject": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3#/properties/processData", + "describedby": "https://registry.example/descriptorgroups/streams/descriptors/pump7-telemetry/versions/1", + "datacontenttype": "application/json", + "data": { + "Speed": 1500, + "Pressure": 4.5 + } +} +``` + +The direct subject matches exactly the `pump7` Reference. The Descriptor now +supplies the schema. Changing just the subject's TD Version to `4` would fail +correlation because this Descriptor names Version `3`. Adding another TD +Reference with that same Version `3` URL would also make the direct match +ambiguous; a named subject would then be REQUIRED. + +### Source-selected Descriptor + +When the agreed source-Descriptor binding is enabled, the Single Affordance +event can use the following complete envelope without `describedby`: + +```json +{ + "specversion": "1.0", + "id": "sample-4715", + "source": "https://registry.example/descriptorgroups/streams/descriptors/pump7-telemetry/versions/1", "type": "com.example.models.tm.pump.properties.processData.v1", - "subject": "#/properties/processData", + "subject": "pump7#/properties/processData", "datacontenttype": "application/json", - "contexturl": "https://registry.example/contextgroups/line1/contexts/pump-process-data/versions/1", "data": { "Speed": 1500, "Pressure": 4.5 @@ -231,21 +406,17 @@ selects version `3.0.0` of the resolved TD and supplies the payload schema. } ``` -The Context metadata is shown in the -[Event Context example](context.md#example). Its `emitter` selects the TD, -and its `schema` selects the payload schema. -The subject selects the property in that resolved view. The payload directly -contains the property's object value without a `processData` wrapper. The -separate schema describes that object, not the TD document. Another pump can -use the same event type with a different Thing identity in its `source` and a -Context whose `emitter` describes that pump. +This intentionally identifies the stream Descriptor as the originating context. +Its source-plus-id namespace differs from the device-source example. A new +Version in `source` changes that namespace; a new Version in `describedby` alone +does not. ### Whole Thing -This CloudEvent carries aggregated data for the same pump. Its Context -selects version `3.0.0` of the resolved TD and the aggregate payload schema. -The publication profile defines a payload containing the `processData` and -`running` property values. The subject selects the whole TD. +This CloudEvent carries aggregated data for the same pump. Its Descriptor +selects version `3` of the resolved TD and the aggregate payload schema. The +publication profile defines a payload containing the `processData` and `running` +property values. The subject selects the whole TD. ```json { @@ -255,7 +426,7 @@ The publication profile defines a payload containing the `processData` and "type": "com.example.models.tm.pump.v1", "subject": "#", "datacontenttype": "application/json", - "contexturl": "https://registry.example/contextgroups/line1/contexts/pump-state/versions/1", + "describedby": "https://registry.example/descriptorgroups/streams/descriptors/pump7-state/versions/1", "data": { "processData": { "Speed": 1500, @@ -266,26 +437,30 @@ The publication profile defines a payload containing the `processData` and } ``` -The referenced Context Version contains the following metadata. +The referenced Descriptor Version contains the following metadata. ```json { - "contextid": "pump-state", + "descriptorid": "pump7-state", "versionid": "1", + "schemaref": "payload", "references": { - "emitter": { - "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3.0.0" + "pump7": { + "class": "thingdescription", + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3" }, - "schema": { - "url": "https://example.com/schemas/pump-state/versions/3" + "payload": { + "class": "schema", + "url": "https://schemas.example/pump-state/versions/3" } } } ``` -The `emitter` selects the TD used to interpret these property values. The TD -document itself is not the payload. The `schema` identifies the aggregate JSON -object, not the TD or either property's DataSchema alone. +The empty subject prefix selects the unique TD Reference. The TD document itself +is not the payload. `schemaref` selects the schema of the aggregate JSON object, +not the TD or either property's DataSchema alone. A direct whole-Thing subject +would end in `#` after its TD document URL. ## Security Considerations @@ -303,3 +478,4 @@ referenced documents or make their dependencies available. [TD]: https://www.w3.org/TR/wot-thing-description11/ [TM]: https://www.w3.org/TR/wot-thing-description11/#thing-model [JSON Pointer]: https://www.rfc-editor.org/rfc/rfc6901 +[WoT Projections]: https://github.com/marcschier/wot-thing-description/pull/1 From 0f5877775911518621c0d33817b8473c71ceab4e Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 14 Nov 2026 17:51:52 +0100 Subject: [PATCH 27/28] Align Descriptor schema selection and native metadata ranges Signed-off-by: Marc --- cloudevents/extensions/README.md | 2 +- .../{describedby.md => descriptors.md} | 55 ++++++++++--------- cloudevents/extensions/opcua.md | 46 ++++++++++------ cloudevents/extensions/wot.md | 37 +++++++------ 4 files changed, 79 insertions(+), 61 deletions(-) rename cloudevents/extensions/{describedby.md => descriptors.md} (90%) diff --git a/cloudevents/extensions/README.md b/cloudevents/extensions/README.md index 7fdfb70f4..b22e5b2a5 100644 --- a/cloudevents/extensions/README.md +++ b/cloudevents/extensions/README.md @@ -45,7 +45,7 @@ for more information. - [Data Classification](data-classification.md) - [Dataref (Claim Check Pattern)](dataref.md) - [Deprecation](deprecation.md) -- [Descriptor Reference](describedby.md) +- [Descriptor Reference](descriptors.md) - [Distributed Tracing](distributed-tracing.md) - [Expiry Time](expirytime.md) - [OPC UA](opcua.md) diff --git a/cloudevents/extensions/describedby.md b/cloudevents/extensions/descriptors.md similarity index 90% rename from cloudevents/extensions/describedby.md rename to cloudevents/extensions/descriptors.md index 27bedc731..efe2c36be 100644 --- a/cloudevents/extensions/describedby.md +++ b/cloudevents/extensions/descriptors.md @@ -2,7 +2,7 @@ The OPTIONAL `describedby` extension attribute identifies a Descriptor containing shared information for interpreting an event. A Descriptor can -reference schemas, Message definitions, WoT Thing Descriptions or other +reference schemas, WoT Thing Descriptions or other representations, and can contain typed metadata such as a publisher identity. It describes information relevant to the event, not necessarily only the event's source or subject. @@ -144,10 +144,10 @@ The [Descriptor Registry][Descriptor Registry] defines these members: | --- | --- | | `references` | REQUIRED map of local Reference names to objects containing `class` and `url`. An empty map is valid. | | `entityid`, `entitytype` | Describe a primary entity when one exists; not replacements for event source identity or authenticated principals. | -| `messageref`, `schemaref` | Select named Message and payload-schema References. | +| `schemaref` | Selects a named payload-schema Reference. | | Profile attributes | Typed shared metadata whose meaning and relation to event fields are defined by the applicable profile. | -Representation classes include `message`, `schema`, `thingdescription`, +Representation classes include `schema`, `thingdescription`, `thingmodel` and `usdasset`. Vendor classes use URI identifiers. Multiple References can have the same class. Reference names do not establish emitter, target, authority or execution roles. @@ -155,8 +155,8 @@ target, authority or execution roles. A Consumer MUST check response status, media type and Descriptor validity against that domain. A missing or incorrectly typed `references` map is invalid. An empty map can be valid but insufficient for a profile requiring a TD or -schema. Explicit selectors MUST name an existing Reference of the expected -class, even when the event supplies `dataschema`. +schema. An explicit `schemaref` MUST name an existing Reference of class +`schema`, even when the event supplies `dataschema`. For a valid Descriptor, the Consumer retrieves only the representations it needs, subject to its access and content-trust policies. It MUST use their URLs @@ -166,24 +166,24 @@ can be ignored if the profile does not need it; an unsupported REQUIRED input makes interpretation unavailable. Entity identity and class URIs are not automatic retrieval instructions. -### Message and schema selection +### Schema selection -The Consumer uses the Descriptor Registry's selection rule: an explicit selector -names one Reference; without a selector, a unique Reference of the requested -class is selected. Zero candidates means absence; multiple candidates mean -ambiguity, not permission to select the first entry. +The Consumer uses the Descriptor Registry's schema-selection rule: an explicit +`schemaref` names one Reference of class `schema`; without that selector, a +unique Reference of class `schema` is selected. Zero candidates means absence; +multiple candidates mean ambiguity, not permission to select the first entry. The Consumer selects the first applicable payload schema source: 1. The event's `dataschema`. 2. The Processing Descriptor's selected `schema` Reference. -3. The schema declared by its selected `message` representation. Only absence permits proceeding to the next source. Ambiguity, an invalid selector, unavailable selected content or an unsupported selected format MUST NOT cause fallback to a different schema. With no Processing Descriptor, the -second and third candidates are absent; the Consumer MUST NOT synthesize one -from the TD or other retrieved content. +second candidate is absent. If neither source supplies a schema REQUIRED by the +profile, the Consumer MUST report that missing input; it MUST NOT derive another +schema from a TD or other unselected representation. When `dataschema` is present, otherwise unselected schema inventory is irrelevant to precedence. Explicit Descriptor selector validity is still @@ -193,9 +193,9 @@ precedence without comparing schema documents. The Producer MUST ensure that the selected schema describes the actual `data` representation, including any protocol envelope. A TD containing several -DataSchemas is not automatically the payload schema. The selected Message -definition applies only according to the publication profile; Consumers MUST NOT -guess it by matching `type` or by inspecting names when selection is ambiguous. +DataSchemas is not automatically the payload schema. Consumers MUST NOT use +event-type matching or Reference-name conventions to resolve an ambiguous +schema selection. ### Failure outcomes @@ -226,11 +226,13 @@ configured Version URLs when that information is unchanged. Producers are not REQUIRED to perform runtime validation of each event by this extension. A Descriptor Resource URL selects its current default Version, which can change. -Consumers SHOULD bound the freshness of cached Resource responses. A Version -fixes its published metadata, not the bytes at referenced URLs. Publishers -SHOULD pin REQUIRED representation Versions and retain them with the Descriptor -for the replay period. There is no lookup of the Version intended at event -`time`, and an ETag does not reconstruct that history. +Consumers SHOULD bound the freshness of cached Resource responses. Under the +Descriptor Registry's publication rule, the Publisher MUST keep referenced +representations stable for the intended processing and replay period. +Publishers SHOULD pin representation Versions and retain them with the +Descriptor for that period. A Version identifier alone is not evidence that +the Publisher has met the stability requirement. There is no lookup of the +Version intended at event `time`, and an ETag does not reconstruct that history. `source` plus `id` retains its Core uniqueness rule. Changing `source`, including a Version segment or query, changes the identity and source-wide @@ -258,11 +260,12 @@ by the [Descriptor Registry][Descriptor Registry]. Each document entry uses the same Reference name and records its exact URL, content type, content or base64 bytes, and redirect base URL when applicable. -The bundle covers each direct `references.*.url` or returns an error. It does -not fetch `planurl`, entity/class identifiers or transitive dependencies, and is -not a snapshot across Registries. An empty Reference map has an empty document -map. Relative dependencies use the target's recorded retrieval base, not the -Descriptor's URL. +The bundle covers each direct `references.*.url` or returns an error. Additional +representations to be included MUST be listed as named References. An empty +Reference map has an empty document map. Relative dependencies use the +referenced representation's recorded retrieval base, not the Descriptor's URL. +Stable published inputs do not make the bundle an atomic snapshot across +Registries. This behavior is additional to Core metadata serving. Content negotiation alone does not collect referenced URLs. A Publisher can prebuild a bundle for a diff --git a/cloudevents/extensions/opcua.md b/cloudevents/extensions/opcua.md index e4db2b9da..afbb8db9b 100644 --- a/cloudevents/extensions/opcua.md +++ b/cloudevents/extensions/opcua.md @@ -6,7 +6,7 @@ DataSetMessages to CloudEvents for routing over supported protocols. It defines mappings for Core and other extension attributes and specifies OPC UA extension attributes. -When used with the [Descriptor Reference extension](describedby.md) and the +When used with the [Descriptor Reference extension](descriptors.md) and the xRegistry Descriptor model, metadata that stays constant for a message stream is preferably stored in the selected Descriptor rather than repeated in each event. The existing OPC UA attributes remain defined and usable. A Producer MUST supply @@ -105,9 +105,9 @@ separate format like [JSON schema](https://json-schema.org/specification) or those cases the attribute references the schema and is used for versioning. When the Descriptor Reference selection rules are used, a Producer MAY select a -Descriptor whose schema Reference or Message definition supplies this schema +Descriptor whose selected schema Reference supplies this schema instead. The Consumer follows that extension's [schema -precedence](describedby.md#message-and-schema-selection), including its selector +precedence](descriptors.md#schema-selection), including its selector and ambiguity rules. The Producer SHOULD omit `dataschema` when the selected Descriptor supplies the schema. The selected schema MUST describe the actual `data`, including the full NetworkMessage envelope when one is retained. @@ -198,6 +198,15 @@ field `MetaDataVersion`. - Constraints - OPTIONAL but MUST NOT be present if `dataschema` is used +Both metadata-version event attributes use the CloudEvents `Integer` type. +Their native [VersionTime][UA VersionTime] values are unsigned, so the +representable event-attribute range is `0` to `2147483647`, inclusive. +A Producer encoding a native value in either event attribute MUST preserve +that value exactly. If it exceeds this range, the Producer MUST report an +encoding error rather than truncate, wrap or reinterpret it as a negative +value. The full native range can be carried in the selected Descriptor +metadata described below, or in an applicable native payload representation. + ### opcuastatus - Type: `Integer` @@ -376,7 +385,7 @@ DataSetWriter with a defined payload contract. It is not necessarily a broker topic or a CloudEvents `source`; either can carry several streams. When Producers and Consumers agree to use this extension with the [Descriptor -Reference selection rules](describedby.md#one-selected-processing-description), +Reference selection rules](descriptors.md#one-selected-processing-description), the Publisher SHOULD store metadata that is constant for the stream as Descriptor Version extension attributes. The agreed binding selects the Descriptor through `describedby` or a profile-defined source or subject mapping. @@ -397,16 +406,21 @@ processed. | `opcuadatasetwriterid` | The stream's native DataSetWriterId, when used. | | `opcuadatasetwritername` | The stream's configured DataSetWriterName, when used. | | `opcuadatasetname` | The non-empty `Name` of the applicable DataSet metadata; descriptive, not a writer identifier. | -| `opcuametadatamajorversion` | The MajorVersion of the applicable native metadata, when no payload schema is selected. | -| `opcuametadataminorversion` | The MinorVersion of the applicable native metadata, when no payload schema is selected. | - -Publisher, writer and metadata-version members MUST use the constraints and -mappings defined in [Attributes](#attributes) and [Publisher and Writer -Metadata](#publisher-and-writer-metadata). For example, a Descriptor's -`opcuadatasetwriterid` is a JSON integer, not a string. The Descriptor's -selected schema Reference identifies an external payload schema. Native -metadata-version attributes MUST NOT be supplied in the event or Descriptor when -a schema is selected through `dataschema` or the Descriptor. +| `opcuametadatamajorversion` | The native MajorVersion as a JSON integer in `0` to `4294967295`, when no payload schema is selected. | +| `opcuametadataminorversion` | The native MinorVersion as a JSON integer in `0` to `4294967295`, when no payload schema is selected. | + +Publisher and writer members MUST use the constraints and mappings defined +in [Publisher and Writer Metadata](#publisher-and-writer-metadata). For +example, a Descriptor's `opcuadatasetwriterid` is a JSON integer, not a string. +The Descriptor metadata-version members MUST preserve the full native +`VersionTime` (`UInt32`) range, `0` to `4294967295`, inclusive. These are +Descriptor members, not CloudEvents `Integer` attributes. Consumers MUST +compare duplicate event and Descriptor values without narrowing either value. +Out-of-range Descriptor values are invalid metadata. + +The Descriptor's selected schema Reference identifies an external payload +schema. Native metadata-version attributes MUST NOT be supplied in the event +or Descriptor when a schema is selected through `dataschema` or the Descriptor. `opcuadatasetname` is an OPTIONAL Descriptor attribute, not a new CloudEvents attribute or source query parameter. If supplied, it MUST be a non-empty String @@ -728,8 +742,7 @@ metadata is omitted from this excerpt. "references": { "pump7": { "class": "thingdescription", - "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3", - "planurl": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7-plan/versions/3" + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3" }, "gateway1": { "class": "thingdescription", @@ -833,4 +846,5 @@ metadata. [UA PublisherId]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.7.1 [UA SequenceNumber]: https://reference.opcfoundation.org/Core/Part14/v105/docs/7.2.3 [UA DataSet Metadata]: https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.3.2.3 +[UA VersionTime]: https://reference.opcfoundation.org/specs/OPC-10000-4/v1.05.07/7.43 [URI]: https://www.rfc-editor.org/rfc/rfc3986 diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index cdd0748ff..1046dfd8a 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -2,7 +2,7 @@ This profile maps a [Web of Things Thing Description][TD] (TD) to CloudEvents Core context attributes and optionally the [Descriptor Reference -extension](describedby.md). The REQUIRED `subject` identifies a Thing or +extension](descriptors.md). The REQUIRED `subject` identifies a Thing or affordance, either through a selected Descriptor or through a self-contained TD document URL. The `type` names the event contract. @@ -178,9 +178,9 @@ CloudEvent's actual `data` adheres to. type. Neither identifies the TD document's format. The Consumer follows the Descriptor Reference extension's [schema -precedence](describedby.md#message-and-schema-selection): `dataschema`, then the -selected Descriptor's schema, then the schema declared by its selected Message -definition. Without a Descriptor, the second and third sources are absent. The +precedence](descriptors.md#schema-selection): `dataschema`, then the +selected Descriptor's schema. Without a Descriptor, the second source is +absent. The Producer SHOULD omit `dataschema` when a selected Descriptor already supplies the schema. @@ -205,7 +205,7 @@ an exact schema Version independently of the selected TD Version. ## Description Selection This profile uses the [Descriptor Reference selection -rules](describedby.md#one-selected-processing-description), including when +rules](descriptors.md#one-selected-processing-description), including when `describedby` is absent. Apply event-format decoding before testing presence; JSON `null` means unset. Selection proceeds as follows: @@ -237,18 +237,20 @@ Descriptor bundle. It MUST NOT construct another URL from the Descriptor URL or Reference. Redirect destinations, TD `id` and equal `entityid` values MUST NOT be used to repair a subject-to-Reference mismatch. -A `thingmodel` Reference cannot replace an instance TD. A document marked as an -unresolved [WoT projection plan][WoT Projections] MUST NOT be used as an -ordinary TD or re-resolved to replace the published result. OPTIONAL `planurl` -provenance follows the Descriptor Registry's class rules. Failure to retrieve -OPTIONAL provenance does not invalidate an otherwise usable TD, unless the -agreed profile requires that provenance. A detected invalid plan/view pairing is -invalid metadata. +A `thingmodel` Reference cannot replace an instance TD. The selected +representation MUST be an ordinary resolved TD. The +[WoT Descriptor profile][WoT Descriptor Profile] defines the TD/TM admission +and provenance rules, including profile-specific `planurl` and plan/view +pairing. Consumers using that profile MUST apply its admission and provenance +rules. Failure to obtain or validate a REQUIRED profile input MUST cause a +reported correlation failure. Unavailable OPTIONAL provenance does not prevent +correlation with an otherwise usable TD. The generic Descriptor model does not +define those provenance attributes. The Producer MUST supply all REQUIRED inputs through the selected route, the event and the agreed publication profile. A direct TD subject without a -Descriptor does not supply missing Message definitions or native publisher -metadata. A Producer needing those shared inputs can use a complete Descriptor +Descriptor does not supply an external payload schema or native publisher +metadata. A Producer needing shared inputs can use a complete Descriptor with `describedby`. A Consumer MUST NOT merge them from source descriptions. If selection, retrieval or correlation fails, the Consumer MUST report the @@ -310,8 +312,7 @@ profile-specific shared metadata are omitted from this excerpt. "references": { "pump7": { "class": "thingdescription", - "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3", - "planurl": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7-plan/versions/3" + "url": "https://registry.example/thingdescriptiongroups/factory/thingdescriptions/pump7/versions/3" }, "gateway1": { "class": "thingdescription", @@ -332,7 +333,7 @@ profile-specific shared metadata are omitted from this excerpt. The subject prefix selects `pump7`, not the first TD in the map. The suffix selects its property. The payload directly contains the property's object value without a `processData` wrapper. The selected schema describes that object, not -the TD document. The OPTIONAL plan and USD asset do not have to be retrieved for +the TD document. The USD asset does not have to be retrieved for this correlation. ### Direct TD Subject @@ -478,4 +479,4 @@ referenced documents or make their dependencies available. [TD]: https://www.w3.org/TR/wot-thing-description11/ [TM]: https://www.w3.org/TR/wot-thing-description11/#thing-model [JSON Pointer]: https://www.rfc-editor.org/rfc/rfc6901 -[WoT Projections]: https://github.com/marcschier/wot-thing-description/pull/1 +[WoT Descriptor Profile]: https://github.com/xregistry/spec/pull/796 From 2b122bb9ea420b24ffccb53c6e0a05c20082ac7a Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 14 Nov 2026 17:52:56 +0100 Subject: [PATCH 28/28] Pin relocated WoT Descriptor profile reference Signed-off-by: Marc --- cloudevents/extensions/wot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudevents/extensions/wot.md b/cloudevents/extensions/wot.md index 1046dfd8a..b5cba94cc 100644 --- a/cloudevents/extensions/wot.md +++ b/cloudevents/extensions/wot.md @@ -479,4 +479,4 @@ referenced documents or make their dependencies available. [TD]: https://www.w3.org/TR/wot-thing-description11/ [TM]: https://www.w3.org/TR/wot-thing-description11/#thing-model [JSON Pointer]: https://www.rfc-editor.org/rfc/rfc6901 -[WoT Descriptor Profile]: https://github.com/xregistry/spec/pull/796 +[WoT Descriptor Profile]: https://github.com/marcschier/xregistry-drafts/blob/28b6d245fb4f634bb172e0505ae8e64fdf882428/workingdrafts/models/wot/spec.md#66-wot-descriptor-profile