@openvtc/trust-tasks 0.17.9 → 0.17.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,6 +86,23 @@ export type ConsumerKind_DeviceV0_2 = Companion_DeviceV0_2 | Service_DeviceV0_2;
86
86
  * Stable identifier for an issued credential — the handle for revocation and audit. Opaque to the holder: it MUST be echoed verbatim when revoking and MUST NOT be parsed.
87
87
  */
88
88
  export type CredentialId = string;
89
+ /**
90
+ * The root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.
91
+ *
92
+ * A room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing; a host that shows two members two different roots has been caught.
93
+ *
94
+ * **The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:
95
+ * 1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.
96
+ * 2. Leaf: `SHA-256(0x00 || JCS(record))`, where JCS is the RFC 8785 canonicalization of the record as this family's `RecordMetadata` plus its stored content, and `0x00` is RFC 6962's leaf-domain prefix.
97
+ * 3. Internal node: `SHA-256(0x01 || left || right)`.
98
+ * 4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.
99
+ * 5. A room holding no records commits to `SHA-256("")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.
100
+ *
101
+ * The leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.
102
+ *
103
+ * The commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail.
104
+ */
105
+ export type DataCommitment = DigestMultibase;
89
106
  /**
90
107
  * The approver's answer. `deny` aborts the pending request; a subsequent submit of the same task starts a fresh one.
91
108
  */
@@ -51,6 +51,10 @@ export interface PersonaCorrelationAnalyzeResponsePayload {
51
51
  * @maxItems 64
52
52
  */
53
53
  disclosedTo?: string[];
54
+ /**
55
+ * The facet the profile at this location belongs to. Absent where it belongs to none, which is a real and common state rather than a gap — most profiles are unarranged until someone arranges them, and a consumer MUST NOT read absence as a facet of its own.
56
+ */
57
+ facetId?: Ulid;
54
58
  }[];
55
59
  /**
56
60
  * Plain-language cause. A severity with no explanation is a warning a holder learns to dismiss.
@@ -113,6 +117,16 @@ export interface PersonaCorrelationAnalyzeResponsePayload {
113
117
  "useDifferentValue" | "reissueCredentialToThisDid" | "correlateDeliberately" | "proceedAndRecord",
114
118
  "useDifferentValue" | "reissueCredentialToThisDid" | "correlateDeliberately" | "proceedAndRecord"
115
119
  ];
120
+ /**
121
+ * Whether this linkage spans two or more of the holder's facets — parts of their life they have said belong apart. **A second axis, not a restatement of `severity`.** `severity` says how strongly a disclosure would link the holder, which is a fact about provenance and proof rung and is true whatever the holder intended; this says whether the holder would mind. A value shared between two profiles in the SAME facet is linkage the holder arranged on purpose — a work email in every work profile — and a consumer that alarmed on it teaches people to dismiss the alarm. A value shared ACROSS facets is the finding worth raising. True only when two or more DISTINCT facets appear among `sharedWith`: a profile belonging to no facet is unarranged, not a second facet, and contributes nothing here. Absent when the maintainer does not implement facets.
122
+ */
123
+ crossesFacets?: boolean;
124
+ /**
125
+ * The distinct facets this linkage touches, so a consumer can name them — "Work and Home share your mobile number" is a sentence a holder can act on, where "a value is shared" is not. Identifiers rather than names, on the same reasoning as every other member here: the caller already holds the facet records and a name repeated on the wire is a second copy to keep correct.
126
+ *
127
+ * @maxItems 64
128
+ */
129
+ facetIds?: Ulid[];
116
130
  }[];
117
131
  ext?: Ext;
118
132
  }
@@ -236,6 +250,10 @@ export const PAYLOAD_SCHEMA = {
236
250
  "items": {
237
251
  "type": "string"
238
252
  }
253
+ },
254
+ "facetId": {
255
+ "$ref": "#/$defs/Ulid",
256
+ "description": "The facet the profile at this location belongs to. Absent where it belongs to none, which is a real and common state rather than a gap — most profiles are unarranged until someone arranges them, and a consumer MUST NOT read absence as a facet of its own."
239
257
  }
240
258
  }
241
259
  }
@@ -258,6 +276,19 @@ export const PAYLOAD_SCHEMA = {
258
276
  ]
