@sanning/proof 0.12.0 → 0.13.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 +25 -12
- package/dist/agent-proof.d.ts +2 -2
- package/dist/agent-proof.js +4 -4
- package/dist/case-file.d.ts +126 -0
- package/dist/case-file.js +349 -0
- package/dist/case.d.ts +173 -26
- package/dist/case.js +469 -123
- package/dist/cli.d.ts +38 -5
- package/dist/cli.js +557 -208
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +5 -1
- package/dist/declaration.d.ts +58 -52
- package/dist/declaration.js +256 -33
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.js +134 -19
- package/dist/exit-codes.d.ts +17 -0
- package/dist/exit-codes.js +50 -0
- package/dist/grading.d.ts +28 -13
- package/dist/grading.js +90 -31
- package/dist/index.d.ts +10 -7
- package/dist/index.js +13 -6
- package/dist/message-parts.d.ts +11 -9
- package/dist/message-parts.js +11 -9
- package/dist/types.d.ts +1 -0
- package/dist/verifier.d.ts +9 -2
- package/dist/verifier.js +28 -12
- package/dist/zip-reader.d.ts +50 -0
- package/dist/zip-reader.js +220 -0
- package/package.json +2 -2
- package/src/agent-proof.ts +4 -4
- package/src/case-file.ts +419 -0
- package/src/case.ts +586 -132
- package/src/cli.ts +651 -206
- package/src/crypto.ts +5 -1
- package/src/declaration.ts +248 -52
- package/src/evidence.ts +142 -21
- package/src/exit-codes.ts +54 -0
- package/src/grading.ts +109 -52
- package/src/index.ts +58 -2
- package/src/message-parts.ts +11 -9
- package/src/types.ts +5 -0
- package/src/verifier.ts +35 -13
- package/src/zip-reader.ts +229 -0
- package/vectors/README.md +23 -5
- package/vectors/case-file-index.json +668 -0
- package/vectors/case-split.json +218 -9
- package/vectors/pack-grading.json +68 -4
- package/vectors/stamp-on-chain.json +3 -3
package/dist/case.js
CHANGED
|
@@ -166,7 +166,9 @@
|
|
|
166
166
|
* gets, since that reads wrapper keys.
|
|
167
167
|
*
|
|
168
168
|
* Not exits: a record whose envelope verifies and whose content does not
|
|
169
|
-
* match its commitment is `RECORD_CONTENT_MISMATCH
|
|
169
|
+
* match its commitment is `RECORD_CONTENT_MISMATCH` where that content is
|
|
170
|
+
* inside the signed body and `RECORD_CONTENT_DEPENDENT` where it was handed
|
|
171
|
+
* in beside the pack (proof#159), and one that fails
|
|
170
172
|
* another check inside its pack is `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`.
|
|
171
173
|
* Both are named and both STAY in the pile, so neither unsigned content
|
|
172
174
|
* handed in beside a pack nor a broken inclusion proof removes a record
|
|
@@ -207,7 +209,9 @@
|
|
|
207
209
|
* verdict, exactly as `counterpart_absent` and `not_a_handoff_record` are
|
|
208
210
|
* findings beside a verdict in 4.1.
|
|
209
211
|
*
|
|
210
|
-
* A case with
|
|
212
|
+
* A case with any `failed` contribution is `failed`: a proven failure keeps
|
|
213
|
+
* the headline (5.5.4 v1.16, A17, proof#158). Otherwise a case with more than
|
|
214
|
+
* one outcome is `mixed` (anchor#198, criterion 4).
|
|
211
215
|
*
|
|
212
216
|
* WHAT THIS READER DOES NOT CLAIM
|
|
213
217
|
* ===============================
|
|
@@ -237,9 +241,10 @@
|
|
|
237
241
|
* sequence. Narrowing any of it here with `handoffs[]` is precisely the move
|
|
238
242
|
* that would reopen proof#137's door.
|
|
239
243
|
*/
|
|
240
|
-
import { checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
244
|
+
import { DECLARATION_VERSION, DECLARATION_VERSION_V2, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
241
245
|
import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
|
|
242
246
|
import { verifyEvidenceBundle } from "./evidence.js";
|
|
247
|
+
import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
|
|
243
248
|
// ---- The vocabulary ---------------------------------------------------------
|
|
244
249
|
/**
|
|
245
250
|
* The verdicts a case can reach. Every one is already in use in
|
|
@@ -578,17 +583,26 @@ export const PACK_KEY_UNREADABLE = "pack_key_unreadable";
|
|
|
578
583
|
*/
|
|
579
584
|
export const PACK_DOES_NOT_VERIFY = "pack_does_not_verify";
|
|
580
585
|
/**
|
|
581
|
-
* The pack's wrapper HOLDS and its
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
586
|
+
* The pack's wrapper HOLDS and its CASE GRADE is not `verified`
|
|
587
|
+
* (evidence-bundle 5.5.1 step 1 v1.20, A23; it replaces
|
|
588
|
+
* `pack_bundle_not_verified`, which carried the bundle status alone). The case
|
|
589
|
+
* grade is the pack grade of 5.4.5, taken twice: the SIGNED grade over the run
|
|
590
|
+
* with the side map empty, and the FULL grade over the run with it. The signed
|
|
591
|
+
* grade decides where it is `failed` or `malformed`; a failure only the full
|
|
592
|
+
* grade reaches is `incomplete`, because it rests on bytes nobody signed;
|
|
593
|
+
* otherwise the full grade. A body the verifier could not read is `malformed`,
|
|
594
|
+
* a body that is not one of the two event-carrying bodies takes the signed
|
|
595
|
+
* run's status, and a verifier that graded a different number of records than
|
|
596
|
+
* the pack holds takes the signed status, or `malformed` where that status is
|
|
597
|
+
* `verified`.
|
|
598
|
+
*
|
|
599
|
+
* The exception carries `status`, the case grade; `causes`, the 5.4.5 cause
|
|
600
|
+
* kinds of the grade that decided it (none in the first three branches); and
|
|
601
|
+
* `reasons`, the verifier's reasons from the run with the side map empty. It
|
|
602
|
+
* CONTRIBUTES the grade, and `incomplete` for `pending`, `not_found` or
|
|
603
|
+
* `mixed`, so the case verdict never reads above its weakest pack.
|
|
590
604
|
*/
|
|
591
|
-
export const
|
|
605
|
+
export const PACK_GRADE_NOT_VERIFIED = "pack_grade_not_verified";
|
|
592
606
|
/**
|
|
593
607
|
* A record in a pack whose wrapper holds, whose OWN envelope does not verify
|
|
594
608
|
* (`EventResult.envelopeOk` is false). Not read by 4.1, and NAMED with the
|
|
@@ -609,12 +623,39 @@ export const RECORD_ENVELOPE_DOES_NOT_VERIFY = "record_envelope_does_not_verify"
|
|
|
609
623
|
export const RECORD_DOES_NOT_VERIFY_IN_ITS_PACK = "record_does_not_verify_in_its_pack";
|
|
610
624
|
/**
|
|
611
625
|
* A record whose envelope verifies and whose disclosed content does not hash to
|
|
612
|
-
* the
|
|
613
|
-
*
|
|
626
|
+
* the hash it commits to, its `content_hash` or a hand-off record's
|
|
627
|
+
* `message_hash` (evidence-bundle v1.19, A20) (`EventResult.contentOk` is false), with the
|
|
628
|
+
* side map EMPTY: the content is inside the signed body. The same mismatch from
|
|
629
|
+
* content handed in beside the pack is `RECORD_CONTENT_DEPENDENT` (proof#159).
|
|
630
|
+
* `failed`, and the record STAYS in the pile: content beside a pack is unsigned and often
|
|
614
631
|
* supplied by whoever assembled the case, and gate 3 on proof#140 showed that
|
|
615
632
|
* party erasing an honest verified pair by swapping it.
|
|
616
633
|
*/
|
|
617
634
|
export const RECORD_CONTENT_MISMATCH = "record_content_mismatch";
|
|
635
|
+
/**
|
|
636
|
+
* The two readings proof#147 did not reach, given its shape (proof#159, ruled
|
|
637
|
+
* by Phil 2026-09-22, evidence-bundle.md 5.5.4 v1.16, A17). A pack whose
|
|
638
|
+
* bundle verdict is `failed` ONLY when read with the unsigned content handed
|
|
639
|
+
* in beside it, and `failed` no longer when read with that map empty, is
|
|
640
|
+
* `PACK_BUNDLE_CONTENT_DEPENDENT`; a record whose content mismatch comes from
|
|
641
|
+
* that map alone is `RECORD_CONTENT_DEPENDENT`. Both are `incomplete`: the
|
|
642
|
+
* reader established that the bytes it was handed are not the bytes the record
|
|
643
|
+
* commits to, and nothing about the party that signed the record, because
|
|
644
|
+
* whoever assembled the case chose those bytes with no key. Up to v1.15 both
|
|
645
|
+
* readings carried `failed`, and one flipped nibble of accuser-supplied content
|
|
646
|
+
* put a `failed` against an intact pack; under A17's headline rule that would
|
|
647
|
+
* have become the case's headline.
|
|
648
|
+
*
|
|
649
|
+
* The test that separates the two is the one proof#159 states for the inverse:
|
|
650
|
+
* every deviation a signature covers still reads `failed` with the side map
|
|
651
|
+
* EMPTY. So the pack is verified twice, with the map and without it, and what
|
|
652
|
+
* fails without it is `PACK_GRADE_NOT_VERIFIED`, `RECORD_CONTENT_MISMATCH`
|
|
653
|
+
* (content inside the signed body) or `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`,
|
|
654
|
+
* each still `failed`. Emptying the map is the most a party that signed
|
|
655
|
+
* nothing can do to starve the check, and it cannot remove any of those.
|
|
656
|
+
*/
|
|
657
|
+
export const PACK_BUNDLE_CONTENT_DEPENDENT = "pack_bundle_content_dependent";
|
|
658
|
+
export const RECORD_CONTENT_DEPENDENT = "record_content_dependent";
|
|
618
659
|
/**
|
|
619
660
|
* A record signed under a ratified profile that is under quarantine
|
|
620
661
|
* (`CASE_QUARANTINED_PROFILES`, pending DL-63). Not read, in any kernel.
|
|
@@ -624,11 +665,13 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
|
624
665
|
/** The reader was handed no packs at all. */
|
|
625
666
|
export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
|
|
626
667
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
668
|
+
* The two NOTEs (5.5.3 v1.20): findings on the reading with no contribution,
|
|
669
|
+
* printed in the scope sentence so a clean verdict never hides either.
|
|
670
|
+
* `pack_undeclared` is a note only where the reading holds no hand-off record;
|
|
671
|
+
* where it holds one, it is the exception of that name.
|
|
630
672
|
*/
|
|
631
|
-
export const
|
|
673
|
+
export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
|
|
674
|
+
export const CASE_NOTE_KINDS = [PACK_UNDECLARED, CONTENT_WITHHELD_BY_DECLARATION];
|
|
632
675
|
/**
|
|
633
676
|
* Every exception this reader can report, as a closed set. A surface may render
|
|
634
677
|
* them and a test may assert over them; nothing outside this array is emitted.
|
|
@@ -652,13 +695,14 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
652
695
|
PACK_HOLDS_NO_RECORDS,
|
|
653
696
|
PACK_KEY_UNREADABLE,
|
|
654
697
|
PACK_DOES_NOT_VERIFY,
|
|
655
|
-
|
|
698
|
+
PACK_GRADE_NOT_VERIFIED,
|
|
699
|
+
PACK_BUNDLE_CONTENT_DEPENDENT,
|
|
656
700
|
RECORD_ENVELOPE_DOES_NOT_VERIFY,
|
|
657
701
|
RECORD_DOES_NOT_VERIFY_IN_ITS_PACK,
|
|
658
702
|
RECORD_CONTENT_MISMATCH,
|
|
703
|
+
RECORD_CONTENT_DEPENDENT,
|
|
659
704
|
RECORD_PROFILE_QUARANTINED,
|
|
660
705
|
CASE_HOLDS_NO_PACKS,
|
|
661
|
-
CASE_HOLDS_NO_HANDOFF_RECORDS,
|
|
662
706
|
];
|
|
663
707
|
/**
|
|
664
708
|
* What each exception kind contributes to the case verdict. It is a TABLE and
|
|
@@ -671,10 +715,10 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
671
715
|
* contributes a clean verdict, which is what makes the invariant in `verdictOf`
|
|
672
716
|
* hold.
|
|
673
717
|
*
|
|
674
|
-
* `
|
|
675
|
-
* contributes the pack's
|
|
676
|
-
*
|
|
677
|
-
*
|
|
718
|
+
* `PACK_GRADE_NOT_VERIFIED` is the one row that is not a constant: it
|
|
719
|
+
* contributes the pack's case grade, which is never `verified` (the exception
|
|
720
|
+
* is emitted only when it is not). `null` marks that row, and `readCase` fills
|
|
721
|
+
* it from `status`.
|
|
678
722
|
*/
|
|
679
723
|
const EXCEPTION_VERDICT = {
|
|
680
724
|
[COUNTERPART_ABSENT]: "incomplete",
|
|
@@ -695,14 +739,26 @@ const EXCEPTION_VERDICT = {
|
|
|
695
739
|
[PACK_HOLDS_NO_RECORDS]: "incomplete",
|
|
696
740
|
[PACK_KEY_UNREADABLE]: "incomplete",
|
|
697
741
|
[PACK_DOES_NOT_VERIFY]: "failed",
|
|
698
|
-
[
|
|
742
|
+
[PACK_GRADE_NOT_VERIFIED]: null,
|
|
743
|
+
[PACK_BUNDLE_CONTENT_DEPENDENT]: "incomplete",
|
|
699
744
|
[RECORD_ENVELOPE_DOES_NOT_VERIFY]: "failed",
|
|
700
745
|
[RECORD_DOES_NOT_VERIFY_IN_ITS_PACK]: "failed",
|
|
701
746
|
[RECORD_CONTENT_MISMATCH]: "failed",
|
|
747
|
+
[RECORD_CONTENT_DEPENDENT]: "incomplete",
|
|
702
748
|
[RECORD_PROFILE_QUARANTINED]: "incomplete",
|
|
703
749
|
[CASE_HOLDS_NO_PACKS]: "partial",
|
|
704
|
-
[CASE_HOLDS_NO_HANDOFF_RECORDS]: "partial",
|
|
705
750
|
};
|
|
751
|
+
/**
|
|
752
|
+
* The case grades `PACK_GRADE_NOT_VERIFIED` contributes as themselves. Every
|
|
753
|
+
* other grade a pack can reach (`pending`, `not_found`, `mixed`, which a pack
|
|
754
|
+
* grade passes through and a case verdict never issues) is `incomplete`.
|
|
755
|
+
*/
|
|
756
|
+
const GRADE_CONTRIBUTES = new Set([
|
|
757
|
+
"partial",
|
|
758
|
+
"incomplete",
|
|
759
|
+
"failed",
|
|
760
|
+
"malformed",
|
|
761
|
+
]);
|
|
706
762
|
// ---- The reading ------------------------------------------------------------
|
|
707
763
|
const bodyTypeOf = (bundle) => typeof bundle === "object" && bundle !== null
|
|
708
764
|
? bundle["body_type"]
|
|
@@ -740,6 +796,23 @@ const wrapperHolds = (result) => result.signatureOk && result.bodyHashOk;
|
|
|
740
796
|
* bundle-level error at all, and an exception that named the verdict and no
|
|
741
797
|
* reason would send a reader looking for a defect in the pack as a whole.
|
|
742
798
|
*/
|
|
799
|
+
/**
|
|
800
|
+
* The reasons a reading carries only because of the side map, in the order the
|
|
801
|
+
* verifier wrote them: what the content-dependent kinds are about, and nothing
|
|
802
|
+
* a signature covers.
|
|
803
|
+
*/
|
|
804
|
+
const onlyIn = (withMap, withoutMap) => withMap.filter((reason) => !withoutMap.includes(reason));
|
|
805
|
+
/**
|
|
806
|
+
* Whether a side-content map holds any key. Python's `if pack.content` is
|
|
807
|
+
* the twin: `None` and an empty map both mean the two verify runs are one run.
|
|
808
|
+
*/
|
|
809
|
+
function sideMapHasKeys(content) {
|
|
810
|
+
if (content === null || content === undefined)
|
|
811
|
+
return false;
|
|
812
|
+
if (content instanceof Map)
|
|
813
|
+
return content.size > 0;
|
|
814
|
+
return Object.keys(content).length > 0;
|
|
815
|
+
}
|
|
743
816
|
const bundleReasons = (result) => [
|
|
744
817
|
...result.errors,
|
|
745
818
|
...(result.stamps ?? []).flatMap((stamp) => stamp.errors),
|
|
@@ -775,8 +848,13 @@ function underQuarantine(specVersion) {
|
|
|
775
848
|
/**
|
|
776
849
|
* The case verdict, from the contributions alone.
|
|
777
850
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
851
|
+
* **A proven failure keeps the headline** (`evidence-bundle.md` 5.5.4 v1.16,
|
|
852
|
+
* A17, proof#158): if any contribution is `failed`, the case is `failed`,
|
|
853
|
+
* whatever else contributes, and every other exception stays named beside it.
|
|
854
|
+
* Otherwise one outcome is that outcome, and more than one is `mixed`
|
|
855
|
+
* (anchor#198, criterion 4), so `mixed` never holds a `failed`. `malformed`
|
|
856
|
+
* does NOT keep the headline: it says a record cannot be read, not that a
|
|
857
|
+
* check ran and did not pass. `mixed` is in the 3 vocabulary and coins no word.
|
|
780
858
|
*
|
|
781
859
|
* THE INVARIANT, stated so a mutation test has something to kill: `verified` is
|
|
782
860
|
* reachable ONLY when `exceptions` is empty. Every exception kind carries a
|
|
@@ -785,14 +863,25 @@ function underQuarantine(specVersion) {
|
|
|
785
863
|
* The assertion is here anyway, because that is the property every one of the
|
|
786
864
|
* seventeen findings on this program violated.
|
|
787
865
|
*/
|
|
788
|
-
function verdictOf(exceptions, contributions) {
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
866
|
+
function verdictOf(exceptions, contributions, holdsPacks = true) {
|
|
867
|
+
// A reading that holds at least one pack and no contribution at all is
|
|
868
|
+
// `verified` (5.5.4 v1.20, A23): every pack in it graded `verified` and
|
|
869
|
+
// nothing in it was an exception. Every exception contributes, so no
|
|
870
|
+
// contribution means no exception. A reading with no packs always carries
|
|
871
|
+
// `case_holds_no_packs`, and is defended here anyway: an empty aggregate over
|
|
872
|
+
// nothing reading as `verified` is the defect class itself.
|
|
873
|
+
if (contributions.length === 0) {
|
|
874
|
+
if (exceptions.length > 0) {
|
|
875
|
+
throw new Error(`exceptions with no contribution: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
876
|
+
}
|
|
877
|
+
return holdsPacks ? "verified" : "partial";
|
|
878
|
+
}
|
|
794
879
|
const outcomes = new Set(contributions.map(([, v]) => v));
|
|
795
|
-
const verdict = outcomes.
|
|
880
|
+
const verdict = outcomes.has("failed")
|
|
881
|
+
? "failed"
|
|
882
|
+
: outcomes.size === 1
|
|
883
|
+
? [...outcomes][0]
|
|
884
|
+
: "mixed";
|
|
796
885
|
if (verdict === "verified" && exceptions.length > 0) {
|
|
797
886
|
throw new Error(`verified over a non-empty exception list: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
798
887
|
}
|
|
@@ -826,23 +915,35 @@ function verdictOf(exceptions, contributions) {
|
|
|
826
915
|
* see is the counterpart that is missing.
|
|
827
916
|
*/
|
|
828
917
|
export async function readCase(packs) {
|
|
918
|
+
// A `null` slot is held for each pack that carries no declaration, and
|
|
919
|
+
// settled once the pile says whether the reading holds a hand-off record.
|
|
829
920
|
const exceptions = [];
|
|
830
921
|
const contributions = [];
|
|
831
922
|
const note = (kind, fields = {}) => {
|
|
832
923
|
exceptions.push({ kind, ...fields });
|
|
833
|
-
// `null` is
|
|
834
|
-
|
|
924
|
+
// `null` is PACK_GRADE_NOT_VERIFIED: the pack's case grade, never
|
|
925
|
+
// verified, and `incomplete` for a word a pack grade passes through and a
|
|
926
|
+
// case verdict never issues (5.5.1 step 1 v1.20).
|
|
927
|
+
let contribution = EXCEPTION_VERDICT[kind];
|
|
928
|
+
if (contribution === null) {
|
|
929
|
+
const status = fields.status;
|
|
930
|
+
contribution = GRADE_CONTRIBUTES.has(status) ? status : "incomplete";
|
|
931
|
+
}
|
|
835
932
|
contributions.push([fields.eventId ?? fields.packRef ?? "case", contribution]);
|
|
836
933
|
};
|
|
837
934
|
if (packs.length === 0) {
|
|
838
935
|
note(CASE_HOLDS_NO_PACKS);
|
|
936
|
+
const named = exceptions;
|
|
937
|
+
const contributed = contributions;
|
|
839
938
|
return {
|
|
840
|
-
verdict: verdictOf(
|
|
841
|
-
exceptions,
|
|
939
|
+
verdict: verdictOf(named, contributed, false),
|
|
940
|
+
exceptions: named,
|
|
842
941
|
records: [],
|
|
843
|
-
contributions,
|
|
942
|
+
contributions: contributed,
|
|
844
943
|
handoffRecords: 0,
|
|
845
944
|
onChainChecked: false,
|
|
945
|
+
notes: [],
|
|
946
|
+
packs: [],
|
|
846
947
|
};
|
|
847
948
|
}
|
|
848
949
|
// Step 1 and 2, pack by pack, in the order the reader was given them.
|
|
@@ -884,6 +985,15 @@ export async function readCase(packs) {
|
|
|
884
985
|
// below, from the pack envelope and never from the body, so no body type, no
|
|
885
986
|
// disclosure choice and no record content decides whether it is read.
|
|
886
987
|
const holderKeys = new Set();
|
|
988
|
+
// One reading per pack, in the order the reader was given them (5.5.4 v1.20).
|
|
989
|
+
const packReadings = [];
|
|
990
|
+
// The NOTEs (5.5.3 v1.20), with the pack position they are about.
|
|
991
|
+
const caseNotes = [];
|
|
992
|
+
// The role each pack's readable v2 declaration signs for each record, keyed
|
|
993
|
+
// by the pack's POSITION for the reason `claimsByPack` is.
|
|
994
|
+
const rolesByPack = new Map();
|
|
995
|
+
// The undeclared packs' slots, settled after the pile is read.
|
|
996
|
+
const undeclaredSlots = [];
|
|
887
997
|
// How many packs this reader could not tie to a key: an unreadable wrapper
|
|
888
998
|
// key, or a wrapper that does not hold. While it is not zero, "no pack in this
|
|
889
999
|
// case is signed by the named key" is not established, so the innocent
|
|
@@ -899,6 +1009,13 @@ export async function readCase(packs) {
|
|
|
899
1009
|
// of these spellings (proof#135) and the reading must not.
|
|
900
1010
|
note(PACK_KEY_UNREADABLE, { packRef: pack.ref });
|
|
901
1011
|
unattributed += 1;
|
|
1012
|
+
packReadings.push({
|
|
1013
|
+
ref: pack.ref,
|
|
1014
|
+
signingKey: null,
|
|
1015
|
+
grade: null,
|
|
1016
|
+
recordsWithheld: 0,
|
|
1017
|
+
declarationVersion: null,
|
|
1018
|
+
});
|
|
902
1019
|
continue;
|
|
903
1020
|
}
|
|
904
1021
|
// The precondition `HeldRecord` states, met HERE rather than trusted to a
|
|
@@ -910,28 +1027,121 @@ export async function readCase(packs) {
|
|
|
910
1027
|
content: (pack.content ?? undefined),
|
|
911
1028
|
});
|
|
912
1029
|
witnessChecked.push(verified.onChainChecked);
|
|
1030
|
+
// The same pack with the side map EMPTY (proof#159): what fails here is
|
|
1031
|
+
// what a signature covers, and what fails only above rests on bytes nobody
|
|
1032
|
+
// signed. Skipped where there is no map, because the two runs are then the
|
|
1033
|
+
// same run.
|
|
1034
|
+
const signed = sideMapHasKeys(pack.content)
|
|
1035
|
+
? await verifyEvidenceBundle(pack.bundle, {})
|
|
1036
|
+
: verified;
|
|
913
1037
|
// WHOSE PACK IS IT. The wrapper, and only the wrapper.
|
|
914
1038
|
if (!wrapperHolds(verified)) {
|
|
915
1039
|
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref });
|
|
916
1040
|
unattributed += 1;
|
|
1041
|
+
packReadings.push({
|
|
1042
|
+
ref: pack.ref,
|
|
1043
|
+
signingKey: holder,
|
|
1044
|
+
grade: null,
|
|
1045
|
+
recordsWithheld: 0,
|
|
1046
|
+
declarationVersion: null,
|
|
1047
|
+
});
|
|
917
1048
|
continue;
|
|
918
1049
|
}
|
|
919
1050
|
holderKeys.add(holder);
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1051
|
+
const bodyType = bodyTypeOf(pack.bundle);
|
|
1052
|
+
const records = packRecords(pack.bundle, pack.content);
|
|
1053
|
+
// The per-record gradings below are joined to `records` BY INDEX, so a
|
|
1054
|
+
// verifier that graded a different number of records than the pack holds
|
|
1055
|
+
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
1056
|
+
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
1057
|
+
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
1058
|
+
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
1059
|
+
// must never crash on a pack a party can build, so the gap is graded rather
|
|
1060
|
+
// than indexed into, and the pack's records are dropped exactly as a
|
|
1061
|
+
// `malformed` pack's are (gate 4 on proof#140, finding 3).
|
|
1062
|
+
const ungraded = verified.events.length !== records.length || signed.events.length !== records.length;
|
|
1063
|
+
const knownBody = CASE_BODY_TYPES.includes(bodyType);
|
|
1064
|
+
// WHAT IS WRONG INSIDE IT, from here on. First the pack's own GRADE, which
|
|
1065
|
+
// propagates: the case never reads above its weakest pack (5.5.1 step 1
|
|
1066
|
+
// v1.20, A23; up to v1.19 it was the bundle status).
|
|
1067
|
+
let check = null;
|
|
1068
|
+
let full = null;
|
|
1069
|
+
let causes = [];
|
|
1070
|
+
let grade;
|
|
1071
|
+
if (signed.status === "malformed") {
|
|
1072
|
+
grade = "malformed";
|
|
1073
|
+
}
|
|
1074
|
+
else if (!knownBody) {
|
|
1075
|
+
grade = signed.status;
|
|
1076
|
+
}
|
|
1077
|
+
else if (ungraded) {
|
|
1078
|
+
grade = signed.status === "verified" ? "malformed" : signed.status;
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
check = checkDeclaration(pack.bundle, pack.content);
|
|
1082
|
+
const completeness = completenessNotes(pack.bundle);
|
|
1083
|
+
full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
|
|
1084
|
+
const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null));
|
|
1085
|
+
// NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
|
|
1086
|
+
// where it is `failed` or `malformed`; a failure only the side map
|
|
1087
|
+
// produced is `incomplete`; otherwise the full grade.
|
|
1088
|
+
if (bareGrade.verdict === "failed" || bareGrade.verdict === "malformed") {
|
|
1089
|
+
grade = bareGrade.verdict;
|
|
1090
|
+
causes = bareGrade.causes.map((c) => c.kind);
|
|
1091
|
+
}
|
|
1092
|
+
else if (full.verdict === "failed" || full.verdict === "malformed") {
|
|
1093
|
+
grade = "incomplete";
|
|
1094
|
+
causes = full.causes.map((c) => c.kind);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
grade = full.verdict;
|
|
1098
|
+
causes = full.causes.map((c) => c.kind);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (grade !== "verified") {
|
|
1102
|
+
const reasons = bundleReasons(signed);
|
|
1103
|
+
if (knownBody && ungraded && signed.status !== "malformed") {
|
|
1104
|
+
// Said, because nothing else says it: the verifier's own reasons are
|
|
1105
|
+
// about the bundle, and this pack's records are dropped for a reason
|
|
1106
|
+
// of this reader's.
|
|
1107
|
+
reasons.push(`this verifier graded ${verified.events.length} of the ${records.length} ` +
|
|
1108
|
+
"record(s) the pack holds, so none of them is read");
|
|
1109
|
+
}
|
|
1110
|
+
note(PACK_GRADE_NOT_VERIFIED, {
|
|
924
1111
|
packRef: pack.ref,
|
|
925
|
-
status:
|
|
926
|
-
|
|
1112
|
+
status: grade,
|
|
1113
|
+
causes,
|
|
1114
|
+
reasons,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
if (verified.status === "failed" && signed.status !== "failed") {
|
|
1118
|
+
note(PACK_BUNDLE_CONTENT_DEPENDENT, {
|
|
1119
|
+
packRef: pack.ref,
|
|
1120
|
+
reasons: onlyIn(bundleReasons(verified), bundleReasons(signed)),
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
const recordsWithheld = full !== null ? full.recordsWithheld : 0;
|
|
1124
|
+
packReadings.push({
|
|
1125
|
+
ref: pack.ref,
|
|
1126
|
+
signingKey: holder,
|
|
1127
|
+
grade,
|
|
1128
|
+
recordsWithheld,
|
|
1129
|
+
declarationVersion: knownBody ? declarationVersionOf(pack.bundle) : null,
|
|
1130
|
+
});
|
|
1131
|
+
if (recordsWithheld > 0) {
|
|
1132
|
+
// A NOTE, with no contribution (5.5.3 v1.20): the holder signed that
|
|
1133
|
+
// these records' content does not travel, and the scope sentence counts
|
|
1134
|
+
// them, so a clean verdict never hides them.
|
|
1135
|
+
caseNotes.push({
|
|
1136
|
+
position,
|
|
1137
|
+
note: { kind: CONTENT_WITHHELD_BY_DECLARATION, packRef: pack.ref, count: recordsWithheld },
|
|
927
1138
|
});
|
|
928
1139
|
}
|
|
929
1140
|
// The body is one the verifier could not read, so it graded no record, and
|
|
930
1141
|
// nothing below has a grading to read. Named above.
|
|
931
|
-
if (
|
|
1142
|
+
if (signed.status === "malformed")
|
|
932
1143
|
continue;
|
|
933
|
-
|
|
934
|
-
if (!CASE_BODY_TYPES.includes(bodyType)) {
|
|
1144
|
+
if (!knownBody) {
|
|
935
1145
|
// `sanning.evidence.export/v2` lands here, and it is NOT unwrapped: its
|
|
936
1146
|
// records sit under `body.source_bundle`, and reading them is a change to
|
|
937
1147
|
// which packs this reader grades, so it is a decision of its own and not
|
|
@@ -941,58 +1151,39 @@ export async function readCase(packs) {
|
|
|
941
1151
|
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref });
|
|
942
1152
|
continue;
|
|
943
1153
|
}
|
|
944
|
-
|
|
945
|
-
//
|
|
946
|
-
|
|
947
|
-
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
948
|
-
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
949
|
-
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
950
|
-
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
951
|
-
// must never crash on a pack a party can build, so the gap is NAMED rather
|
|
952
|
-
// than indexed into, and the pack's records are dropped exactly as a
|
|
953
|
-
// `malformed` pack's are (gate 4 on proof#140, finding 3: under the
|
|
954
|
-
// mutation that dropped `bodyHashOk` from the wrapper test, this kernel
|
|
955
|
-
// threw `Cannot read properties of undefined (reading 'envelopeOk')` here
|
|
956
|
-
// and nothing in the ladder said so).
|
|
957
|
-
if (verified.events.length !== records.length) {
|
|
958
|
-
note(PACK_BUNDLE_NOT_VERIFIED, {
|
|
959
|
-
packRef: pack.ref,
|
|
960
|
-
status: verified.status,
|
|
961
|
-
reasons: [
|
|
962
|
-
`this verifier graded ${verified.events.length} of the ` +
|
|
963
|
-
`${records.length} record(s) the pack holds, so none of them is read`,
|
|
964
|
-
],
|
|
965
|
-
});
|
|
1154
|
+
// Graded above (`malformed` where the verifier called it `verified`), and
|
|
1155
|
+
// none of its records are read.
|
|
1156
|
+
if (ungraded)
|
|
966
1157
|
continue;
|
|
967
|
-
}
|
|
968
1158
|
if (records.length === 0) {
|
|
969
1159
|
note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref });
|
|
970
1160
|
continue;
|
|
971
1161
|
}
|
|
972
|
-
const
|
|
1162
|
+
const checked = check;
|
|
973
1163
|
claimsByPack.set(position, declarationClaims(pack.bundle));
|
|
974
|
-
|
|
1164
|
+
rolesByPack.set(position, new Map((checked.roles ?? []).map((r) => [r.eventId, r.role])));
|
|
1165
|
+
if (checked.contentFindings.length > 0) {
|
|
975
1166
|
// BESIDE the deviation exception and never instead of it: a pack can do
|
|
976
1167
|
// both, over different records, and the two say different things. This
|
|
977
1168
|
// one is `incomplete` because whoever assembled the case chose the bytes
|
|
978
1169
|
// it turns on (proof#147).
|
|
979
1170
|
note(PACK_DECLARATION_CONTENT_DEPENDENT, {
|
|
980
1171
|
packRef: pack.ref,
|
|
981
|
-
count:
|
|
1172
|
+
count: checked.contentFindings.length,
|
|
982
1173
|
});
|
|
983
1174
|
}
|
|
984
|
-
if (
|
|
1175
|
+
if (checked.verdict === "failed") {
|
|
985
1176
|
note(PACK_DECLARATION_DEVIATES, {
|
|
986
1177
|
packRef: pack.ref,
|
|
987
|
-
count:
|
|
1178
|
+
count: checked.deviations.length,
|
|
988
1179
|
});
|
|
989
1180
|
}
|
|
990
|
-
else if (
|
|
1181
|
+
else if (checked.contentFindings.length > 0) {
|
|
991
1182
|
// Already named above. `partial` here is not "the reader cannot read the
|
|
992
1183
|
// claim": the claim was read, and part of it rests on bytes nobody
|
|
993
1184
|
// signed.
|
|
994
1185
|
}
|
|
995
|
-
else if (
|
|
1186
|
+
else if (checked.verdict === "verified") {
|
|
996
1187
|
// A pack that agrees with its own claim contributes NOTHING, rather than
|
|
997
1188
|
// a `verified` of its own. The case verdict is over the hand-off
|
|
998
1189
|
// readings, and criterion 4's "mixed" is about mixed HAND-OFF outcomes: a
|
|
@@ -1001,7 +1192,7 @@ export async function readCase(packs) {
|
|
|
1001
1192
|
// read `mixed` and the failure would be the quieter half of a two-word
|
|
1002
1193
|
// answer. A pack-level PROBLEM still contributes, through its exception.
|
|
1003
1194
|
}
|
|
1004
|
-
else if (
|
|
1195
|
+
else if (checked.present) {
|
|
1005
1196
|
// `partial` with a declaration present is 5.4.3's "a claim this reader
|
|
1006
1197
|
// cannot read", which `gradePack` already carries to `incomplete`. The
|
|
1007
1198
|
// same posture here, for the same reason: the field has no accept-set and
|
|
@@ -1009,7 +1200,18 @@ export async function readCase(packs) {
|
|
|
1009
1200
|
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref });
|
|
1010
1201
|
}
|
|
1011
1202
|
else {
|
|
1012
|
-
|
|
1203
|
+
// WHAT IT CONTRIBUTES TURNS ON THE WHOLE READING (5.5.1 step 2 v1.20,
|
|
1204
|
+
// A23, I4): `partial` where the reading holds a hand-off record, and a
|
|
1205
|
+
// NOTE where it holds none. That is not known until the pile is read, so
|
|
1206
|
+
// the slot is held HERE, in pack order, and settled below.
|
|
1207
|
+
undeclaredSlots.push({
|
|
1208
|
+
exceptionAt: exceptions.length,
|
|
1209
|
+
contributionAt: contributions.length,
|
|
1210
|
+
position,
|
|
1211
|
+
ref: pack.ref,
|
|
1212
|
+
});
|
|
1213
|
+
exceptions.push(null);
|
|
1214
|
+
contributions.push(null);
|
|
1013
1215
|
}
|
|
1014
1216
|
// Each record on its OWN envelope. `packRecords` and the verifier both walk
|
|
1015
1217
|
// `body.events` in order and emit one entry per element, so the index is
|
|
@@ -1017,6 +1219,7 @@ export async function readCase(packs) {
|
|
|
1017
1219
|
for (let index = 0; index < records.length; index += 1) {
|
|
1018
1220
|
const record = records[index];
|
|
1019
1221
|
const graded = verified.events[index];
|
|
1222
|
+
const bare = signed.events[index];
|
|
1020
1223
|
if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
|
|
1021
1224
|
note(RECORD_PROFILE_QUARANTINED, { packRef: pack.ref, eventId: record.eventId });
|
|
1022
1225
|
continue;
|
|
@@ -1043,20 +1246,31 @@ export async function readCase(packs) {
|
|
|
1043
1246
|
});
|
|
1044
1247
|
continue;
|
|
1045
1248
|
}
|
|
1046
|
-
// Named, and the record stays in the pile (see the kinds).
|
|
1047
|
-
|
|
1249
|
+
// Named, and the record stays in the pile (see the kinds). A mismatch
|
|
1250
|
+
// the signed body carries on its own is `failed`; one that appears only
|
|
1251
|
+
// with the side map is `incomplete` (proof#159).
|
|
1252
|
+
if (bare.contentOk === false) {
|
|
1048
1253
|
note(RECORD_CONTENT_MISMATCH, {
|
|
1049
1254
|
packRef: pack.ref,
|
|
1050
1255
|
eventId: record.eventId,
|
|
1051
|
-
reasons: [...
|
|
1256
|
+
reasons: [...bare.errors],
|
|
1052
1257
|
});
|
|
1053
1258
|
}
|
|
1054
|
-
if (
|
|
1055
|
-
(
|
|
1259
|
+
else if (graded.contentOk === false) {
|
|
1260
|
+
note(RECORD_CONTENT_DEPENDENT, {
|
|
1261
|
+
packRef: pack.ref,
|
|
1262
|
+
eventId: record.eventId,
|
|
1263
|
+
reasons: onlyIn(graded.errors, bare.errors),
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
// Every other check on the SIGNED reading, so side content can neither
|
|
1267
|
+
// add a failure here nor take one away.
|
|
1268
|
+
if (!(bare.inclusionOk && bare.checkpointBound) ||
|
|
1269
|
+
(!bare.ok && bare.contentOk !== false)) {
|
|
1056
1270
|
note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
|
|
1057
1271
|
packRef: pack.ref,
|
|
1058
1272
|
eventId: record.eventId,
|
|
1059
|
-
reasons: [...
|
|
1273
|
+
reasons: [...bare.errors],
|
|
1060
1274
|
});
|
|
1061
1275
|
}
|
|
1062
1276
|
held.push({
|
|
@@ -1085,6 +1299,7 @@ export async function readCase(packs) {
|
|
|
1085
1299
|
findings: [...reading.findings],
|
|
1086
1300
|
failedChecks: [...reading.failedChecks],
|
|
1087
1301
|
counterparts: [...reading.candidates],
|
|
1302
|
+
role: rolesByPack.get(position)?.get(eventId) ?? null,
|
|
1088
1303
|
});
|
|
1089
1304
|
// proof#137's door, named. Read from the ONE reader of the declaration
|
|
1090
1305
|
// (`declarationClaims`), never from a second parse here. Only a READABLE
|
|
@@ -1098,9 +1313,13 @@ export async function readCase(packs) {
|
|
|
1098
1313
|
// `not_a_handoff_record` exit it was a check the audited party switched off
|
|
1099
1314
|
// by respelling two fields, and behind the `malformed` exit it was one a
|
|
1100
1315
|
// party switched off by breaking its own record.
|
|
1316
|
+
//
|
|
1317
|
+
// v1's ALONE (5.5.1 v1.20, A21). A v2 pack's claim check is rule 9, which
|
|
1318
|
+
// the declaration check already ran on the signed body.
|
|
1101
1319
|
const claims = claimsByPack.get(position);
|
|
1102
1320
|
if (claims !== undefined &&
|
|
1103
1321
|
claims.readable &&
|
|
1322
|
+
claims.version === DECLARATION_VERSION &&
|
|
1104
1323
|
!claims.claimedSides.includes(eventId)) {
|
|
1105
1324
|
note(RECORD_UNCLAIMED, { packRef, eventId });
|
|
1106
1325
|
}
|
|
@@ -1190,17 +1409,54 @@ export async function readCase(packs) {
|
|
|
1190
1409
|
contributions.push([`${packRef}/${eventId}`, "verified"]);
|
|
1191
1410
|
}
|
|
1192
1411
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1412
|
+
// A reading with no hand-off record in it is not an exception (5.5.3 v1.20,
|
|
1413
|
+
// A23, D6). The undeclared packs, settled: the exception `pack_undeclared`,
|
|
1414
|
+
// contributing `partial`, in the slot it was given in pack order where the
|
|
1415
|
+
// reading holds a hand-off record; a NOTE with no contribution where it holds
|
|
1416
|
+
// none.
|
|
1417
|
+
for (const slot of undeclaredSlots) {
|
|
1418
|
+
if (handoffRecords > 0) {
|
|
1419
|
+
exceptions[slot.exceptionAt] = { kind: PACK_UNDECLARED, packRef: slot.ref };
|
|
1420
|
+
contributions[slot.contributionAt] = [slot.ref, EXCEPTION_VERDICT[PACK_UNDECLARED]];
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
caseNotes.push({ position: slot.position, note: { kind: PACK_UNDECLARED, packRef: slot.ref } });
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const named = exceptions.filter((e) => e !== null);
|
|
1427
|
+
const contributed = contributions.filter((c) => c !== null);
|
|
1428
|
+
// Pack order, stable. A pack carries at most one note: one with no
|
|
1429
|
+
// declaration withholds nothing by one.
|
|
1430
|
+
const notes = caseNotes
|
|
1431
|
+
.map((n, i) => ({ ...n, i }))
|
|
1432
|
+
.sort((a, b) => a.position - b.position || a.i - b.i)
|
|
1433
|
+
.map((n) => n.note);
|
|
1195
1434
|
return {
|
|
1196
|
-
verdict: verdictOf(
|
|
1197
|
-
exceptions,
|
|
1435
|
+
verdict: verdictOf(named, contributed),
|
|
1436
|
+
exceptions: named,
|
|
1198
1437
|
records,
|
|
1199
|
-
contributions,
|
|
1438
|
+
contributions: contributed,
|
|
1200
1439
|
handoffRecords,
|
|
1201
1440
|
onChainChecked: witnessChecked.length > 0 && witnessChecked.every((c) => c),
|
|
1441
|
+
notes,
|
|
1442
|
+
packs: packReadings,
|
|
1202
1443
|
};
|
|
1203
1444
|
}
|
|
1445
|
+
/**
|
|
1446
|
+
* `body.declaration.declaration_version` where it is one this reader knows,
|
|
1447
|
+
* and `null` otherwise.
|
|
1448
|
+
*/
|
|
1449
|
+
function declarationVersionOf(bundle) {
|
|
1450
|
+
const at = (v, k) => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
1451
|
+
? v[k]
|
|
1452
|
+
: undefined;
|
|
1453
|
+
const declaration = at(at(bundle, "body"), "declaration");
|
|
1454
|
+
if (typeof declaration !== "object" || declaration === null || Array.isArray(declaration)) {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
const version = declaration["declaration_version"];
|
|
1458
|
+
return version === DECLARATION_VERSION || version === DECLARATION_VERSION_V2 ? version : null;
|
|
1459
|
+
}
|
|
1204
1460
|
// ---- Presentation (optional) ------------------------------------------------
|
|
1205
1461
|
/**
|
|
1206
1462
|
* The house wording for one exception.
|
|
@@ -1285,13 +1541,20 @@ export function renderException(exception) {
|
|
|
1285
1541
|
return (`${exception.packRef} carries a wrapper signature over its body that ` +
|
|
1286
1542
|
"does not verify, so none of its records are read and the key it " +
|
|
1287
1543
|
"names is not counted as its signer");
|
|
1288
|
-
case
|
|
1289
|
-
let head = `${exception.packRef}
|
|
1290
|
-
|
|
1544
|
+
case PACK_GRADE_NOT_VERIFIED: {
|
|
1545
|
+
let head = `${exception.packRef} grades ${(exception.status ?? "").toUpperCase()} as a pack`;
|
|
1546
|
+
if (exception.causes !== undefined && exception.causes.length > 0) {
|
|
1547
|
+
head += ` (${exception.causes.join(", ")})`;
|
|
1548
|
+
}
|
|
1291
1549
|
if (exception.status === "malformed")
|
|
1292
1550
|
head += ", so none of its records are read";
|
|
1293
1551
|
return head + because(exception.reasons);
|
|
1294
1552
|
}
|
|
1553
|
+
case PACK_BUNDLE_CONTENT_DEPENDENT:
|
|
1554
|
+
return (`${exception.packRef} reads FAILED as a bundle only together with ` +
|
|
1555
|
+
"the unsigned content handed in beside it, which anyone could have " +
|
|
1556
|
+
"supplied, so whether that failure is the pack's is UNKNOWN" +
|
|
1557
|
+
because(exception.reasons));
|
|
1295
1558
|
case RECORD_ENVELOPE_DOES_NOT_VERIFY:
|
|
1296
1559
|
return (`${where} is in a pack whose wrapper verifies, and its own envelope ` +
|
|
1297
1560
|
"does not verify, so it is not read as a hand-off record" +
|
|
@@ -1304,45 +1567,128 @@ export function renderException(exception) {
|
|
|
1304
1567
|
return (`${where} is read with content that does not match what it commits ` +
|
|
1305
1568
|
"to, and it is still read as a hand-off record" +
|
|
1306
1569
|
because(exception.reasons));
|
|
1570
|
+
case RECORD_CONTENT_DEPENDENT:
|
|
1571
|
+
return (`${where} commits to content that the unsigned content handed in ` +
|
|
1572
|
+
"beside its pack does not match, and anyone could have supplied " +
|
|
1573
|
+
"that content, so what it says about the record is UNKNOWN; the " +
|
|
1574
|
+
"record is still read as a hand-off record" +
|
|
1575
|
+
because(exception.reasons));
|
|
1307
1576
|
case RECORD_PROFILE_QUARANTINED:
|
|
1308
1577
|
return (`${where} is signed under a profile this case reader does not read ` +
|
|
1309
1578
|
"while its admission is undecided, so what it says is UNKNOWN");
|
|
1310
1579
|
case CASE_HOLDS_NO_PACKS:
|
|
1311
1580
|
return "this case holds no packs";
|
|
1312
|
-
case CASE_HOLDS_NO_HANDOFF_RECORDS:
|
|
1313
|
-
return ("no record this verifier read in this case is a hand-off record, " +
|
|
1314
|
-
"so no hand-off was verified");
|
|
1315
1581
|
default:
|
|
1316
1582
|
return exception.kind;
|
|
1317
1583
|
}
|
|
1318
1584
|
}
|
|
1585
|
+
/** The house wording for one NOTE. Offered, not imposed, as `renderException` is. */
|
|
1586
|
+
export function renderCaseNote(note) {
|
|
1587
|
+
switch (note.kind) {
|
|
1588
|
+
case CONTENT_WITHHELD_BY_DECLARATION:
|
|
1589
|
+
return (`${note.packRef} withholds the content of ${note.count} record(s) by ` +
|
|
1590
|
+
"its own signed declaration: their signatures and commitments were " +
|
|
1591
|
+
"checked, their content was not");
|
|
1592
|
+
case PACK_UNDECLARED:
|
|
1593
|
+
return `${note.packRef} does not say what it holds`;
|
|
1594
|
+
default:
|
|
1595
|
+
return note.kind;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1319
1598
|
/**
|
|
1320
|
-
*
|
|
1599
|
+
* The scope sentence, printed directly under the headline (evidence-bundle
|
|
1600
|
+
* 5.5.6 v1.20, A26), byte for byte the text `render_case_summary` returns.
|
|
1601
|
+
*
|
|
1602
|
+
* The packs in the reading; the distinct keys, compared as keys, that signed
|
|
1603
|
+
* the wrapper of a pack whose wrapper held; the hand-off records; the
|
|
1604
|
+
* exceptions; and that the reading was made offline. Where any pack holds
|
|
1605
|
+
* records withheld by its declaration, their total; where any pack is noted
|
|
1606
|
+
* `pack_undeclared`, how many. Those two lines are part of what the headline
|
|
1607
|
+
* MEANS, and a surface MUST print them with it.
|
|
1321
1608
|
*
|
|
1322
|
-
*
|
|
1323
|
-
* the
|
|
1324
|
-
* that differ by a word.
|
|
1609
|
+
* What was not checked on the public record is no longer a clause here. It is
|
|
1610
|
+
* the witness line (`renderWitnessLine`), printed with every headline.
|
|
1325
1611
|
*/
|
|
1326
1612
|
export function renderCaseSummary(reading) {
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
? ""
|
|
1339
|
-
: "; no anchor_ref in it was re-fetched on chain, which this reader never does";
|
|
1340
|
-
if (reading.verdict === "verified") {
|
|
1341
|
-
return (`this case verifies across ${reading.records.length} record(s): ` +
|
|
1342
|
-
`${n} hand-off record(s), every one matched, no exceptions` +
|
|
1343
|
-
scope);
|
|
1613
|
+
const keys = new Set(reading.packs.filter((p) => p.grade !== null).map((p) => p.signingKey));
|
|
1614
|
+
const withheld = reading.packs.reduce((n, p) => n + p.recordsWithheld, 0);
|
|
1615
|
+
const undeclared = reading.notes.filter((n) => n.kind === PACK_UNDECLARED).length;
|
|
1616
|
+
const lines = [
|
|
1617
|
+
`${reading.packs.length} pack(s), ${keys.size} signing key(s), ` +
|
|
1618
|
+
`${reading.handoffRecords} hand-off record(s), ` +
|
|
1619
|
+
`${reading.exceptions.length} exception(s), read offline.`,
|
|
1620
|
+
];
|
|
1621
|
+
if (withheld > 0) {
|
|
1622
|
+
lines.push(`${withheld} record(s)' content withheld by their holder: their ` +
|
|
1623
|
+
"signatures and commitments were checked, their content was not.");
|
|
1344
1624
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1625
|
+
if (undeclared > 0)
|
|
1626
|
+
lines.push(`${undeclared} pack(s) do not say what they hold.`);
|
|
1627
|
+
return lines.join(" ");
|
|
1628
|
+
}
|
|
1629
|
+
// ---- The headline and the witness line (evidence-bundle 5.5.6 v1.20) -------
|
|
1630
|
+
/**
|
|
1631
|
+
* The one display title of each of the eight 3 words. A surface prints the
|
|
1632
|
+
* title, never the word, and never a title of its own (#173, A18). The wording
|
|
1633
|
+
* is Will's to change through the glossary (DL-71); what each MEANS is pinned
|
|
1634
|
+
* in the spec.
|
|
1635
|
+
*/
|
|
1636
|
+
export const VERDICT_DISPLAY_TITLES = {
|
|
1637
|
+
verified: "Verified",
|
|
1638
|
+
partial: "Partial",
|
|
1639
|
+
pending: "Pending",
|
|
1640
|
+
incomplete: "Incomplete",
|
|
1641
|
+
failed: "Failed",
|
|
1642
|
+
malformed: "Not readable",
|
|
1643
|
+
not_found: "Not found",
|
|
1644
|
+
mixed: "Mixed",
|
|
1645
|
+
};
|
|
1646
|
+
/**
|
|
1647
|
+
* What a reader may rely on, for each of the six words a case reading reaches
|
|
1648
|
+
* (5.5.6 v1.20, A26), the same for every drop. `pending` and `not_found`
|
|
1649
|
+
* never head a case reading: they are witness states.
|
|
1650
|
+
*/
|
|
1651
|
+
export const CASE_RELIANCE = {
|
|
1652
|
+
verified: "Every pack in this drop is genuine and unaltered and agrees with its " +
|
|
1653
|
+
"own signed declaration. Every hand-off with both sides here is " +
|
|
1654
|
+
"intact. Every other record is one its holder signed as a step. " +
|
|
1655
|
+
"Nothing here could not be checked. This was read offline, on this " +
|
|
1656
|
+
"device. It does not say these packs are everything the parties " +
|
|
1657
|
+
"recorded.",
|
|
1658
|
+
partial: "Nothing here was proven wrong, and a check could not be completed: a " +
|
|
1659
|
+
"pack names no witness, or a pack in a hand-off reading does not say " +
|
|
1660
|
+
"what it holds. The exceptions say which.",
|
|
1661
|
+
incomplete: "Nothing here was proven wrong, and part of it could not be checked: " +
|
|
1662
|
+
"the other side of a hand-off is not in this drop, bytes were not " +
|
|
1663
|
+
"handed over, or a claim could not be read. Each exception says what " +
|
|
1664
|
+
"is missing and, where a key is known, whose pack would supply it.",
|
|
1665
|
+
failed: "A check ran and did not pass. Do not rely on this drop until the " +
|
|
1666
|
+
"named failure is explained. The failure is named on the pack or " +
|
|
1667
|
+
"record it is about, and every other exception is listed beside it.",
|
|
1668
|
+
malformed: "Part of this drop could not be read as evidence, and nothing was " +
|
|
1669
|
+
"proven wrong. The exceptions name what could not be read.",
|
|
1670
|
+
mixed: "The readings here differ and none of them failed: some are verified " +
|
|
1671
|
+
"and some could not be checked. Read each pack's row and each " +
|
|
1672
|
+
"exception before relying on any of it.",
|
|
1673
|
+
};
|
|
1674
|
+
/** The three witness states, and how each is printed (5.5.6, D7). No other. */
|
|
1675
|
+
export const WITNESS_STATES = {
|
|
1676
|
+
confirmed: "Confirmed on the public record",
|
|
1677
|
+
not_checked: "Not checked",
|
|
1678
|
+
differs: "The public record holds different bytes",
|
|
1679
|
+
};
|
|
1680
|
+
/** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
|
|
1681
|
+
export const WITNESS_NOT_LOOKED_UP = Object.freeze({
|
|
1682
|
+
state: "not_checked",
|
|
1683
|
+
reasons: ["no lookup was run"],
|
|
1684
|
+
services: [],
|
|
1685
|
+
});
|
|
1686
|
+
/** The witness line as both kernels print it. */
|
|
1687
|
+
export function renderWitnessLine(line) {
|
|
1688
|
+
let text = `Witness: ${WITNESS_STATES[line.state]}`;
|
|
1689
|
+
if (line.reasons.length > 0)
|
|
1690
|
+
text += `: ${line.reasons.join("; ")}`;
|
|
1691
|
+
if (line.services.length > 0)
|
|
1692
|
+
text += ` (answered: ${line.services.join(", ")})`;
|
|
1693
|
+
return text;
|
|
1348
1694
|
}
|