@sanning/proof 0.9.2 → 0.11.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 +7 -7
- package/dist/agent-proof.js +8 -2
- package/dist/case.d.ts +745 -0
- package/dist/case.js +1318 -0
- package/dist/chain.js +24 -5
- package/dist/cli.js +194 -37
- package/dist/compose.js +3 -3
- package/dist/crypto.d.ts +1 -0
- package/dist/crypto.js +30 -1
- package/dist/declaration.d.ts +284 -0
- package/dist/declaration.js +674 -0
- package/dist/evidence.d.ts +29 -54
- package/dist/evidence.js +622 -206
- package/dist/grading.d.ts +66 -5
- package/dist/grading.js +134 -9
- package/dist/handoff.d.ts +209 -0
- package/dist/handoff.js +598 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +56 -6
- package/dist/message-parts.d.ts +283 -0
- package/dist/message-parts.js +903 -0
- package/dist/verifier.d.ts +36 -0
- package/dist/verifier.js +157 -6
- package/package.json +2 -2
- package/src/agent-proof.ts +8 -2
- package/src/case.ts +1573 -0
- package/src/chain.ts +24 -5
- package/src/cli.ts +226 -39
- package/src/compose.ts +4 -4
- package/src/crypto.ts +29 -1
- package/src/declaration.ts +1005 -0
- package/src/evidence.ts +707 -259
- package/src/grading.ts +168 -11
- package/src/handoff.ts +674 -0
- package/src/index.ts +132 -8
- package/src/message-parts.ts +998 -0
- package/src/verifier.ts +157 -6
- package/vectors/README.md +37 -2
- package/vectors/chain-reading.json +4 -4
- package/vectors/jcs-reader.json +166 -0
- package/vectors/pack-grading.json +131 -3
- package/vectors/stamp-on-chain.json +3 -3
package/src/grading.ts
CHANGED
|
@@ -68,7 +68,18 @@
|
|
|
68
68
|
// implementation is wrong.
|
|
69
69
|
|
|
70
70
|
import { hexToBytes } from "./crypto.js";
|
|
71
|
-
import {
|
|
71
|
+
import type {
|
|
72
|
+
DeclarationCheck,
|
|
73
|
+
DeclarationContentKind,
|
|
74
|
+
DeclarationDeviationKind,
|
|
75
|
+
DeclarationVerdict,
|
|
76
|
+
} from "./declaration.js";
|
|
77
|
+
import { renderContentFinding, renderDeviation } from "./declaration.js";
|
|
78
|
+
import { hasSideContent, sideContentKeys, type SideContentMap } from "./side-content.js";
|
|
79
|
+
// The kernel's one reader for bytes a producer signed: strict UTF-8, no leading
|
|
80
|
+
// byte-order mark, no non-finite literal. `jcs_loads` in
|
|
81
|
+
// `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
|
|
82
|
+
import { jcsParse } from "./verifier.js";
|
|
72
83
|
|
|
73
84
|
// ---- The verdict a surface may print ----------------------------------------
|
|
74
85
|
|
|
@@ -140,7 +151,14 @@ export type PackGradeCauseKind =
|
|
|
140
151
|
| "no-disclosure"
|
|
141
152
|
| "missing-disclosure"
|
|
142
153
|
| "unreadable-disclosure"
|
|
143
|
-
| "unaccounted-disclosure"
|
|
154
|
+
| "unaccounted-disclosure"
|
|
155
|
+
| "declaration-deviation"
|
|
156
|
+
/**
|
|
157
|
+
* The pack and the UNSIGNED content handed in beside it do not line up
|
|
158
|
+
* (proof#147). A cause, so the pack cannot read clean over it and a reader
|
|
159
|
+
* is told which record; NOT a deviation, so it never reaches `failed`.
|
|
160
|
+
*/
|
|
161
|
+
| "declaration-content-dependent";
|
|
144
162
|
|
|
145
163
|
/**
|
|
146
164
|
* One reason a pack is not `verified`, as data.
|
|
@@ -166,6 +184,12 @@ export interface PackGradeCause {
|
|
|
166
184
|
firstEventId?: string;
|
|
167
185
|
/** Why one supplied disclosure could not be used. */
|
|
168
186
|
reason?: string;
|
|
187
|
+
/**
|
|
188
|
+
* Which deviation from the pack's own declaration, for
|
|
189
|
+
* `declaration-deviation`, or which content-dependent finding, for
|
|
190
|
+
* `declaration-content-dependent`.
|
|
191
|
+
*/
|
|
192
|
+
deviation?: DeclarationDeviationKind | DeclarationContentKind;
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
export interface PackGrade {
|
|
@@ -175,6 +199,12 @@ export interface PackGrade {
|
|
|
175
199
|
recordsFailed: number;
|
|
176
200
|
recordsUndetermined: number;
|
|
177
201
|
causes: PackGradeCause[];
|
|
202
|
+
/**
|
|
203
|
+
* The declaration check's own verdict, carried so a consumer that reads only
|
|
204
|
+
* this value can tell a declared pack from an undeclared one. Absent when no
|
|
205
|
+
* check was passed, which is the pre-§5.4 call.
|
|
206
|
+
*/
|
|
207
|
+
declaration?: DeclarationVerdict;
|
|
178
208
|
}
|
|
179
209
|
|
|
180
210
|
// ---- Reading a bundle --------------------------------------------------------
|
|
@@ -185,7 +215,7 @@ interface BundleLike {
|
|
|
185
215
|
envelope?: { event_id?: unknown };
|
|
186
216
|
inclusion?: { checkpoint_ref?: unknown; checkpoint_tx_id?: unknown };
|
|
187
217
|
}[];
|
|
188
|
-
checkpoints?: { checkpoint_ref?: unknown; tx_id?: unknown;
|
|
218
|
+
checkpoints?: { checkpoint_ref?: unknown; tx_id?: unknown; event_bytes?: unknown }[];
|
|
189
219
|
};
|
|
190
220
|
}
|
|
191
221
|
|
|
@@ -225,6 +255,22 @@ function inclusionKeyOf(
|
|
|
225
255
|
return (hasV2 ? incl.checkpoint_ref : incl.checkpoint_tx_id) as string;
|
|
226
256
|
}
|
|
227
257
|
|
|
258
|
+
// The key a checkpoint and an inclusion are MATCHED on. A `checkpoint_ref` is
|
|
259
|
+
// folded to lower case, because the verifier lowercases it before comparing.
|
|
260
|
+
// A retired `tx_id` / `checkpoint_tx_id` is compared exactly as written, as the
|
|
261
|
+
// retired reader did, even when it happens to look like hex.
|
|
262
|
+
function checkpointMatchKey(o: { checkpoint_ref?: unknown; tx_id?: unknown } | null | undefined): string | null {
|
|
263
|
+
const k = joinKeyOf(o);
|
|
264
|
+
return k !== null && typeof o?.checkpoint_ref === "string" ? k.toLowerCase() : k;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function inclusionMatchKey(
|
|
268
|
+
incl: { checkpoint_ref?: unknown; checkpoint_tx_id?: unknown } | null | undefined,
|
|
269
|
+
): string | null {
|
|
270
|
+
const k = inclusionKeyOf(incl);
|
|
271
|
+
return k !== null && typeof incl?.checkpoint_ref === "string" ? k.toLowerCase() : k;
|
|
272
|
+
}
|
|
273
|
+
|
|
228
274
|
const eventsOf = (bundle: unknown) => {
|
|
229
275
|
const e = (bundle as BundleLike | null)?.body?.events;
|
|
230
276
|
return Array.isArray(e) ? e : [];
|
|
@@ -356,7 +402,11 @@ function unsafeEventIdReason(eventId: string): string | null {
|
|
|
356
402
|
*/
|
|
357
403
|
export function accountDisclosure(
|
|
358
404
|
bundle: unknown,
|
|
359
|
-
|
|
405
|
+
// `SideContentMap`, which is what `hasSideContent` and `sideContentKeys`
|
|
406
|
+
// below already take: an entry holding a string that is not hex IS an entry,
|
|
407
|
+
// and typing it out of this signature made the one map a caller builds
|
|
408
|
+
// unusable here (proof#147). A widening, so no caller moves.
|
|
409
|
+
content: SideContentMap,
|
|
360
410
|
): DisclosureAccount {
|
|
361
411
|
const expected: string[] = [];
|
|
362
412
|
const present: string[] = [];
|
|
@@ -402,11 +452,17 @@ export function accountDisclosure(
|
|
|
402
452
|
}
|
|
403
453
|
|
|
404
454
|
/** The `leaf_count` a checkpoint's SIGNED record commits to, or null if unreadable. */
|
|
405
|
-
export function signedLeafCount(checkpoint: {
|
|
406
|
-
const hex = checkpoint?.
|
|
455
|
+
export function signedLeafCount(checkpoint: { event_bytes?: unknown } | null | undefined): number | null {
|
|
456
|
+
const hex = checkpoint?.event_bytes;
|
|
407
457
|
if (typeof hex !== "string" || hex.length === 0) return null;
|
|
408
458
|
try {
|
|
409
|
-
|
|
459
|
+
// `jcsParse`, matching `signed_leaf_count` in `src/sanning_proof/grading.py`.
|
|
460
|
+
// The pair used to agree by pointing two LENIENT decoders at one input, and
|
|
461
|
+
// that held for invalid UTF-8 and nowhere else: `TextDecoder` EATS a
|
|
462
|
+
// leading byte-order mark where Python's `errors="replace"` keeps it, so
|
|
463
|
+
// `BOM + JCS(checkpoint)` yielded a signed leaf count here and unreadable
|
|
464
|
+
// there, and that count decides a completeness note (proof#140 gate 5).
|
|
465
|
+
const record = jcsParse(hexToBytes(hex)) as {
|
|
410
466
|
event?: { leaf_count?: unknown };
|
|
411
467
|
};
|
|
412
468
|
const n = record?.event?.leaf_count;
|
|
@@ -442,7 +498,14 @@ export function completenessNotes(bundle: unknown): CompletenessNote[] {
|
|
|
442
498
|
notes.push({ checkpointRef, signed: null, presented: null, kind: "unreadable" });
|
|
443
499
|
continue;
|
|
444
500
|
}
|
|
445
|
-
|
|
501
|
+
// Compared case-folded. The verifier recomputes `checkpoint_ref` and
|
|
502
|
+
// accepts it in any case (evidence-bundle.md §5.1 names lowercase hex as
|
|
503
|
+
// the form, and the kernel lowercases before comparing), so a pack whose
|
|
504
|
+
// checkpoint spells its ref in upper case still verifies. Grading it
|
|
505
|
+
// case-sensitively reported every record as omitted: a completeness
|
|
506
|
+
// accusation manufactured by letter case. The ref shown is the pack's own.
|
|
507
|
+
const want = checkpointMatchKey(cp);
|
|
508
|
+
const presented = events.filter((e) => inclusionMatchKey(e?.inclusion) === want).length;
|
|
446
509
|
if (presented < signed) notes.push({ checkpointRef, signed, presented, kind: "omitted" });
|
|
447
510
|
else if (presented > signed) notes.push({ checkpointRef, signed, presented, kind: "surplus" });
|
|
448
511
|
}
|
|
@@ -458,9 +521,36 @@ export function completenessNotes(bundle: unknown): CompletenessNote[] {
|
|
|
458
521
|
export function gradeCauses(
|
|
459
522
|
account: DisclosureAccount,
|
|
460
523
|
notes: readonly CompletenessNote[],
|
|
524
|
+
declaration?: DeclarationCheck | null,
|
|
461
525
|
): PackGradeCause[] {
|
|
462
526
|
const causes: PackGradeCause[] = [];
|
|
463
527
|
|
|
528
|
+
// What the pack CLAIMED to hold comes first, because it frames everything
|
|
529
|
+
// below it: a reader told "four records are missing from what this pack says
|
|
530
|
+
// it contains" reads the per-record findings differently. Every deviation
|
|
531
|
+
// gets its own cause (one defect, one line), so a pack that deviates in
|
|
532
|
+
// four ways can never be reported as one vague shortfall.
|
|
533
|
+
for (const deviation of declaration?.deviations ?? []) {
|
|
534
|
+
causes.push({
|
|
535
|
+
kind: "declaration-deviation",
|
|
536
|
+
deviation: deviation.kind,
|
|
537
|
+
...(deviation.eventId === undefined ? {} : { firstEventId: deviation.eventId }),
|
|
538
|
+
reason: renderDeviation(deviation),
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Then what the pack and the UNSIGNED content beside it do not agree on
|
|
543
|
+
// (proof#147). A cause, so the pack cannot read clean over it; NOT a
|
|
544
|
+
// deviation, so it never reaches `failed`.
|
|
545
|
+
for (const finding of declaration?.contentFindings ?? []) {
|
|
546
|
+
causes.push({
|
|
547
|
+
kind: "declaration-content-dependent",
|
|
548
|
+
deviation: finding.kind,
|
|
549
|
+
...(finding.eventId === undefined ? {} : { firstEventId: finding.eventId }),
|
|
550
|
+
reason: renderContentFinding(finding),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
464
554
|
for (const note of notes) {
|
|
465
555
|
if (note.kind === "omitted") {
|
|
466
556
|
causes.push({
|
|
@@ -555,25 +645,78 @@ function passThrough(status: string | null | undefined): PackVerdict {
|
|
|
555
645
|
* (§5.1: undetermined, not failed), and a proven mismatch is never softened
|
|
556
646
|
* into absence. The last branch passes the kernel's headline through unchanged:
|
|
557
647
|
* this rule DOWNGRADES a verdict, it never invents one.
|
|
648
|
+
*
|
|
649
|
+
* ── the declaration (optional fourth argument) ──────────────────────────────
|
|
650
|
+
*
|
|
651
|
+
* `declaration` is what `checkDeclaration` in `./declaration.ts` made of the
|
|
652
|
+
* pack's own signed statement of what it holds (`evidence-bundle.md` §5.4). A
|
|
653
|
+
* deviation from it is a PROVEN mismatch between what the pack claimed and
|
|
654
|
+
* what it carries, so it FAILS the pack and each deviation becomes its own
|
|
655
|
+
* cause. It must never soften into absence: that is how a trimmed hand-over
|
|
656
|
+
* reads as an innocent gap.
|
|
657
|
+
*
|
|
658
|
+
* A check that came back `partial` splits in THREE (proof#147), and the first
|
|
659
|
+
* of the three is not about the declaration's readability at all. The other
|
|
660
|
+
* two split on whether the body carried a `declaration` member.
|
|
661
|
+
*
|
|
662
|
+
* content-dependent the pack and the UNSIGNED content beside it do not
|
|
663
|
+
* line up (proof#147). Each finding is its own cause
|
|
664
|
+
* and the pack is `incomplete`. It must never reach
|
|
665
|
+
* `failed`: whoever assembles a case fills in the
|
|
666
|
+
* side-content map, so an accuser holding an intact
|
|
667
|
+
* pack could otherwise put its holder into `failed`
|
|
668
|
+
* with no key at all, by dropping one file or adding
|
|
669
|
+
* one. `failed` is left to what a signature covers.
|
|
670
|
+
* no declaration at all nothing moves. Every pack built before §5.4 is
|
|
671
|
+
* this case, and re-grading them would move pinned
|
|
672
|
+
* exit codes for artifacts already in customers'
|
|
673
|
+
* hands.
|
|
674
|
+
* present and unreadable the pack is `incomplete`. `declaration_version` is
|
|
675
|
+
* chosen by the same party that assembles the pack
|
|
676
|
+
* and has no accept-set, so a holder who trims a
|
|
677
|
+
* record and then writes one unreadable field can
|
|
678
|
+
* re-seal the pack with its own key and hand a
|
|
679
|
+
* reader a clean verdict over evidence of the trim
|
|
680
|
+
* sitting inside the signed body. UNKNOWN is the
|
|
681
|
+
* only honest answer to a claim the reader cannot
|
|
682
|
+
* read, and it adds no cause: `PackGrade.declaration`
|
|
683
|
+
* says why.
|
|
684
|
+
*
|
|
685
|
+
* `verified` adds nothing and moves nothing.
|
|
686
|
+
*
|
|
687
|
+
* Omitting the argument entirely is the pre-§5.4 behavior, unchanged, and
|
|
688
|
+
* `PackGrade.declaration` is then absent.
|
|
558
689
|
*/
|
|
559
690
|
export function gradePack(
|
|
560
691
|
result: GradeInput,
|
|
561
692
|
account: DisclosureAccount,
|
|
562
693
|
notes: readonly CompletenessNote[],
|
|
694
|
+
declaration?: DeclarationCheck | null,
|
|
563
695
|
): PackGrade {
|
|
564
696
|
const events = Array.isArray(result?.events) ? result.events : [];
|
|
565
697
|
const recordsVerified = events.filter((e) => e.ok && e.contentOk === true).length;
|
|
566
698
|
const recordsFailed = events.filter((e) => e.ok === false || e.contentOk === false).length;
|
|
567
699
|
const recordsUndetermined = events.length - recordsVerified - recordsFailed;
|
|
568
|
-
const causes = gradeCauses(account, notes);
|
|
700
|
+
const causes = gradeCauses(account, notes, declaration);
|
|
569
701
|
|
|
570
702
|
let verdict: PackVerdict;
|
|
571
703
|
if (result?.status === "malformed") verdict = "malformed";
|
|
572
704
|
else if (result?.status === "failed" || recordsFailed > 0) verdict = "failed";
|
|
705
|
+
else if (declaration?.verdict === "failed") verdict = "failed";
|
|
573
706
|
else if (recordsUndetermined > 0 || causes.length > 0) verdict = "incomplete";
|
|
707
|
+
else if (declaration?.present === true && declaration.verdict === "partial")
|
|
708
|
+
verdict = "incomplete";
|
|
574
709
|
else verdict = passThrough(result?.status);
|
|
575
710
|
|
|
576
|
-
|
|
711
|
+
const grade: PackGrade = {
|
|
712
|
+
verdict,
|
|
713
|
+
recordsVerified,
|
|
714
|
+
recordsFailed,
|
|
715
|
+
recordsUndetermined,
|
|
716
|
+
causes,
|
|
717
|
+
};
|
|
718
|
+
if (declaration != null) grade.declaration = declaration.verdict;
|
|
719
|
+
return grade;
|
|
577
720
|
}
|
|
578
721
|
|
|
579
722
|
// ---- Presentation (optional) -------------------------------------------------
|
|
@@ -604,7 +747,7 @@ export function renderCause(cause: PackGradeCause, subject = "this pack"): strin
|
|
|
604
747
|
return `checkpoint ${cause.checkpointRef} has no readable signed leaf_count — completeness is UNKNOWN`;
|
|
605
748
|
case "no-disclosure":
|
|
606
749
|
return (
|
|
607
|
-
`no
|
|
750
|
+
`no event bytes were disclosed — content binding is UNKNOWN for all ${cause.count} ` +
|
|
608
751
|
`record(s), not verified`
|
|
609
752
|
);
|
|
610
753
|
case "missing-disclosure":
|
|
@@ -619,5 +762,19 @@ export function renderCause(cause: PackGradeCause, subject = "this pack"): strin
|
|
|
619
762
|
`${cause.count} disclosed file(s) are not accounted for by any record in this bundle ` +
|
|
620
763
|
`(first: ${cause.firstEventId})`
|
|
621
764
|
);
|
|
765
|
+
// Rendered once, by `renderDeviation` in `./declaration.ts`, and carried
|
|
766
|
+
// here on `reason`. Re-deriving the sentence from the cause fields would
|
|
767
|
+
// put the declaration's wording in two places, and one of the two would be
|
|
768
|
+
// edited alone.
|
|
769
|
+
case "declaration-deviation":
|
|
770
|
+
return `this pack deviates from its own declaration: ${cause.reason}`;
|
|
771
|
+
case "declaration-content-dependent":
|
|
772
|
+
// Named as what the reader could not establish, never as what the
|
|
773
|
+
// holder did: the bytes it turns on are unsigned and whoever assembled
|
|
774
|
+
// the case chose them (proof#147).
|
|
775
|
+
return (
|
|
776
|
+
`${subject} and the unsigned content handed in beside it do not line up: ` +
|
|
777
|
+
`${cause.reason}`
|
|
778
|
+
);
|
|
622
779
|
}
|
|
623
780
|
}
|