259
277
  },
260
278
  "description": "What the holder can actually do. Naming reissueCredentialToThisDid matters more than it looks: without it, a holder told 'this links your personas' has no action but to abandon the attribute, and the honest fix — a credential re-issued against the persona actually using it — stays invisible unless the analysis names it."
279
+ },
280
+ "crossesFacets": {
281
+ "type": "boolean",
282
+ "description": "Whether this linkage spans two or more of the holder's facets — parts of their life they have said belong apart. **A second axis, not a restatement of `severity`.** `severity` says how strongly a disclosure would link the holder, which is a fact about provenance and proof rung and is true whatever the holder intended; this says whether the holder would mind. A value shared between two profiles in the SAME facet is linkage the holder arranged on purpose — a work email in every work profile — and a consumer that alarmed on it teaches people to dismiss the alarm. A value shared ACROSS facets is the finding worth raising. True only when two or more DISTINCT facets appear among `sharedWith`: a profile belonging to no facet is unarranged, not a second facet, and contributes nothing here. Absent when the maintainer does not implement facets."
283
+ },
284
+ "facetIds": {
285
+ "type": "array",
286
+ "maxItems": 64,
287
+ "uniqueItems": true,
288
+ "items": {
289
+ "$ref": "#/$defs/Ulid"
290
+ },
291
+ "description": "The distinct facets this linkage touches, so a consumer can name them — \"Work and Home share your mobile number\" is a sentence a holder can act on, where \"a value is shared\" is not. Identifiers rather than names, on the same reasoning as every other member here: the caller already holds the facet records and a name repeated on the wire is a second copy to keep correct."
261
292
  }
262
293
  }
263
294
  }
@@ -366,6 +397,10 @@ export const RESPONSE_PAYLOAD_SCHEMA = {
366
397
  "items": {
367
398
  "type": "string"
368
399
  }
400
+ },
401
+ "facetId": {
402
+ "$ref": "#/$defs/Ulid",
403
+ "description": "The facet the profile at this location belongs to. Absent where it belongs to none, which is a real and common state rather than a gap — most profiles are unarranged until someone arranges them, and a consumer MUST NOT read absence as a facet of its own."
369
404
  }
370
405
  }
371
406
  }
@@ -388,6 +423,19 @@ export const RESPONSE_PAYLOAD_SCHEMA = {
388
423
  ]
389
424
  },
390
425
  "description": "What the holder can actually do. Naming reissueCredentialToThisDid matters more than it looks: without it, a holder told 'this links your personas' has no action but to abandon the attribute, and the honest fix — a credential re-issued against the persona actually using it — stays invisible unless the analysis names it."
426
+ },
427
+ "crossesFacets": {
428
+ "type": "boolean",
429
+ "description": "Whether this linkage spans two or more of the holder's facets — parts of their life they have said belong apart. **A second axis, not a restatement of `severity`.** `severity` says how strongly a disclosure would link the holder, which is a fact about provenance and proof rung and is true whatever the holder intended; this says whether the holder would mind. A value shared between two profiles in the SAME facet is linkage the holder arranged on purpose — a work email in every work profile — and a consumer that alarmed on it teaches people to dismiss the alarm. A value shared ACROSS facets is the finding worth raising. True only when two or more DISTINCT facets appear among `sharedWith`: a profile belonging to no facet is unarranged, not a second facet, and contributes nothing here. Absent when the maintainer does not implement facets."
430
+ },
431
+ "facetIds": {
432
+ "type": "array",
433
+ "maxItems": 64,
434
+ "uniqueItems": true,
435
+ "items": {
436
+ "$ref": "#/$defs/Ulid"
437
+ },
438
+ "description": "The distinct facets this linkage touches, so a consumer can name them — \"Work and Home share your mobile number\" is a sentence a holder can act on, where \"a value is shared\" is not. Identifiers rather than names, on the same reasoning as every other member here: the caller already holds the facet records and a name repeated on the wire is a second copy to keep correct."
391
439
  }
392
440
  }
393
441
  }
