@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/src/declaration.ts
CHANGED
|
@@ -1,136 +1,35 @@
|
|
|
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
|
-
|
|
41
|
-
//
|
|
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.
|
|
130
|
-
|
|
131
|
-
// The kernel's one reader for bytes a producer signed: strict UTF-8, no leading
|
|
132
|
-
// byte-order mark, no non-finite literal. `jcs_loads` in
|
|
133
|
-
// `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
|
|
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.
|
|
30
|
+
|
|
31
|
+
// The one reader for bytes a producer signed: strict UTF-8, no leading BOM, no
|
|
32
|
+
// non-finite literal. Python's twin is `jcs_loads`.
|
|
134
33
|
import type { ChainLink } from "./chain.js";
|
|
135
34
|
import { hexToBytes, utf8 } from "./crypto.js";
|
|
136
35
|
import { jcs, jcsParse } from "./verifier.js";
|
|
@@ -142,25 +41,20 @@ import {
|
|
|
142
41
|
type SideContentMap,
|
|
143
42
|
} from "./side-content.js";
|
|
144
43
|
|
|
145
|
-
/** The first declaration schema
|
|
44
|
+
/** The first declaration schema. */
|
|
146
45
|
export const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
147
46
|
|
|
148
|
-
/**
|
|
149
|
-
* v1 plus a signed `records[].role` on every record (evidence-bundle.md
|
|
150
|
-
* 5.4.1 v1.20, A21). A reader that knows only v1 reads a v2 declaration as one
|
|
151
|
-
* it cannot read, which is the safe direction.
|
|
152
|
-
*/
|
|
47
|
+
/** v1 plus a signed `records[].role` on every record (§5.4.1). */
|
|
153
48
|
export const DECLARATION_VERSION_V2 = "sanning.declaration/v2";
|
|
154
49
|
|
|
155
50
|
/**
|
|
156
51
|
* v2 plus a signed `chains[]`: the `chain_key`s the holder claims are complete
|
|
157
|
-
* and in order (
|
|
158
|
-
* v1 and v2 reads a v3 declaration as one it cannot read, which is the safe
|
|
52
|
+
* and in order (§5.4.1). An older reader cannot read it, which is the safe
|
|
159
53
|
* direction.
|
|
160
54
|
*/
|
|
161
55
|
export const DECLARATION_VERSION_V3 = "sanning.declaration/v3";
|
|
162
56
|
|
|
163
|
-
/** Every declaration schema this reader knows,
|
|
57
|
+
/** Every declaration schema this reader knows, oldest first. */
|
|
164
58
|
export const DECLARATION_VERSIONS = [
|
|
165
59
|
DECLARATION_VERSION,
|
|
166
60
|
DECLARATION_VERSION_V2,
|
|
@@ -171,15 +65,13 @@ export const DECLARATION_VERSIONS = [
|
|
|
171
65
|
const ROLE_VERSIONS: readonly string[] = [DECLARATION_VERSION_V2, DECLARATION_VERSION_V3];
|
|
172
66
|
|
|
173
67
|
/**
|
|
174
|
-
*
|
|
175
|
-
* record 4.1 governs that the pack claims
|
|
176
|
-
* `step`, any other record.
|
|
68
|
+
* The roles a v2 declaration can sign, compared exactly: `handoff` for a
|
|
69
|
+
* record §4.1 governs that the pack claims in `handoffs[]`, `step` otherwise.
|
|
177
70
|
*/
|
|
178
71
|
export const DECLARATION_ROLES = ["handoff", "step"] as const;
|
|
179
72
|
export type DeclarationRole = (typeof DECLARATION_ROLES)[number];
|
|
180
73
|
|
|
181
|
-
|
|
182
|
-
export const DECLARATION_VERDICTS = ["verified", "failed", "partial"] as const;
|
|
74
|
+
export const DECLARATION_VERDICTS = ["verified", "failed", "incomplete"] as const;
|
|
183
75
|
export type DeclarationVerdict = (typeof DECLARATION_VERDICTS)[number];
|
|
184
76
|
|
|
185
77
|
export const DECLARATION_DEVIATION_KINDS = [
|
|
@@ -189,47 +81,27 @@ export const DECLARATION_DEVIATION_KINDS = [
|
|
|
189
81
|
"declared_absent",
|
|
190
82
|
/** A record in the pack the declaration never expected. */
|
|
191
83
|
"undeclared_present",
|
|
192
|
-
/**
|
|
193
|
-
* A record declared `content_withheld` whose bytes the pack supplies INSIDE
|
|
194
|
-
* its own signed body, in `events[].content`.
|
|
195
|
-
*/
|
|
84
|
+
/** A record declared `content_withheld` whose bytes the pack's own body supplies. */
|
|
196
85
|
"withheld_but_disclosed",
|
|
197
86
|
/** The pack carries one `event_id` on two or more records. */
|
|
198
87
|
"duplicate_event_id",
|
|
199
88
|
/** A claimed hand-off names a record the declaration does not declare. */
|
|
200
89
|
"pair_record_undeclared",
|
|
201
|
-
/**
|
|
202
|
-
* A claimed hand-off names a record that is not a conformant hand-off
|
|
203
|
-
* record of the side it is claimed on (envelope-spec.md 4.1).
|
|
204
|
-
*/
|
|
90
|
+
/** A claimed hand-off side that is not a conformant hand-off record of that side (§4.1). */
|
|
205
91
|
"pair_record_not_a_handoff",
|
|
206
|
-
/**
|
|
207
|
-
* Rule 9 (v1.20, A21), `sanning.declaration/v2` only. A record declared
|
|
208
|
-
* `handoff` that no `handoffs[]` entry names as a side.
|
|
209
|
-
*/
|
|
92
|
+
/** v2 only: a record declared `handoff` that no `handoffs[]` entry claims. */
|
|
210
93
|
"role_handoff_unclaimed",
|
|
211
94
|
/** A record a `handoffs[]` entry names as a side, declared `step`. */
|
|
212
95
|
"role_step_claimed",
|
|
213
|
-
/**
|
|
214
|
-
* A record declared `step` that 4.1's scope test puts in scope: its `event`
|
|
215
|
-
* carries `counterparty_public_key`, or its folded `event_type` has a
|
|
216
|
-
* `handoff` segment. The holder signed that a hand-off record is not one.
|
|
217
|
-
*/
|
|
96
|
+
/** A record declared `step` that §4.1's scope test puts in scope. */
|
|
218
97
|
"role_step_governed",
|
|
219
|
-
/**
|
|
220
|
-
* A record declared `step` whose supplied `event_bytes` this reader cannot
|
|
221
|
-
* read under rule 7's parser rule.
|
|
222
|
-
*/
|
|
98
|
+
/** A record declared `step` whose supplied `event_bytes` cannot be read. */
|
|
223
99
|
"role_step_unreadable",
|
|
224
|
-
/**
|
|
225
|
-
* Rule 11 (v1.36, A45), `sanning.declaration/v3` only. The holder signed
|
|
226
|
-
* that a chain is complete and in order, and a second record under that key,
|
|
227
|
-
* under any producing key, begins it.
|
|
228
|
-
*/
|
|
100
|
+
/** v3 only: a second record beginning a claimed chain, under any producing key. */
|
|
229
101
|
"chain_second_root",
|
|
230
102
|
/** A record of a claimed chain is the second successor of one predecessor. */
|
|
231
103
|
"chain_fork",
|
|
232
|
-
/** A
|
|
104
|
+
/** A claimed chain's record points at a predecessor the pack does not present. */
|
|
233
105
|
"chain_link_dangling",
|
|
234
106
|
/** A claimed chain of which the pack presents no record at all. */
|
|
235
107
|
"chain_claimed_absent",
|
|
@@ -237,115 +109,67 @@ export const DECLARATION_DEVIATION_KINDS = [
|
|
|
237
109
|
export type DeclarationDeviationKind = (typeof DECLARATION_DEVIATION_KINDS)[number];
|
|
238
110
|
|
|
239
111
|
/**
|
|
240
|
-
* Every way a pack and the
|
|
241
|
-
* up
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* WHY THEY ARE NOT DEVIATIONS. 5.4 grades a pack's declaration partly against
|
|
245
|
-
* content the reader was HANDED, and that content is not signed by anybody.
|
|
246
|
-
* `events[].content` rides inside `body`, so the wrapper signature and
|
|
247
|
-
* `body_hash` cover it and what it says about the holder is the holder's own
|
|
248
|
-
* word. The side-content map is the other channel, and whoever assembles a
|
|
249
|
-
* case fills it in: an accuser could put an intact pack into
|
|
250
|
-
* `pack_declaration_deviates`, which contributed `failed`, WITH NO KEY AT
|
|
251
|
-
* ALL, by dropping the bytes of a record the pack declared `disclosed` or by
|
|
252
|
-
* adding bytes for one it declared `content_withheld`. The pack was intact,
|
|
253
|
-
* its wrapper verified, and its holder had done nothing.
|
|
254
|
-
*
|
|
255
|
-
* This is the mirror of proof#143. There a counterparty makes itself look
|
|
256
|
-
* better by choosing what it hands over; here an accuser makes a counterparty
|
|
257
|
-
* look worse the same way. Both now read as what the READER established, so
|
|
258
|
-
* `failed` stays for what a signature covers.
|
|
259
|
-
*
|
|
260
|
-
* WHAT A HOSTILE HOLDER GAINS FROM THE WEAKER WORD: nothing, because a
|
|
261
|
-
* cheaper move already dominates it. Declaring a record `content_withheld` is
|
|
262
|
-
* legal and grades `verified`, which is strictly better than the `incomplete`
|
|
263
|
-
* these carry.
|
|
112
|
+
* Every way a pack and the unsigned content handed in beside it fail to line
|
|
113
|
+
* up. Not deviations, and never `failed`: whoever assembles a case fills the
|
|
114
|
+
* side-content map, so an accuser could otherwise fail an intact pack with no
|
|
115
|
+
* key at all. `failed` stays for what a signature covers.
|
|
264
116
|
*/
|
|
265
117
|
export const DECLARATION_CONTENT_KINDS = [
|
|
266
118
|
/**
|
|
267
|
-
* A record declared `disclosed` whose bytes
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* fact about what the READER was handed and never a fact about what the
|
|
271
|
-
* holder supplied. No signature covers the absence of a file nobody signed.
|
|
119
|
+
* A record declared `disclosed` whose bytes arrived from neither channel.
|
|
120
|
+
* Always content-dependent: the bytes may travel beside the pack, so this is
|
|
121
|
+
* a fact about what the reader was handed.
|
|
272
122
|
*/
|
|
273
123
|
"content_dependent_withholding",
|
|
274
124
|
/**
|
|
275
|
-
* A record declared `content_withheld` whose bytes
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* rule is the `withheld_but_disclosed` DEVIATION and still fails.
|
|
125
|
+
* A record declared `content_withheld` whose bytes arrived only through the
|
|
126
|
+
* side-content map. The in-body case is the `withheld_but_disclosed`
|
|
127
|
+
* deviation.
|
|
279
128
|
*/
|
|
280
129
|
"content_dependent_disclosure",
|
|
281
130
|
] as const;
|
|
282
131
|
export type DeclarationContentKind = (typeof DECLARATION_CONTENT_KINDS)[number];
|
|
283
132
|
|
|
284
|
-
/**
|
|
285
|
-
* One way the pack and the unsigned content beside it do not line up. Named
|
|
286
|
-
* like a deviation and weighed like an absence.
|
|
287
|
-
*/
|
|
133
|
+
/** One content finding: named like a deviation, weighed like an absence. */
|
|
288
134
|
export interface DeclarationContentFinding {
|
|
289
135
|
kind: DeclarationContentKind;
|
|
290
|
-
/** The record this is about.
|
|
136
|
+
/** The record this is about. */
|
|
291
137
|
eventId?: string;
|
|
292
138
|
}
|
|
293
139
|
|
|
294
|
-
/** Everything this check reports that is
|
|
140
|
+
/** Everything this check reports that is not a deviation. */
|
|
295
141
|
export const DECLARATION_NOTE_KINDS = [
|
|
296
|
-
/**
|
|
297
|
-
* Every record declared `disclosed` supplied no bytes, so nothing was
|
|
298
|
-
* selected: the bytes did not travel with the pack.
|
|
299
|
-
*/
|
|
142
|
+
/** No record declared `disclosed` supplied bytes: they did not travel with the pack. */
|
|
300
143
|
"disclosure_did_not_travel",
|
|
301
144
|
/**
|
|
302
|
-
* A claimed hand-off side whose `event_bytes` the pack withholds.
|
|
303
|
-
*
|
|
304
|
-
* hand-off record nor a non-conformant one.
|
|
145
|
+
* A claimed hand-off side whose `event_bytes` the pack withholds. Withholding
|
|
146
|
+
* is minimum disclosure, so it is neither conformant nor non-conformant.
|
|
305
147
|
*/
|
|
306
148
|
"pair_record_withheld",
|
|
307
149
|
/**
|
|
308
|
-
* A record a v2 declaration signs `step` whose `event_bytes`
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* because its payload binding is undetermined.
|
|
150
|
+
* A record a v2 declaration signs `step` whose `event_bytes` are withheld, so
|
|
151
|
+
* its role cannot be checked. The undetermined binding keeps the pack at
|
|
152
|
+
* `incomplete`.
|
|
312
153
|
*/
|
|
313
154
|
"role_step_withheld",
|
|
314
155
|
/**
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* in, never the record: whether those bytes bind to the record's
|
|
320
|
-
* `content_hash` is the bundle verifier's check, and an accuser can hand in
|
|
321
|
-
* declaration-shaped bytes for an honest record. What the pack holds is what
|
|
322
|
-
* the declaration in its body says, so this moves no verdict.
|
|
156
|
+
* A record signed as `event_type` `declaration` whose handed-in content is
|
|
157
|
+
* `JCS` of a known declaration other than this pack's own. Names the content,
|
|
158
|
+
* not the record: an accuser can hand in such bytes for an honest record.
|
|
159
|
+
* Moves no verdict.
|
|
323
160
|
*/
|
|
324
161
|
"other_declaration_held",
|
|
325
|
-
/**
|
|
326
|
-
* Rule 10. A record whose signed `event_type` is `declaration` and whose
|
|
327
|
-
* content did not reach this reader from either channel, so whether it is a
|
|
328
|
-
* declaration other than this pack's own is UNKNOWN.
|
|
329
|
-
*/
|
|
162
|
+
/** A record signed `declaration` whose content did not arrive, so what it declares is unknown. */
|
|
330
163
|
"other_declaration_withheld",
|
|
331
164
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* record takes no part in a chain, so whether the missing link is it is
|
|
335
|
-
* UNKNOWN. Moves nothing: the undetermined binding holds the pack at
|
|
336
|
-
* `incomplete`.
|
|
165
|
+
* A claimed chain has a dangling link, and a same-key record with an
|
|
166
|
+
* undetermined payload binding could be the missing link. Moves nothing.
|
|
337
167
|
*/
|
|
338
168
|
"chain_claim_withheld",
|
|
339
169
|
] as const;
|
|
340
170
|
export type DeclarationNoteKind = (typeof DECLARATION_NOTE_KINDS)[number];
|
|
341
171
|
|
|
342
|
-
/**
|
|
343
|
-
* One named non-deviation finding, as data.
|
|
344
|
-
*
|
|
345
|
-
* A note never moves the declaration verdict. It exists so the one shape rule
|
|
346
|
-
* 3 deliberately does not fail is still SAID, rather than leaving a reader to
|
|
347
|
-
* notice nothing was reported.
|
|
348
|
-
*/
|
|
172
|
+
/** One named finding that is not a deviation. A note never moves the verdict. */
|
|
349
173
|
export interface DeclarationNote {
|
|
350
174
|
kind: DeclarationNoteKind;
|
|
351
175
|
/** How many records the note is about, where it is about a count. */
|
|
@@ -360,44 +184,38 @@ export interface DeclarationNote {
|
|
|
360
184
|
chainKey?: string;
|
|
361
185
|
}
|
|
362
186
|
|
|
363
|
-
/** One deviation, as data.
|
|
187
|
+
/** One deviation, as data. */
|
|
364
188
|
export interface DeclarationDeviation {
|
|
365
189
|
kind: DeclarationDeviationKind;
|
|
366
190
|
/** The record this is about, where it is about one. */
|
|
367
191
|
eventId?: string;
|
|
368
|
-
/** Which side of a claimed hand-off
|
|
192
|
+
/** Which side of a claimed hand-off, for the `pair_record_` deviations. */
|
|
369
193
|
side?: "sent" | "received";
|
|
370
|
-
/** Which claimed hand-off, by position, for the
|
|
194
|
+
/** Which claimed hand-off, by position, for the `pair_record_` deviations. */
|
|
371
195
|
pairIndex?: number;
|
|
372
196
|
/** What is wrong with the declaration itself, for `declaration_malformed`. */
|
|
373
197
|
reason?: string;
|
|
374
|
-
/**
|
|
375
|
-
* The `declaration_version` the object claimed, for `declaration_malformed`,
|
|
376
|
-
* so the rendered sentence names the version the holder wrote to rather than
|
|
377
|
-
* the first one this reader learned.
|
|
378
|
-
*/
|
|
198
|
+
/** The `declaration_version` the object claimed, for `declaration_malformed`. */
|
|
379
199
|
version?: string;
|
|
380
|
-
/** The claimed chain, for the
|
|
200
|
+
/** The claimed chain, for the `chain_` deviations. */
|
|
381
201
|
chainKey?: string;
|
|
382
202
|
}
|
|
383
203
|
|
|
384
204
|
export interface DeclarationCheck {
|
|
385
205
|
verdict: DeclarationVerdict;
|
|
386
|
-
/** Every deviation, in a fixed order.
|
|
206
|
+
/** Every deviation, in a fixed order. */
|
|
387
207
|
deviations: DeclarationDeviation[];
|
|
388
208
|
/**
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* `partial` and the pack to `incomplete`, never to `failed`.
|
|
209
|
+
* Mismatches with unsigned content, in a fixed order. They carry the check to
|
|
210
|
+
* `incomplete` and the pack to `incomplete`, never to `failed`.
|
|
392
211
|
*/
|
|
393
212
|
contentFindings: DeclarationContentFinding[];
|
|
394
|
-
/** Named findings that are
|
|
213
|
+
/** Named findings that are not deviations, in a fixed order. */
|
|
395
214
|
notes: DeclarationNote[];
|
|
396
215
|
/**
|
|
397
|
-
* Whether the body carried a `declaration` member at all,
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* that claimed nothing: `gradePack` grades the two differently.
|
|
216
|
+
* Whether the body carried a `declaration` member at all, whatever its shape.
|
|
217
|
+
* An unreadable claim is not the same as no claim: `gradePack` grades them
|
|
218
|
+
* differently.
|
|
401
219
|
*/
|
|
402
220
|
present: boolean;
|
|
403
221
|
/** `event_id`s the declaration expects, in declaration order. */
|
|
@@ -405,28 +223,16 @@ export interface DeclarationCheck {
|
|
|
405
223
|
/** Of those, the ones declared withheld on purpose. */
|
|
406
224
|
declaredWithheld: string[];
|
|
407
225
|
/**
|
|
408
|
-
* The
|
|
409
|
-
*
|
|
410
|
-
* one whose version this reader does not know. Set for a malformed object
|
|
411
|
-
* too: the version was read, the rest was not. Optional so a check a caller
|
|
412
|
-
* builds by hand, as rule version 5 adapters do, still type-checks.
|
|
226
|
+
* The version the object was read as, or `null` where there is no
|
|
227
|
+
* declaration or its version is unknown. Set for a malformed object too.
|
|
413
228
|
*/
|
|
414
229
|
version?: string | null;
|
|
415
|
-
/**
|
|
416
|
-
* Every record a v2 declaration signs a role for, in declaration order.
|
|
417
|
-
* Empty for v1, and for anything this reader could not read.
|
|
418
|
-
*/
|
|
230
|
+
/** Every record a v2 or v3 declaration signs a role for, in declaration order. */
|
|
419
231
|
roles?: { eventId: string; role: DeclarationRole }[];
|
|
420
|
-
/**
|
|
421
|
-
* Every `chain_key` a v3 declaration claims is complete and in order, in
|
|
422
|
-
* declaration order. Empty for v1 and v2, and for anything this reader could
|
|
423
|
-
* not read.
|
|
424
|
-
*/
|
|
232
|
+
/** Every `chain_key` a v3 declaration claims, in declaration order. */
|
|
425
233
|
chains?: string[];
|
|
426
234
|
}
|
|
427
235
|
|
|
428
|
-
// ---- Reading the pack --------------------------------------------------------
|
|
429
|
-
|
|
430
236
|
interface DeclarationBundleLike {
|
|
431
237
|
body?: {
|
|
432
238
|
declaration?: unknown;
|
|
@@ -439,25 +245,10 @@ interface DeclarationBundleLike {
|
|
|
439
245
|
}
|
|
440
246
|
|
|
441
247
|
/**
|
|
442
|
-
* The record committed inside one `events[]` entry, or `null
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* and this reader cannot read is a deviation, because the claim is that it IS
|
|
447
|
-
* a hand-off record and nothing here supports that. It is not the same as "the
|
|
448
|
-
* pack supplied no bytes" either: see `bytesSupplied`.
|
|
449
|
-
*
|
|
450
|
-
* ONE RULE, SPELLED OUT, BECAUSE TWO BUILT-INS ARE NOT ONE RULE. The bytes are
|
|
451
|
-
* `JCS(event record)` (RFC 8785) or they are not a conformant record. Three
|
|
452
|
-
* checks carry that: lowercase-or-uppercase hex in pairs, no leading byte-order
|
|
453
|
-
* mark, and no non-finite literal. The hex is `hexToBytes`, because it and
|
|
454
|
-
* `bytes.fromhex` disagree about embedded whitespace (proof#135); the rest is
|
|
455
|
-
* `jcsParse`, which every site in either kernel that reads signed bytes now
|
|
456
|
-
* shares (proof#140 gate 5). Both halves were a hand-rolled copy in this module
|
|
457
|
-
* until proof#146, and both are the shared reader now: the answer on every
|
|
458
|
-
* input is the same, and there is one fewer place for the rule to drift. The
|
|
459
|
-
* cross-kernel declaration leg and `declaration.test.ts` hold both kernels to
|
|
460
|
-
* the same answer on the same bytes rather than to either built-in.
|
|
248
|
+
* The record committed inside one `events[]` entry, or `null` where this
|
|
249
|
+
* reader cannot read it. Bytes that are not `JCS(event record)` are not a
|
|
250
|
+
* conformant record; parsing goes through the shared `hexToBytes` and
|
|
251
|
+
* `jcsParse` so both kernels agree on every input.
|
|
461
252
|
*/
|
|
462
253
|
function committedRecord(raw: unknown): HandoffRecord | null {
|
|
463
254
|
let parsed: unknown;
|
|
@@ -469,33 +260,19 @@ function committedRecord(raw: unknown): HandoffRecord | null {
|
|
|
469
260
|
return isObject(parsed) ? (parsed as HandoffRecord) : null;
|
|
470
261
|
}
|
|
471
262
|
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
// layer grades the two types differently.
|
|
475
|
-
const SIDE_EVENT_TYPES = { sent: "handoff.sent", received: "handoff.received" } as const;
|
|
263
|
+
// A pack that names its receipt as the sent side has mislabelled a record.
|
|
264
|
+
const SIDE_EVENT_TYPES = { sent: "agent.handoff.sent", received: "agent.handoff.received" } as const;
|
|
476
265
|
|
|
477
266
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
* one of them, and both are wrong: `event_bytes` is OPTIONAL and withholding
|
|
482
|
-
* it is minimum disclosure, which the corpus calls a customer's right and not
|
|
483
|
-
* a defect. Grading it "not a conformant hand-off record" made an honest
|
|
484
|
-
* minimum-disclosure pack prove a deviation, and the cheapest way out of that
|
|
485
|
-
* was to stop claiming pairs at all.
|
|
267
|
+
* How a claimed hand-off side reads. Three readings, not two: `event_bytes` is
|
|
268
|
+
* optional, and withholding it is minimum disclosure, not a non-conformant
|
|
269
|
+
* record.
|
|
486
270
|
*/
|
|
487
271
|
type SideReading = "conformant" | "withheld" | "not-a-handoff";
|
|
488
272
|
|
|
489
273
|
/**
|
|
490
|
-
* How a claimed side reads
|
|
491
|
-
*
|
|
492
|
-
* The definition of conformant is 4.1's own (`isConformantHandoffRecord`),
|
|
493
|
-
* read once and never restated here, plus the record type the side names.
|
|
494
|
-
*
|
|
495
|
-
* WITHHELD IS ITS OWN READING. The pack supplied no `event_bytes`, so this
|
|
496
|
-
* reader cannot see what the record is in either direction. Saying it is not a
|
|
497
|
-
* hand-off record is a claim the reader cannot support, and it punishes the
|
|
498
|
-
* disclosure posture the product sells.
|
|
274
|
+
* How a claimed side reads. Conformant is §4.1's own rule
|
|
275
|
+
* (`isConformantHandoffRecord`) plus the record type the side names.
|
|
499
276
|
*/
|
|
500
277
|
function sideReading(
|
|
501
278
|
p: {
|
|
@@ -517,60 +294,41 @@ function sideReading(
|
|
|
517
294
|
const bodyOf = (bundle: unknown) => (bundle as DeclarationBundleLike | null)?.body;
|
|
518
295
|
|
|
519
296
|
/**
|
|
520
|
-
* One of the pack's own records
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* An event with no readable `event_id` is given a positional name, the same
|
|
524
|
-
* `#n` convention `accountDisclosure` uses, so it is still reported rather
|
|
525
|
-
* than dropped. An unnameable record cannot be declared, so it can only ever
|
|
526
|
-
* be surplus, which is a deviation and is named as one.
|
|
297
|
+
* One of the pack's own records. An event with no readable `event_id` gets a
|
|
298
|
+
* positional `#n` name, so it is reported as surplus rather than dropped.
|
|
527
299
|
*/
|
|
528
300
|
export interface PackRecord {
|
|
529
301
|
eventId: string;
|
|
530
302
|
nameable: boolean;
|
|
531
|
-
/**
|
|
532
|
-
* Whether bytes for this record reached the reader at ALL, from either
|
|
533
|
-
* channel. This is what the disclosure account counts.
|
|
534
|
-
*/
|
|
303
|
+
/** Whether bytes reached the reader from either channel. */
|
|
535
304
|
disclosed: boolean;
|
|
536
305
|
/**
|
|
537
|
-
* Whether the bytes came from `events[].content`,
|
|
538
|
-
*
|
|
539
|
-
* apart because only this one is the holder's own word: the side-content
|
|
540
|
-
* map is filled in by whoever assembled the case (proof#147).
|
|
306
|
+
* Whether the bytes came from `events[].content`, inside the signed body.
|
|
307
|
+
* Only this channel is the holder's own word.
|
|
541
308
|
*/
|
|
542
309
|
disclosedInBody: boolean;
|
|
543
310
|
publicKey: string;
|
|
544
311
|
record: HandoffRecord | null;
|
|
545
312
|
/**
|
|
546
|
-
* Whether the pack
|
|
547
|
-
*
|
|
548
|
-
* withholding it is minimum disclosure rather than a defect, so "the pack
|
|
549
|
-
* supplied bytes this reader cannot read" and "the pack supplied none" are
|
|
550
|
-
* two different readings and never one.
|
|
313
|
+
* Whether the pack supplied `event_bytes` at all. Supplied-but-unreadable and
|
|
314
|
+
* not supplied are different readings: withholding is minimum disclosure.
|
|
551
315
|
*/
|
|
552
316
|
bytesSupplied: boolean;
|
|
553
317
|
/**
|
|
554
|
-
* Whether
|
|
555
|
-
*
|
|
556
|
-
* verifier binds `payload_hash` against. Rule 11 excuses a dangling link
|
|
557
|
-
* only for such a record's producing key.
|
|
318
|
+
* Whether the payload binding is undetermined: no `event_bytes` and no inline
|
|
319
|
+
* `payload`. Rule 11 excuses a dangling link only for such a record's key.
|
|
558
320
|
*/
|
|
559
321
|
bindingUndetermined: boolean;
|
|
560
322
|
/**
|
|
561
|
-
* The envelope's signed `payload_hash
|
|
562
|
-
*
|
|
563
|
-
* names: rule 11 excuses a dangling link only where the pointer equals it.
|
|
323
|
+
* The envelope's signed `payload_hash`, or `""`. Rule 11 excuses a dangling
|
|
324
|
+
* link only where the pointer equals it.
|
|
564
325
|
*/
|
|
565
326
|
payloadHash: string;
|
|
566
327
|
}
|
|
567
328
|
|
|
568
329
|
/**
|
|
569
|
-
* The pack's own records, in bundle order. Exported so
|
|
570
|
-
*
|
|
571
|
-
* it here, rather than through a second one beside it. A validation copied
|
|
572
|
-
* next to the one that enforces it drifts, and a copy that is looser blesses
|
|
573
|
-
* a value the real one then rejects.
|
|
330
|
+
* The pack's own records, in bundle order. Exported so `./case.ts` reads a
|
|
331
|
+
* pack through the same parser that grades it.
|
|
574
332
|
*/
|
|
575
333
|
export function packRecords(
|
|
576
334
|
bundle: unknown,
|
|
@@ -582,19 +340,13 @@ export function packRecords(
|
|
|
582
340
|
const raw = ev?.envelope?.event_id;
|
|
583
341
|
const nameable = typeof raw === "string" && raw.length > 0;
|
|
584
342
|
const eventId = nameable ? (raw as string) : `#${i + 1}`;
|
|
585
|
-
// One definition of "the holder supplied bytes", in both of the two
|
|
586
|
-
// shapes a pack can supply them: in the body, or beside it. Both are
|
|
587
|
-
// `./side-content.ts`, never restated here.
|
|
588
343
|
const disclosedInBody = inBodyContentOffered(ev?.content);
|
|
589
344
|
const disclosed = disclosedInBody || (nameable && hasSideContent(content, eventId));
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
// `malformed` there, so a claimed side carrying one is a deviation here.
|
|
345
|
+
// From the envelope, never the record: an unparseable key makes the record
|
|
346
|
+
// `malformed` under §4.1.
|
|
593
347
|
const key = ev?.envelope?.public_key;
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
// the field not travelling, which is minimum disclosure and not a record
|
|
597
|
-
// this reader was handed and could not read.
|
|
348
|
+
// Only a non-empty string counts as supplied; anything else is the field
|
|
349
|
+
// not travelling.
|
|
598
350
|
const rawBytes = ev?.event_bytes;
|
|
599
351
|
return {
|
|
600
352
|
eventId,
|
|
@@ -610,14 +362,9 @@ export function packRecords(
|
|
|
610
362
|
});
|
|
611
363
|
}
|
|
612
364
|
|
|
613
|
-
// ---- Reading the declaration -------------------------------------------------
|
|
614
|
-
|
|
615
365
|
const DISCLOSURE_STATES = new Set(["disclosed", "content_withheld"]);
|
|
616
366
|
|
|
617
|
-
// RFC 3339 `date-time` with an uppercase `Z
|
|
618
|
-
// instant. Format only: this is not an ordering check and not a calendar
|
|
619
|
-
// check, and it deliberately does not restate the `signed_at` rule of
|
|
620
|
-
// envelope-spec §2, which grades a different field on a different layer.
|
|
367
|
+
// RFC 3339 `date-time` with an uppercase `Z`. Format only, not ordering.
|
|
621
368
|
const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
622
369
|
|
|
623
370
|
const isObject = (v: unknown): v is Record<string, unknown> =>
|
|
@@ -633,21 +380,15 @@ interface ReadDeclaration {
|
|
|
633
380
|
}
|
|
634
381
|
|
|
635
382
|
/**
|
|
636
|
-
* Read a declaration
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
* Every malformation is collected, not the first: a declaration is a claim,
|
|
640
|
-
* and a reader that names one defect in a claim with four sends its holder
|
|
641
|
-
* round the loop four times.
|
|
383
|
+
* Read a declaration of a known version, or collect every malformation in it
|
|
384
|
+
* (all of them, so the holder fixes them in one pass).
|
|
642
385
|
*/
|
|
643
386
|
function readDeclaration(
|
|
644
387
|
declaration: Record<string, unknown>,
|
|
645
388
|
version: string = DECLARATION_VERSION,
|
|
646
389
|
): { ok: true; read: ReadDeclaration } | { ok: false; reasons: string[] } {
|
|
647
390
|
const reasons: string[] = [];
|
|
648
|
-
//
|
|
649
|
-
// or `step` (5.4.1 v1.20, A21); under v1 the member is not read at all, so a
|
|
650
|
-
// v1 declaration carrying `role` reads exactly as the same object without it.
|
|
391
|
+
// v2 and v3 require `role` on every entry; v1 does not read the member.
|
|
651
392
|
const roles: { eventId: string; role: DeclarationRole }[] = [];
|
|
652
393
|
const v2 = ROLE_VERSIONS.includes(version);
|
|
653
394
|
const note = (r: string) => reasons.push(r);
|
|
@@ -686,8 +427,7 @@ function readDeclaration(
|
|
|
686
427
|
note(`records[${i}] (${eventId}) has no disclosure of "disclosed" or "content_withheld"`);
|
|
687
428
|
continue;
|
|
688
429
|
}
|
|
689
|
-
//
|
|
690
|
-
// and a non-string never equals one.
|
|
430
|
+
// Exact: `Handoff`, `hand-off` and `step ` are other values.
|
|
691
431
|
const role = r["role"];
|
|
692
432
|
if (v2 && !(role === "handoff" || role === "step")) {
|
|
693
433
|
note(`records[${i}] (${eventId}) has no role of "handoff" or "step"`);
|
|
@@ -727,8 +467,7 @@ function readDeclaration(
|
|
|
727
467
|
}
|
|
728
468
|
}
|
|
729
469
|
|
|
730
|
-
// v3
|
|
731
|
-
// read the member.
|
|
470
|
+
// v3 requires `chains[]`; v1 and v2 do not read the member.
|
|
732
471
|
const chains: string[] = [];
|
|
733
472
|
if (version === DECLARATION_VERSION_V3) {
|
|
734
473
|
const rawChains = declaration["chains"];
|
|
@@ -754,10 +493,8 @@ function readDeclaration(
|
|
|
754
493
|
: { ok: true, read: { records, pairs, roles, chains } };
|
|
755
494
|
}
|
|
756
495
|
|
|
757
|
-
// ---- The check ---------------------------------------------------------------
|
|
758
|
-
|
|
759
496
|
const noDeclaration = (): DeclarationCheck => ({
|
|
760
|
-
verdict: "
|
|
497
|
+
verdict: "incomplete",
|
|
761
498
|
deviations: [],
|
|
762
499
|
contentFindings: [],
|
|
763
500
|
notes: [],
|
|
@@ -770,9 +507,7 @@ const noDeclaration = (): DeclarationCheck => ({
|
|
|
770
507
|
});
|
|
771
508
|
|
|
772
509
|
/**
|
|
773
|
-
* The `declaration_version` of
|
|
774
|
-
* declaration that is not an object, an absent or `null` version and a version
|
|
775
|
-
* outside `DECLARATION_VERSIONS` are each a claim this reader cannot read.
|
|
510
|
+
* The `declaration_version` of a declaration this reader knows, or `null`.
|
|
776
511
|
*/
|
|
777
512
|
function knownVersion(declaration: unknown): string | null {
|
|
778
513
|
if (!isObject(declaration)) return null;
|
|
@@ -783,10 +518,8 @@ function knownVersion(declaration: unknown): string | null {
|
|
|
783
518
|
}
|
|
784
519
|
|
|
785
520
|
/**
|
|
786
|
-
* Rule 9
|
|
787
|
-
* `null` where the signed role agrees
|
|
788
|
-
* of the four deviations that applies, in the order 5.4.2 lists them. Whether
|
|
789
|
-
* 4.1 governs the record is `isHandoffNamespace`, CALLED, never restated.
|
|
521
|
+
* Rule 9 for one held record: the first deviation that applies in §5.4.2
|
|
522
|
+
* order, a note, or `null` where the signed role agrees.
|
|
790
523
|
*/
|
|
791
524
|
function roleReading(
|
|
792
525
|
p: { eventId: string; publicKey: string; record: HandoffRecord | null; bytesSupplied: boolean },
|
|
@@ -809,17 +542,10 @@ function roleReading(
|
|
|
809
542
|
return null;
|
|
810
543
|
}
|
|
811
544
|
|
|
812
|
-
/**
|
|
813
|
-
* The `event_type` a declaration record is signed under: a reserved
|
|
814
|
-
* single-segment name, and the one `@sanning/anchor`'s `bundle` anchors a
|
|
815
|
-
* declaration as.
|
|
816
|
-
*/
|
|
545
|
+
/** The reserved `event_type` a declaration record is signed under. */
|
|
817
546
|
const DECLARATION_EVENT_TYPE = "declaration";
|
|
818
547
|
|
|
819
|
-
/**
|
|
820
|
-
* The keys a declaration object carries, under every version this reader
|
|
821
|
-
* knows: exactly these, no more and no fewer (5.4.1).
|
|
822
|
-
*/
|
|
548
|
+
/** The exact keys of a declaration object, under every known version (§5.4.1). */
|
|
823
549
|
const DECLARATION_KEYS = ["declaration_version", "case_id", "agents", "period", "records", "handoffs"];
|
|
824
550
|
|
|
825
551
|
function hasExactKeys(value: unknown, keys: string[]): value is Record<string, unknown> {
|
|
@@ -829,14 +555,9 @@ function hasExactKeys(value: unknown, keys: string[]): value is Record<string, u
|
|
|
829
555
|
}
|
|
830
556
|
|
|
831
557
|
/**
|
|
832
|
-
* Whether `obj`
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* It is the shape test `@sanning/anchor`'s `isDeclarationShape` makes, and it
|
|
837
|
-
* checks shape only, not the 5.4.1 rules `readDeclaration` grades: a stale
|
|
838
|
-
* declaration is somebody else's claim, and this reader does not grade it, it
|
|
839
|
-
* names it.
|
|
558
|
+
* Whether `obj` has the shape of a known declaration: a known version, exactly
|
|
559
|
+
* its keys, and the value types read here. Shape only: another holder's stale
|
|
560
|
+
* declaration is named, not graded.
|
|
840
561
|
*/
|
|
841
562
|
function isDeclarationShape(obj: unknown): boolean {
|
|
842
563
|
if (!isObject(obj)) return false;
|
|
@@ -871,9 +592,8 @@ function isDeclarationShape(obj: unknown): boolean {
|
|
|
871
592
|
}
|
|
872
593
|
|
|
873
594
|
/**
|
|
874
|
-
* The content bytes disclosed for one record, or `null`
|
|
875
|
-
*
|
|
876
|
-
* bundle verifier chooses the bytes it binds.
|
|
595
|
+
* The content bytes disclosed for one record, or `null`: `events[].content`
|
|
596
|
+
* first, then the side-content map, as the bundle verifier chooses.
|
|
877
597
|
*/
|
|
878
598
|
function disclosedBytes(
|
|
879
599
|
ev: unknown,
|
|
@@ -908,9 +628,8 @@ function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
|
|
|
908
628
|
}
|
|
909
629
|
|
|
910
630
|
/**
|
|
911
|
-
* Whether `raw` is exactly `JCS
|
|
912
|
-
*
|
|
913
|
-
* `JCS(declaration)`, and nothing an agent writes by accident.
|
|
631
|
+
* Whether `raw` is exactly `JCS` of a declaration-shaped object. Canonical
|
|
632
|
+
* bytes only, so an agent does not write one by accident.
|
|
914
633
|
*/
|
|
915
634
|
function isDeclarationBytes(raw: Uint8Array): boolean {
|
|
916
635
|
try {
|
|
@@ -923,13 +642,8 @@ function isDeclarationBytes(raw: Uint8Array): boolean {
|
|
|
923
642
|
|
|
924
643
|
/**
|
|
925
644
|
* Rule 10: every record that is a declaration other than the pack's own, in
|
|
926
|
-
* bundle order.
|
|
927
|
-
*
|
|
928
|
-
* The record's `event_type` is read from its signed `event_bytes`, so a record
|
|
929
|
-
* whose bytes the pack withholds is not named: the reader cannot see what it
|
|
930
|
-
* was signed as. Its content is read from either channel; a record whose
|
|
931
|
-
* content is byte for byte `JCS` of the pack's own declaration is the pack's
|
|
932
|
-
* own and is never named.
|
|
645
|
+
* bundle order. The `event_type` comes from the signed `event_bytes`, so a
|
|
646
|
+
* record with withheld bytes is not named.
|
|
933
647
|
*/
|
|
934
648
|
function otherDeclarationNotes(
|
|
935
649
|
bundle: unknown,
|
|
@@ -949,9 +663,8 @@ function otherDeclarationNotes(
|
|
|
949
663
|
if (p.record === null || p.record.event_type !== DECLARATION_EVENT_TYPE) return;
|
|
950
664
|
const raw = disclosedBytes(events[i], p, content);
|
|
951
665
|
if (raw === null) {
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
// Nothing arrived is the reading the reader cannot settle.
|
|
666
|
+
// Unusable content is the bundle verifier's finding; only content that
|
|
667
|
+
// never arrived is unknown here.
|
|
955
668
|
if (!p.disclosed) notes.push({ kind: "other_declaration_withheld", eventId: p.eventId });
|
|
956
669
|
return;
|
|
957
670
|
}
|
|
@@ -964,50 +677,27 @@ function otherDeclarationNotes(
|
|
|
964
677
|
/**
|
|
965
678
|
* Check a pack against the declaration inside its body.
|
|
966
679
|
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
* already built.
|
|
971
|
-
*
|
|
972
|
-
* `chains` is `readChains` over the SAME bundle. Only a v3 declaration that
|
|
973
|
-
* claims a chain reads it (rule 11). It is an argument rather than computed
|
|
974
|
-
* here because `readChains` is asynchronous and this check is not, and Python's
|
|
975
|
-
* `check_declaration` takes it the same way so the two kernels share one
|
|
976
|
-
* contract. A v3 declaration that claims a chain, checked without it, reads as
|
|
977
|
-
* a reader that does not know v3 reads it: `partial`, nothing named, and the
|
|
978
|
-
* pack `incomplete`. Leaving it out can never read `verified`.
|
|
680
|
+
* @param content - The side-content map `verifyEvidenceBundle` takes.
|
|
681
|
+
* @param chains - `readChains` over the same bundle, read only for a v3 chain
|
|
682
|
+
* claim. Omitted, such a claim reads `incomplete`, never `verified`.
|
|
979
683
|
*
|
|
980
|
-
* The verdict
|
|
981
|
-
*
|
|
982
|
-
* signatures are forged, and saying so is `gradePack`'s job, which takes both.
|
|
684
|
+
* The verdict does not read the bundle verdict: a declaration can match over
|
|
685
|
+
* forged signatures, and `gradePack` combines the two.
|
|
983
686
|
*/
|
|
984
687
|
export function checkDeclaration(
|
|
985
688
|
bundle: unknown,
|
|
986
689
|
content?: SideContentMap | null,
|
|
987
690
|
links?: readonly ChainLink[] | null,
|
|
988
691
|
): DeclarationCheck {
|
|
989
|
-
// `
|
|
990
|
-
//
|
|
991
|
-
// not collapse into one another: a body with no `declaration` key claimed
|
|
992
|
-
// nothing, which every pack built before §5.4 did, while a body whose
|
|
993
|
-
// `declaration` will not read DID claim something. Reading the second as the
|
|
994
|
-
// first is how a one-field edit to a re-sealed pack graded clean.
|
|
692
|
+
// An absent `declaration` claimed nothing; one present but unreadable did
|
|
693
|
+
// claim something. The two must not collapse into one.
|
|
995
694
|
const body = bodyOf(bundle);
|
|
996
695
|
if (!isObject(body) || !("declaration" in body)) return noDeclaration();
|
|
997
696
|
const declaration = (body as Record<string, unknown>)["declaration"];
|
|
998
697
|
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
// THERE, so a surface can tell "nothing was claimed" from "something was
|
|
1003
|
-
// claimed in a language I do not speak", and `gradePack` carries the second
|
|
1004
|
-
// to `incomplete`.
|
|
1005
|
-
//
|
|
1006
|
-
// Every shape that lands here is one field away from a valid declared pack
|
|
1007
|
-
// the holder can re-seal with its own key: an unknown `declaration_version`,
|
|
1008
|
-
// a deleted one, a null one, and a `declaration` that is a string, an array
|
|
1009
|
-
// or null. Each of those hides the evidence of a trim inside a signed body,
|
|
1010
|
-
// so none of them may reach a clean pack verdict.
|
|
698
|
+
// An unreadable declaration is unknown, not an accusation, but each such
|
|
699
|
+
// shape is one field from a valid re-sealed pack, so none may reach a clean
|
|
700
|
+
// pack verdict.
|
|
1011
701
|
const version = knownVersion(declaration);
|
|
1012
702
|
if (version === null) {
|
|
1013
703
|
return { ...noDeclaration(), present: true };
|
|
@@ -1038,13 +728,8 @@ export function checkDeclaration(
|
|
|
1038
728
|
const declared = records.map((r) => r.eventId);
|
|
1039
729
|
const declaredSet = new Set(declared);
|
|
1040
730
|
const pack = packRecords(bundle, content);
|
|
1041
|
-
//
|
|
1042
|
-
// by id
|
|
1043
|
-
// `event_id` twice had its second record graded by rule 7 and its first by
|
|
1044
|
-
// nothing, or the reverse once the order flipped, and neither record was
|
|
1045
|
-
// ever reported as surplus (proof#137, defect 2). First in bundle order is
|
|
1046
|
-
// the record every other rule grades, and it does not depend on how the
|
|
1047
|
-
// holder ordered `events[]`.
|
|
731
|
+
// First in bundle order wins; later records under the same id are named.
|
|
732
|
+
// A map by id would be last-write-wins and grade one of them by nothing.
|
|
1048
733
|
const byId = new Map<string, (typeof pack)[number]>();
|
|
1049
734
|
for (const p of pack) {
|
|
1050
735
|
if (p.nameable && !byId.has(p.eventId)) byId.set(p.eventId, p);
|
|
@@ -1052,24 +737,13 @@ export function checkDeclaration(
|
|
|
1052
737
|
|
|
1053
738
|
const deviations: DeclarationDeviation[] = [];
|
|
1054
739
|
|
|
1055
|
-
// Declared and absent
|
|
1056
|
-
// declaration exists: nothing else in the pack can see it.
|
|
740
|
+
// Declared and absent: the trimmed hand-over.
|
|
1057
741
|
for (const r of records) {
|
|
1058
742
|
if (!byId.has(r.eventId)) deviations.push({ kind: "declared_absent", eventId: r.eventId });
|
|
1059
743
|
}
|
|
1060
744
|
|
|
1061
|
-
// Present and undeclared
|
|
1062
|
-
//
|
|
1063
|
-
// An unnameable record is present-and-undeclared, because a record the
|
|
1064
|
-
// declaration could not have named is surplus by construction.
|
|
1065
|
-
//
|
|
1066
|
-
// A record under an id the pack already carries is named SEPARATELY, in
|
|
1067
|
-
// bundle order, and is not also reported as surplus: one defect, one
|
|
1068
|
-
// finding. 5.4.1 says declaring an id twice is a malformed declaration and
|
|
1069
|
-
// said nothing about the PACK carrying one twice, so a second record under a
|
|
1070
|
-
// declared id was graded by no rule at all and was never reported
|
|
1071
|
-
// (proof#137, defect 2). A declaration names records by id, so an id that
|
|
1072
|
-
// names two records names neither.
|
|
745
|
+
// Present and undeclared. An unnameable record is surplus by construction.
|
|
746
|
+
// A repeated id is named once as `duplicate_event_id`, not also as surplus.
|
|
1073
747
|
const seenInPack = new Set<string>();
|
|
1074
748
|
for (const p of pack) {
|
|
1075
749
|
if (p.nameable && seenInPack.has(p.eventId)) {
|
|
@@ -1082,11 +756,8 @@ export function checkDeclaration(
|
|
|
1082
756
|
}
|
|
1083
757
|
}
|
|
1084
758
|
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
// selected over another and the honest reading is that the bytes travelled
|
|
1088
|
-
// separately, so this is a note and not a deviation. One of them supplying
|
|
1089
|
-
// bytes and another not IS a selection, and every shortfall is named.
|
|
759
|
+
// If no held `disclosed` record supplied bytes, nothing was selected: a note.
|
|
760
|
+
// Some supplying and some not is a selection, and every shortfall is named.
|
|
1090
761
|
const disclosedHeld = records
|
|
1091
762
|
.filter((r) => !r.withheld)
|
|
1092
763
|
.map((r) => byId.get(r.eventId))
|
|
@@ -1096,28 +767,16 @@ export function checkDeclaration(
|
|
|
1096
767
|
? [{ kind: "disclosure_did_not_travel", count: disclosedHeld.length }]
|
|
1097
768
|
: [];
|
|
1098
769
|
|
|
1099
|
-
// The
|
|
1100
|
-
//
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
// EACH OF THEM SPLITS ON WHICH CHANNEL THE BYTES CAME FROM (proof#147,
|
|
1104
|
-
// ratified by Phil 2026-09-22). `events[].content` is inside the signed
|
|
1105
|
-
// body, so what it says is the holder's own word and a mismatch with the
|
|
1106
|
-
// holder's own declaration is a proven deviation. The side-content map is
|
|
1107
|
-
// unsigned and is filled in by whoever assembled the case, so a reading
|
|
1108
|
-
// that rests on it says what the READER established and not what the holder
|
|
1109
|
-
// did. The weaker readings are named, and they carry the check to `partial`
|
|
1110
|
-
// and the pack to `incomplete`.
|
|
770
|
+
// The disclosure rules, over held records only. Each splits on the channel:
|
|
771
|
+
// in-body content is the holder's signed word and a mismatch is a deviation;
|
|
772
|
+
// side content is unsigned and yields a content finding.
|
|
1111
773
|
const contentFindings: DeclarationContentFinding[] = [];
|
|
1112
774
|
for (const r of records) {
|
|
1113
775
|
const p = byId.get(r.eventId);
|
|
1114
776
|
if (p === undefined) continue;
|
|
1115
777
|
if (!r.withheld && !p.disclosed) {
|
|
1116
778
|
if (nothingTravelled) continue;
|
|
1117
|
-
//
|
|
1118
|
-
// the pack, so no pack can prove it supplied them and no reader can
|
|
1119
|
-
// prove it did not: an accuser holding a twenty-file drop reaches this
|
|
1120
|
-
// reading by deleting one file.
|
|
779
|
+
// Always content-dependent: deleting one file beside the pack reaches it.
|
|
1121
780
|
contentFindings.push({ kind: "content_dependent_withholding", eventId: r.eventId });
|
|
1122
781
|
} else if (r.withheld && p.disclosed) {
|
|
1123
782
|
if (p.disclosedInBody) {
|
|
@@ -1128,26 +787,9 @@ export function checkDeclaration(
|
|
|
1128
787
|
}
|
|
1129
788
|
}
|
|
1130
789
|
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
// The PAIR itself, that the hashes match and the sequence holds, is the case
|
|
1135
|
-
// verdict (anchor#198) and is deliberately not checked here. Each SIDE is
|
|
1136
|
-
// checked, and that is the belt for 4.1's own repair: a record respelled out
|
|
1137
|
-
// of 4.1's scope was unreadable at the record layer and still usable here,
|
|
1138
|
-
// because this array carried the claim and checked nothing about it
|
|
1139
|
-
// (proof#136). A record declared and absent is already named above and is
|
|
1140
|
-
// not named twice.
|
|
1141
|
-
//
|
|
1142
|
-
// A claimed side whose `event_bytes` the pack WITHHELD is a NAMED NOTE and
|
|
1143
|
-
// not a deviation (proof#137, defect 3). `event_bytes` is OPTIONAL, and a
|
|
1144
|
-
// holder that withholds it is exercising minimum disclosure, which this
|
|
1145
|
-
// corpus calls a customer's right and not a defect. Grading it "not a
|
|
1146
|
-
// conformant hand-off record" made an honest pack prove a deviation over a
|
|
1147
|
-
// record the reader cannot see in either direction, and the cheapest escape
|
|
1148
|
-
// from that was to stop claiming pairs, which is the door proof#137 is
|
|
1149
|
-
// about. A withheld side is still not a clean claim: the pack's own
|
|
1150
|
-
// disclosure account already carries those absent bytes to `incomplete`.
|
|
790
|
+
// Each claimed side must be declared and be a conformant hand-off record of
|
|
791
|
+
// that side. A withheld side is a note, not a deviation. The pair itself is
|
|
792
|
+
// the case verdict's to check.
|
|
1151
793
|
for (const [i, pair] of pairs.entries()) {
|
|
1152
794
|
for (const side of ["sent", "received"] as const) {
|
|
1153
795
|
const id = pair[side];
|
|
@@ -1167,12 +809,8 @@ export function checkDeclaration(
|
|
|
1167
809
|
}
|
|
1168
810
|
}
|
|
1169
811
|
|
|
1170
|
-
// Rule 9
|
|
1171
|
-
//
|
|
1172
|
-
// every deviation rules 1 to 8 report, and its note follows every other
|
|
1173
|
-
// note, both in `records[]` order. It reads the signed body alone and NEVER
|
|
1174
|
-
// the side-content map, so no party that signed nothing can create, move or
|
|
1175
|
-
// remove one of its findings.
|
|
812
|
+
// Rule 9, v2 and v3: reads the signed body alone, never the side-content
|
|
813
|
+
// map, so no party that signed nothing can move its findings.
|
|
1176
814
|
if (roles.length > 0) {
|
|
1177
815
|
const claimed = new Set<string>();
|
|
1178
816
|
for (const pair of pairs) {
|
|
@@ -1181,7 +819,7 @@ export function checkDeclaration(
|
|
|
1181
819
|
}
|
|
1182
820
|
for (const { eventId, role } of roles) {
|
|
1183
821
|
const p = byId.get(eventId);
|
|
1184
|
-
//
|
|
822
|
+
// Already named by rule 1.
|
|
1185
823
|
if (p === undefined) continue;
|
|
1186
824
|
const kind = roleReading(p, role, claimed.has(eventId));
|
|
1187
825
|
if (kind === "role_step_withheld") notes.push({ kind, eventId });
|
|
@@ -1189,12 +827,8 @@ export function checkDeclaration(
|
|
|
1189
827
|
}
|
|
1190
828
|
}
|
|
1191
829
|
|
|
1192
|
-
// Rule 11
|
|
1193
|
-
//
|
|
1194
|
-
// same pack. Its deviations follow rule 9's, and its notes follow rule 9's
|
|
1195
|
-
// and precede rule 10's, both in `chains[]` order.
|
|
1196
|
-
// A caller that passed no links leaves the claim UNREAD: rules 1 to 10 still
|
|
1197
|
-
// run, and the verdict below is never `verified`.
|
|
830
|
+
// Rule 11, v3 only. Without chain links the claim is unread, and the verdict
|
|
831
|
+
// is never `verified`.
|
|
1198
832
|
const unread = claimedChains.length > 0 && (links === undefined || links === null);
|
|
1199
833
|
if (claimedChains.length > 0 && links !== undefined && links !== null) {
|
|
1200
834
|
const found = chainClaimFindings(claimedChains, links, pack);
|
|
@@ -1202,23 +836,16 @@ export function checkDeclaration(
|
|
|
1202
836
|
notes.push(...found.notes);
|
|
1203
837
|
}
|
|
1204
838
|
|
|
1205
|
-
// Rule 10
|
|
1206
|
-
// `declaration` that is a declaration other than this pack's own. Notes only, in bundle order, after every other note, and
|
|
1207
|
-
// never a deviation: the pack holds what its own declaration says whatever
|
|
1208
|
-
// one of its records says.
|
|
839
|
+
// Rule 10: notes only, after every other note.
|
|
1209
840
|
notes.push(...otherDeclarationNotes(bundle, pack, declaration, content));
|
|
1210
841
|
|
|
1211
|
-
// A
|
|
1212
|
-
//
|
|
1213
|
-
// With no deviation and a content-dependent finding, the pack lines up with
|
|
1214
|
-
// its claim everywhere a signature reaches and does not line up somewhere
|
|
1215
|
-
// only unsigned bytes could settle, which is UNDETERMINED: `partial` is
|
|
1216
|
-
// 3's word for that, and it adds none.
|
|
842
|
+
// A deviation always wins. Otherwise a content finding is undetermined:
|
|
843
|
+
// `incomplete`.
|
|
1217
844
|
const verdict: DeclarationVerdict =
|
|
1218
845
|
deviations.length > 0
|
|
1219
846
|
? "failed"
|
|
1220
847
|
: contentFindings.length > 0 || unread
|
|
1221
|
-
? "
|
|
848
|
+
? "incomplete"
|
|
1222
849
|
: "verified";
|
|
1223
850
|
|
|
1224
851
|
return {
|
|
@@ -1236,22 +863,17 @@ export function checkDeclaration(
|
|
|
1236
863
|
}
|
|
1237
864
|
|
|
1238
865
|
/**
|
|
1239
|
-
* Rule 11 over every claimed `chain_key`, in `chains[]` order.
|
|
1240
|
-
*
|
|
866
|
+
* Rule 11 over every claimed `chain_key`, in `chains[]` order. Python's twin is
|
|
867
|
+
* `_chain_claim_findings`.
|
|
1241
868
|
*
|
|
1242
|
-
* A claim covers every record whose
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
* `chain_claimed_absent`. Links are read from the record each envelope binds,
|
|
1251
|
-
* `event_bytes` or an inline `payload`. A dangling link is the note
|
|
1252
|
-
* `chain_claim_withheld` instead, once per key, only where its pointer equals
|
|
1253
|
-
* the signed `payload_hash` of a same-key record whose binding is
|
|
1254
|
-
* undetermined: exactly the missing link. Nothing else is excused.
|
|
869
|
+
* A claim covers every record whose own signed linkage names the key, under
|
|
870
|
+
* every producing key; adapter metadata linkage is never read. In bundle order:
|
|
871
|
+
* a record after the first root is `chain_second_root`; a second successor of
|
|
872
|
+
* one predecessor is `chain_fork`; a pointer to no record of its own chain is
|
|
873
|
+
* `chain_link_dangling`; a claimed key with no record is `chain_claimed_absent`.
|
|
874
|
+
* A dangling link is excused as the note `chain_claim_withheld` only where its
|
|
875
|
+
* pointer equals the `payload_hash` of a same-key record with an undetermined
|
|
876
|
+
* binding.
|
|
1255
877
|
*/
|
|
1256
878
|
function chainClaimFindings(
|
|
1257
879
|
claimed: string[],
|
|
@@ -1300,43 +922,10 @@ function chainClaimFindings(
|
|
|
1300
922
|
return { deviations, notes };
|
|
1301
923
|
}
|
|
1302
924
|
|
|
1303
|
-
// ---- Presentation (optional) -------------------------------------------------
|
|
1304
|
-
|
|
1305
|
-
/**
|
|
1306
|
-
* The house wording for one deviation.
|
|
1307
|
-
*
|
|
1308
|
-
* Offered, not imposed, exactly as `renderCause` is: what must not vary
|
|
1309
|
-
* between surfaces is the verdict and the set of deviations, and those come
|
|
1310
|
-
* from `checkDeclaration`. Both kernels return this same text for the same
|
|
1311
|
-
* deviation, and `cross-kernel/run_cli.sh` holds the two commands to it.
|
|
1312
|
-
*/
|
|
1313
925
|
/**
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
1317
|
-
* Both kernels' commands print this sentence, so a reader who runs
|
|
1318
|
-
* `sanning-proof verify` and `npx @sanning/proof verify` over one pack reads
|
|
1319
|
-
* one sentence rather than two that differ by a word.
|
|
1320
|
-
*
|
|
1321
|
-
* `partial` is spelled out rather than printed as a word, because "partial" on
|
|
1322
|
-
* its own beside a green pack reads as a hedge. What it means is that nobody
|
|
1323
|
-
* said what the pack was meant to contain, and a reader deciding whether this
|
|
1324
|
-
* is a full hand-over needs that in words.
|
|
1325
|
-
*/
|
|
1326
|
-
/**
|
|
1327
|
-
* What a pack's declaration CLAIMS, for a reader one layer out.
|
|
1328
|
-
*
|
|
1329
|
-
* `checkDeclaration` answers "does this pack agree with its own claim". A case
|
|
1330
|
-
* reader (`./case.ts`, anchor#198) needs a different thing from the same
|
|
1331
|
-
* object: which records the holder claimed as a hand-off side at all, so that
|
|
1332
|
-
* a conformant hand-off record the holder claimed NOWHERE is a named finding
|
|
1333
|
-
* rather than a silence. That is the door proof#137 ratified open at this
|
|
1334
|
-
* layer: `handoffs[]` MAY be empty, so rule 2 cannot reach a declared record
|
|
1335
|
-
* and rule 7 cannot reach an unclaimed one.
|
|
1336
|
-
*
|
|
1337
|
-
* It is read HERE, from the same two functions `checkDeclaration` reads,
|
|
1338
|
-
* rather than restated in the case layer. A second reader of one object drifts
|
|
1339
|
-
* from the first, and a looser copy blesses a claim the real one rejects.
|
|
926
|
+
* What a pack's declaration claims, for `./case.ts`: which records the holder
|
|
927
|
+
* claimed as a hand-off side, so a hand-off record claimed nowhere is a named
|
|
928
|
+
* finding. Read from the same functions `checkDeclaration` uses.
|
|
1340
929
|
*/
|
|
1341
930
|
export interface DeclarationClaims {
|
|
1342
931
|
/**
|
|
@@ -1345,37 +934,23 @@ export interface DeclarationClaims {
|
|
|
1345
934
|
*/
|
|
1346
935
|
present: boolean;
|
|
1347
936
|
/**
|
|
1348
|
-
* Whether
|
|
1349
|
-
*
|
|
1350
|
-
* version and a malformed object, and a case reader must not read any of the
|
|
1351
|
-
* three as "the holder claimed nothing to check".
|
|
937
|
+
* Whether the claim reads as a known version and satisfies §5.4.1. `false`
|
|
938
|
+
* never means "the holder claimed nothing to check".
|
|
1352
939
|
*/
|
|
1353
940
|
readable: boolean;
|
|
1354
941
|
/** The case the declaration names, where it names one. */
|
|
1355
942
|
caseId: string | null;
|
|
1356
|
-
/**
|
|
1357
|
-
* Every `event_id` a `handoffs[]` entry names on either side, in declaration
|
|
1358
|
-
* order, with no duplicates. Empty where the holder claimed no pair, which
|
|
1359
|
-
* is legal and is exactly the shape a case reader has to name.
|
|
1360
|
-
*/
|
|
943
|
+
/** Every `event_id` a `handoffs[]` entry names, in order, without duplicates. */
|
|
1361
944
|
claimedSides: string[];
|
|
1362
|
-
/**
|
|
1363
|
-
* The version the claim was read as, where `readable`; else `null`. The case
|
|
1364
|
-
* reader's claim check is v1's alone (5.5.1 v1.20): a v2 pack's claim check
|
|
1365
|
-
* is rule 9, which the declaration check already ran.
|
|
1366
|
-
*/
|
|
945
|
+
/** The version the claim was read as, where `readable`; else `null`. */
|
|
1367
946
|
version: string | null;
|
|
1368
|
-
/** Every record a
|
|
947
|
+
/** Every record a readable v2 or v3 declaration signs a role for. */
|
|
1369
948
|
roles: { eventId: string; role: DeclarationRole }[];
|
|
1370
949
|
}
|
|
1371
950
|
|
|
1372
951
|
/**
|
|
1373
|
-
* Read the claim a pack's declaration makes, without grading the pack.
|
|
1374
|
-
*
|
|
1375
|
-
* Deliberately total: a bundle this reader cannot read comes back with
|
|
1376
|
-
* `readable: false` rather than throwing, because a case reader grades what it
|
|
1377
|
-
* was handed and a reader that throws on a hostile pack cannot report that the
|
|
1378
|
-
* pack is hostile.
|
|
952
|
+
* Read the claim a pack's declaration makes, without grading the pack. Total:
|
|
953
|
+
* never throws, so a hostile pack is reported rather than crashing the reader.
|
|
1379
954
|
*/
|
|
1380
955
|
export function declarationClaims(bundle: unknown): DeclarationClaims {
|
|
1381
956
|
const none: DeclarationClaims = {
|
|
@@ -1413,55 +988,47 @@ export function declarationClaims(bundle: unknown): DeclarationClaims {
|
|
|
1413
988
|
};
|
|
1414
989
|
}
|
|
1415
990
|
|
|
991
|
+
/**
|
|
992
|
+
* One line a surface can print beside the pack verdict. Both kernels print the
|
|
993
|
+
* same sentence.
|
|
994
|
+
*/
|
|
1416
995
|
export function renderDeclarationSummary(check: DeclarationCheck): string {
|
|
1417
996
|
if (!check.present) {
|
|
1418
|
-
return "this pack does not say what it holds, so whether it is complete is
|
|
997
|
+
return "this pack does not say what it holds, so whether it is complete is unknown";
|
|
1419
998
|
}
|
|
1420
999
|
if (check.verdict === "failed") {
|
|
1421
|
-
//
|
|
1422
|
-
// wins (section 5.4.3: a pack with one deviation and one content-dependent
|
|
1423
|
-
// finding is `failed`, and the finding is still named), so a pack that
|
|
1424
|
-
// deviates gets the deviating sentence even when it also carries findings.
|
|
1425
|
-
// Ordered the other way round, a pack holding both printed "this pack
|
|
1426
|
-
// agrees with its own declaration everywhere its signature reaches" with
|
|
1427
|
-
// its own named deviations listed underneath, which is the reading this
|
|
1428
|
-
// section exists to prevent: a deviation surfacing as something a reader
|
|
1429
|
-
// is left to interpret.
|
|
1000
|
+
// Before the content-findings branch: a deviation always wins.
|
|
1430
1001
|
if (check.contentFindings.length > 0) {
|
|
1431
|
-
//
|
|
1432
|
-
// reader shown one of them would look in the wrong place.
|
|
1002
|
+
// Both counts: the two facts are about different records.
|
|
1433
1003
|
return (
|
|
1434
|
-
`this pack
|
|
1004
|
+
`this pack deviates from its own declaration, in ${check.deviations.length} way(s), ` +
|
|
1435
1005
|
`and in ${check.contentFindings.length} further way(s) it does not line up with ` +
|
|
1436
1006
|
"the unsigned content handed in beside it, which anyone could have supplied"
|
|
1437
1007
|
);
|
|
1438
1008
|
}
|
|
1439
|
-
return `this pack
|
|
1009
|
+
return `this pack deviates from its own declaration, in ${check.deviations.length} way(s)`;
|
|
1440
1010
|
}
|
|
1441
1011
|
if (check.contentFindings.length > 0) {
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
// could not establish; the one below says the reader could not read the
|
|
1445
|
-
// claim at all.
|
|
1012
|
+
// Before the `incomplete` branch below: this says what the reader could not
|
|
1013
|
+
// establish, that one says the claim could not be read.
|
|
1446
1014
|
return (
|
|
1447
1015
|
"this pack agrees with its own declaration everywhere its signature reaches, " +
|
|
1448
1016
|
`and in ${check.contentFindings.length} way(s) it does not line up with the ` +
|
|
1449
1017
|
"unsigned content handed in beside it, which anyone could have supplied"
|
|
1450
1018
|
);
|
|
1451
1019
|
}
|
|
1452
|
-
if (check.verdict === "
|
|
1453
|
-
//
|
|
1454
|
-
// claim was not checked because the caller gave no reading.
|
|
1020
|
+
if (check.verdict === "incomplete" && (check.chains ?? []).length > 0) {
|
|
1021
|
+
// The declaration was read; its chain claim was not checked.
|
|
1455
1022
|
return (
|
|
1456
1023
|
"this pack holds what it declares, and says its chains are complete " +
|
|
1457
1024
|
"and in order, which this verifier did not check, so whether they " +
|
|
1458
|
-
"are is
|
|
1025
|
+
"are is unknown"
|
|
1459
1026
|
);
|
|
1460
1027
|
}
|
|
1461
|
-
if (check.verdict === "
|
|
1028
|
+
if (check.verdict === "incomplete") {
|
|
1462
1029
|
return (
|
|
1463
1030
|
"this pack says what it holds in a declaration this verifier cannot read, " +
|
|
1464
|
-
"so whether it is complete is
|
|
1031
|
+
"so whether it is complete is unknown"
|
|
1465
1032
|
);
|
|
1466
1033
|
}
|
|
1467
1034
|
return (
|
|
@@ -1470,27 +1037,24 @@ export function renderDeclarationSummary(check: DeclarationCheck): string {
|
|
|
1470
1037
|
);
|
|
1471
1038
|
}
|
|
1472
1039
|
|
|
1473
|
-
/**
|
|
1474
|
-
* The house wording for one note. Offered, not imposed, as `renderDeviation`
|
|
1475
|
-
* is.
|
|
1476
|
-
*/
|
|
1040
|
+
/** The house wording for one note. */
|
|
1477
1041
|
export function renderNote(note: DeclarationNote): string {
|
|
1478
1042
|
switch (note.kind) {
|
|
1479
1043
|
case "disclosure_did_not_travel":
|
|
1480
1044
|
return (
|
|
1481
|
-
`this pack declares ${note.count} record
|
|
1482
|
-
"any of them: the bytes did not travel with the pack, so their content is
|
|
1045
|
+
`this pack declares ${note.count} ${note.count === 1 ? "record" : "records"} disclosed and supplies no bytes for ` +
|
|
1046
|
+
"any of them: the bytes did not travel with the pack, so their content is unknown"
|
|
1483
1047
|
);
|
|
1484
1048
|
case "pair_record_withheld":
|
|
1485
1049
|
return (
|
|
1486
1050
|
`the claimed hand-off at position ${note.pairIndex} names ${note.eventId} on its ` +
|
|
1487
1051
|
`${note.side} side, whose record bytes this pack withholds, so what that record ` +
|
|
1488
|
-
"
|
|
1052
|
+
"is, is unknown"
|
|
1489
1053
|
);
|
|
1490
1054
|
case "role_step_withheld":
|
|
1491
1055
|
return (
|
|
1492
1056
|
`${note.eventId} is signed as a step and this pack withholds its ` +
|
|
1493
|
-
"record bytes, so whether it is one is
|
|
1057
|
+
"record bytes, so whether it is one is unknown"
|
|
1494
1058
|
);
|
|
1495
1059
|
case "other_declaration_held":
|
|
1496
1060
|
return (
|
|
@@ -1502,7 +1066,7 @@ export function renderNote(note: DeclarationNote): string {
|
|
|
1502
1066
|
return (
|
|
1503
1067
|
`${note.eventId} is signed as a declaration and no content for it ` +
|
|
1504
1068
|
"reached this verifier, so whether it is a declaration other than " +
|
|
1505
|
-
"this pack's own is
|
|
1069
|
+
"this pack's own is unknown"
|
|
1506
1070
|
);
|
|
1507
1071
|
case "chain_claim_withheld":
|
|
1508
1072
|
return (
|
|
@@ -1515,12 +1079,8 @@ export function renderNote(note: DeclarationNote): string {
|
|
|
1515
1079
|
}
|
|
1516
1080
|
|
|
1517
1081
|
/**
|
|
1518
|
-
* The house wording for one content
|
|
1519
|
-
*
|
|
1520
|
-
* EVERY SENTENCE HERE NAMES THE CONTENT AND NOT THE HOLDER. That is the whole
|
|
1521
|
-
* point of the kind: the bytes are unsigned and whoever assembled the case
|
|
1522
|
-
* chose them, so a sentence that reads as an accusation would put the weaker
|
|
1523
|
-
* word beside the stronger claim and an auditor would read the claim.
|
|
1082
|
+
* The house wording for one content finding. Names the content, never the
|
|
1083
|
+
* holder: the bytes are unsigned.
|
|
1524
1084
|
*/
|
|
1525
1085
|
export function renderContentFinding(finding: DeclarationContentFinding): string {
|
|
1526
1086
|
switch (finding.kind) {
|
|
@@ -1528,17 +1088,20 @@ export function renderContentFinding(finding: DeclarationContentFinding): string
|
|
|
1528
1088
|
return (
|
|
1529
1089
|
`${finding.eventId} is declared disclosed and no bytes for it reached this ` +
|
|
1530
1090
|
"verifier, from the pack or from beside it, so whether the holder supplied " +
|
|
1531
|
-
"them is
|
|
1091
|
+
"them is unknown"
|
|
1532
1092
|
);
|
|
1533
1093
|
case "content_dependent_disclosure":
|
|
1534
1094
|
return (
|
|
1535
1095
|
`${finding.eventId} is declared withheld and bytes for it were handed in ` +
|
|
1536
1096
|
"beside this pack, which the pack does not carry and nobody signed, so " +
|
|
1537
|
-
"whether the holder disclosed them is
|
|
1097
|
+
"whether the holder disclosed them is unknown"
|
|
1538
1098
|
);
|
|
1539
1099
|
}
|
|
1540
1100
|
}
|
|
1541
1101
|
|
|
1102
|
+
/**
|
|
1103
|
+
* The house wording for one deviation. Both kernels return the same text.
|
|
1104
|
+
*/
|
|
1542
1105
|
export function renderDeviation(deviation: DeclarationDeviation): string {
|
|
1543
1106
|
switch (deviation.kind) {
|
|
1544
1107
|
case "declaration_malformed":
|
|
@@ -1547,9 +1110,9 @@ export function renderDeviation(deviation: DeclarationDeviation): string {
|
|
|
1547
1110
|
`${deviation.version || DECLARATION_VERSION}: ${deviation.reason}`
|
|
1548
1111
|
);
|
|
1549
1112
|
case "declared_absent":
|
|
1550
|
-
return `${deviation.eventId} is declared and is
|
|
1113
|
+
return `${deviation.eventId} is declared and is not in this pack`;
|
|
1551
1114
|
case "undeclared_present":
|
|
1552
|
-
return `${deviation.eventId} is in this pack and is
|
|
1115
|
+
return `${deviation.eventId} is in this pack and is not declared`;
|
|
1553
1116
|
case "withheld_but_disclosed":
|
|
1554
1117
|
return (
|
|
1555
1118
|
`${deviation.eventId} is declared withheld and this pack carries its bytes ` +
|