@sanning/proof 0.19.1 → 0.20.0
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.
- package/README.md +30 -29
- package/dist/agent-record.d.ts +62 -0
- package/dist/agent-record.js +527 -0
- package/dist/agent-schemas.generated.d.ts +15 -0
- package/dist/agent-schemas.generated.js +5759 -0
- package/dist/case-file.d.ts +32 -89
- package/dist/case-file.js +41 -93
- package/dist/case.d.ts +339 -822
- package/dist/case.js +667 -1160
- package/dist/chain.d.ts +19 -47
- package/dist/chain.js +78 -205
- package/dist/claims.d.ts +211 -0
- package/dist/claims.js +1081 -0
- package/dist/cli.d.ts +45 -68
- package/dist/cli.js +618 -1022
- package/dist/compose.js +23 -65
- package/dist/crypto.d.ts +7 -8
- package/dist/crypto.js +60 -147
- package/dist/declaration.d.ts +68 -194
- package/dist/declaration.js +175 -518
- package/dist/environment.d.ts +9 -30
- package/dist/environment.js +17 -66
- package/dist/evidence.d.ts +119 -160
- package/dist/evidence.js +744 -1103
- package/dist/exit-codes.d.ts +11 -27
- package/dist/exit-codes.js +24 -59
- package/dist/file-match.d.ts +33 -79
- package/dist/file-match.js +33 -69
- package/dist/grading.d.ts +98 -135
- package/dist/grading.js +224 -380
- package/dist/handoff.d.ts +62 -160
- package/dist/handoff.js +153 -393
- package/dist/index.d.ts +14 -10
- package/dist/index.js +60 -167
- package/dist/json-schema.d.ts +29 -0
- package/dist/json-schema.js +298 -0
- package/dist/merkle.js +16 -28
- package/dist/message-parts.d.ts +56 -212
- package/dist/message-parts.js +142 -502
- package/dist/side-content.d.ts +11 -22
- package/dist/side-content.js +22 -65
- package/dist/types.d.ts +17 -0
- package/dist/types.js +19 -5
- package/dist/verifier.d.ts +40 -114
- package/dist/verifier.js +196 -535
- package/dist/witness-cache.d.ts +24 -45
- package/dist/witness-cache.js +38 -81
- package/dist/witness-lookup.d.ts +36 -57
- package/dist/witness-lookup.js +89 -139
- package/dist/zip-reader.d.ts +18 -33
- package/dist/zip-reader.js +19 -34
- package/package.json +11 -3
- package/src/agent-record.ts +608 -0
- package/src/agent-schemas.generated.ts +5764 -0
- package/src/case-file.ts +45 -109
- package/src/case.ts +814 -1328
- package/src/chain.ts +85 -224
- package/src/claims.ts +1253 -0
- package/src/cli.ts +714 -1176
- package/src/compose.ts +33 -79
- package/src/crypto.ts +60 -147
- package/src/declaration.ts +219 -656
- package/src/environment.ts +17 -65
- package/src/evidence.ts +958 -1381
- package/src/exit-codes.ts +24 -63
- package/src/file-match.ts +40 -89
- package/src/grading.ts +287 -421
- package/src/handoff.ts +175 -427
- package/src/index.ts +131 -177
- package/src/json-schema.ts +295 -0
- package/src/merkle.ts +16 -28
- package/src/message-parts.ts +155 -533
- package/src/side-content.ts +25 -68
- package/src/types.ts +55 -36
- package/src/verifier.ts +202 -526
- package/src/witness-cache.ts +45 -94
- package/src/witness-lookup.ts +107 -177
- package/src/zip-reader.ts +20 -38
- package/vectors/README.md +52 -34
- package/vectors/agent-record.json +2175 -0
- package/vectors/case-file-index.json +1 -1
- package/vectors/case-split.json +2 -2
- package/vectors/chain-reading.json +3 -3
- package/vectors/file-match.json +285 -148
- package/vectors/jcs-reader.json +6 -6
- package/vectors/pack-grading.json +102 -25
- package/vectors/sandbox-evidence.json +573 -581
- package/vectors/stamp-on-chain.json +18 -18
- package/vectors/witness-cache.json +49 -49
- package/dist/agent-proof.d.ts +0 -28
- package/dist/agent-proof.js +0 -226
- package/dist/run-adapters.d.ts +0 -10
- package/dist/run-adapters.js +0 -35
- package/dist/run.d.ts +0 -209
- package/dist/run.js +0 -1405
- package/src/agent-proof.ts +0 -264
- package/src/run-adapters.ts +0 -44
- package/src/run.ts +0 -1598
package/dist/declaration.js
CHANGED
|
@@ -1,152 +1,47 @@
|
|
|
1
|
-
// The declaration: what a pack
|
|
1
|
+
// The declaration: what a pack says it holds, checked against what it does
|
|
2
|
+
// (evidence-bundle.md §5.4).
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// still verifies perfectly, so an auditor holding a pack cannot tell a full
|
|
8
|
-
// hand-over from a trimmed one. `completenessNotes` in `./grading.ts` closes
|
|
9
|
-
// one narrow case of that, where a checkpoint's own signed record commits to a
|
|
10
|
-
// leaf count. Nothing closes it for a hand-over pack built out of a stamp, and
|
|
11
|
-
// nothing at all says which records were withheld ON PURPOSE.
|
|
4
|
+
// Every per-record check verifies a record that is present, so a trimmed pack
|
|
5
|
+
// verifies as well as a full one. The declaration is the pack's own signed
|
|
6
|
+
// statement of what it was meant to contain. It rides inside `body`, so the
|
|
7
|
+
// bundle signature and `body_hash` cover it.
|
|
12
8
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
9
|
+
// verified the pack holds exactly what the declaration says. A record
|
|
10
|
+
// declared `content_withheld` is checked for its signature and
|
|
11
|
+
// commitment only.
|
|
12
|
+
// failed any deviation, and every deviation is named. A deviation never
|
|
13
|
+
// surfaces as an absence a reader is left to interpret.
|
|
14
|
+
// incomplete no declaration, or one this reader cannot read. Nothing to
|
|
15
|
+
// check against, and nothing proven wrong.
|
|
19
16
|
//
|
|
20
|
-
//
|
|
17
|
+
// When no record declared `disclosed` supplied bytes, nothing was selected:
|
|
18
|
+
// the bytes travelled separately. That is a note, not a deviation, and the
|
|
19
|
+
// missing bytes still carry the pack to `incomplete`. Partial disclosure is a
|
|
20
|
+
// selection and fails.
|
|
21
21
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// failed ANY deviation, and EVERY deviation is named. Declared and
|
|
27
|
-
// absent, present and undeclared, the pack carrying one
|
|
28
|
-
// `event_id` twice, withheld without being declared, disclosed
|
|
29
|
-
// after being declared withheld, or a claimed hand-off naming a
|
|
30
|
-
// record the declaration never declared.
|
|
31
|
-
// partial no declaration, or one the reader cannot interpret as this
|
|
32
|
-
// version. The pack makes no claim this reader can check, so
|
|
33
|
-
// what it was meant to contain is UNKNOWN. Never `verified`,
|
|
34
|
-
// because there is nothing to check against, and never `failed`,
|
|
35
|
-
// because nothing is wrong: absence is not failure.
|
|
22
|
+
// Pure: data in, data out. "The holder supplied bytes" is defined once, in
|
|
23
|
+
// `./side-content.ts`. This check does not verify that a claimed hand-off pair
|
|
24
|
+
// is a real pair (the case verdict does), but it does require each claimed side
|
|
25
|
+
// to be a conformant hand-off record (`./handoff.ts`).
|
|
36
26
|
//
|
|
37
|
-
// A
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// turns a deviation into two records that did not pair for no stated reason,
|
|
41
|
-
// and a reader reads a missing thing as an innocent one.
|
|
42
|
-
//
|
|
43
|
-
// ── the bytes that did not travel ────────────────────────────────────
|
|
44
|
-
//
|
|
45
|
-
// One case inside rule 3 is not a selection and is not graded as one. When NO
|
|
46
|
-
// record declared `disclosed` supplied bytes, nothing was selected: either the
|
|
47
|
-
// bytes travelled separately or the reader was handed the JSON alone. A
|
|
48
|
-
// hostile holder gains nothing from that shape, because declaring those
|
|
49
|
-
// records `content_withheld` grades `verified` today and is cheaper, so the
|
|
50
|
-
// only party who reaches it is the honest one who forgot `--logs`. This check
|
|
51
|
-
// emits a NAMED note instead of a deviation, and the pack's missing bytes
|
|
52
|
-
// carry it to `incomplete` through the disclosure account that already counts
|
|
53
|
-
// them.
|
|
54
|
-
//
|
|
55
|
-
// PARTIAL disclosure is a selection and stays a deviation: nineteen of twenty
|
|
56
|
-
// declared-disclosed records supplying bytes and one not is a choice about
|
|
57
|
-
// which record, and it fails.
|
|
58
|
-
//
|
|
59
|
-
// ── the seam ────────────────────────────────────────────────────────────────
|
|
60
|
-
//
|
|
61
|
-
// PURE, like `./grading.ts`: data in, data out, no I/O and no key lifecycle.
|
|
62
|
-
// It reads the bundle it is handed and, optionally, the same side-content map
|
|
63
|
-
// `verifyEvidenceBundle` takes, because "the holder supplied bytes for this
|
|
64
|
-
// record" has exactly one definition in this kernel (`./side-content.ts`) and
|
|
65
|
-
// this module calls it rather than restating it.
|
|
66
|
-
//
|
|
67
|
-
// ── what this check does NOT claim ──────────────────────────────────────────
|
|
68
|
-
//
|
|
69
|
-
// It compares the declaration to the pack. It does not read a record's
|
|
70
|
-
// internals and cross-check `agents`, `period` or `case_id` against them, and
|
|
71
|
-
// it does not verify that a claimed hand-off PAIR is a real pair: the hashes
|
|
72
|
-
// match, the sequence holds, the two sides name each other's keys. Those are
|
|
73
|
-
// the CASE verdict, across packs and across organizations, and they are
|
|
74
|
-
// anchor#198. Doing half of that here would put two implementations of one
|
|
75
|
-
// rule in the stack, which is the defect `./grading.ts` was brought into the
|
|
76
|
-
// kernel to end.
|
|
77
|
-
//
|
|
78
|
-
// It does now check that each SIDE of a claimed hand-off is a conformant
|
|
79
|
-
// hand-off record (proof#136, ratified by Phil 2026-09-22), and it asks
|
|
80
|
-
// `./handoff.ts` rather than restating that rule. `handoffs[]` carried the
|
|
81
|
-
// claim and checked nothing about it, and that is what made an unreadable
|
|
82
|
-
// record useful: a receiving side respelled one code point of `event_type`
|
|
83
|
-
// and one of `event.counterparty_public_key`, which put the record outside
|
|
84
|
-
// envelope-spec.md 4.1 by both of its scope clauses, then declared it and
|
|
85
|
-
// claimed it as the received half. The record layer said nothing and the pack
|
|
86
|
-
// layer said `verified`. A record cannot be unreadable now and usable later.
|
|
87
|
-
//
|
|
88
|
-
// What that does NOT close (proof#137, ratified by Phil 2026-09-22). The
|
|
89
|
-
// sentence deleted from the spec beside this one said the other door was
|
|
90
|
-
// closed too: omit the record from `handoffs[]` and it is a
|
|
91
|
-
// present-and-undeclared record. It is not. `handoffs[]` MAY be empty, so a
|
|
92
|
-
// holder declares the record in `records[]` and claims it nowhere, and then
|
|
93
|
-
// rule 2 cannot reach it because it is declared and rule 7 cannot reach it
|
|
94
|
-
// because it is unclaimed: this check reads `verified`, exactly as it does
|
|
95
|
-
// over the honest pack. The corpus pins that pack as EXPECTED
|
|
96
|
-
// (`declaration/declaration-unclaimed-record-01`). A pack a party assembles
|
|
97
|
-
// about itself is not self-proving about what that party LEFT OUT, because
|
|
98
|
-
// the omission happens before any reader runs; the closure is the case
|
|
99
|
-
// verdict across BOTH parties' packs, anchor#198.
|
|
100
|
-
//
|
|
101
|
-
// ── the three defects the sixth hunt landed on this check (proof#137) ───────
|
|
102
|
-
//
|
|
103
|
-
// Three. The fourth door the hunt found, whether a pack a party assembles
|
|
104
|
-
// about itself can prove what it LEFT OUT, is a founder call and is not
|
|
105
|
-
// answered here.
|
|
106
|
-
//
|
|
107
|
-
// ONE PARSER, NOT TWO BUILT-INS. The two kernels read one `event_bytes`
|
|
108
|
-
// differently, three of the four ways verdict-level on identical bytes:
|
|
109
|
-
// Python's `json.loads` accepts `NaN`, `Infinity` and `-Infinity` and this
|
|
110
|
-
// kernel's `JSON.parse` rejects them, and Python's `.decode("utf-8")` keeps
|
|
111
|
-
// a leading byte-order mark where `TextDecoder` eats it. One pack read
|
|
112
|
-
// `verified` here and `failed[pair-record-not-a-handoff]` there. The rule is
|
|
113
|
-
// now stated rather than delegated: bytes that are not RFC 8785 JSON are not
|
|
114
|
-
// a conformant record, so a leading BOM and each of the three non-finite
|
|
115
|
-
// literals are rejected in BOTH kernels, spelled out in each, exactly as the
|
|
116
|
-
// hex charset already was.
|
|
117
|
-
//
|
|
118
|
-
// ONE `event_id`, TWO RECORDS. Both kernels built a map by id, which is
|
|
119
|
-
// last-write-wins in both languages, so a second record under a declared id
|
|
120
|
-
// was graded by neither rule 2 nor rule 7 and was never reported as surplus.
|
|
121
|
-
// FIRST in bundle order is now the record every other rule grades, and every
|
|
122
|
-
// record after it under the same id is a `duplicate_event_id` deviation.
|
|
123
|
-
//
|
|
124
|
-
// MINIMUM DISCLOSURE IS NOT A DEVIATION. A claimed side whose `event_bytes`
|
|
125
|
-
// the pack withholds was graded "not a conformant hand-off record", so an
|
|
126
|
-
// honest holder exercising the disclosure posture this product sells proved
|
|
127
|
-
// a deviation, and the cheapest escape from that was to stop claiming pairs
|
|
128
|
-
// at all. A withheld side now gets its OWN reading, distinct from both
|
|
129
|
-
// conformant and non-conformant, and is a NAMED NOTE.
|
|
27
|
+
// A pack cannot prove what its holder left out: a record declared in
|
|
28
|
+
// `records[]` and claimed in no `handoffs[]` entry reads `verified` here. The
|
|
29
|
+
// case verdict across both parties' packs closes that.
|
|
130
30
|
import { hexToBytes, utf8 } from "./crypto.js";
|
|
131
31
|
import { jcs, jcsParse } from "./verifier.js";
|
|
132
32
|
import { isConformantHandoffRecord, isHandoffNamespace } from "./handoff.js";
|
|
133
33
|
import { hasSideContent, inBodyContentOffered, sideContentEntry, } from "./side-content.js";
|
|
134
|
-
/** The first declaration schema
|
|
34
|
+
/** The first declaration schema. */
|
|
135
35
|
export const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
136
|
-
/**
|
|
137
|
-
* v1 plus a signed `records[].role` on every record (evidence-bundle.md
|
|
138
|
-
* 5.4.1 v1.20, A21). A reader that knows only v1 reads a v2 declaration as one
|
|
139
|
-
* it cannot read, which is the safe direction.
|
|
140
|
-
*/
|
|
36
|
+
/** v1 plus a signed `records[].role` on every record (§5.4.1). */
|
|
141
37
|
export const DECLARATION_VERSION_V2 = "sanning.declaration/v2";
|
|
142
38
|
/**
|
|
143
39
|
* v2 plus a signed `chains[]`: the `chain_key`s the holder claims are complete
|
|
144
|
-
* and in order (
|
|
145
|
-
* v1 and v2 reads a v3 declaration as one it cannot read, which is the safe
|
|
40
|
+
* and in order (§5.4.1). An older reader cannot read it, which is the safe
|
|
146
41
|
* direction.
|
|
147
42
|
*/
|
|
148
43
|
export const DECLARATION_VERSION_V3 = "sanning.declaration/v3";
|
|
149
|
-
/** Every declaration schema this reader knows,
|
|
44
|
+
/** Every declaration schema this reader knows, oldest first. */
|
|
150
45
|
export const DECLARATION_VERSIONS = [
|
|
151
46
|
DECLARATION_VERSION,
|
|
152
47
|
DECLARATION_VERSION_V2,
|
|
@@ -155,13 +50,11 @@ export const DECLARATION_VERSIONS = [
|
|
|
155
50
|
/** The versions whose every `records[]` entry carries a signed `role`. */
|
|
156
51
|
const ROLE_VERSIONS = [DECLARATION_VERSION_V2, DECLARATION_VERSION_V3];
|
|
157
52
|
/**
|
|
158
|
-
*
|
|
159
|
-
* record 4.1 governs that the pack claims
|
|
160
|
-
* `step`, any other record.
|
|
53
|
+
* The roles a v2 declaration can sign, compared exactly: `handoff` for a
|
|
54
|
+
* record §4.1 governs that the pack claims in `handoffs[]`, `step` otherwise.
|
|
161
55
|
*/
|
|
162
56
|
export const DECLARATION_ROLES = ["handoff", "step"];
|
|
163
|
-
|
|
164
|
-
export const DECLARATION_VERDICTS = ["verified", "failed", "partial"];
|
|
57
|
+
export const DECLARATION_VERDICTS = ["verified", "failed", "incomplete"];
|
|
165
58
|
export const DECLARATION_DEVIATION_KINDS = [
|
|
166
59
|
/** The declaration is written to this version and is not readable as one. */
|
|
167
60
|
"declaration_malformed",
|
|
@@ -169,160 +62,86 @@ export const DECLARATION_DEVIATION_KINDS = [
|
|
|
169
62
|
"declared_absent",
|
|
170
63
|
/** A record in the pack the declaration never expected. */
|
|
171
64
|
"undeclared_present",
|
|
172
|
-
/**
|
|
173
|
-
* A record declared `content_withheld` whose bytes the pack supplies INSIDE
|
|
174
|
-
* its own signed body, in `events[].content`.
|
|
175
|
-
*/
|
|
65
|
+
/** A record declared `content_withheld` whose bytes the pack's own body supplies. */
|
|
176
66
|
"withheld_but_disclosed",
|
|
177
67
|
/** The pack carries one `event_id` on two or more records. */
|
|
178
68
|
"duplicate_event_id",
|
|
179
69
|
/** A claimed hand-off names a record the declaration does not declare. */
|
|
180
70
|
"pair_record_undeclared",
|
|
181
|
-
/**
|
|
182
|
-
* A claimed hand-off names a record that is not a conformant hand-off
|
|
183
|
-
* record of the side it is claimed on (envelope-spec.md 4.1).
|
|
184
|
-
*/
|
|
71
|
+
/** A claimed hand-off side that is not a conformant hand-off record of that side (§4.1). */
|
|
185
72
|
"pair_record_not_a_handoff",
|
|
186
|
-
/**
|
|
187
|
-
* Rule 9 (v1.20, A21), `sanning.declaration/v2` only. A record declared
|
|
188
|
-
* `handoff` that no `handoffs[]` entry names as a side.
|
|
189
|
-
*/
|
|
73
|
+
/** v2 only: a record declared `handoff` that no `handoffs[]` entry claims. */
|
|
190
74
|
"role_handoff_unclaimed",
|
|
191
75
|
/** A record a `handoffs[]` entry names as a side, declared `step`. */
|
|
192
76
|
"role_step_claimed",
|
|
193
|
-
/**
|
|
194
|
-
* A record declared `step` that 4.1's scope test puts in scope: its `event`
|
|
195
|
-
* carries `counterparty_public_key`, or its folded `event_type` has a
|
|
196
|
-
* `handoff` segment. The holder signed that a hand-off record is not one.
|
|
197
|
-
*/
|
|
77
|
+
/** A record declared `step` that §4.1's scope test puts in scope. */
|
|
198
78
|
"role_step_governed",
|
|
199
|
-
/**
|
|
200
|
-
* A record declared `step` whose supplied `event_bytes` this reader cannot
|
|
201
|
-
* read under rule 7's parser rule.
|
|
202
|
-
*/
|
|
79
|
+
/** A record declared `step` whose supplied `event_bytes` cannot be read. */
|
|
203
80
|
"role_step_unreadable",
|
|
204
|
-
/**
|
|
205
|
-
* Rule 11 (v1.36, A45), `sanning.declaration/v3` only. The holder signed
|
|
206
|
-
* that a chain is complete and in order, and a second record under that key,
|
|
207
|
-
* under any producing key, begins it.
|
|
208
|
-
*/
|
|
81
|
+
/** v3 only: a second record beginning a claimed chain, under any producing key. */
|
|
209
82
|
"chain_second_root",
|
|
210
83
|
/** A record of a claimed chain is the second successor of one predecessor. */
|
|
211
84
|
"chain_fork",
|
|
212
|
-
/** A
|
|
85
|
+
/** A claimed chain's record points at a predecessor the pack does not present. */
|
|
213
86
|
"chain_link_dangling",
|
|
214
87
|
/** A claimed chain of which the pack presents no record at all. */
|
|
215
88
|
"chain_claimed_absent",
|
|
216
89
|
];
|
|
217
90
|
/**
|
|
218
|
-
* Every way a pack and the
|
|
219
|
-
* up
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* WHY THEY ARE NOT DEVIATIONS. 5.4 grades a pack's declaration partly against
|
|
223
|
-
* content the reader was HANDED, and that content is not signed by anybody.
|
|
224
|
-
* `events[].content` rides inside `body`, so the wrapper signature and
|
|
225
|
-
* `body_hash` cover it and what it says about the holder is the holder's own
|
|
226
|
-
* word. The side-content map is the other channel, and whoever assembles a
|
|
227
|
-
* case fills it in: an accuser could put an intact pack into
|
|
228
|
-
* `pack_declaration_deviates`, which contributed `failed`, WITH NO KEY AT
|
|
229
|
-
* ALL, by dropping the bytes of a record the pack declared `disclosed` or by
|
|
230
|
-
* adding bytes for one it declared `content_withheld`. The pack was intact,
|
|
231
|
-
* its wrapper verified, and its holder had done nothing.
|
|
232
|
-
*
|
|
233
|
-
* This is the mirror of proof#143. There a counterparty makes itself look
|
|
234
|
-
* better by choosing what it hands over; here an accuser makes a counterparty
|
|
235
|
-
* look worse the same way. Both now read as what the READER established, so
|
|
236
|
-
* `failed` stays for what a signature covers.
|
|
237
|
-
*
|
|
238
|
-
* WHAT A HOSTILE HOLDER GAINS FROM THE WEAKER WORD: nothing, because a
|
|
239
|
-
* cheaper move already dominates it. Declaring a record `content_withheld` is
|
|
240
|
-
* legal and grades `verified`, which is strictly better than the `incomplete`
|
|
241
|
-
* these carry.
|
|
91
|
+
* Every way a pack and the unsigned content handed in beside it fail to line
|
|
92
|
+
* up. Not deviations, and never `failed`: whoever assembles a case fills the
|
|
93
|
+
* side-content map, so an accuser could otherwise fail an intact pack with no
|
|
94
|
+
* key at all. `failed` stays for what a signature covers.
|
|
242
95
|
*/
|
|
243
96
|
export const DECLARATION_CONTENT_KINDS = [
|
|
244
97
|
/**
|
|
245
|
-
* A record declared `disclosed` whose bytes
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* fact about what the READER was handed and never a fact about what the
|
|
249
|
-
* holder supplied. No signature covers the absence of a file nobody signed.
|
|
98
|
+
* A record declared `disclosed` whose bytes arrived from neither channel.
|
|
99
|
+
* Always content-dependent: the bytes may travel beside the pack, so this is
|
|
100
|
+
* a fact about what the reader was handed.
|
|
250
101
|
*/
|
|
251
102
|
"content_dependent_withholding",
|
|
252
103
|
/**
|
|
253
|
-
* A record declared `content_withheld` whose bytes
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* rule is the `withheld_but_disclosed` DEVIATION and still fails.
|
|
104
|
+
* A record declared `content_withheld` whose bytes arrived only through the
|
|
105
|
+
* side-content map. The in-body case is the `withheld_but_disclosed`
|
|
106
|
+
* deviation.
|
|
257
107
|
*/
|
|
258
108
|
"content_dependent_disclosure",
|
|
259
109
|
];
|
|
260
|
-
/** Everything this check reports that is
|
|
110
|
+
/** Everything this check reports that is not a deviation. */
|
|
261
111
|
export const DECLARATION_NOTE_KINDS = [
|
|
262
|
-
/**
|
|
263
|
-
* Every record declared `disclosed` supplied no bytes, so nothing was
|
|
264
|
-
* selected: the bytes did not travel with the pack.
|
|
265
|
-
*/
|
|
112
|
+
/** No record declared `disclosed` supplied bytes: they did not travel with the pack. */
|
|
266
113
|
"disclosure_did_not_travel",
|
|
267
114
|
/**
|
|
268
|
-
* A claimed hand-off side whose `event_bytes` the pack withholds.
|
|
269
|
-
*
|
|
270
|
-
* hand-off record nor a non-conformant one.
|
|
115
|
+
* A claimed hand-off side whose `event_bytes` the pack withholds. Withholding
|
|
116
|
+
* is minimum disclosure, so it is neither conformant nor non-conformant.
|
|
271
117
|
*/
|
|
272
118
|
"pair_record_withheld",
|
|
273
119
|
/**
|
|
274
|
-
* A record a v2 declaration signs `step` whose `event_bytes`
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* because its payload binding is undetermined.
|
|
120
|
+
* A record a v2 declaration signs `step` whose `event_bytes` are withheld, so
|
|
121
|
+
* its role cannot be checked. The undetermined binding keeps the pack at
|
|
122
|
+
* `incomplete`.
|
|
278
123
|
*/
|
|
279
124
|
"role_step_withheld",
|
|
280
125
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* in, never the record: whether those bytes bind to the record's
|
|
286
|
-
* `content_hash` is the bundle verifier's check, and an accuser can hand in
|
|
287
|
-
* declaration-shaped bytes for an honest record. What the pack holds is what
|
|
288
|
-
* the declaration in its body says, so this moves no verdict.
|
|
126
|
+
* A record signed as `event_type` `declaration` whose handed-in content is
|
|
127
|
+
* `JCS` of a known declaration other than this pack's own. Names the content,
|
|
128
|
+
* not the record: an accuser can hand in such bytes for an honest record.
|
|
129
|
+
* Moves no verdict.
|
|
289
130
|
*/
|
|
290
131
|
"other_declaration_held",
|
|
291
|
-
/**
|
|
292
|
-
* Rule 10. A record whose signed `event_type` is `declaration` and whose
|
|
293
|
-
* content did not reach this reader from either channel, so whether it is a
|
|
294
|
-
* declaration other than this pack's own is UNKNOWN.
|
|
295
|
-
*/
|
|
132
|
+
/** A record signed `declaration` whose content did not arrive, so what it declares is unknown. */
|
|
296
133
|
"other_declaration_withheld",
|
|
297
134
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* record takes no part in a chain, so whether the missing link is it is
|
|
301
|
-
* UNKNOWN. Moves nothing: the undetermined binding holds the pack at
|
|
302
|
-
* `incomplete`.
|
|
135
|
+
* A claimed chain has a dangling link, and a same-key record with an
|
|
136
|
+
* undetermined payload binding could be the missing link. Moves nothing.
|
|
303
137
|
*/
|
|
304
138
|
"chain_claim_withheld",
|
|
305
139
|
];
|
|
306
140
|
/**
|
|
307
|
-
* The record committed inside one `events[]` entry, or `null
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* and this reader cannot read is a deviation, because the claim is that it IS
|
|
312
|
-
* a hand-off record and nothing here supports that. It is not the same as "the
|
|
313
|
-
* pack supplied no bytes" either: see `bytesSupplied`.
|
|
314
|
-
*
|
|
315
|
-
* ONE RULE, SPELLED OUT, BECAUSE TWO BUILT-INS ARE NOT ONE RULE. The bytes are
|
|
316
|
-
* `JCS(event record)` (RFC 8785) or they are not a conformant record. Three
|
|
317
|
-
* checks carry that: lowercase-or-uppercase hex in pairs, no leading byte-order
|
|
318
|
-
* mark, and no non-finite literal. The hex is `hexToBytes`, because it and
|
|
319
|
-
* `bytes.fromhex` disagree about embedded whitespace (proof#135); the rest is
|
|
320
|
-
* `jcsParse`, which every site in either kernel that reads signed bytes now
|
|
321
|
-
* shares (proof#140 gate 5). Both halves were a hand-rolled copy in this module
|
|
322
|
-
* until proof#146, and both are the shared reader now: the answer on every
|
|
323
|
-
* input is the same, and there is one fewer place for the rule to drift. The
|
|
324
|
-
* cross-kernel declaration leg and `declaration.test.ts` hold both kernels to
|
|
325
|
-
* the same answer on the same bytes rather than to either built-in.
|
|
141
|
+
* The record committed inside one `events[]` entry, or `null` where this
|
|
142
|
+
* reader cannot read it. Bytes that are not `JCS(event record)` are not a
|
|
143
|
+
* conformant record; parsing goes through the shared `hexToBytes` and
|
|
144
|
+
* `jcsParse` so both kernels agree on every input.
|
|
326
145
|
*/
|
|
327
146
|
function committedRecord(raw) {
|
|
328
147
|
let parsed;
|
|
@@ -334,20 +153,11 @@ function committedRecord(raw) {
|
|
|
334
153
|
}
|
|
335
154
|
return isObject(parsed) ? parsed : null;
|
|
336
155
|
}
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
// layer grades the two types differently.
|
|
340
|
-
const SIDE_EVENT_TYPES = { sent: "handoff.sent", received: "handoff.received" };
|
|
156
|
+
// A pack that names its receipt as the sent side has mislabelled a record.
|
|
157
|
+
const SIDE_EVENT_TYPES = { sent: "agent.handoff.sent", received: "agent.handoff.received" };
|
|
341
158
|
/**
|
|
342
|
-
* How a claimed side reads
|
|
343
|
-
*
|
|
344
|
-
* The definition of conformant is 4.1's own (`isConformantHandoffRecord`),
|
|
345
|
-
* read once and never restated here, plus the record type the side names.
|
|
346
|
-
*
|
|
347
|
-
* WITHHELD IS ITS OWN READING. The pack supplied no `event_bytes`, so this
|
|
348
|
-
* reader cannot see what the record is in either direction. Saying it is not a
|
|
349
|
-
* hand-off record is a claim the reader cannot support, and it punishes the
|
|
350
|
-
* disclosure posture the product sells.
|
|
159
|
+
* How a claimed side reads. Conformant is §4.1's own rule
|
|
160
|
+
* (`isConformantHandoffRecord`) plus the record type the side names.
|
|
351
161
|
*/
|
|
352
162
|
function sideReading(p, side) {
|
|
353
163
|
if (!p.bytesSupplied)
|
|
@@ -361,11 +171,8 @@ function sideReading(p, side) {
|
|
|
361
171
|
}
|
|
362
172
|
const bodyOf = (bundle) => bundle?.body;
|
|
363
173
|
/**
|
|
364
|
-
* The pack's own records, in bundle order. Exported so
|
|
365
|
-
*
|
|
366
|
-
* it here, rather than through a second one beside it. A validation copied
|
|
367
|
-
* next to the one that enforces it drifts, and a copy that is looser blesses
|
|
368
|
-
* a value the real one then rejects.
|
|
174
|
+
* The pack's own records, in bundle order. Exported so `./case.ts` reads a
|
|
175
|
+
* pack through the same parser that grades it.
|
|
369
176
|
*/
|
|
370
177
|
export function packRecords(bundle, content) {
|
|
371
178
|
const events = bodyOf(bundle)?.events;
|
|
@@ -375,19 +182,13 @@ export function packRecords(bundle, content) {
|
|
|
375
182
|
const raw = ev?.envelope?.event_id;
|
|
376
183
|
const nameable = typeof raw === "string" && raw.length > 0;
|
|
377
184
|
const eventId = nameable ? raw : `#${i + 1}`;
|
|
378
|
-
// One definition of "the holder supplied bytes", in both of the two
|
|
379
|
-
// shapes a pack can supply them: in the body, or beside it. Both are
|
|
380
|
-
// `./side-content.ts`, never restated here.
|
|
381
185
|
const disclosedInBody = inBodyContentOffered(ev?.content);
|
|
382
186
|
const disclosed = disclosedInBody || (nameable && hasSideContent(content, eventId));
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
// `malformed` there, so a claimed side carrying one is a deviation here.
|
|
187
|
+
// From the envelope, never the record: an unparseable key makes the record
|
|
188
|
+
// `malformed` under §4.1.
|
|
386
189
|
const key = ev?.envelope?.public_key;
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
// the field not travelling, which is minimum disclosure and not a record
|
|
390
|
-
// this reader was handed and could not read.
|
|
190
|
+
// Only a non-empty string counts as supplied; anything else is the field
|
|
191
|
+
// not travelling.
|
|
391
192
|
const rawBytes = ev?.event_bytes;
|
|
392
193
|
return {
|
|
393
194
|
eventId,
|
|
@@ -402,28 +203,18 @@ export function packRecords(bundle, content) {
|
|
|
402
203
|
};
|
|
403
204
|
});
|
|
404
205
|
}
|
|
405
|
-
// ---- Reading the declaration -------------------------------------------------
|
|
406
206
|
const DISCLOSURE_STATES = new Set(["disclosed", "content_withheld"]);
|
|
407
|
-
// RFC 3339 `date-time` with an uppercase `Z
|
|
408
|
-
// instant. Format only: this is not an ordering check and not a calendar
|
|
409
|
-
// check, and it deliberately does not restate the `signed_at` rule of
|
|
410
|
-
// envelope-spec §2, which grades a different field on a different layer.
|
|
207
|
+
// RFC 3339 `date-time` with an uppercase `Z`. Format only, not ordering.
|
|
411
208
|
const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
412
209
|
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
413
210
|
const isId = (v) => typeof v === "string" && v.length > 0;
|
|
414
211
|
/**
|
|
415
|
-
* Read a declaration
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
* Every malformation is collected, not the first: a declaration is a claim,
|
|
419
|
-
* and a reader that names one defect in a claim with four sends its holder
|
|
420
|
-
* round the loop four times.
|
|
212
|
+
* Read a declaration of a known version, or collect every malformation in it
|
|
213
|
+
* (all of them, so the holder fixes them in one pass).
|
|
421
214
|
*/
|
|
422
215
|
function readDeclaration(declaration, version = DECLARATION_VERSION) {
|
|
423
216
|
const reasons = [];
|
|
424
|
-
//
|
|
425
|
-
// or `step` (5.4.1 v1.20, A21); under v1 the member is not read at all, so a
|
|
426
|
-
// v1 declaration carrying `role` reads exactly as the same object without it.
|
|
217
|
+
// v2 and v3 require `role` on every entry; v1 does not read the member.
|
|
427
218
|
const roles = [];
|
|
428
219
|
const v2 = ROLE_VERSIONS.includes(version);
|
|
429
220
|
const note = (r) => reasons.push(r);
|
|
@@ -463,8 +254,7 @@ function readDeclaration(declaration, version = DECLARATION_VERSION) {
|
|
|
463
254
|
note(`records[${i}] (${eventId}) has no disclosure of "disclosed" or "content_withheld"`);
|
|
464
255
|
continue;
|
|
465
256
|
}
|
|
466
|
-
//
|
|
467
|
-
// and a non-string never equals one.
|
|
257
|
+
// Exact: `Handoff`, `hand-off` and `step ` are other values.
|
|
468
258
|
const role = r["role"];
|
|
469
259
|
if (v2 && !(role === "handoff" || role === "step")) {
|
|
470
260
|
note(`records[${i}] (${eventId}) has no role of "handoff" or "step"`);
|
|
@@ -504,8 +294,7 @@ function readDeclaration(declaration, version = DECLARATION_VERSION) {
|
|
|
504
294
|
pairs.push({ sent: sent, received: received });
|
|
505
295
|
}
|
|
506
296
|
}
|
|
507
|
-
// v3
|
|
508
|
-
// read the member.
|
|
297
|
+
// v3 requires `chains[]`; v1 and v2 do not read the member.
|
|
509
298
|
const chains = [];
|
|
510
299
|
if (version === DECLARATION_VERSION_V3) {
|
|
511
300
|
const rawChains = declaration["chains"];
|
|
@@ -530,9 +319,8 @@ function readDeclaration(declaration, version = DECLARATION_VERSION) {
|
|
|
530
319
|
? { ok: false, reasons }
|
|
531
320
|
: { ok: true, read: { records, pairs, roles, chains } };
|
|
532
321
|
}
|
|
533
|
-
// ---- The check ---------------------------------------------------------------
|
|
534
322
|
const noDeclaration = () => ({
|
|
535
|
-
verdict: "
|
|
323
|
+
verdict: "incomplete",
|
|
536
324
|
deviations: [],
|
|
537
325
|
contentFindings: [],
|
|
538
326
|
notes: [],
|
|
@@ -544,9 +332,7 @@ const noDeclaration = () => ({
|
|
|
544
332
|
chains: [],
|
|
545
333
|
});
|
|
546
334
|
/**
|
|
547
|
-
* The `declaration_version` of
|
|
548
|
-
* declaration that is not an object, an absent or `null` version and a version
|
|
549
|
-
* outside `DECLARATION_VERSIONS` are each a claim this reader cannot read.
|
|
335
|
+
* The `declaration_version` of a declaration this reader knows, or `null`.
|
|
550
336
|
*/
|
|
551
337
|
function knownVersion(declaration) {
|
|
552
338
|
if (!isObject(declaration))
|
|
@@ -557,10 +343,8 @@ function knownVersion(declaration) {
|
|
|
557
343
|
: null;
|
|
558
344
|
}
|
|
559
345
|
/**
|
|
560
|
-
* Rule 9
|
|
561
|
-
* `null` where the signed role agrees
|
|
562
|
-
* of the four deviations that applies, in the order 5.4.2 lists them. Whether
|
|
563
|
-
* 4.1 governs the record is `isHandoffNamespace`, CALLED, never restated.
|
|
346
|
+
* Rule 9 for one held record: the first deviation that applies in §5.4.2
|
|
347
|
+
* order, a note, or `null` where the signed role agrees.
|
|
564
348
|
*/
|
|
565
349
|
function roleReading(p, role, claimed) {
|
|
566
350
|
if (role === "handoff")
|
|
@@ -576,16 +360,9 @@ function roleReading(p, role, claimed) {
|
|
|
576
360
|
}
|
|
577
361
|
return null;
|
|
578
362
|
}
|
|
579
|
-
/**
|
|
580
|
-
* The `event_type` a declaration record is signed under: a reserved
|
|
581
|
-
* single-segment name, and the one `@sanning/anchor`'s `bundle` anchors a
|
|
582
|
-
* declaration as.
|
|
583
|
-
*/
|
|
363
|
+
/** The reserved `event_type` a declaration record is signed under. */
|
|
584
364
|
const DECLARATION_EVENT_TYPE = "declaration";
|
|
585
|
-
/**
|
|
586
|
-
* The keys a declaration object carries, under every version this reader
|
|
587
|
-
* knows: exactly these, no more and no fewer (5.4.1).
|
|
588
|
-
*/
|
|
365
|
+
/** The exact keys of a declaration object, under every known version (§5.4.1). */
|
|
589
366
|
const DECLARATION_KEYS = ["declaration_version", "case_id", "agents", "period", "records", "handoffs"];
|
|
590
367
|
function hasExactKeys(value, keys) {
|
|
591
368
|
if (!isObject(value))
|
|
@@ -594,14 +371,9 @@ function hasExactKeys(value, keys) {
|
|
|
594
371
|
return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(value, k));
|
|
595
372
|
}
|
|
596
373
|
/**
|
|
597
|
-
* Whether `obj`
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* It is the shape test `@sanning/anchor`'s `isDeclarationShape` makes, and it
|
|
602
|
-
* checks shape only, not the 5.4.1 rules `readDeclaration` grades: a stale
|
|
603
|
-
* declaration is somebody else's claim, and this reader does not grade it, it
|
|
604
|
-
* names it.
|
|
374
|
+
* Whether `obj` has the shape of a known declaration: a known version, exactly
|
|
375
|
+
* its keys, and the value types read here. Shape only: another holder's stale
|
|
376
|
+
* declaration is named, not graded.
|
|
605
377
|
*/
|
|
606
378
|
function isDeclarationShape(obj) {
|
|
607
379
|
if (!isObject(obj))
|
|
@@ -644,9 +416,8 @@ function isDeclarationShape(obj) {
|
|
|
644
416
|
chains.every((c) => hasExactKeys(c, ["chain_key"]) && typeof c["chain_key"] === "string"));
|
|
645
417
|
}
|
|
646
418
|
/**
|
|
647
|
-
* The content bytes disclosed for one record, or `null`
|
|
648
|
-
*
|
|
649
|
-
* bundle verifier chooses the bytes it binds.
|
|
419
|
+
* The content bytes disclosed for one record, or `null`: `events[].content`
|
|
420
|
+
* first, then the side-content map, as the bundle verifier chooses.
|
|
650
421
|
*/
|
|
651
422
|
function disclosedBytes(ev, p, content) {
|
|
652
423
|
const inBody = isObject(ev) ? ev["content"] : undefined;
|
|
@@ -682,9 +453,8 @@ function sameBytes(a, b) {
|
|
|
682
453
|
return true;
|
|
683
454
|
}
|
|
684
455
|
/**
|
|
685
|
-
* Whether `raw` is exactly `JCS
|
|
686
|
-
*
|
|
687
|
-
* `JCS(declaration)`, and nothing an agent writes by accident.
|
|
456
|
+
* Whether `raw` is exactly `JCS` of a declaration-shaped object. Canonical
|
|
457
|
+
* bytes only, so an agent does not write one by accident.
|
|
688
458
|
*/
|
|
689
459
|
function isDeclarationBytes(raw) {
|
|
690
460
|
try {
|
|
@@ -697,13 +467,8 @@ function isDeclarationBytes(raw) {
|
|
|
697
467
|
}
|
|
698
468
|
/**
|
|
699
469
|
* Rule 10: every record that is a declaration other than the pack's own, in
|
|
700
|
-
* bundle order.
|
|
701
|
-
*
|
|
702
|
-
* The record's `event_type` is read from its signed `event_bytes`, so a record
|
|
703
|
-
* whose bytes the pack withholds is not named: the reader cannot see what it
|
|
704
|
-
* was signed as. Its content is read from either channel; a record whose
|
|
705
|
-
* content is byte for byte `JCS` of the pack's own declaration is the pack's
|
|
706
|
-
* own and is never named.
|
|
470
|
+
* bundle order. The `event_type` comes from the signed `event_bytes`, so a
|
|
471
|
+
* record with withheld bytes is not named.
|
|
707
472
|
*/
|
|
708
473
|
function otherDeclarationNotes(bundle, pack, declaration, content) {
|
|
709
474
|
const events = pack.length > 0 ? (bodyOf(bundle)?.events ?? []) : [];
|
|
@@ -720,9 +485,8 @@ function otherDeclarationNotes(bundle, pack, declaration, content) {
|
|
|
720
485
|
return;
|
|
721
486
|
const raw = disclosedBytes(events[i], p, content);
|
|
722
487
|
if (raw === null) {
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
// Nothing arrived is the reading the reader cannot settle.
|
|
488
|
+
// Unusable content is the bundle verifier's finding; only content that
|
|
489
|
+
// never arrived is unknown here.
|
|
726
490
|
if (!p.disclosed)
|
|
727
491
|
notes.push({ kind: "other_declaration_withheld", eventId: p.eventId });
|
|
728
492
|
return;
|
|
@@ -737,46 +501,23 @@ function otherDeclarationNotes(bundle, pack, declaration, content) {
|
|
|
737
501
|
/**
|
|
738
502
|
* Check a pack against the declaration inside its body.
|
|
739
503
|
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
* already built.
|
|
744
|
-
*
|
|
745
|
-
* `chains` is `readChains` over the SAME bundle. Only a v3 declaration that
|
|
746
|
-
* claims a chain reads it (rule 11). It is an argument rather than computed
|
|
747
|
-
* here because `readChains` is asynchronous and this check is not, and Python's
|
|
748
|
-
* `check_declaration` takes it the same way so the two kernels share one
|
|
749
|
-
* contract. A v3 declaration that claims a chain, checked without it, reads as
|
|
750
|
-
* a reader that does not know v3 reads it: `partial`, nothing named, and the
|
|
751
|
-
* pack `incomplete`. Leaving it out can never read `verified`.
|
|
504
|
+
* @param content - The side-content map `verifyEvidenceBundle` takes.
|
|
505
|
+
* @param chains - `readChains` over the same bundle, read only for a v3 chain
|
|
506
|
+
* claim. Omitted, such a claim reads `incomplete`, never `verified`.
|
|
752
507
|
*
|
|
753
|
-
* The verdict
|
|
754
|
-
*
|
|
755
|
-
* signatures are forged, and saying so is `gradePack`'s job, which takes both.
|
|
508
|
+
* The verdict does not read the bundle verdict: a declaration can match over
|
|
509
|
+
* forged signatures, and `gradePack` combines the two.
|
|
756
510
|
*/
|
|
757
511
|
export function checkDeclaration(bundle, content, links) {
|
|
758
|
-
// `
|
|
759
|
-
//
|
|
760
|
-
// not collapse into one another: a body with no `declaration` key claimed
|
|
761
|
-
// nothing, which every pack built before §5.4 did, while a body whose
|
|
762
|
-
// `declaration` will not read DID claim something. Reading the second as the
|
|
763
|
-
// first is how a one-field edit to a re-sealed pack graded clean.
|
|
512
|
+
// An absent `declaration` claimed nothing; one present but unreadable did
|
|
513
|
+
// claim something. The two must not collapse into one.
|
|
764
514
|
const body = bodyOf(bundle);
|
|
765
515
|
if (!isObject(body) || !("declaration" in body))
|
|
766
516
|
return noDeclaration();
|
|
767
517
|
const declaration = body["declaration"];
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
// THERE, so a surface can tell "nothing was claimed" from "something was
|
|
772
|
-
// claimed in a language I do not speak", and `gradePack` carries the second
|
|
773
|
-
// to `incomplete`.
|
|
774
|
-
//
|
|
775
|
-
// Every shape that lands here is one field away from a valid declared pack
|
|
776
|
-
// the holder can re-seal with its own key: an unknown `declaration_version`,
|
|
777
|
-
// a deleted one, a null one, and a `declaration` that is a string, an array
|
|
778
|
-
// or null. Each of those hides the evidence of a trim inside a signed body,
|
|
779
|
-
// so none of them may reach a clean pack verdict.
|
|
518
|
+
// An unreadable declaration is unknown, not an accusation, but each such
|
|
519
|
+
// shape is one field from a valid re-sealed pack, so none may reach a clean
|
|
520
|
+
// pack verdict.
|
|
780
521
|
const version = knownVersion(declaration);
|
|
781
522
|
if (version === null) {
|
|
782
523
|
return { ...noDeclaration(), present: true };
|
|
@@ -804,37 +545,21 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
804
545
|
const declared = records.map((r) => r.eventId);
|
|
805
546
|
const declaredSet = new Set(declared);
|
|
806
547
|
const pack = packRecords(bundle, content);
|
|
807
|
-
//
|
|
808
|
-
// by id
|
|
809
|
-
// `event_id` twice had its second record graded by rule 7 and its first by
|
|
810
|
-
// nothing, or the reverse once the order flipped, and neither record was
|
|
811
|
-
// ever reported as surplus (proof#137, defect 2). First in bundle order is
|
|
812
|
-
// the record every other rule grades, and it does not depend on how the
|
|
813
|
-
// holder ordered `events[]`.
|
|
548
|
+
// First in bundle order wins; later records under the same id are named.
|
|
549
|
+
// A map by id would be last-write-wins and grade one of them by nothing.
|
|
814
550
|
const byId = new Map();
|
|
815
551
|
for (const p of pack) {
|
|
816
552
|
if (p.nameable && !byId.has(p.eventId))
|
|
817
553
|
byId.set(p.eventId, p);
|
|
818
554
|
}
|
|
819
555
|
const deviations = [];
|
|
820
|
-
// Declared and absent
|
|
821
|
-
// declaration exists: nothing else in the pack can see it.
|
|
556
|
+
// Declared and absent: the trimmed hand-over.
|
|
822
557
|
for (const r of records) {
|
|
823
558
|
if (!byId.has(r.eventId))
|
|
824
559
|
deviations.push({ kind: "declared_absent", eventId: r.eventId });
|
|
825
560
|
}
|
|
826
|
-
// Present and undeclared
|
|
827
|
-
//
|
|
828
|
-
// An unnameable record is present-and-undeclared, because a record the
|
|
829
|
-
// declaration could not have named is surplus by construction.
|
|
830
|
-
//
|
|
831
|
-
// A record under an id the pack already carries is named SEPARATELY, in
|
|
832
|
-
// bundle order, and is not also reported as surplus: one defect, one
|
|
833
|
-
// finding. 5.4.1 says declaring an id twice is a malformed declaration and
|
|
834
|
-
// said nothing about the PACK carrying one twice, so a second record under a
|
|
835
|
-
// declared id was graded by no rule at all and was never reported
|
|
836
|
-
// (proof#137, defect 2). A declaration names records by id, so an id that
|
|
837
|
-
// names two records names neither.
|
|
561
|
+
// Present and undeclared. An unnameable record is surplus by construction.
|
|
562
|
+
// A repeated id is named once as `duplicate_event_id`, not also as surplus.
|
|
838
563
|
const seenInPack = new Set();
|
|
839
564
|
for (const p of pack) {
|
|
840
565
|
if (p.nameable && seenInPack.has(p.eventId)) {
|
|
@@ -847,11 +572,8 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
847
572
|
deviations.push({ kind: "undeclared_present", eventId: p.eventId });
|
|
848
573
|
}
|
|
849
574
|
}
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
// selected over another and the honest reading is that the bytes travelled
|
|
853
|
-
// separately, so this is a note and not a deviation. One of them supplying
|
|
854
|
-
// bytes and another not IS a selection, and every shortfall is named.
|
|
575
|
+
// If no held `disclosed` record supplied bytes, nothing was selected: a note.
|
|
576
|
+
// Some supplying and some not is a selection, and every shortfall is named.
|
|
855
577
|
const disclosedHeld = records
|
|
856
578
|
.filter((r) => !r.withheld)
|
|
857
579
|
.map((r) => byId.get(r.eventId))
|
|
@@ -860,18 +582,9 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
860
582
|
const notes = nothingTravelled
|
|
861
583
|
? [{ kind: "disclosure_did_not_travel", count: disclosedHeld.length }]
|
|
862
584
|
: [];
|
|
863
|
-
// The
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
// EACH OF THEM SPLITS ON WHICH CHANNEL THE BYTES CAME FROM (proof#147,
|
|
868
|
-
// ratified by Phil 2026-09-22). `events[].content` is inside the signed
|
|
869
|
-
// body, so what it says is the holder's own word and a mismatch with the
|
|
870
|
-
// holder's own declaration is a proven deviation. The side-content map is
|
|
871
|
-
// unsigned and is filled in by whoever assembled the case, so a reading
|
|
872
|
-
// that rests on it says what the READER established and not what the holder
|
|
873
|
-
// did. The weaker readings are named, and they carry the check to `partial`
|
|
874
|
-
// and the pack to `incomplete`.
|
|
585
|
+
// The disclosure rules, over held records only. Each splits on the channel:
|
|
586
|
+
// in-body content is the holder's signed word and a mismatch is a deviation;
|
|
587
|
+
// side content is unsigned and yields a content finding.
|
|
875
588
|
const contentFindings = [];
|
|
876
589
|
for (const r of records) {
|
|
877
590
|
const p = byId.get(r.eventId);
|
|
@@ -880,10 +593,7 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
880
593
|
if (!r.withheld && !p.disclosed) {
|
|
881
594
|
if (nothingTravelled)
|
|
882
595
|
continue;
|
|
883
|
-
//
|
|
884
|
-
// the pack, so no pack can prove it supplied them and no reader can
|
|
885
|
-
// prove it did not: an accuser holding a twenty-file drop reaches this
|
|
886
|
-
// reading by deleting one file.
|
|
596
|
+
// Always content-dependent: deleting one file beside the pack reaches it.
|
|
887
597
|
contentFindings.push({ kind: "content_dependent_withholding", eventId: r.eventId });
|
|
888
598
|
}
|
|
889
599
|
else if (r.withheld && p.disclosed) {
|
|
@@ -895,26 +605,9 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
895
605
|
}
|
|
896
606
|
}
|
|
897
607
|
}
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
// The PAIR itself, that the hashes match and the sequence holds, is the case
|
|
902
|
-
// verdict (anchor#198) and is deliberately not checked here. Each SIDE is
|
|
903
|
-
// checked, and that is the belt for 4.1's own repair: a record respelled out
|
|
904
|
-
// of 4.1's scope was unreadable at the record layer and still usable here,
|
|
905
|
-
// because this array carried the claim and checked nothing about it
|
|
906
|
-
// (proof#136). A record declared and absent is already named above and is
|
|
907
|
-
// not named twice.
|
|
908
|
-
//
|
|
909
|
-
// A claimed side whose `event_bytes` the pack WITHHELD is a NAMED NOTE and
|
|
910
|
-
// not a deviation (proof#137, defect 3). `event_bytes` is OPTIONAL, and a
|
|
911
|
-
// holder that withholds it is exercising minimum disclosure, which this
|
|
912
|
-
// corpus calls a customer's right and not a defect. Grading it "not a
|
|
913
|
-
// conformant hand-off record" made an honest pack prove a deviation over a
|
|
914
|
-
// record the reader cannot see in either direction, and the cheapest escape
|
|
915
|
-
// from that was to stop claiming pairs, which is the door proof#137 is
|
|
916
|
-
// about. A withheld side is still not a clean claim: the pack's own
|
|
917
|
-
// disclosure account already carries those absent bytes to `incomplete`.
|
|
608
|
+
// Each claimed side must be declared and be a conformant hand-off record of
|
|
609
|
+
// that side. A withheld side is a note, not a deviation. The pair itself is
|
|
610
|
+
// the case verdict's to check.
|
|
918
611
|
for (const [i, pair] of pairs.entries()) {
|
|
919
612
|
for (const side of ["sent", "received"]) {
|
|
920
613
|
const id = pair[side];
|
|
@@ -936,12 +629,8 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
936
629
|
}
|
|
937
630
|
}
|
|
938
631
|
}
|
|
939
|
-
// Rule 9
|
|
940
|
-
//
|
|
941
|
-
// every deviation rules 1 to 8 report, and its note follows every other
|
|
942
|
-
// note, both in `records[]` order. It reads the signed body alone and NEVER
|
|
943
|
-
// the side-content map, so no party that signed nothing can create, move or
|
|
944
|
-
// remove one of its findings.
|
|
632
|
+
// Rule 9, v2 and v3: reads the signed body alone, never the side-content
|
|
633
|
+
// map, so no party that signed nothing can move its findings.
|
|
945
634
|
if (roles.length > 0) {
|
|
946
635
|
const claimed = new Set();
|
|
947
636
|
for (const pair of pairs) {
|
|
@@ -952,7 +641,7 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
952
641
|
}
|
|
953
642
|
for (const { eventId, role } of roles) {
|
|
954
643
|
const p = byId.get(eventId);
|
|
955
|
-
//
|
|
644
|
+
// Already named by rule 1.
|
|
956
645
|
if (p === undefined)
|
|
957
646
|
continue;
|
|
958
647
|
const kind = roleReading(p, role, claimed.has(eventId));
|
|
@@ -962,33 +651,22 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
962
651
|
deviations.push({ kind, eventId });
|
|
963
652
|
}
|
|
964
653
|
}
|
|
965
|
-
// Rule 11
|
|
966
|
-
//
|
|
967
|
-
// same pack. Its deviations follow rule 9's, and its notes follow rule 9's
|
|
968
|
-
// and precede rule 10's, both in `chains[]` order.
|
|
969
|
-
// A caller that passed no links leaves the claim UNREAD: rules 1 to 10 still
|
|
970
|
-
// run, and the verdict below is never `verified`.
|
|
654
|
+
// Rule 11, v3 only. Without chain links the claim is unread, and the verdict
|
|
655
|
+
// is never `verified`.
|
|
971
656
|
const unread = claimedChains.length > 0 && (links === undefined || links === null);
|
|
972
657
|
if (claimedChains.length > 0 && links !== undefined && links !== null) {
|
|
973
658
|
const found = chainClaimFindings(claimedChains, links, pack);
|
|
974
659
|
deviations.push(...found.deviations);
|
|
975
660
|
notes.push(...found.notes);
|
|
976
661
|
}
|
|
977
|
-
// Rule 10
|
|
978
|
-
// `declaration` that is a declaration other than this pack's own. Notes only, in bundle order, after every other note, and
|
|
979
|
-
// never a deviation: the pack holds what its own declaration says whatever
|
|
980
|
-
// one of its records says.
|
|
662
|
+
// Rule 10: notes only, after every other note.
|
|
981
663
|
notes.push(...otherDeclarationNotes(bundle, pack, declaration, content));
|
|
982
|
-
// A
|
|
983
|
-
//
|
|
984
|
-
// With no deviation and a content-dependent finding, the pack lines up with
|
|
985
|
-
// its claim everywhere a signature reaches and does not line up somewhere
|
|
986
|
-
// only unsigned bytes could settle, which is UNDETERMINED: `partial` is
|
|
987
|
-
// 3's word for that, and it adds none.
|
|
664
|
+
// A deviation always wins. Otherwise a content finding is undetermined:
|
|
665
|
+
// `incomplete`.
|
|
988
666
|
const verdict = deviations.length > 0
|
|
989
667
|
? "failed"
|
|
990
668
|
: contentFindings.length > 0 || unread
|
|
991
|
-
? "
|
|
669
|
+
? "incomplete"
|
|
992
670
|
: "verified";
|
|
993
671
|
return {
|
|
994
672
|
verdict,
|
|
@@ -1004,22 +682,17 @@ export function checkDeclaration(bundle, content, links) {
|
|
|
1004
682
|
};
|
|
1005
683
|
}
|
|
1006
684
|
/**
|
|
1007
|
-
* Rule 11 over every claimed `chain_key`, in `chains[]` order.
|
|
1008
|
-
*
|
|
685
|
+
* Rule 11 over every claimed `chain_key`, in `chains[]` order. Python's twin is
|
|
686
|
+
* `_chain_claim_findings`.
|
|
1009
687
|
*
|
|
1010
|
-
* A claim covers every record whose
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
* `chain_claimed_absent`. Links are read from the record each envelope binds,
|
|
1019
|
-
* `event_bytes` or an inline `payload`. A dangling link is the note
|
|
1020
|
-
* `chain_claim_withheld` instead, once per key, only where its pointer equals
|
|
1021
|
-
* the signed `payload_hash` of a same-key record whose binding is
|
|
1022
|
-
* undetermined: exactly the missing link. Nothing else is excused.
|
|
688
|
+
* A claim covers every record whose own signed linkage names the key, under
|
|
689
|
+
* every producing key; adapter metadata linkage is never read. In bundle order:
|
|
690
|
+
* a record after the first root is `chain_second_root`; a second successor of
|
|
691
|
+
* one predecessor is `chain_fork`; a pointer to no record of its own chain is
|
|
692
|
+
* `chain_link_dangling`; a claimed key with no record is `chain_claimed_absent`.
|
|
693
|
+
* A dangling link is excused as the note `chain_claim_withheld` only where its
|
|
694
|
+
* pointer equals the `payload_hash` of a same-key record with an undetermined
|
|
695
|
+
* binding.
|
|
1023
696
|
*/
|
|
1024
697
|
function chainClaimFindings(claimed, links, pack) {
|
|
1025
698
|
const deviations = [];
|
|
@@ -1068,12 +741,8 @@ function chainClaimFindings(claimed, links, pack) {
|
|
|
1068
741
|
return { deviations, notes };
|
|
1069
742
|
}
|
|
1070
743
|
/**
|
|
1071
|
-
* Read the claim a pack's declaration makes, without grading the pack.
|
|
1072
|
-
*
|
|
1073
|
-
* Deliberately total: a bundle this reader cannot read comes back with
|
|
1074
|
-
* `readable: false` rather than throwing, because a case reader grades what it
|
|
1075
|
-
* was handed and a reader that throws on a hostile pack cannot report that the
|
|
1076
|
-
* pack is hostile.
|
|
744
|
+
* Read the claim a pack's declaration makes, without grading the pack. Total:
|
|
745
|
+
* never throws, so a hostile pack is reported rather than crashing the reader.
|
|
1077
746
|
*/
|
|
1078
747
|
export function declarationClaims(bundle) {
|
|
1079
748
|
const none = {
|
|
@@ -1112,68 +781,57 @@ export function declarationClaims(bundle) {
|
|
|
1112
781
|
roles: read.read.roles,
|
|
1113
782
|
};
|
|
1114
783
|
}
|
|
784
|
+
/**
|
|
785
|
+
* One line a surface can print beside the pack verdict. Both kernels print the
|
|
786
|
+
* same sentence.
|
|
787
|
+
*/
|
|
1115
788
|
export function renderDeclarationSummary(check) {
|
|
1116
789
|
if (!check.present) {
|
|
1117
|
-
return "this pack does not say what it holds, so whether it is complete is
|
|
790
|
+
return "this pack does not say what it holds, so whether it is complete is unknown";
|
|
1118
791
|
}
|
|
1119
792
|
if (check.verdict === "failed") {
|
|
1120
|
-
//
|
|
1121
|
-
// wins (section 5.4.3: a pack with one deviation and one content-dependent
|
|
1122
|
-
// finding is `failed`, and the finding is still named), so a pack that
|
|
1123
|
-
// deviates gets the deviating sentence even when it also carries findings.
|
|
1124
|
-
// Ordered the other way round, a pack holding both printed "this pack
|
|
1125
|
-
// agrees with its own declaration everywhere its signature reaches" with
|
|
1126
|
-
// its own named deviations listed underneath, which is the reading this
|
|
1127
|
-
// section exists to prevent: a deviation surfacing as something a reader
|
|
1128
|
-
// is left to interpret.
|
|
793
|
+
// Before the content-findings branch: a deviation always wins.
|
|
1129
794
|
if (check.contentFindings.length > 0) {
|
|
1130
|
-
//
|
|
1131
|
-
|
|
1132
|
-
return (`this pack DEVIATES from its own declaration, in ${check.deviations.length} way(s), ` +
|
|
795
|
+
// Both counts: the two facts are about different records.
|
|
796
|
+
return (`this pack deviates from its own declaration, in ${check.deviations.length} way(s), ` +
|
|
1133
797
|
`and in ${check.contentFindings.length} further way(s) it does not line up with ` +
|
|
1134
798
|
"the unsigned content handed in beside it, which anyone could have supplied");
|
|
1135
799
|
}
|
|
1136
|
-
return `this pack
|
|
800
|
+
return `this pack deviates from its own declaration, in ${check.deviations.length} way(s)`;
|
|
1137
801
|
}
|
|
1138
802
|
if (check.contentFindings.length > 0) {
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
// could not establish; the one below says the reader could not read the
|
|
1142
|
-
// claim at all.
|
|
803
|
+
// Before the `incomplete` branch below: this says what the reader could not
|
|
804
|
+
// establish, that one says the claim could not be read.
|
|
1143
805
|
return ("this pack agrees with its own declaration everywhere its signature reaches, " +
|
|
1144
806
|
`and in ${check.contentFindings.length} way(s) it does not line up with the ` +
|
|
1145
807
|
"unsigned content handed in beside it, which anyone could have supplied");
|
|
1146
808
|
}
|
|
1147
|
-
if (check.verdict === "
|
|
1148
|
-
//
|
|
1149
|
-
// claim was not checked because the caller gave no reading.
|
|
809
|
+
if (check.verdict === "incomplete" && (check.chains ?? []).length > 0) {
|
|
810
|
+
// The declaration was read; its chain claim was not checked.
|
|
1150
811
|
return ("this pack holds what it declares, and says its chains are complete " +
|
|
1151
812
|
"and in order, which this verifier did not check, so whether they " +
|
|
1152
|
-
"are is
|
|
813
|
+
"are is unknown");
|
|
1153
814
|
}
|
|
1154
|
-
if (check.verdict === "
|
|
815
|
+
if (check.verdict === "incomplete") {
|
|
1155
816
|
return ("this pack says what it holds in a declaration this verifier cannot read, " +
|
|
1156
|
-
"so whether it is complete is
|
|
817
|
+
"so whether it is complete is unknown");
|
|
1157
818
|
}
|
|
1158
819
|
return (`this pack holds exactly what it declares: ${check.declared.length} record(s), ` +
|
|
1159
820
|
`${check.declaredWithheld.length} withheld on purpose`);
|
|
1160
821
|
}
|
|
1161
|
-
/**
|
|
1162
|
-
* The house wording for one note. Offered, not imposed, as `renderDeviation`
|
|
1163
|
-
* is.
|
|
1164
|
-
*/
|
|
822
|
+
/** The house wording for one note. */
|
|
1165
823
|
export function renderNote(note) {
|
|
1166
824
|
switch (note.kind) {
|
|
1167
825
|
case "disclosure_did_not_travel":
|
|
1168
|
-
return (`this pack declares ${note.count} record
|
|
1169
|
-
"any of them: the bytes did not travel with the pack, so their content is
|
|
826
|
+
return (`this pack declares ${note.count} ${note.count === 1 ? "record" : "records"} disclosed and supplies no bytes for ` +
|
|
827
|
+
"any of them: the bytes did not travel with the pack, so their content is unknown");
|
|
1170
828
|
case "pair_record_withheld":
|
|
1171
829
|
return (`the claimed hand-off at position ${note.pairIndex} names ${note.eventId} on its ` +
|
|
1172
830
|
`${note.side} side, whose record bytes this pack withholds, so what that record ` +
|
|
1173
|
-
"
|
|
831
|
+
"is, is unknown");
|
|
1174
832
|
case "role_step_withheld":
|
|
1175
833
|
return (`${note.eventId} is signed as a step and this pack withholds its ` +
|
|
1176
|
-
"record bytes, so whether it is one is
|
|
834
|
+
"record bytes, so whether it is one is unknown");
|
|
1177
835
|
case "other_declaration_held":
|
|
1178
836
|
return (`the content handed in for ${note.eventId} is a declaration that is ` +
|
|
1179
837
|
"not this pack's own: it says what some pack holds, and what this " +
|
|
@@ -1181,7 +839,7 @@ export function renderNote(note) {
|
|
|
1181
839
|
case "other_declaration_withheld":
|
|
1182
840
|
return (`${note.eventId} is signed as a declaration and no content for it ` +
|
|
1183
841
|
"reached this verifier, so whether it is a declaration other than " +
|
|
1184
|
-
"this pack's own is
|
|
842
|
+
"this pack's own is unknown");
|
|
1185
843
|
case "chain_claim_withheld":
|
|
1186
844
|
return (`this pack declares chain ${note.chainKey} complete and in order, ` +
|
|
1187
845
|
"and a link in it points at a record its producer signed and this " +
|
|
@@ -1190,34 +848,33 @@ export function renderNote(note) {
|
|
|
1190
848
|
}
|
|
1191
849
|
}
|
|
1192
850
|
/**
|
|
1193
|
-
* The house wording for one content
|
|
1194
|
-
*
|
|
1195
|
-
* EVERY SENTENCE HERE NAMES THE CONTENT AND NOT THE HOLDER. That is the whole
|
|
1196
|
-
* point of the kind: the bytes are unsigned and whoever assembled the case
|
|
1197
|
-
* chose them, so a sentence that reads as an accusation would put the weaker
|
|
1198
|
-
* word beside the stronger claim and an auditor would read the claim.
|
|
851
|
+
* The house wording for one content finding. Names the content, never the
|
|
852
|
+
* holder: the bytes are unsigned.
|
|
1199
853
|
*/
|
|
1200
854
|
export function renderContentFinding(finding) {
|
|
1201
855
|
switch (finding.kind) {
|
|
1202
856
|
case "content_dependent_withholding":
|
|
1203
857
|
return (`${finding.eventId} is declared disclosed and no bytes for it reached this ` +
|
|
1204
858
|
"verifier, from the pack or from beside it, so whether the holder supplied " +
|
|
1205
|
-
"them is
|
|
859
|
+
"them is unknown");
|
|
1206
860
|
case "content_dependent_disclosure":
|
|
1207
861
|
return (`${finding.eventId} is declared withheld and bytes for it were handed in ` +
|
|
1208
862
|
"beside this pack, which the pack does not carry and nobody signed, so " +
|
|
1209
|
-
"whether the holder disclosed them is
|
|
863
|
+
"whether the holder disclosed them is unknown");
|
|
1210
864
|
}
|
|
1211
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* The house wording for one deviation. Both kernels return the same text.
|
|
868
|
+
*/
|
|
1212
869
|
export function renderDeviation(deviation) {
|
|
1213
870
|
switch (deviation.kind) {
|
|
1214
871
|
case "declaration_malformed":
|
|
1215
872
|
return ("the declaration is not readable as " +
|
|
1216
873
|
`${deviation.version || DECLARATION_VERSION}: ${deviation.reason}`);
|
|
1217
874
|
case "declared_absent":
|
|
1218
|
-
return `${deviation.eventId} is declared and is
|
|
875
|
+
return `${deviation.eventId} is declared and is not in this pack`;
|
|
1219
876
|
case "undeclared_present":
|
|
1220
|
-
return `${deviation.eventId} is in this pack and is
|
|
877
|
+
return `${deviation.eventId} is in this pack and is not declared`;
|
|
1221
878
|
case "withheld_but_disclosed":
|
|
1222
879
|
return (`${deviation.eventId} is declared withheld and this pack carries its bytes ` +
|
|
1223
880
|
"in its own signed body");
|