@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.
Files changed (48) hide show
  1. package/README.md +25 -12
  2. package/dist/agent-proof.d.ts +2 -2
  3. package/dist/agent-proof.js +4 -4
  4. package/dist/case-file.d.ts +126 -0
  5. package/dist/case-file.js +349 -0
  6. package/dist/case.d.ts +173 -26
  7. package/dist/case.js +469 -123
  8. package/dist/cli.d.ts +38 -5
  9. package/dist/cli.js +557 -208
  10. package/dist/crypto.d.ts +1 -1
  11. package/dist/crypto.js +5 -1
  12. package/dist/declaration.d.ts +58 -52
  13. package/dist/declaration.js +256 -33
  14. package/dist/evidence.d.ts +2 -0
  15. package/dist/evidence.js +134 -19
  16. package/dist/exit-codes.d.ts +17 -0
  17. package/dist/exit-codes.js +50 -0
  18. package/dist/grading.d.ts +28 -13
  19. package/dist/grading.js +90 -31
  20. package/dist/index.d.ts +10 -7
  21. package/dist/index.js +13 -6
  22. package/dist/message-parts.d.ts +11 -9
  23. package/dist/message-parts.js +11 -9
  24. package/dist/types.d.ts +1 -0
  25. package/dist/verifier.d.ts +9 -2
  26. package/dist/verifier.js +28 -12
  27. package/dist/zip-reader.d.ts +50 -0
  28. package/dist/zip-reader.js +220 -0
  29. package/package.json +2 -2
  30. package/src/agent-proof.ts +4 -4
  31. package/src/case-file.ts +419 -0
  32. package/src/case.ts +586 -132
  33. package/src/cli.ts +651 -206
  34. package/src/crypto.ts +5 -1
  35. package/src/declaration.ts +248 -52
  36. package/src/evidence.ts +142 -21
  37. package/src/exit-codes.ts +54 -0
  38. package/src/grading.ts +109 -52
  39. package/src/index.ts +58 -2
  40. package/src/message-parts.ts +11 -9
  41. package/src/types.ts +5 -0
  42. package/src/verifier.ts +35 -13
  43. package/src/zip-reader.ts +229 -0
  44. package/vectors/README.md +23 -5
  45. package/vectors/case-file-index.json +668 -0
  46. package/vectors/case-split.json +218 -9
  47. package/vectors/pack-grading.json +68 -4
  48. package/vectors/stamp-on-chain.json +3 -3
package/dist/grading.js CHANGED
@@ -41,13 +41,13 @@
41
41
  // presents a different number of records than a checkpoint
42
42
  // signed for — UNKNOWN
43
43
  //
44
- // `incomplete` is the surface's word for undetermined, and it is deliberately
45
- // NOT `verdict.status` from `evidence-bundle.md` §3. That enum grades a signed
46
- // bundle; this one grades a PACK — a strictly larger object, the bundle plus
47
- // whatever bytes travelled beside it. A pack can be incomplete while the bundle
48
- // it carries is impeccable, which is exactly the case that used to render green
49
- // over zero hashed bytes. Widening §3 to carry `incomplete` would be a spec
50
- // change; keeping the two enums distinct is not.
44
+ // `incomplete` is one of the eight words of `evidence-bundle.md` §3 (v1.17,
45
+ // A18), and it is a PACK verdict, never a bundle status. The bundle status
46
+ // grades a signed bundle; this grades a PACK — a strictly larger object, the
47
+ // bundle plus whatever bytes travelled beside it. A pack can be incomplete
48
+ // while the bundle it carries is impeccable, which is exactly the case that
49
+ // used to render green over zero hashed bytes. One vocabulary, two layers,
50
+ // each issuing the subset it can reach.
51
51
  //
52
52
  // `recordsVerified` counts CONTENT-BOUND records only. The kernel's own
53
53
  // `event.ok` tolerates undisclosed content by design — a bundle may legitimately
@@ -73,6 +73,37 @@ import { hasSideContent, sideContentKeys } from "./side-content.js";
73
73
  // byte-order mark, no non-finite literal. `jcs_loads` in
74
74
  // `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
75
75
  import { jcsParse } from "./verifier.js";
76
+ // ---- The verdict a surface may print ----------------------------------------
77
+ // A superset of `EvidenceStatus`: the four the kernel can return, plus the
78
+ // pack-level `incomplete`, plus `pending` from the shared §3 vocabulary so a
79
+ // kernel headline this module does not downgrade can pass through unchanged.
80
+ export const PACK_VERDICTS = [
81
+ "verified",
82
+ "partial",
83
+ "pending",
84
+ "not_found",
85
+ "mixed",
86
+ "failed",
87
+ "malformed",
88
+ "incomplete",
89
+ ];
90
+ // ---- Output ------------------------------------------------------------------
91
+ export const PACK_GRADE_CAUSE_KINDS = [
92
+ "records_omitted",
93
+ "records_surplus",
94
+ "leaf_count_unreadable",
95
+ "no_disclosure",
96
+ "missing_disclosure",
97
+ "unreadable_disclosure",
98
+ "unaccounted_disclosure",
99
+ "declaration_deviation",
100
+ /**
101
+ * The pack and the UNSIGNED content handed in beside it do not line up
102
+ * (proof#147). A cause, so the pack cannot read clean over it and a reader
103
+ * is told which record; NOT a deviation, so it never reaches `failed`.
104
+ */
105
+ "declaration_content_dependent",
106
+ ];
76
107
  /**
77
108
  * The checkpoint's join key, in whichever vocabulary the bundle speaks.
78
109
  *
@@ -358,7 +389,19 @@ export function completenessNotes(bundle) {
358
389
  * Every reason this pack is not `verified`, in the order a reader should hear
359
390
  * them: what is missing from the whole, then what is missing from the parts.
360
391
  */
