@sanning/proof 0.10.0 → 0.11.1
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 +4 -4
- package/dist/agent-proof.js +8 -2
- package/dist/case.d.ts +745 -0
- package/dist/case.js +1318 -0
- package/dist/chain.js +20 -1
- package/dist/cli.js +181 -27
- package/dist/crypto.d.ts +1 -0
- package/dist/crypto.js +29 -0
- package/dist/declaration.d.ts +284 -0
- package/dist/declaration.js +674 -0
- package/dist/evidence.js +296 -73
- package/dist/grading.d.ts +65 -4
- package/dist/grading.js +112 -6
- package/dist/handoff.d.ts +209 -0
- package/dist/handoff.js +617 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +50 -0
- 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 +150 -3
- package/package.json +1 -1
- package/src/agent-proof.ts +8 -2
- package/src/case.ts +1573 -0
- package/src/chain.ts +20 -1
- package/src/cli.ts +213 -29
- package/src/crypto.ts +28 -0
- package/src/declaration.ts +1005 -0
- package/src/evidence.ts +306 -71
- package/src/grading.ts +140 -6
- package/src/handoff.ts +694 -0
- package/src/index.ts +127 -0
- package/src/message-parts.ts +998 -0
- package/src/verifier.ts +150 -3
- package/vectors/README.md +36 -1
- package/vectors/jcs-reader.json +166 -0
- package/vectors/pack-grading.json +131 -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 --------------------------------------------------------
|
|
@@ -372,7 +402,11 @@ function unsafeEventIdReason(eventId: string): string | null {
|
|
|
372
402
|
*/
|
|
373
403
|
export function accountDisclosure(
|
|
374
404
|
bundle: unknown,
|
|
375
|
-
|
|
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,
|
|
376
410
|
): DisclosureAccount {
|
|
377
411
|
const expected: string[] = [];
|
|
378
412
|
const present: string[] = [];
|
|
@@ -422,7 +456,13 @@ export function signedLeafCount(checkpoint: { event_bytes?: unknown } | null | u
|
|
|
422
456
|
const hex = checkpoint?.event_bytes;
|
|
423
457
|
if (typeof hex !== "string" || hex.length === 0) return null;
|
|
424
458
|
try {
|
|
425
|
-
|
|
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 {
|
|
426
466
|
event?: { leaf_count?: unknown };
|
|
427
467
|
};
|
|
428
468
|
const n = record?.event?.leaf_count;
|
|
@@ -481,9 +521,36 @@ export function completenessNotes(bundle: unknown): CompletenessNote[] {
|
|
|
481
521
|
export function gradeCauses(
|
|
482
522
|
account: DisclosureAccount,
|
|
483
523
|
notes: readonly CompletenessNote[],
|
|
524
|
+
declaration?: DeclarationCheck | null,
|
|
484
525
|
): PackGradeCause[] {
|
|
485
526
|
const causes: PackGradeCause[] = [];
|
|
486
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
|
+
|
|
487
554
|
for (const note of notes) {
|
|
488
555
|
if (note.kind === "omitted") {
|
|
489
556
|
causes.push({
|
|
@@ -578,25 +645,78 @@ function passThrough(status: string | null | undefined): PackVerdict {
|
|
|
578
645
|
* (§5.1: undetermined, not failed), and a proven mismatch is never softened
|
|
579
646
|
* into absence. The last branch passes the kernel's headline through unchanged:
|
|
580
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.
|
|
581
689
|
*/
|
|
582
690
|
export function gradePack(
|
|
583
691
|
result: GradeInput,
|
|
584
692
|
account: DisclosureAccount,
|
|
585
693
|
notes: readonly CompletenessNote[],
|
|
694
|
+
declaration?: DeclarationCheck | null,
|
|
586
695
|
): PackGrade {
|
|
587
696
|
const events = Array.isArray(result?.events) ? result.events : [];
|
|
588
697
|
const recordsVerified = events.filter((e) => e.ok && e.contentOk === true).length;
|
|
589
698
|
const recordsFailed = events.filter((e) => e.ok === false || e.contentOk === false).length;
|
|
590
699
|
const recordsUndetermined = events.length - recordsVerified - recordsFailed;
|
|
591
|
-
const causes = gradeCauses(account, notes);
|
|
700
|
+
const causes = gradeCauses(account, notes, declaration);
|
|
592
701
|
|
|
593
702
|
let verdict: PackVerdict;
|
|
594
703
|
if (result?.status === "malformed") verdict = "malformed";
|
|
595
704
|
else if (result?.status === "failed" || recordsFailed > 0) verdict = "failed";
|
|
705
|
+
else if (declaration?.verdict === "failed") verdict = "failed";
|
|
596
706
|
else if (recordsUndetermined > 0 || causes.length > 0) verdict = "incomplete";
|
|
707
|
+
else if (declaration?.present === true && declaration.verdict === "partial")
|
|
708
|
+
verdict = "incomplete";
|
|
597
709
|
else verdict = passThrough(result?.status);
|
|
598
710
|
|
|
599
|
-
|
|
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;
|
|
600
720
|
}
|
|
601
721
|
|
|
602
722
|
// ---- Presentation (optional) -------------------------------------------------
|
|
@@ -642,5 +762,19 @@ export function renderCause(cause: PackGradeCause, subject = "this pack"): strin
|
|
|
642
762
|
`${cause.count} disclosed file(s) are not accounted for by any record in this bundle ` +
|
|
643
763
|
`(first: ${cause.firstEventId})`
|
|
644
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
|
+
);
|
|
645
779
|
}
|
|
646
780
|
}
|