@@ -14,4 +14,4 @@ export interface RoomsSharedTypes {
14
14
  * The definitions this shared schema publishes, hoisted to one declaration each.
15
15
  * See "../../../_shared/components.js".
16
16
  */
17
- export type { AuthorityPresentation, EpochLink, RecordMetadata, RetentionPolicy, SealedRecord, Visibility } from "../../../_shared/components.js";
17
+ export type { AuthorityPresentation, DataCommitment, EpochLink, RecordMetadata, RetentionPolicy, SealedRecord, Visibility } from "../../../_shared/components.js";
@@ -3,7 +3,7 @@
3
3
  * Source: specs/rooms/records/get/0.1/payload.schema.json
4
4
  */
5
5
 
6
- import type { AuthorityPresentation, Ext, SealedRecord } from "../../../../_shared/components.js";
6
+ import type { AuthorityPresentation, DataCommitment, DigestMultibase, Ext, SealedRecord } from "../../../../_shared/components.js";
7
7
 
8
8
 
9
9
  export interface RoomsRecordsGetPayload {
@@ -40,11 +40,15 @@ export interface RoomsRecordsGetResponsePayload {
40
40
  cleartext?: {
41
41
  [k: string]: unknown | undefined;
42
42
  };
43
+ /**
44
+ * The room's data commitment at the moment this record was read. Carried on a single-record read as well as a listing so a reader can tell whether the room moved between two reads — and, once traces are specified, so this record can be proved to sit inside this root.
45
+ */
46
+ dataCommitment?: DataCommitment;
43
47
  ext?: Ext;
44
48
  }
45
49
 
46
50
  /** Shared definitions this specification references, re-exported under the names it used to declare them with. */
47
- export type { AuthorityPresentation, Ext, SealedRecord };
51
+ export type { AuthorityPresentation, DataCommitment, DigestMultibase, Ext, SealedRecord };
48
52
 
49
53
  /** Trust Task type URI. */
50
54
  export const TYPE_URI = "https://trusttasks.org/spec/rooms/records/get/0.1" as const;
@@ -125,6 +129,10 @@ export const PAYLOAD_SCHEMA = {
125
129
  "description": "Present on an `open` room.",
126
130
  "additionalProperties": true
127
131
  },
132
+ "dataCommitment": {
133
+ "$ref": "#/$defs/DataCommitment",
134
+ "description": "The room's data commitment at the moment this record was read. Carried on a single-record read as well as a listing so a reader can tell whether the room moved between two reads — and, once traces are specified, so this record can be proved to sit inside this root."
135
+ },
128
136
  "ext": {
129
137
  "$ref": "#/$defs/Ext"
130
138
  }
@@ -140,6 +148,21 @@ export const PAYLOAD_SCHEMA = {
140
148
  "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
141
149
  }
142
150
  },
151
+ "DataCommitment": {
152
+ "title": "DataCommitment",
153
+ "$ref": "#/$defs/DigestMultibase",
154
+ "description": "The root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.\n\nA room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing; a host that shows two members two different roots has been caught.\n\n**The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:\n 1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.\n 2. Leaf: `SHA-256(0x00 || JCS(record))`, where JCS is the RFC 8785 canonicalization of the record as this family's `RecordMetadata` plus its stored content, and `0x00` is RFC 6962's leaf-domain prefix.\n 3. Internal node: `SHA-256(0x01 || left || right)`.\n 4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.\n 5. A room holding no records commits to `SHA-256(\"\")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.\n\nThe leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.\n\nThe commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail."
155
+ },
156
+ "DigestMultibase": {
157
+ "title": "DigestMultibase",
158
+ "description": "A cryptographic digest as a multibase-encoded multihash — the encoding the W3C Verifiable Credentials Data Model 2.0 defines for `digestMultibase`, and the one `did:webvh` uses for its SCID and entry hashes.\n\nMultihash carries the hash algorithm in-band, so the value is self-describing and the wire format survives an algorithm change without a schema revision; multibase does the same for the base encoding, so a verifier never infers base58 from base64url by context. A bare hex string or a `sha-256:`-style prefix hard-codes one algorithm into the wire contract and is non-conforming here.\n\nThis definition constrains the *encoding only*. What the digest is computed over is stated by each referencing field, because it differs legitimately: a digest over a JSON document is taken over its RFC 8785 (JCS) canonicalization, while a digest over an opaque artifact is taken over its bytes. A field whose input is a JSON document and which does not name a canonicalization is not reproducible.\n\nRestricted to the two multibase headers W3C Controlled Identifiers 1.0 §2.4 normatively requires — `z` (base58btc) and `u` (base64url-no-pad). CID permits others but states that \"interoperability is not guaranteed between implementations using such values\", and a registry whose purpose is interoperability should not mint digests a conforming verifier may be unable to read. The alphabets are enforced rather than assumed: base58btc excludes 0, O, I and l, and an earlier permissive pattern let three published examples carry digests that were not valid base58 at all. base58btc is RECOMMENDED, for consistency with `did:key` and `did:webvh`.",
159
+ "type": "string",
160
+ "minLength": 16,
161
+ "pattern": "^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$",
162
+ "examples": [
163
+ "zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
164
+ ]
165
+ },
143
166
  "SealedRecord": {
144
167
  "title": "SealedRecord",
145
168
  "type": "object",
@@ -230,6 +253,10 @@ export const RESPONSE_PAYLOAD_SCHEMA = {
230
253
  "description": "Present on an `open` room.",
231
254
  "additionalProperties": true
232
255
  },
256
+ "dataCommitment": {
257
+ "$ref": "#/$defs/DataCommitment",
258
+ "description": "The room's data commitment at the moment this record was read. Carried on a single-record read as well as a listing so a reader can tell whether the room moved between two reads — and, once traces are specified, so this record can be proved to sit inside this root."
259
+ },
233
260
  "ext": {
234
261
  "$ref": "#/$defs/Ext"
235
262
  }
@@ -245,6 +272,21 @@ export const RESPONSE_PAYLOAD_SCHEMA = {
245
272
  "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
246
273
  }
247
274
  },
275
+ "DataCommitment": {
276
+ "title": "DataCommitment",
277
+ "$ref": "#/$defs/DigestMultibase",
278
+ "description": "The root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.\n\nA room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing; a host that shows two members two different roots has been caught.\n\n**The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:\n 1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.\n 2. Leaf: `SHA-256(0x00 || JCS(record))`, where JCS is the RFC 8785 canonicalization of the record as this family's `RecordMetadata` plus its stored content, and `0x00` is RFC 6962's leaf-domain prefix.\n 3. Internal node: `SHA-256(0x01 || left || right)`.\n 4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.\n 5. A room holding no records commits to `SHA-256(\"\")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.\n\nThe leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.\n\nThe commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail."
279
+ },
280
+ "DigestMultibase": {
281
+ "title": "DigestMultibase",
282
+ "description": "A cryptographic digest as a multibase-encoded multihash — the encoding the W3C Verifiable Credentials Data Model 2.0 defines for `digestMultibase`, and the one `did:webvh` uses for its SCID and entry hashes.\n\nMultihash carries the hash algorithm in-band, so the value is self-describing and the wire format survives an algorithm change without a schema revision; multibase does the same for the base encoding, so a verifier never infers base58 from base64url by context. A bare hex string or a `sha-256:`-style prefix hard-codes one algorithm into the wire contract and is non-conforming here.\n\nThis definition constrains the *encoding only*. What the digest is computed over is stated by each referencing field, because it differs legitimately: a digest over a JSON document is taken over its RFC 8785 (JCS) canonicalization, while a digest over an opaque artifact is taken over its bytes. A field whose input is a JSON document and which does not name a canonicalization is not reproducible.\n\nRestricted to the two multibase headers W3C Controlled Identifiers 1.0 §2.4 normatively requires — `z` (base58btc) and `u` (base64url-no-pad). CID permits others but states that \"interoperability is not guaranteed between implementations using such values\", and a registry whose purpose is interoperability should not mint digests a conforming verifier may be unable to read. The alphabets are enforced rather than assumed: base58btc excludes 0, O, I and l, and an earlier permissive pattern let three published examples carry digests that were not valid base58 at all. base58btc is RECOMMENDED, for consistency with `did:key` and `did:webvh`.",
283
+ "type": "string",
284
+ "minLength": 16,
285
+ "pattern": "^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$",
286
+ "examples": [
287
+ "zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
288
+ ]
289
+ },
248
290
  "SealedRecord": {
249
291
  "title": "SealedRecord",
250
292
  "type": "object",
@@ -3,7 +3,7 @@
3
3
  * Source: specs/rooms/records/list/0.1/payload.schema.json
4
4
  */
5
5
 
6
- import type { AuthorityPresentation, Ext, RecordMetadata } from "../../../../_shared/components.js";
6
+ import type { AuthorityPresentation, DataCommitment, DigestMultibase, Ext, RecordMetadata } from "../../../../_shared/components.js";
7
7
 
8
8
 
9
9
  export interface RoomsRecordsListPayload {
@@ -45,11 +45,15 @@ export interface RoomsRecordsListResponsePayload {
45
45
  * Present when more records remain.
46
46
  */
47
47
  cursor?: string;
48
+ /**
49
+ * The room's data commitment at the moment this listing was taken. OPTIONAL because a host that does not maintain the tree cannot honestly assert one, and a fabricated root is worse than an absent one — its absence says 'this host offers no completeness guarantee', which is true and useful. Present, it is signed along with the rest of the response, which is what makes it evidence rather than a claim.
50
+ */
51
+ dataCommitment?: DataCommitment;
48
52
  ext?: Ext;
49
53
  }
50
54
 
51
55
  /** Shared definitions this specification references, re-exported under the names it used to declare them with. */
52
- export type { AuthorityPresentation, Ext, RecordMetadata };
56
+ export type { AuthorityPresentation, DataCommitment, DigestMultibase, Ext, RecordMetadata };
53
57
 
54
58
  /** Trust Task type URI. */
55
59
  export const TYPE_URI = "https://trusttasks.org/spec/rooms/records/list/0.1" as const;
@@ -139,6 +143,10 @@ export const PAYLOAD_SCHEMA = {
139
143
  "maxLength": 4096,
140
144
  "description": "Present when more records remain."
141
145
  },
146
+ "dataCommitment": {
147
+ "$ref": "#/$defs/DataCommitment",
148
+ "description": "The room's data commitment at the moment this listing was taken. OPTIONAL because a host that does not maintain the tree cannot honestly assert one, and a fabricated root is worse than an absent one — its absence says 'this host offers no completeness guarantee', which is true and useful. Present, it is signed along with the rest of the response, which is what makes it evidence rather than a claim."
149
+ },
142
150
  "ext": {
143
151
  "$ref": "#/$defs/Ext"
144
152
  }
@@ -154,6 +162,21 @@ export const PAYLOAD_SCHEMA = {
154
162
  "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
155
163
  }
156
164
  },
165
+ "DataCommitment": {
166
+ "title": "DataCommitment",
167
+ "$ref": "#/$defs/DigestMultibase",
168
+ "description": "The root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.\n\nA room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing; a host that shows two members two different roots has been caught.\n\n**The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:\n 1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.\n 2. Leaf: `SHA-256(0x00 || JCS(record))`, where JCS is the RFC 8785 canonicalization of the record as this family's `RecordMetadata` plus its stored content, and `0x00` is RFC 6962's leaf-domain prefix.\n 3. Internal node: `SHA-256(0x01 || left || right)`.\n 4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.\n 5. A room holding no records commits to `SHA-256(\"\")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.\n\nThe leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.\n\nThe commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail."
169
+ },
170
+ "DigestMultibase": {
171
+ "title": "DigestMultibase",
172
+ "description": "A cryptographic digest as a multibase-encoded multihash — the encoding the W3C Verifiable Credentials Data Model 2.0 defines for `digestMultibase`, and the one `did:webvh` uses for its SCID and entry hashes.\n\nMultihash carries the hash algorithm in-band, so the value is self-describing and the wire format survives an algorithm change without a schema revision; multibase does the same for the base encoding, so a verifier never infers base58 from base64url by context. A bare hex string or a `sha-256:`-style prefix hard-codes one algorithm into the wire contract and is non-conforming here.\n\nThis definition constrains the *encoding only*. What the digest is computed over is stated by each referencing field, because it differs legitimately: a digest over a JSON document is taken over its RFC 8785 (JCS) canonicalization, while a digest over an opaque artifact is taken over its bytes. A field whose input is a JSON document and which does not name a canonicalization is not reproducible.\n\nRestricted to the two multibase headers W3C Controlled Identifiers 1.0 §2.4 normatively requires — `z` (base58btc) and `u` (base64url-no-pad). CID permits others but states that \"interoperability is not guaranteed between implementations using such values\", and a registry whose purpose is interoperability should not mint digests a conforming verifier may be unable to read. The alphabets are enforced rather than assumed: base58btc excludes 0, O, I and l, and an earlier permissive pattern let three published examples carry digests that were not valid base58 at all. base58btc is RECOMMENDED, for consistency with `did:key` and `did:webvh`.",
173
+ "type": "string",
174
+ "minLength": 16,
175
+ "pattern": "^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$",
176
+ "examples": [
177
+ "zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
178
+ ]
179
+ },
157
180
  "RecordMetadata": {
158
181
  "title": "RecordMetadata",
159
182
  "type": "object",
@@ -266,6 +289,10 @@ export const RESPONSE_PAYLOAD_SCHEMA = {
266
289
  "maxLength": 4096,
267
290
  "description": "Present when more records remain."
268
291
  },
292
+ "dataCommitment": {
293
+ "$ref": "#/$defs/DataCommitment",
294
+ "description": "The room's data commitment at the moment this listing was taken. OPTIONAL because a host that does not maintain the tree cannot honestly assert one, and a fabricated root is worse than an absent one — its absence says 'this host offers no completeness guarantee', which is true and useful. Present, it is signed along with the rest of the response, which is what makes it evidence rather than a claim."
295
+ },
269
296
  "ext": {
270
297
  "$ref": "#/$defs/Ext"
271
298
  }
@@ -281,6 +308,21 @@ export const RESPONSE_PAYLOAD_SCHEMA = {
281
308
  "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
282
309
  }
283
310
  },
311
+ "DataCommitment": {
312
+ "title": "DataCommitment",
313
+ "$ref": "#/$defs/DigestMultibase",
314
+ "description": "The root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.\n\nA room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing; a host that shows two members two different roots has been caught.\n\n**The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:\n 1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.\n 2. Leaf: `SHA-256(0x00 || JCS(record))`, where JCS is the RFC 8785 canonicalization of the record as this family's `RecordMetadata` plus its stored content, and `0x00` is RFC 6962's leaf-domain prefix.\n 3. Internal node: `SHA-256(0x01 || left || right)`.\n 4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.\n 5. A room holding no records commits to `SHA-256(\"\")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.\n\nThe leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.\n\nThe commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail."
315
+ },
316
+ "DigestMultibase": {
317
+ "title": "DigestMultibase",
318
+ "description": "A cryptographic digest as a multibase-encoded multihash — the encoding the W3C Verifiable Credentials Data Model 2.0 defines for `digestMultibase`, and the one `did:webvh` uses for its SCID and entry hashes.\n\nMultihash carries the hash algorithm in-band, so the value is self-describing and the wire format survives an algorithm change without a schema revision; multibase does the same for the base encoding, so a verifier never infers base58 from base64url by context. A bare hex string or a `sha-256:`-style prefix hard-codes one algorithm into the wire contract and is non-conforming here.\n\nThis definition constrains the *encoding only*. What the digest is computed over is stated by each referencing field, because it differs legitimately: a digest over a JSON document is taken over its RFC 8785 (JCS) canonicalization, while a digest over an opaque artifact is taken over its bytes. A field whose input is a JSON document and which does not name a canonicalization is not reproducible.\n\nRestricted to the two multibase headers W3C Controlled Identifiers 1.0 §2.4 normatively requires — `z` (base58btc) and `u` (base64url-no-pad). CID permits others but states that \"interoperability is not guaranteed between implementations using such values\", and a registry whose purpose is interoperability should not mint digests a conforming verifier may be unable to read. The alphabets are enforced rather than assumed: base58btc excludes 0, O, I and l, and an earlier permissive pattern let three published examples carry digests that were not valid base58 at all. base58btc is RECOMMENDED, for consistency with `did:key` and `did:webvh`.",
319
+ "type": "string",
320
+ "minLength": 16,
321
+ "pattern": "^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$",
322
+ "examples": [
323
+ "zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
324
+ ]
325
+ },
284
326
  "RecordMetadata": {
285
327
  "title": "RecordMetadata",
286
328
  "type": "object",