361
- export function gradeCauses(account, notes, declaration) {
392
+ export function gradeCauses(account, notes, declaration, withheld = []) {
393
+ // The `event_id`s WITHHELD BY THEIR DECLARATION (rule version 6) are taken
394
+ // out of `expected` and `missing` before the disclosure causes are computed,
395
+ // so a pack whose every record its declaration withholds earns neither
396
+ // `no_disclosure` nor `missing_disclosure`.
397
+ if (withheld.length > 0) {
398
+ const gone = new Set(withheld);
399
+ account = {
400
+ ...account,
401
+ expected: account.expected.filter((e) => !gone.has(e)),
402
+ missing: account.missing.filter((e) => !gone.has(e)),
403
+ };
404
+ }
362
405
  const causes = [];
363
406
  // What the pack CLAIMED to hold comes first, because it frames everything
364
407
  // below it: a reader told "four records are missing from what this pack says
@@ -367,7 +410,7 @@ export function gradeCauses(account, notes, declaration) {
367
410
  // four ways can never be reported as one vague shortfall.
368
411
  for (const deviation of declaration?.deviations ?? []) {
369
412
  causes.push({
370
- kind: "declaration-deviation",
413
+ kind: "declaration_deviation",
371
414
  deviation: deviation.kind,
372
415
  ...(deviation.eventId === undefined ? {} : { firstEventId: deviation.eventId }),
373
416
  reason: renderDeviation(deviation),
@@ -378,7 +421,7 @@ export function gradeCauses(account, notes, declaration) {
378
421
  // deviation, so it never reaches `failed`.
379
422
  for (const finding of declaration?.contentFindings ?? []) {
380
423
  causes.push({
381
- kind: "declaration-content-dependent",
424
+ kind: "declaration_content_dependent",
382
425
  deviation: finding.kind,
383
426
  ...(finding.eventId === undefined ? {} : { firstEventId: finding.eventId }),
384
427
  reason: renderContentFinding(finding),
@@ -387,7 +430,7 @@ export function gradeCauses(account, notes, declaration) {
387
430
  for (const note of notes) {
388
431
  if (note.kind === "omitted") {
389
432
  causes.push({
390
- kind: "records-omitted",
433
+ kind: "records_omitted",
391
434
  checkpointRef: note.checkpointRef,
392
435
  signed: note.signed,
393
436
  presented: note.presented,
@@ -396,7 +439,7 @@ export function gradeCauses(account, notes, declaration) {
396
439
  }
397
440
  else if (note.kind === "surplus") {
398
441
  causes.push({
399
- kind: "records-surplus",
442
+ kind: "records_surplus",
400
443
  checkpointRef: note.checkpointRef,
401
444
  signed: note.signed,
402
445
  presented: note.presented,
@@ -404,7 +447,7 @@ export function gradeCauses(account, notes, declaration) {
404
447
  });
405
448
  }
406
449
  else {
407
- causes.push({ kind: "leaf-count-unreadable", checkpointRef: note.checkpointRef });
450
+ causes.push({ kind: "leaf_count_unreadable", checkpointRef: note.checkpointRef });
408
451
  }
409
452
  }
410
453
  // "Nothing at all was disclosed" is reported once, as itself. It is the
@@ -412,22 +455,22 @@ export function gradeCauses(account, notes, declaration) {
412
455
  // reader deserves to be told the pack disclosed nothing rather than counting
413
456
  // N absences.
414
457
  if (account.expected.length > 0 && account.present.length === 0 && account.unreadable.length === 0) {
415
- causes.push({ kind: "no-disclosure", count: account.expected.length, of: account.expected.length });
458
+ causes.push({ kind: "no_disclosure", count: account.expected.length, of: account.expected.length });
416
459
  }
417
460
  else if (account.missing.length > 0) {
418
461
  causes.push({
419
- kind: "missing-disclosure",
462
+ kind: "missing_disclosure",
420
463
  count: account.missing.length,
421
464
  of: account.expected.length,
422
465
  ...(account.missing[0] === undefined ? {} : { firstEventId: account.missing[0] }),
423
466
  });
424
467
  }
425
468
  for (const u of account.unreadable) {
426
- causes.push({ kind: "unreadable-disclosure", firstEventId: u.eventId, reason: u.reason });
469
+ causes.push({ kind: "unreadable_disclosure", firstEventId: u.eventId, reason: u.reason });
427
470
  }
428
471
  if (account.extra.length > 0) {
429
472
  causes.push({
430
- kind: "unaccounted-disclosure",
473
+ kind: "unaccounted_disclosure",
431
474
  count: account.extra.length,
432
475
  ...(account.extra[0] === undefined ? {} : { firstEventId: account.extra[0] }),
433
476
  });
@@ -445,7 +488,8 @@ export function gradeCauses(account, notes, declaration) {
445
488
  * the type and renders the verdict verbatim would then print the one word
446
489
  * `evidence-bundle.md` §6 forbids, over a pack this rule never graded.
447
490
  *
448
- * So: the six states of the §3 vocabulary pass through, and ANYTHING ELSE is
491
+ * So: the five words of the §3 vocabulary that a bundle status can pass up to
492
+ * a pack pass through, and ANYTHING ELSE is
449
493
  * `incomplete`. A verdict we cannot interpret is a check we could not run, and
450
494
  * the answer to that is UNKNOWN — never a pass, and never an accusation.
451
495
  * (`failed` and `malformed` are handled before this is reached.)
@@ -466,8 +510,9 @@ function passThrough(status) {
466
510
  * The verdict a surface may print, and the counts it may print beside it.
467
511
  *
468
512
  * The invariant, stated so a mutation test can kill it: `verified` is reachable
469
- * ONLY when every record's supplied bytes bound to its own commitment and
470
- * nothing is missing, unreadable, surplus or omitted.
513
+ * ONLY when every record's supplied bytes bound to its own commitment, or
514
+ * (rule version 6) the record is withheld by its declaration, and nothing is
515
+ * missing, unreadable, surplus or omitted.
471
516
  *
472
517
  * Precedence: malformed > failed > incomplete > the kernel's own headline.
473
518
  * A hard verdict wins — "the bytes are absent" is never reported as failure
@@ -520,8 +565,20 @@ export function gradePack(result, account, notes, declaration) {
520
565
  const events = Array.isArray(result?.events) ? result.events : [];
521
566
  const recordsVerified = events.filter((e) => e.ok && e.contentOk === true).length;
522
567
  const recordsFailed = events.filter((e) => e.ok === false || e.contentOk === false).length;
523
- const recordsUndetermined = events.length - recordsVerified - recordsFailed;
524
- const causes = gradeCauses(account, notes, declaration);
568
+ // WITHHELD BY ITS DECLARATION (rule version 6, 5.4.5 v1.20, A22, D5). All
569
+ // four conditions, each on its own. `declaredWithheld` is empty for an
570
+ // absent, unreadable or malformed declaration.
571
+ const declaredWithheld = new Set(declaration?.declaredWithheld ?? []);
572
+ const withheldEventIds = events
573
+ .filter((e) => typeof e.eventId === "string" &&
574
+ declaredWithheld.has(e.eventId) &&
575
+ e.ok === true &&
576
+ e.contentOk === null &&
577
+ e.payloadBindingOk === true)
578
+ .map((e) => e.eventId);
579
+ const recordsWithheld = withheldEventIds.length;
580
+ const recordsUndetermined = events.length - recordsVerified - recordsFailed - recordsWithheld;
581
+ const causes = gradeCauses(account, notes, declaration, withheldEventIds);
525
582
  let verdict;
526
583
  if (result?.status === "malformed")
527
584
  verdict = "malformed";
@@ -541,6 +598,8 @@ export function gradePack(result, account, notes, declaration) {
541
598
  recordsFailed,
542
599
  recordsUndetermined,
543
600
  causes,
601
+ recordsWithheld,
602
+ withheldEventIds,
544
603
  };
545
604
  if (declaration != null)
546
605
  grade.declaration = declaration.verdict;
@@ -559,32 +618,32 @@ export function gradePack(result, account, notes, declaration) {
559
618
  */
560
619
  export function renderCause(cause, subject = "this pack") {
561
620
  switch (cause.kind) {
562
- case "records-omitted":
621
+ case "records_omitted":
563
622
  return (`checkpoint ${cause.checkpointRef} commits to ${cause.signed} record(s); ${subject} presents ` +
564
623
  `${cause.presented} — ${cause.count} record(s) OMITTED`);
565
- case "records-surplus":
624
+ case "records_surplus":
566
625
  return (`checkpoint ${cause.checkpointRef} commits to ${cause.signed} record(s); ${subject} presents ` +
567
626
  `${cause.presented} — more records than the signed tree accounts for`);
568
- case "leaf-count-unreadable":
627
+ case "leaf_count_unreadable":
569
628
  return `checkpoint ${cause.checkpointRef} has no readable signed leaf_count — completeness is UNKNOWN`;
570
- case "no-disclosure":
629
+ case "no_disclosure":
571
630
  return (`no event bytes were disclosed — content binding is UNKNOWN for all ${cause.count} ` +
572
631
  `record(s), not verified`);
573
- case "missing-disclosure":
632
+ case "missing_disclosure":
574
633
  return (`${cause.count} of ${cause.of} record(s) disclose no bytes (first: ${cause.firstEventId}) — ` +
575
634
  `content binding is UNKNOWN, not verified`);
576
- case "unreadable-disclosure":
635
+ case "unreadable_disclosure":
577
636
  return `the disclosure for ${cause.firstEventId} is unreadable: ${cause.reason}`;
578
- case "unaccounted-disclosure":
637
+ case "unaccounted_disclosure":
579
638
  return (`${cause.count} disclosed file(s) are not accounted for by any record in this bundle ` +
580
639
  `(first: ${cause.firstEventId})`);
581
640
  // Rendered once, by `renderDeviation` in `./declaration.ts`, and carried
582
641
  // here on `reason`. Re-deriving the sentence from the cause fields would
583
642
  // put the declaration's wording in two places, and one of the two would be
584
643
  // edited alone.
585
- case "declaration-deviation":
644
+ case "declaration_deviation":
586
645
  return `this pack deviates from its own declaration: ${cause.reason}`;
587
- case "declaration-content-dependent":
646
+ case "declaration_content_dependent":
588
647
  // Named as what the reader could not establish, never as what the
589
648
  // holder did: the bytes it turns on are unsigned and whoever assembled
590
649
  // the case chose them (proof#147).
package/dist/index.d.ts CHANGED
@@ -2,19 +2,22 @@ export { ACCEPTED_SPEC_MAJORS, contentHashes, jcs, specVersionSupported, verifyE
2
2
  export { bytesToHex, deriveOperatorAddress, ed25519Verify, hexToBytes, sha256Bytes, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
3
3
  export { EMPTY_TREE_ROOT_HEX, auditPath, leafHash, merkleRoot, nodeHash, verifyInclusion, } from "./merkle.js";
4
4
  export type { ContentRole, Envelope, Subject, VerificationResult, VerifyOptions } from "./types.js";
5
- export { ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, STAMP_EVIDENCE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
5
+ export { ACCEPTED_EVIDENCE_MAJORS, ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, STAMP_EVIDENCE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
6
6
  export type { AnchorTraceBody, AttestationPayload, AttestationRecord, AttestationResult, CheckpointResult, ComposeVerdictResult, DisclosedContentResult, DisclosedEventContent, DisclosureStatus, EvidenceBundle, EvidenceBundleResult, EvidenceIssuer, EvidenceStatus, EvidenceVerdict, EventResult, ExportBody, ExportResult, OnChainOutcome, OnChainResult, PerGatewayOutcome, AnchorRef, StampBody, StampEntry, StampResult, StampSubtree, StampSubtreeResult, StampTraceBody, StampEvidenceBody, StampEvidenceEvent, StampEvidenceInclusion, TraceCheckpoint, TraceEvent, TraceInclusion, VerdictAttestation, VerdictCheckpoint, VerdictEvent, VerdictObject, VerdictOnChain, VerdictOnChainGateway, VerifyEvidenceOptions, } from "./evidence.js";
7
- export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
7
+ export { PACK_GRADE_CAUSE_KINDS, PACK_VERDICTS, accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
8
8
  export type { CompletenessNote, DisclosureAccount, GradeInput, PackGrade, PackGradeCause, PackGradeCauseKind, PackVerdict, } from "./grading.js";
9
- export { DECLARATION_VERSION, checkDeclaration, declarationClaims, packRecords, renderContentFinding, renderDeclarationSummary, renderDeviation, renderNote, } from "./declaration.js";
10
- export type { DeclarationCheck, DeclarationClaims, DeclarationContentFinding, DeclarationContentKind, DeclarationDeviation, DeclarationDeviationKind, DeclarationNote, DeclarationNoteKind, DeclarationVerdict, PackRecord, } from "./declaration.js";
11
- export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_VERDICTS, readCase, renderCaseSummary, renderException, } from "./case.js";
12
- export type { CaseException, CaseExceptionKind, CasePack, CaseReading, CaseRecordReading, CaseVerdict, } from "./case.js";
9
+ export { DECLARATION_CONTENT_KINDS, DECLARATION_DEVIATION_KINDS, DECLARATION_NOTE_KINDS, DECLARATION_ROLES, DECLARATION_VERDICTS, DECLARATION_VERSION, DECLARATION_VERSION_V2, DECLARATION_VERSIONS, checkDeclaration, declarationClaims, packRecords, renderContentFinding, renderDeclarationSummary, renderDeviation, renderNote, } from "./declaration.js";
10
+ export type { DeclarationCheck, DeclarationClaims, DeclarationContentFinding, DeclarationContentKind, DeclarationDeviation, DeclarationDeviationKind, DeclarationNote, DeclarationNoteKind, DeclarationRole, DeclarationVerdict, PackRecord, } from "./declaration.js";
11
+ export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_NOTE_KINDS, CASE_RELIANCE, CASE_VERDICTS, CONTENT_WITHHELD_BY_DECLARATION, VERDICT_DISPLAY_TITLES, WITNESS_NOT_LOOKED_UP, WITNESS_STATES, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
12
+ export type { CaseException, CaseExceptionKind, CaseNote, CaseNoteKind, CasePack, CasePackReading, CaseReading, CaseRecordReading, CaseVerdict, WitnessLine, WitnessState, } from "./case.js";
13
+ export { CASE_FILE_FINDING_KINDS, CASE_FILE_INDEX_NAME, CASE_FILE_VERSION, HOW_TO_VERIFY_NAME, REPORT_README_FIRST_LINE, howToVerifyText, isPackRef, readCaseFileIndex, renderCaseFileFinding, } from "./case-file.js";
14
+ export type { CaseFileFinding, CaseFileFindingKind, CaseFileIndexReading, } from "./case-file.js";
13
15
  export { ADAPTER_CHAIN_NAMESPACES, CHAIN_READING_SCOPE, readChains, renderChainFinding, } from "./chain.js";
14
16
  export type { ChainFinding, ChainFindingKind, ChainReading, ChainSource, ChainSummary, } from "./chain.js";
17
+ export { EXIT_CODES, EXIT_CODES_HELP, EXIT_FAILED, EXIT_MALFORMED, EXIT_MIXED, EXIT_UNDETERMINED, EXIT_VERIFIED, exitCodeFor, } from "./exit-codes.js";
15
18
  export { attestationSignatureToHex, composeExport } from "./compose.js";
16
19
  export type { ComposeExportOptions, ComposeExportResult, ExporterKey } from "./compose.js";
17
- export { AGENT_PROOF_SPEC_PREFIX, isAgentProofSpec, verifyAgentProofBundle } from "./agent-proof.js";
20
+ export { AGENT_PROOF_SPEC_VERSION, isAgentProofSpec, verifyAgentProof } from "./agent-proof.js";
18
21
  export type { AgentProofBundle, AgentProofResult, VerifyAgentProofOptions, } from "./agent-proof.js";
19
22
  export { CANDIDATE_NAMESPACE, DEFINED_EVENT_TYPES, NOT_A_HANDOFF_FINDING, isConformantHandoffRecord, readHandoff, readHandoffs, recordIsMalformed, } from "./handoff.js";
20
23
  export type { CandidateReading, HandoffReading, HandoffRecord, HandoffVerdict, HeldRecord, } from "./handoff.js";
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  // envelope. Chain walking (previous_hash), checkpoint reconciliation, and
12
12
  // gateway/transport concerns are consumer-layer logic composed above it.
13
13
  // ACCEPTED_SPEC_MAJORS is exported for parity with the sibling kernels'
14
- // registries (Python ACCEPTED_SPEC_VERSIONS, Go SupportedSpecMajor): a consumer
14
+ // registries (Python ACCEPTED_SPEC_MAJORS, Go SupportedSpecMajor): a consumer
15
15
  // can see which profiles this build admits without inferring it from probe
16
16
  // calls. It is frozen at its definition (`Object.freeze`, typed
17
17
  // `readonly string[]`) because the verifier reads that same array — an exported
@@ -34,7 +34,7 @@ export { EMPTY_TREE_ROOT_HEX, auditPath, leafHash, merkleRoot, nodeHash, verifyI
34
34
  // files that failed verification on genuine evidence. This hands back DECODED
35
35
  // bytes, and only for events whose verification bound them to the committed
36
36
  // content_hash. Additive; verifyEvidenceBundle's behavior is unchanged.
37
- export { ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, STAMP_EVIDENCE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
37
+ export { ACCEPTED_EVIDENCE_MAJORS, ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, STAMP_EVIDENCE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
38
38
  // Pack grading (src/grading.ts): what a verification SURFACE may claim about a
39
39
  // pack — the signed bundle PLUS whatever disclosed bytes travelled beside it.
40
40
  //
@@ -50,7 +50,7 @@ export { ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRA
50
50
  // consumer that cannot yet import `gradePack` can vendor the table, hold its own
51
51
  // derivation to it, and diff its copy against ours. Additive — no existing
52
52
  // export changes.
53
- export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
53
+ export { PACK_GRADE_CAUSE_KINDS, PACK_VERDICTS, accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
54
54
  // The declaration (src/declaration.ts): what a pack SAYS it holds, checked
55
55
  // against what it does (evidence-bundle.md §5.4).
56
56
  //
@@ -67,7 +67,7 @@ export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCau
67
67
  // Pure: data in, data out, no I/O. `gradePack` takes the result as an
68
68
  // optional fourth argument, so a surface gets one verdict; omitting it is the
69
69
  // pre-§5.4 behavior, unchanged. Additive: no existing export changes.
70
- export { DECLARATION_VERSION, checkDeclaration, declarationClaims, packRecords, renderContentFinding, renderDeclarationSummary, renderDeviation, renderNote, } from "./declaration.js";
70
+ export { DECLARATION_CONTENT_KINDS, DECLARATION_DEVIATION_KINDS, DECLARATION_NOTE_KINDS, DECLARATION_ROLES, DECLARATION_VERDICTS, DECLARATION_VERSION, DECLARATION_VERSION_V2, DECLARATION_VERSIONS, checkDeclaration, declarationClaims, packRecords, renderContentFinding, renderDeclarationSummary, renderDeviation, renderNote, } from "./declaration.js";
71
71
  // The case (src/case.ts): one verdict over SEVERAL packs, across organizations
72
72
  // (anchor#198). The layer above the declaration, and the closure for what a
73
73
  // declaration cannot reach.
@@ -85,7 +85,11 @@ export { DECLARATION_VERSION, checkDeclaration, declarationClaims, packRecords,
85
85
  //
86
86
  // Pure: data in, data out, no I/O. Additive: a new module, no existing export
87
87
  // changes, and no new verdict word anywhere.
88
- export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_VERDICTS, readCase, renderCaseSummary, renderException, } from "./case.js";
88
+ export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_NOTE_KINDS, CASE_RELIANCE, CASE_VERDICTS, CONTENT_WITHHELD_BY_DECLARATION, VERDICT_DISPLAY_TITLES, WITNESS_NOT_LOOKED_UP, WITNESS_STATES, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
89
+ // The case file's index (src/case-file.ts, evidence-bundle.md §5.6, A25):
90
+ // compared with the drop it describes, with NO verdict contribution. The index
91
+ // is never an input to `readCase`. Pure: bytes and a file list in.
92
+ export { CASE_FILE_FINDING_KINDS, CASE_FILE_INDEX_NAME, CASE_FILE_VERSION, HOW_TO_VERIFY_NAME, REPORT_README_FIRST_LINE, howToVerifyText, isPackRef, readCaseFileIndex, renderCaseFileFinding, } from "./case-file.js";
89
93
  // Chain reading (src/chain.ts): following the pointer every sanning.events/v2
90
94
  // record carries to the record before it, over the records ONE bundle presents.
91
95
  //
@@ -111,11 +115,14 @@ export { ADAPTER_CHAIN_NAMESPACES, CHAIN_READING_SCOPE, readChains, renderChainF
111
115
  // signed, offline-verifiable sanning.evidence.export/v2 artifact from a source
112
116
  // anchor-trace bundle + operator attestations + an exporter Ed25519 key. The
113
117
  // inverse of verifyEvidenceBundle's verifyExportBody dispatch.
118
+ // One exit-code table for every verb (0.13.0): the verdict word to the exit
119
+ // code, and the help text every verb prints. Additive.
120
+ export { EXIT_CODES, EXIT_CODES_HELP, EXIT_FAILED, EXIT_MALFORMED, EXIT_MIXED, EXIT_UNDETERMINED, EXIT_VERIFIED, exitCodeFor, } from "./exit-codes.js";
114
121
  export { attestationSignatureToHex, composeExport } from "./compose.js";
115
122
  // Agent inclusion-proof bundle verification: the
116
123
  // sanning.agent.proof/v1 bundle. The CLI sniffs spec_version and routes here so
117
124
  // one verifier covers both agent and anchor bundles.
118
- export { AGENT_PROOF_SPEC_PREFIX, isAgentProofSpec, verifyAgentProofBundle } from "./agent-proof.js";
125
+ export { AGENT_PROOF_SPEC_VERSION, isAgentProofSpec, verifyAgentProof } from "./agent-proof.js";
119
126
  // The hand-off pairing rule (src/handoff.ts): which records are candidates to
120
127
  // be the counterpart of a hand-off record, how a candidate set of more than one
121
128
  // is ordered, and what a formed pair grades to (specs/envelope-spec.md §4.1
@@ -118,17 +118,19 @@ export declare const REFUSAL_ORDER: readonly ["bytes_for_unknown_part", "conflic
118
118
  export declare const MESSAGE_RESULT_ORDER: readonly ["parts_not_an_array"];
119
119
  /**
120
120
  * The keys that make a part carry its content INLINE, in the order they are
121
- * read. Named rather than inferred: `bytes` is A2A's spelling of an inline file
122
- * part, and `data` and `content` are the two other spellings in the wild. The
123
- * FIRST of them present with a value that is not `null` is the inline content,
124
- * so a part spelling it twice reads one way in both kernels.
121
+ * read. Named rather than inferred: `bytes`, `data` and `content` are the
122
+ * spellings in the wild. None is A2A 1.0.0's: its `Part` has no `kind` and
123
+ * carries a file as `raw` or `url`, so a literal A2A part reads
124
+ * `part_not_a_file` (envelope-spec §4.1, A19). The FIRST of them present with
125
+ * a value that is not `null` is the inline content, so a part spelling it
126
+ * twice reads one way in both kernels.
125
127
  *
126
128
  * A key present with `null` is an ABSENT inline value, on A14's ruling that an
127
- * absent value and one written as `null` name one thing. That matters: an A2A
128
- * `FilePart` with `uri` set carries `"bytes": null`, and so does proto3 JSON,
129
- * and reading those as inline made a conformant DESCRIPTOR part uncheckable
130
- * (proof#156 gate). They fall through to the descriptor rule, which is what
131
- * they are.
129
+ * absent value and one written as `null` name one thing. That matters: a
130
+ * proto3 JSON encoder can write an unset field as `"bytes": null`, and reading
131
+ * that as inline made a conformant DESCRIPTOR part uncheckable
132
+ * (proof#156 gate). Such a part falls through to the descriptor rule, which is
133
+ * what it is.
132
134
  */
133
135
  export declare const INLINE_BYTES_KEYS: readonly ["bytes", "content", "data"];
134
136
  /**
@@ -262,17 +262,19 @@ const HELD_UNCHECKED_RESULTS = [
262
262
  ];
263
263
  /**
264
264
  * The keys that make a part carry its content INLINE, in the order they are
265
- * read. Named rather than inferred: `bytes` is A2A's spelling of an inline file
266
- * part, and `data` and `content` are the two other spellings in the wild. The
267
- * FIRST of them present with a value that is not `null` is the inline content,
268
- * so a part spelling it twice reads one way in both kernels.
265
+ * read. Named rather than inferred: `bytes`, `data` and `content` are the
266
+ * spellings in the wild. None is A2A 1.0.0's: its `Part` has no `kind` and
267
+ * carries a file as `raw` or `url`, so a literal A2A part reads
268
+ * `part_not_a_file` (envelope-spec §4.1, A19). The FIRST of them present with
269
+ * a value that is not `null` is the inline content, so a part spelling it
270
+ * twice reads one way in both kernels.
269
271
  *
270
272
  * A key present with `null` is an ABSENT inline value, on A14's ruling that an
271
- * absent value and one written as `null` name one thing. That matters: an A2A
272
- * `FilePart` with `uri` set carries `"bytes": null`, and so does proto3 JSON,
273
- * and reading those as inline made a conformant DESCRIPTOR part uncheckable
274
- * (proof#156 gate). They fall through to the descriptor rule, which is what
275
- * they are.
273
+ * absent value and one written as `null` name one thing. That matters: a
274
+ * proto3 JSON encoder can write an unset field as `"bytes": null`, and reading
275
+ * that as inline made a conformant DESCRIPTOR part uncheckable
276
+ * (proof#156 gate). Such a part falls through to the descriptor rule, which is
277
+ * what it is.
276
278
  */
277
279
  // A conformant DESCRIPTOR part that also carries a `content` or `data` key
278
280
  // meaning something else (a filename, a metadata blob) is reclassified by this
package/dist/types.d.ts CHANGED
@@ -22,6 +22,7 @@ export type ContentRole = "asset" | "baseline" | "observed";
22
22
  export interface VerifyOptions {
23
23
  payloadBytes?: Uint8Array;
24
24
  expectedContentHash?: string;
25
+ acceptedSpecMajors?: readonly string[];
25
26
  }
26
27
  export interface VerificationResult {
27
28
  ok: boolean;
@@ -38,11 +38,18 @@ export declare function jcs(value: unknown): string;
38
38
  * Throws for anything that is not conformant; every call site treats a throw as
39
39
  * "this reader cannot read these bytes" and names the check that failed.
40
40
  */
41
+ /**
42
+ * Bytes as strict UTF-8, a leading byte-order mark KEPT as a character.
43
+ * `utf8_strict` in the Python twin's `canonicalize.py` is the same rule, and
44
+ * `jcsParse` below performs the identical decode. Throws on bytes that are not
45
+ * UTF-8.
46
+ */
47
+ export declare function utf8Strict(bytes: Uint8Array): string;
41
48
  export declare function jcsParse(bytes: Uint8Array): unknown;
42
- export declare function specVersionSupported(specVersion: string): boolean;
49
+ export declare function specVersionSupported(specVersion: string, accepted?: readonly string[]): boolean;
43
50
  export declare function isRfc3339UtcInstant(value: unknown): boolean;
44
51
  export declare function contentHashes(env: Envelope): {
45
52
  role: ContentRole;
46
53
  hash: string;
47
54
  }[];
48
- export declare function verifyEnvelope(env: Envelope, optionsOrContentHash?: string | VerifyOptions): Promise<VerificationResult>;
55
+ export declare function verifyEnvelope(env: Envelope, options?: VerifyOptions): Promise<VerificationResult>;
package/dist/verifier.js CHANGED
@@ -19,7 +19,7 @@ import { ed25519Verify, isHexField, sha256Hex, utf8 } from "./crypto.js";
19
19
  // profile-specific dialect (no underscore strip, that is mlflow-only).
20
20
  //
21
21
  // Exported for parity with the sibling kernels' registries — Python
22
- // `ACCEPTED_SPEC_VERSIONS`, Go `SupportedSpecMajor` — so the accept-set is
22
+ // `ACCEPTED_SPEC_MAJORS`, Go `SupportedSpecMajor` — so the accept-set is
23
23
  // assertable rather than inferable: test/accept-set.test.ts holds it to the
24
24
  // ratified §4/§5 set transcribed in cross-kernel/accept-set.json, and the
25
25
  // cross-kernel gate probes one signed envelope per identifier. This entry list
@@ -217,6 +217,15 @@ const JCS_BOM = "";
217
217
  * Throws for anything that is not conformant; every call site treats a throw as
218
218
  * "this reader cannot read these bytes" and names the check that failed.
219
219
  */
220
+ /**
221
+ * Bytes as strict UTF-8, a leading byte-order mark KEPT as a character.
222
+ * `utf8_strict` in the Python twin's `canonicalize.py` is the same rule, and
223
+ * `jcsParse` below performs the identical decode. Throws on bytes that are not
224
+ * UTF-8.
225
+ */
226
+ export function utf8Strict(bytes) {
227
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
228
+ }
220
229
  export function jcsParse(bytes) {
221
230
  const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
222
231
  if (text.startsWith(JCS_BOM))
@@ -299,10 +308,14 @@ function hasLoneSurrogate(s) {
299
308
  // `<major>.<minor>` where minor is `[0-9]+`. A non-numeric minor suffix is
300
309
  // malformed, not a tolerated future version — matching the Python kernel's
301
310
  // 0.1.1 semantics (the TS/Go fail-open lenience was closed deliberately).
302
- export function specVersionSupported(specVersion) {
311
+ //
312
+ // `accepted` NARROWS the registry and never widens it, as the Python twin's
313
+ // `spec_version_supported(spec_version, accepted)` does (P-10, 0.13.0).
314
+ export function specVersionSupported(specVersion, accepted) {
303
315
  if (typeof specVersion !== "string" || specVersion === "")
304
316
  return false;
305
- return ACCEPTED_SPEC_MAJORS.some((m) => {
317
+ const registry = accepted === undefined ? ACCEPTED_SPEC_MAJORS : ACCEPTED_SPEC_MAJORS.filter((m) => accepted.includes(m));
318
+ return registry.some((m) => {
306
319
  if (specVersion === m)
307
320
  return true;
308
321
  if (!specVersion.startsWith(`${m}.`))
@@ -444,18 +457,21 @@ export function contentHashes(env) {
444
457
  // signature, so stripping it to fake "external" — or injecting one to fake
445
458
  // "inline" — breaks the signature).
446
459
  //
447
- // The second argument accepts the legacy `expectedContentHash` string form
448
- // or a VerifyOptions object ({ payloadBytes, expectedContentHash }).
449
- export async function verifyEnvelope(env, optionsOrContentHash) {
450
- const opts = typeof optionsOrContentHash === "string"
451
- ? { expectedContentHash: optionsOrContentHash }
452
- : (optionsOrContentHash ?? {});
460
+ // The second argument is a VerifyOptions object ({ payloadBytes,
461
+ // expectedContentHash, acceptedSpecMajors }). Until 0.13.0 it could also be a
462
+ // bare `expectedContentHash` string; that overload is gone (P-10), so the two
463
+ // kernels take one options shape: write `{ expectedContentHash: hash }`.
464
+ export async function verifyEnvelope(env, options) {
465
+ if (typeof options === "string") {
466
+ throw new TypeError("verifyEnvelope: pass { expectedContentHash } rather than a bare string (0.13.0)");
467
+ }
468
+ const opts = options ?? {};
453
469
  // REFUSE an unknown option key rather than ignore it. `expected_content_hash`
454
470
  // — the Python kernel's spelling — previously fell through as `undefined`, so
455
471
  // the content bind silently never ran and the caller got a clean pass with no
456
472
  // errors at all. A check that quietly does not happen is worse than one that
457
473
  // fails: the caller believes they verified an artifact they never checked.
458
- const KNOWN_VERIFY_OPTIONS = new Set(["payloadBytes", "expectedContentHash"]);
474
+ const KNOWN_VERIFY_OPTIONS = new Set(["payloadBytes", "expectedContentHash", "acceptedSpecMajors"]);
459
475
  const unknownOptions = Object.keys(opts).filter((k) => !KNOWN_VERIFY_OPTIONS.has(k));
460
476
  if (unknownOptions.length > 0) {
461
477
  throw new TypeError(`verifyEnvelope: unknown option(s) ${unknownOptions.join(", ")}. ` +
@@ -528,7 +544,7 @@ async function verifyEnvelopeChecked(env, opts) {
528
544
  errors: ["envelope is not a JSON object"],
529
545
  };
530
546
  }
531
- const specVersionOk = specVersionSupported(env.spec_version);
547
+ const specVersionOk = specVersionSupported(env.spec_version, opts.acceptedSpecMajors);
532
548
  if (!specVersionOk)
533
549
  errors.push(`unsupported spec_version: ${JSON.stringify(env.spec_version)}`);
534
550
  // Skeleton field format (envelope-spec §2, proof#95): a present `signed_at`
@@ -598,7 +614,7 @@ async function verifyEnvelopeChecked(env, opts) {
598
614
  let signatureOk = false;
599
615
  try {
600
616
  const { signature: _signature, co_signatures: _coSignatures, ...envelopeForSig } = env;
601
- signatureOk = await ed25519Verify(env.signature, utf8(jcs(envelopeForSig)), env.public_key);
617
+ signatureOk = await ed25519Verify(utf8(jcs(envelopeForSig)), env.signature, env.public_key);
602
618
  if (!signatureOk)
603
619
  errors.push("Ed25519 signature verification failed");
604
620
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The one ZIP reader `case` uses, for a case file named on the command line.
3
+ *
4
+ * The TypeScript half of `src/sanning_proof/zip_reader.py`. Node has no ZIP
5
+ * reader, and Python's `zipfile` is not used, so the two kernels read one
6
+ * archive by one rule rather than by two libraries that disagree on the edges:
7
+ * a central directory that contradicts a local header, a name that is not
8
+ * UTF-8, data after the end of a deflate stream. Where either library would
9
+ * have made a choice, this module refuses instead.
10
+ *
11
+ * What it reads (evidence-bundle.md 5.6.1): the central directory, and each
12
+ * entry's data through its local header. Stored and deflate only. What it
13
+ * REFUSES, as a whole archive: no end-of-central-directory record, ZIP64, more
14
+ * than one disk, an entry the central directory places outside the archive, a
15
+ * local header that does not match its central entry, a size or CRC-32 that
16
+ * does not match the data, two entries with one name, a name that is not
17
+ * UTF-8, more than MAX_ZIP_ENTRIES entries or MAX_ZIP_BYTES bytes in all, and
18
+ * a name nested deeper than MAX_ZIP_DEPTH folders. What it REPORTS and never
19
+ * reads, entry by entry: an encrypted entry, a symbolic link, a method other
20
+ * than stored or deflate, and a name that is absolute, holds a backslash or a
21
+ * NUL, or has an empty, `.` or `..` segment.
22
+ *
23
+ * Pure: bytes in, entries out. The caller turns them into the tree `case`
24
+ * splits.
25
+ */
26
+ /** More entries than this refuse the archive: the walk's own bound on names. */
27
+ export declare const MAX_ZIP_ENTRIES = 50000;
28
+ /** More uncompressed bytes than this, across every entry, refuse the archive. */
29
+ export declare const MAX_ZIP_BYTES: number;
30
+ /** A name nested deeper than this many folders refuses the archive. */
31
+ export declare const MAX_ZIP_DEPTH = 32;
32
+ /** The archive cannot be read as a case file. `message` is the whole report, identical in both kernels. */
33
+ export declare class ZipReadError extends Error {
34
+ constructor(message: string);
35
+ }
36
+ /**
37
+ * One file entry. `refused` is why the entry is reported and not read, or
38
+ * `null`; `data` is its bytes where it was read.
39
+ */
40
+ export interface ZipEntry {
41
+ name: string;
42
+ refused: string | null;
43
+ data: Uint8Array | null;
44
+ }
45
+ /**
46
+ * Every FILE entry of a ZIP archive, in central-directory order. A directory
47
+ * entry (a name ending in `/`) is not a file and is not returned. Throws
48
+ * `ZipReadError`.
49
+ */
50
+ export declare function readZip(data: Uint8Array): ZipEntry[];