@openvtc/trust-tasks 0.18.2 → 0.18.3

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.
@@ -0,0 +1,536 @@
1
+ /**
2
+ * Generated by scripts/build-ts-bindings.mjs — DO NOT EDIT BY HAND.
3
+ * Source: specs/rooms/keys/browse/0.1/payload.schema.json
4
+ */
5
+
6
+ import type { DataCommitment, DigestMultibase, Ext, HeadVersion, ReadVerification, RecordCount, RecordMetadata } from "../../../../_shared/components.js";
7
+
8
+
9
+ /**
10
+ * TODO: what the request payload of rooms/keys/browse carries. The outer document members (id, type, issuer, recipient, issuedAt, expiresAt, proof) are owned by the framework — SPEC §6.3.
11
+ */
12
+ export interface RoomsKeysBrowsePayload {
13
+ /**
14
+ * The room to list. The recipient MUST already hold group state for it.
15
+ */
16
+ roomId: string;
17
+ /**
18
+ * The host to list from, as a DID. Named by the caller for the same reason it is on `rooms/keys/read`: nothing maps a room to its host.
19
+ */
20
+ host: string;
21
+ /**
22
+ * Restrict to keys with this prefix. Passed through to the host. **Setting it makes the listing non-comparable against the room's `recordCount`** — see `verification.count`.
23
+ */
24
+ prefix?: string;
25
+ /**
26
+ * Return only records above this version. Passed through. Also makes the listing non-comparable against `recordCount`.
27
+ */
28
+ sinceVersion?: number;
29
+ /**
30
+ * Page size to ask the host for. A cap on what a page holds, never a cap on the result: a recipient MUST follow the host's cursor to the end unless the caller asked it to stop, because a listing read to its end is the only one whose length means anything.
31
+ */
32
+ limit?: number;
33
+ /**
34
+ * Ecosystem-defined extension members per SPEC.md §4.5.1.
35
+ */
36
+ ext?: Ext;
37
+ }
38
+ /**
39
+ * Success response to rooms/keys/browse. Type https://trusttasks.org/spec/rooms/keys/browse/0.1#response.
40
+ */
41
+ export interface RoomsKeysBrowseResponsePayload {
42
+ roomId: string;
43
+ /**
44
+ * Metadata, never bodies — the same projection the host serves, passed through. A member who wants a body asks for one record, which is also the only shape a trace can be produced for.
45
+ */
46
+ records: RecordMetadata[];
47
+ /**
48
+ * Whether the recipient followed the host's cursor to the end. `false` means a page bound was hit, and is what makes `verification.count` `notComparable` even on an unfiltered listing — a short listing and a truncated one are indistinguishable by length alone, which is exactly the confusion this member exists to prevent.
49
+ */
50
+ complete?: boolean;
51
+ /**
52
+ * What the recipient checked and what it found. REQUIRED for the same reason it is on `rooms/keys/read`.
53
+ */
54
+ verification: ReadVerification;
55
+ ext?: Ext;
56
+ }
57
+
58
+ /** Shared definitions this specification references, re-exported under the names it used to declare them with. */
59
+ export type { DataCommitment, DigestMultibase, Ext, HeadVersion, ReadVerification, RecordCount, RecordMetadata };
60
+
61
+ /** Trust Task type URI. */
62
+ export const TYPE_URI = "https://trusttasks.org/spec/rooms/keys/browse/0.1" as const;
63
+
64
+ /** Stable alias for this specification's request payload shape. */
65
+ export type Payload = RoomsKeysBrowsePayload;
66
+
67
+ /** Trust Task response type URI (request type URI + "#response"). */
68
+ export const RESPONSE_TYPE_URI = "https://trusttasks.org/spec/rooms/keys/browse/0.1#response" as const;
69
+
70
+ /** Stable alias for this specification's success-response payload shape. */
71
+ export type Response = RoomsKeysBrowseResponsePayload;
72
+
73
+ /**
74
+ * This specification's payload schema, as a value.
75
+ *
76
+ * SPEC.md §7.2 item 2 is performed against this. It is shipped as data
77
+ * rather than only as a `.json` file because TypeScript types are erased
78
+ * at runtime: without a schema a consumer has nothing to validate, and
79
+ * every REQUIRED payload member is optional in practice. Cross-file
80
+ * `$ref`s are already inlined, so it needs no resolver.
81
+ */
82
+ export const PAYLOAD_SCHEMA = {
83
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
84
+ "$id": "https://trusttasks.org/spec/rooms/keys/browse/0.1",
85
+ "title": "Rooms Keys Browse — payload",
86
+ "description": "TODO: what the request payload of rooms/keys/browse carries. The outer document members (id, type, issuer, recipient, issuedAt, expiresAt, proof) are owned by the framework — SPEC §6.3.",
87
+ "type": "object",
88
+ "additionalProperties": false,
89
+ "required": [
90
+ "roomId",
91
+ "host"
92
+ ],
93
+ "properties": {
94
+ "roomId": {
95
+ "type": "string",
96
+ "description": "The room to list. The recipient MUST already hold group state for it."
97
+ },
98
+ "host": {
99
+ "type": "string",
100
+ "description": "The host to list from, as a DID. Named by the caller for the same reason it is on `rooms/keys/read`: nothing maps a room to its host."
101
+ },
102
+ "prefix": {
103
+ "type": "string",
104
+ "maxLength": 512,
105
+ "description": "Restrict to keys with this prefix. Passed through to the host. **Setting it makes the listing non-comparable against the room's `recordCount`** — see `verification.count`."
106
+ },
107
+ "sinceVersion": {
108
+ "type": "integer",
109
+ "minimum": 0,
110
+ "description": "Return only records above this version. Passed through. Also makes the listing non-comparable against `recordCount`."
111
+ },
112
+ "limit": {
113
+ "type": "integer",
114
+ "minimum": 1,
115
+ "maximum": 1000,
116
+ "description": "Page size to ask the host for. A cap on what a page holds, never a cap on the result: a recipient MUST follow the host's cursor to the end unless the caller asked it to stop, because a listing read to its end is the only one whose length means anything."
117
+ },
118
+ "ext": {
119
+ "$ref": "#/$defs/Ext",
120
+ "description": "Ecosystem-defined extension members per SPEC.md §4.5.1."
121
+ }
122
+ },
123
+ "$defs": {
124
+ "Response": {
125
+ "$anchor": "response",
126
+ "type": "object",
127
+ "additionalProperties": false,
128
+ "required": [
129
+ "roomId",
130
+ "records",
131
+ "verification"
132
+ ],
133
+ "description": "Success response to rooms/keys/browse. Type https://trusttasks.org/spec/rooms/keys/browse/0.1#response.",
134
+ "properties": {
135
+ "roomId": {
136
+ "type": "string"
137
+ },
138
+ "records": {
139
+ "type": "array",
140
+ "items": {
141
+ "$ref": "#/$defs/RecordMetadata"
142
+ },
143
+ "description": "Metadata, never bodies — the same projection the host serves, passed through. A member who wants a body asks for one record, which is also the only shape a trace can be produced for."
144
+ },
145
+ "complete": {
146
+ "type": "boolean",
147
+ "description": "Whether the recipient followed the host's cursor to the end. `false` means a page bound was hit, and is what makes `verification.count` `notComparable` even on an unfiltered listing — a short listing and a truncated one are indistinguishable by length alone, which is exactly the confusion this member exists to prevent."
148
+ },
149
+ "verification": {
150
+ "$ref": "#/$defs/ReadVerification",
151
+ "description": "What the recipient checked and what it found. REQUIRED for the same reason it is on `rooms/keys/read`."
152
+ },
153
+ "ext": {
154
+ "$ref": "#/$defs/Ext"
155
+ }
156
+ },
157
+ "title": "Rooms Keys Browse — response payload"
158
+ },
159
+ "Ext": {
160
+ "title": "Ext",
161
+ "description": "Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.",
162
+ "type": "object",
163
+ "minProperties": 1,
164
+ "additionalProperties": true,
165
+ "propertyNames": {
166
+ "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
167
+ }
168
+ },
169
+ "ReadVerification": {
170
+ "title": "ReadVerification",
171
+ "type": "object",
172
+ "additionalProperties": false,
173
+ "required": [
174
+ "priorRoots"
175
+ ],
176
+ "description": "What the agent checked on the member's behalf, and what it found.\n\n**A verdict, never an error.** None of these values fails the task, including the ones that report a host caught out. A member's own agent refusing to hand over a record because the *host* misbehaved punishes the member for somebody else's act — and locks them out of the room holding the records that would show what happened, at the moment they most need them. The consequence belongs on the **write** path, where continuing to hand material to a party you have caught is what compounds the damage.\n\nSo a consumer **MUST NOT** treat any value here as a failed read, and **MUST** surface an adverse one rather than logging it. What that costs is words: a member shown a bare warning icon dismisses it, and a member later refused a write with no explanation blames their own agent. A detection the member attributes to the wrong party is worse than no detection.\n\nMembers are **absent where the task cannot produce them** rather than carrying a not-applicable value — a listing has no `trace` because there is no single record to trace, and a single read has no `count` because a count is only checkable against a listing read to its end.",
177
+ "properties": {
178
+ "trace": {
179
+ "type": "string",
180
+ "enum": [
181
+ "verified",
182
+ "failed",
183
+ "notOffered"
184
+ ],
185
+ "description": "Whether the record's `RecordTrace` reached the `dataCommitment` served beside it.\n\n`verified` says the record is under the root the host asserted — and **nothing about whether that root is the room's**, which is what `priorRoots` and an anchor are for. `notOffered` is a host that maintains no tree, which is legal and informative. `failed` is arithmetic that does not close: the host served a path that does not reach its own root, which is either a defect or a fabrication and is not distinguishable from here."
186
+ },
187
+ "priorRoots": {
188
+ "type": "string",
189
+ "enum": [
190
+ "agree",
191
+ "conflict",
192
+ "noneHeld",
193
+ "notChecked"
194
+ ],
195
+ "description": "Whether this root matches what the agent has seen from this host for this room **at this `headVersion`**.\n\nThis is the comparison a member cannot make for themselves. A tab does not outlive itself and a CLI holds nothing; the agent is the only party on the member's side of the boundary that saw both reads.\n\n - `agree` — seen at this head before, same root.\n - `conflict` — seen at this head before, **different root**. A host caught: there is no write to attribute the difference to, because a write would have moved the head.\n - `noneHeld` — first read at this head. Not evidence of anything; a memory of one is not a comparison.\n - `notChecked` — this agent keeps no root history. An honest answer for an agent that cannot make the comparison, and **not** a synonym for `noneHeld`: one says nothing was found, the other says nothing was looked for.\n\nREQUIRED, so that an agent which does not check has to say so rather than omit the question."
196
+ },
197
+ "count": {
198
+ "type": "string",
199
+ "enum": [
200
+ "agrees",
201
+ "short",
202
+ "notComparable",
203
+ "notOffered"
204
+ ],
205
+ "description": "Whether the number of records returned matches the `recordCount` the host committed to.\n\nOnly ever comparable against a listing with **no** `prefix`, **no** `sinceVersion` and read to its end — anything else legitimately holds fewer, and comparing it is a discrepancy the reader manufactured. `notComparable` is that case, and it is the common one.\n\n`short` is a host contradicting itself inside one exchange: it committed to a tree of N records and served fewer, with no filter to explain the difference. That is the omission the commitment exists to make detectable, caught without a second party and without an anchor."
206
+ },
207
+ "head": {
208
+ "type": "object",
209
+ "additionalProperties": false,
210
+ "required": [
211
+ "dataCommitment",
212
+ "recordCount",
213
+ "headVersion"
214
+ ],
215
+ "description": "What the host asserted about the room, passed through unaltered so the member can compare it somewhere this agent cannot reach — with another member, or against a witnessed anchor. All three or none: a root without the state it describes is not comparable to another root, which is the whole of `HeadVersion`.",
216
+ "properties": {
217
+ "dataCommitment": {
218
+ "$ref": "#/$defs/DataCommitment"
219
+ },
220
+ "recordCount": {
221
+ "$ref": "#/$defs/RecordCount"
222
+ },
223
+ "headVersion": {
224
+ "$ref": "#/$defs/HeadVersion"
225
+ }
226
+ }
227
+ }
228
+ }
229
+ },
230
+ "HeadVersion": {
231
+ "title": "HeadVersion",
232
+ "type": "integer",
233
+ "minimum": 0,
234
+ "description": "The highest version among the records `DataCommitment` covers. `0` for a room that holds none.\n\n**Derived from the same set as the root, and not read from the room's own counter.** The two agree for any host that has never erased a record — versions are assigned strictly increasing and a retraction keeps its tombstone — but they are not interchangeable, because a root and a counter are *two reads*, and two reads are not a snapshot. A write landing between them yields a pair that is individually correct and jointly false: two members holding roots taken over different trees, labelled with one version. That reads as equivocation and is not, and a **false accusation discredits the mechanism rather than the host** — the worst outcome available here. Taken from the committed set, the version cannot disagree with the root it labels, whatever else is happening to the room.\n\nThe corollary is worth stating: a host that **erases** a record — as distinct from retracting it, which leaves a tombstone in the tree — moves the root without necessarily moving this value, and two members straddling that erasure would see one version over two roots. Erasure is a retention act with its own answer, and a family that exposes one owes this definition another look.\n\n**This is what makes two roots comparable at all**, and without it the comparison this family is built on cannot be performed. A room moves: every put, curate and retraction assigns a new version, so two roots taken at two moments differ legitimately and a reader learns nothing from the difference. Shown two different roots, a host that equivocated and a host that was merely written to are indistinguishable — the first can always answer *the room moved between your reads*, and nothing contradicts it.\n\nA version is assigned by exactly the mutations that change the tree — every put, curate and retraction takes the next one — so the highest of them names the **state** the root describes. Two roots carrying the same `headVersion` and differing is a host caught: there is no write to attribute the difference to. Two roots carrying different ones are simply two moments, and a reader should draw nothing from them.\n\nA host can lie about this number too, and it is then lying about the counter it also uses for optimistic concurrency (`expectedVersion`) and for incremental sync (`sinceVersion`) — so a member holding a signed acknowledgement of a write at version `V` contradicts any head below `V` directly.\n\n**Not a timestamp.** A time is host-asserted, unverifiable and useless for this: two roots a second apart are not evidence of anything, while two roots at one version are."
235
+ },
236
+ "RecordCount": {
237
+ "title": "RecordCount",
238
+ "type": "integer",
239
+ "minimum": 0,
240
+ "description": "How many records the room held when `DataCommitment` was computed — the number of leaves in that tree, tombstones included, since a tombstone is a record.\n\n**Why a root needs this.** Certificate Transparency's signed tree head is a root *and a tree size*; this family shipped the root alone, and the half that was dropped is the half that makes a listing checkable. A reader holding a **complete, unfiltered** listing cannot recompute the root — a leaf commits to a whole record and a listing returns a projection without the body — but it can count. A host that omits a record from a listing while committing to a tree that holds it now contradicts itself in the same response, with no second party and no anchor involved.\n\nA host can of course understate both together. That is the point rather than a hole: the omission stops being silence and becomes a **specific claim about how many records the room holds**, which any other member's view, or any writer's signed put acknowledgement, contradicts. Making an omission attributable is the whole of what this machinery buys; it never claimed to make one impossible.\n\n**It counts the room, never the page.** The same rule `DataCommitment` states, and the same trap: a count scoped to what was returned is one a host satisfies by construction. So this is only comparable against a listing read to the end with **no** `prefix` and **no** `sinceVersion` — a filtered listing legitimately holds fewer, and a reader that compares one against this has found a discrepancy it created itself."
241
+ },
242
+ "DataCommitment": {
243
+ "title": "DataCommitment",
244
+ "$ref": "#/$defs/DigestMultibase",
245
+ "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.\n\n**A root on its own is not comparable, and an earlier revision of this description said it was.** It claimed a host showing two members two different roots had been caught, which is false while a room can move between two reads: the host answers *there was a write*, and nothing contradicts it. Comparison needs the state each root describes, which is what `HeadVersion` names — and `RecordCount` is the other half of the same omission, since Certificate Transparency's signed tree head is a root *and a size* and this family shipped only the root. A reader that receives a `dataCommitment` **without** them can use it against a witnessed anchor, where the epoch pins the state, and **MUST NOT** compare it against another root.\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 the record is a `CommittedRecord` — that definition fixes the members exactly, and this step used to name a *projection* instead, which two implementations could read two ways — JCS is its RFC 8785 canonicalization, 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.\n\nProving that a *particular* record sits under this root is a separate question, answered by `RecordTrace` on a single-record read. A commitment catches a host that equivocates; a trace binds one record to what the host committed to. Neither is the other, and a reader wanting completeness needs both plus a root it did not get from the host it is checking."
246
+ },
247
+ "DigestMultibase": {
248
+ "title": "DigestMultibase",
249
+ "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`.",
250
+ "type": "string",
251
+ "minLength": 16,
252
+ "pattern": "^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$",
253
+ "examples": [
254
+ "zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
255
+ ]
256
+ },
257
+ "RecordMetadata": {
258
+ "title": "RecordMetadata",
259
+ "type": "object",
260
+ "additionalProperties": false,
261
+ "required": [
262
+ "key",
263
+ "version",
264
+ "updatedAt"
265
+ ],
266
+ "description": "What `rooms/records/list` returns. Never the body: ranking happens on the client, and a host that returned every body would make the caller pay for the whole room on every listing.",
267
+ "properties": {
268
+ "key": {
269
+ "type": "string",
270
+ "description": "The record's key within the room. On `attributed` and `private` rooms this MUST be opaque — a random identifier, never a descriptive slug. A key reading `decision/acquire-northwind` defeats the encryption sitting beside it; structured naming belongs inside the sealed body."
271
+ },
272
+ "version": {
273
+ "type": "integer",
274
+ "minimum": 1,
275
+ "description": "Server-assigned, monotonic per room."
276
+ },
277
+ "epoch": {
278
+ "type": "integer",
279
+ "minimum": 1,
280
+ "description": "The epoch the record was sealed under. Absent on an `open` room."
281
+ },
282
+ "status": {
283
+ "type": "string",
284
+ "enum": [
285
+ "active",
286
+ "deprecated",
287
+ "retracted"
288
+ ],
289
+ "description": "Curation state. `retracted` is a tombstone: the body is gone, the key and version remain so incremental sync converges and the audit chain stays intact."
290
+ },
291
+ "updatedAt": {
292
+ "type": "string",
293
+ "format": "date-time"
294
+ },
295
+ "title": {
296
+ "type": "string",
297
+ "maxLength": 512,
298
+ "description": "Present only on an `open` room; sealed with the body otherwise."
299
+ },
300
+ "description": {
301
+ "type": "string",
302
+ "maxLength": 2048,
303
+ "description": "Present only on an `open` room; sealed with the body otherwise."
304
+ },
305
+ "author": {
306
+ "type": "string",
307
+ "description": "The member who wrote it. Present on `open` and `attributed`; on `private` the author is inside the sealed body, where only members can read it."
308
+ }
309
+ }
310
+ }
311
+ }
312
+ } as const;
313
+
314
+ /** As {@link PAYLOAD_SCHEMA}, for the success-response variant. */
315
+ export const RESPONSE_PAYLOAD_SCHEMA = {
316
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
317
+ "$ref": "#/$defs/Response",
318
+ "$defs": {
319
+ "Response": {
320
+ "$anchor": "response",
321
+ "type": "object",
322
+ "additionalProperties": false,
323
+ "required": [
324
+ "roomId",
325
+ "records",
326
+ "verification"
327
+ ],
328
+ "description": "Success response to rooms/keys/browse. Type https://trusttasks.org/spec/rooms/keys/browse/0.1#response.",
329
+ "properties": {
330
+ "roomId": {
331
+ "type": "string"
332
+ },
333
+ "records": {
334
+ "type": "array",
335
+ "items": {
336
+ "$ref": "#/$defs/RecordMetadata"
337
+ },
338
+ "description": "Metadata, never bodies — the same projection the host serves, passed through. A member who wants a body asks for one record, which is also the only shape a trace can be produced for."
339
+ },
340
+ "complete": {
341
+ "type": "boolean",
342
+ "description": "Whether the recipient followed the host's cursor to the end. `false` means a page bound was hit, and is what makes `verification.count` `notComparable` even on an unfiltered listing — a short listing and a truncated one are indistinguishable by length alone, which is exactly the confusion this member exists to prevent."
343
+ },
344
+ "verification": {
345
+ "$ref": "#/$defs/ReadVerification",
346
+ "description": "What the recipient checked and what it found. REQUIRED for the same reason it is on `rooms/keys/read`."
347
+ },
348
+ "ext": {
349
+ "$ref": "#/$defs/Ext"
350
+ }
351
+ },
352
+ "title": "Rooms Keys Browse — response payload"
353
+ },
354
+ "Ext": {
355
+ "title": "Ext",
356
+ "description": "Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.",
357
+ "type": "object",
358
+ "minProperties": 1,
359
+ "additionalProperties": true,
360
+ "propertyNames": {
361
+ "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
362
+ }
363
+ },
364
+ "ReadVerification": {
365
+ "title": "ReadVerification",
366
+ "type": "object",
367
+ "additionalProperties": false,
368
+ "required": [
369
+ "priorRoots"
370
+ ],
371
+ "description": "What the agent checked on the member's behalf, and what it found.\n\n**A verdict, never an error.** None of these values fails the task, including the ones that report a host caught out. A member's own agent refusing to hand over a record because the *host* misbehaved punishes the member for somebody else's act — and locks them out of the room holding the records that would show what happened, at the moment they most need them. The consequence belongs on the **write** path, where continuing to hand material to a party you have caught is what compounds the damage.\n\nSo a consumer **MUST NOT** treat any value here as a failed read, and **MUST** surface an adverse one rather than logging it. What that costs is words: a member shown a bare warning icon dismisses it, and a member later refused a write with no explanation blames their own agent. A detection the member attributes to the wrong party is worse than no detection.\n\nMembers are **absent where the task cannot produce them** rather than carrying a not-applicable value — a listing has no `trace` because there is no single record to trace, and a single read has no `count` because a count is only checkable against a listing read to its end.",
372
+ "properties": {
373
+ "trace": {
374
+ "type": "string",
375
+ "enum": [
376
+ "verified",
377
+ "failed",
378
+ "notOffered"
379
+ ],
380
+ "description": "Whether the record's `RecordTrace` reached the `dataCommitment` served beside it.\n\n`verified` says the record is under the root the host asserted — and **nothing about whether that root is the room's**, which is what `priorRoots` and an anchor are for. `notOffered` is a host that maintains no tree, which is legal and informative. `failed` is arithmetic that does not close: the host served a path that does not reach its own root, which is either a defect or a fabrication and is not distinguishable from here."
381
+ },
382
+ "priorRoots": {
383
+ "type": "string",
384
+ "enum": [
385
+ "agree",
386
+ "conflict",
387
+ "noneHeld",
388
+ "notChecked"
389
+ ],
390
+ "description": "Whether this root matches what the agent has seen from this host for this room **at this `headVersion`**.\n\nThis is the comparison a member cannot make for themselves. A tab does not outlive itself and a CLI holds nothing; the agent is the only party on the member's side of the boundary that saw both reads.\n\n - `agree` — seen at this head before, same root.\n - `conflict` — seen at this head before, **different root**. A host caught: there is no write to attribute the difference to, because a write would have moved the head.\n - `noneHeld` — first read at this head. Not evidence of anything; a memory of one is not a comparison.\n - `notChecked` — this agent keeps no root history. An honest answer for an agent that cannot make the comparison, and **not** a synonym for `noneHeld`: one says nothing was found, the other says nothing was looked for.\n\nREQUIRED, so that an agent which does not check has to say so rather than omit the question."
391
+ },
392
+ "count": {
393
+ "type": "string",
394
+ "enum": [
395
+ "agrees",
396
+ "short",
397
+ "notComparable",
398
+ "notOffered"
399
+ ],
400
+ "description": "Whether the number of records returned matches the `recordCount` the host committed to.\n\nOnly ever comparable against a listing with **no** `prefix`, **no** `sinceVersion` and read to its end — anything else legitimately holds fewer, and comparing it is a discrepancy the reader manufactured. `notComparable` is that case, and it is the common one.\n\n`short` is a host contradicting itself inside one exchange: it committed to a tree of N records and served fewer, with no filter to explain the difference. That is the omission the commitment exists to make detectable, caught without a second party and without an anchor."
401
+ },
402
+ "head": {
403
+ "type": "object",
404
+ "additionalProperties": false,
405
+ "required": [
406
+ "dataCommitment",
407
+ "recordCount",
408
+ "headVersion"
409
+ ],
410
+ "description": "What the host asserted about the room, passed through unaltered so the member can compare it somewhere this agent cannot reach — with another member, or against a witnessed anchor. All three or none: a root without the state it describes is not comparable to another root, which is the whole of `HeadVersion`.",
411
+ "properties": {
412
+ "dataCommitment": {
413
+ "$ref": "#/$defs/DataCommitment"
414
+ },
415
+ "recordCount": {
416
+ "$ref": "#/$defs/RecordCount"
417
+ },
418
+ "headVersion": {
419
+ "$ref": "#/$defs/HeadVersion"
420
+ }
421
+ }
422
+ }
423
+ }
424
+ },
425
+ "HeadVersion": {
426
+ "title": "HeadVersion",
427
+ "type": "integer",
428
+ "minimum": 0,
429
+ "description": "The highest version among the records `DataCommitment` covers. `0` for a room that holds none.\n\n**Derived from the same set as the root, and not read from the room's own counter.** The two agree for any host that has never erased a record — versions are assigned strictly increasing and a retraction keeps its tombstone — but they are not interchangeable, because a root and a counter are *two reads*, and two reads are not a snapshot. A write landing between them yields a pair that is individually correct and jointly false: two members holding roots taken over different trees, labelled with one version. That reads as equivocation and is not, and a **false accusation discredits the mechanism rather than the host** — the worst outcome available here. Taken from the committed set, the version cannot disagree with the root it labels, whatever else is happening to the room.\n\nThe corollary is worth stating: a host that **erases** a record — as distinct from retracting it, which leaves a tombstone in the tree — moves the root without necessarily moving this value, and two members straddling that erasure would see one version over two roots. Erasure is a retention act with its own answer, and a family that exposes one owes this definition another look.\n\n**This is what makes two roots comparable at all**, and without it the comparison this family is built on cannot be performed. A room moves: every put, curate and retraction assigns a new version, so two roots taken at two moments differ legitimately and a reader learns nothing from the difference. Shown two different roots, a host that equivocated and a host that was merely written to are indistinguishable — the first can always answer *the room moved between your reads*, and nothing contradicts it.\n\nA version is assigned by exactly the mutations that change the tree — every put, curate and retraction takes the next one — so the highest of them names the **state** the root describes. Two roots carrying the same `headVersion` and differing is a host caught: there is no write to attribute the difference to. Two roots carrying different ones are simply two moments, and a reader should draw nothing from them.\n\nA host can lie about this number too, and it is then lying about the counter it also uses for optimistic concurrency (`expectedVersion`) and for incremental sync (`sinceVersion`) — so a member holding a signed acknowledgement of a write at version `V` contradicts any head below `V` directly.\n\n**Not a timestamp.** A time is host-asserted, unverifiable and useless for this: two roots a second apart are not evidence of anything, while two roots at one version are."
430
+ },
431
+ "RecordCount": {
432
+ "title": "RecordCount",
433
+ "type": "integer",
434
+ "minimum": 0,
435
+ "description": "How many records the room held when `DataCommitment` was computed — the number of leaves in that tree, tombstones included, since a tombstone is a record.\n\n**Why a root needs this.** Certificate Transparency's signed tree head is a root *and a tree size*; this family shipped the root alone, and the half that was dropped is the half that makes a listing checkable. A reader holding a **complete, unfiltered** listing cannot recompute the root — a leaf commits to a whole record and a listing returns a projection without the body — but it can count. A host that omits a record from a listing while committing to a tree that holds it now contradicts itself in the same response, with no second party and no anchor involved.\n\nA host can of course understate both together. That is the point rather than a hole: the omission stops being silence and becomes a **specific claim about how many records the room holds**, which any other member's view, or any writer's signed put acknowledgement, contradicts. Making an omission attributable is the whole of what this machinery buys; it never claimed to make one impossible.\n\n**It counts the room, never the page.** The same rule `DataCommitment` states, and the same trap: a count scoped to what was returned is one a host satisfies by construction. So this is only comparable against a listing read to the end with **no** `prefix` and **no** `sinceVersion` — a filtered listing legitimately holds fewer, and a reader that compares one against this has found a discrepancy it created itself."
436
+ },
437
+ "DataCommitment": {
438
+ "title": "DataCommitment",
439
+ "$ref": "#/$defs/DigestMultibase",
440
+ "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.\n\n**A root on its own is not comparable, and an earlier revision of this description said it was.** It claimed a host showing two members two different roots had been caught, which is false while a room can move between two reads: the host answers *there was a write*, and nothing contradicts it. Comparison needs the state each root describes, which is what `HeadVersion` names — and `RecordCount` is the other half of the same omission, since Certificate Transparency's signed tree head is a root *and a size* and this family shipped only the root. A reader that receives a `dataCommitment` **without** them can use it against a witnessed anchor, where the epoch pins the state, and **MUST NOT** compare it against another root.\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 the record is a `CommittedRecord` — that definition fixes the members exactly, and this step used to name a *projection* instead, which two implementations could read two ways — JCS is its RFC 8785 canonicalization, 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.\n\nProving that a *particular* record sits under this root is a separate question, answered by `RecordTrace` on a single-record read. A commitment catches a host that equivocates; a trace binds one record to what the host committed to. Neither is the other, and a reader wanting completeness needs both plus a root it did not get from the host it is checking."
441
+ },
442
+ "DigestMultibase": {
443
+ "title": "DigestMultibase",
444
+ "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`.",
445
+ "type": "string",
446
+ "minLength": 16,
447
+ "pattern": "^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$",
448
+ "examples": [
449
+ "zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
450
+ ]
451
+ },
452
+ "RecordMetadata": {
453
+ "title": "RecordMetadata",
454
+ "type": "object",
455
+ "additionalProperties": false,
456
+ "required": [
457
+ "key",
458
+ "version",
459
+ "updatedAt"
460
+ ],
461
+ "description": "What `rooms/records/list` returns. Never the body: ranking happens on the client, and a host that returned every body would make the caller pay for the whole room on every listing.",
462
+ "properties": {
463
+ "key": {
464
+ "type": "string",
465
+ "description": "The record's key within the room. On `attributed` and `private` rooms this MUST be opaque — a random identifier, never a descriptive slug. A key reading `decision/acquire-northwind` defeats the encryption sitting beside it; structured naming belongs inside the sealed body."
466
+ },
467
+ "version": {
468
+ "type": "integer",
469
+ "minimum": 1,
470
+ "description": "Server-assigned, monotonic per room."
471
+ },
472
+ "epoch": {
473
+ "type": "integer",
474
+ "minimum": 1,
475
+ "description": "The epoch the record was sealed under. Absent on an `open` room."
476
+ },
477
+ "status": {
478
+ "type": "string",
479
+ "enum": [
480
+ "active",
481
+ "deprecated",
482
+ "retracted"
483
+ ],
484
+ "description": "Curation state. `retracted` is a tombstone: the body is gone, the key and version remain so incremental sync converges and the audit chain stays intact."
485
+ },
486
+ "updatedAt": {
487
+ "type": "string",
488
+ "format": "date-time"
489
+ },
490
+ "title": {
491
+ "type": "string",
492
+ "maxLength": 512,
493
+ "description": "Present only on an `open` room; sealed with the body otherwise."
494
+ },
495
+ "description": {
496
+ "type": "string",
497
+ "maxLength": 2048,
498
+ "description": "Present only on an `open` room; sealed with the body otherwise."
499
+ },
500
+ "author": {
501
+ "type": "string",
502
+ "description": "The member who wrote it. Present on `open` and `attributed`; on `private` the author is inside the sealed body, where only members can read it."
503
+ }
504
+ }
505
+ }
506
+ }
507
+ } as const;
508
+
509
+ /**
510
+ * SPEC.md §7.2 policy for the request variant, from this specification's
511
+ * front matter. Pass to `consumeInbound` — items 5b, 7 and 8 are
512
+ * per-specification and cannot be derived from the document alone, and
513
+ * item 2 needs the schema this carries.
514
+ */
515
+ export const SPEC = {
516
+ typeUri: TYPE_URI,
517
+ isBearer: false,
518
+ isProofRequired: true,
519
+ isRecipientRequired: true,
520
+ isIssuedAtRequired: true,
521
+ payloadSchema: PAYLOAD_SCHEMA,
522
+ } as const;
523
+
524
+ /**
525
+ * SPEC.md §7.2 policy for the success-response variant. `isRecipientRequired`
526
+ * tracks the *issuer* party's requirement because a response swaps the
527
+ * parties (§7.3 item 5).
528
+ */
529
+ export const RESPONSE_SPEC = {
530
+ typeUri: RESPONSE_TYPE_URI,
531
+ isBearer: false,
532
+ isProofRequired: true,
533
+ isRecipientRequired: true,
534
+ isIssuedAtRequired: true,
535
+ payloadSchema: RESPONSE_PAYLOAD_SCHEMA,
536
+ } as const;