@sanning/proof 0.7.0 → 0.8.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/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ 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, ANCHOR_TRACE_BODY_TYPE_V1, ANCHOR_TRACE_BODY_TYPES, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
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, TraceCheckpoint, TraceCheckpointV1, TraceEvent, TraceInclusion, VerdictAttestation, VerdictCheckpoint, VerdictEvent, VerdictObject, VerdictOnChain, VerdictOnChainGateway, VerifyEvidenceOptions, } from "./evidence.js";
5
+ export { ANCHOR_TRACE_BODY_TYPE, ANCHOR_TRACE_BODY_TYPE_V1, ANCHOR_TRACE_BODY_TYPES, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, STAMP_EVIDENCE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
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, TraceCheckpointV1, TraceEvent, TraceInclusion, VerdictAttestation, VerdictCheckpoint, VerdictEvent, VerdictObject, VerdictOnChain, VerdictOnChainGateway, VerifyEvidenceOptions, } from "./evidence.js";
7
7
  export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
8
8
  export type { CompletenessNote, DisclosureAccount, GradeInput, PackGrade, PackGradeCause, PackGradeCauseKind, PackVerdict, } from "./grading.js";
9
9
  export { ADAPTER_CHAIN_NAMESPACES, CHAIN_READING_SCOPE, readChains, renderChainFinding, } from "./chain.js";
package/dist/index.js CHANGED
@@ -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, ANCHOR_TRACE_BODY_TYPE_V1, ANCHOR_TRACE_BODY_TYPES, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
37
+ export { ANCHOR_TRACE_BODY_TYPE, ANCHOR_TRACE_BODY_TYPE_V1, ANCHOR_TRACE_BODY_TYPES, 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
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanning/proof",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1, sanning.mlflow/v1, sanning.events/v1. Conformance-gated byte-for-byte against the test-vectors corpus.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/chain.ts CHANGED
@@ -236,7 +236,15 @@ function adapterLinkage(
236
236
  // here: nothing to resolve is a beginning.
237
237
  const prev = str(fields.prev_event_id);
238
238
  const seqRaw = fields.seq;
239
- const seq = Number.isInteger(seqRaw) ? (seqRaw as number) : null;
239
+ // SAFE integer, not merely integral. Beyond 2^53 a JSON number does not
240
+ // survive `JSON.parse` intact — `9007199254740993` arrives as
241
+ // `...992` — while Python's parser keeps it exactly, so the two kernels
242
+ // read DIFFERENT sequence numbers off identical bytes and report gaps of
243
+ // different widths. Measured at 2^53+1: a `chain-gap` count of
244
+ // 9007199254740990 here against 9007199254740992 there (proof#49). A value
245
+ // this kernel cannot represent is one it cannot read, which is the same
246
+ // answer it gives every other unreadable field: no sequence, take no part.
247
+ const seq = Number.isSafeInteger(seqRaw) ? (seqRaw as number) : null;
240
248
  return { namespace, chainKey, prev, seq };
241
249
  }
242
250
  return null;
@@ -322,6 +330,20 @@ function byChainOrder(a: Member, b: Member): number {
322
330
  return a.order - b.order;
323
331
  }
324
332
 
333
+ /**
334
+ * The smallest slot between the lowest and highest occupied ones that no record
335
+ * occupies, found by walking the SORTED occupants rather than the range they
336
+ * span. See the call site: the range's width is chosen by whoever wrote the
337
+ * bundle.
338
+ */
339
+ function firstMissingSlot(present: Set<number>): number | null {
340
+ const ordered = [...present].sort((a, b) => a - b);
341
+ for (let i = 0; i < ordered.length - 1; i++) {
342
+ if (ordered[i + 1]! > ordered[i]! + 1) return ordered[i]! + 1;
343
+ }
344
+ return null;
345
+ }
346
+
325
347
  function findingsFor(group: Group): ChainFinding[] {
326
348
  const { members, chainKey, source, publicKey } = group;
327
349
  const of = members.length;
@@ -383,16 +405,23 @@ function findingsFor(group: Group): ChainFinding[] {
383
405
  const present = new Set(sequenced.map((m) => m.seq as number));
384
406
  const low = Math.min(...present);
385
407
  const high = Math.max(...present);
386
- const missing: number[] = [];
387
- for (let s = low; s <= high; s++) if (!present.has(s)) missing.push(s);
388
- if (missing.length > 0) {
389
- const after = sequenced.filter((m) => (m.seq as number) > missing[0]!).sort(byChainOrder)[0];
408
+ // Counted, never walked. `seq` is attacker-chosen, so a bundle presenting
409
+ // two records at seq 0 and seq 1e15 made the old `for (let s = low; s <=
410
+ // high; s++)` spin for days — a verifier that HANGS on adversarial input is
411
+ // worse than one that raises, and this reader promises neither (proof#49).
412
+ // The present set is a subset of [low, high], so the absent slots are the
413
+ // width minus the occupants, and the first of them is found by walking the
414
+ // occupants in order.
415
+ const missingCount = high - low + 1 - present.size;
416
+ const firstMissing = missingCount > 0 ? firstMissingSlot(present) : null;
417
+ if (firstMissing !== null) {
418
+ const after = sequenced.filter((m) => (m.seq as number) > firstMissing).sort(byChainOrder)[0];
390
419
  findings.push({
391
420
  kind: "chain-gap",
392
421
  ...base,
393
- count: missing.length,
422
+ count: missingCount,
394
423
  eventIds: after === undefined ? [] : [after.eventId],
395
- firstMissingSeq: missing[0]!,
424
+ firstMissingSeq: firstMissing,
396
425
  });
397
426
  }
398
427
  }
@@ -411,7 +440,18 @@ function findingsFor(group: Group): ChainFinding[] {
411
440
  export async function readChains(bundle: unknown): Promise<ChainReading> {
412
441
  const groups = await groupChains(bundle);
413
442
  const order = (g: Group) => `${g.source} ${g.namespace ?? ""} ${g.chainKey}`;
414
- groups.sort((a, b) => order(a).localeCompare(order(b)));
443
+ // Ordered by UTF-16 code unit, NOT `localeCompare`. A chain key is caller
444
+ // text, and `localeCompare` with no locale argument answers from the host's
445
+ // ICU collation: measured here, it puts "chain-2" AFTER "chain_1" where every
446
+ // code-wise comparison puts it before, and it can differ between two
447
+ // TypeScript installs, let alone between kernels. The Python reader sorts the
448
+ // same key encoded as `utf-16-be`, which is exactly this comparison — so the
449
+ // two kernels emit findings in one order (proof#49).
450
+ groups.sort((a, b) => {
451
+ const x = order(a);
452
+ const y = order(b);
453
+ return x < y ? -1 : x > y ? 1 : 0;
454
+ });
415
455
 
416
456
  return {
417
457
  chains: groups.map((g) => ({
package/src/cli.ts CHANGED
@@ -697,6 +697,35 @@ function reportEvidence(
697
697
  }
698
698
  }
699
699
 
700
+ // Stamps (evidence-bundle.md §5.2 / §5.3). Printed rather than summarised,
701
+ // because for a stamp body the WITNESS is the entire independent part
702
+ // (§5.2.4) — and until now this reporter showed nothing about it at all, so a
703
+ // §5.3 pack printed a rollup line claiming an on-chain scope with no visible
704
+ // on-chain result anywhere above it.
705
+ //
706
+ // The on-chain column distinguishes all four states in the same three
707
+ // characters the checkpoint block uses, plus its absence: `✓` confirmed,
708
+ // `✗` mismatched, `~` asked and unreachable, and NO COLUMN AT ALL when
709
+ // nothing was checked. A reader must never have to infer "not checked" from
710
+ // a symbol that also means "could not be reached".
711
+ if (result.stamps && result.stamps.length > 0) {
712
+ io.out("");
713
+ io.out(paint(" Stamps", DIM));
714
+ for (const st of result.stamps) {
715
+ const witness =
716
+ st.anchorRef === null
717
+ ? paint("~ no witness", Y)
718
+ : `witness ${st.anchorRef.kind}:${shortTx(st.anchorRef.id)}`;
719
+ const onChain =
720
+ st.onChain === null ? "" : ` on-chain ${mark(st.onChainOk)} (${st.onChain.rollup})`;
721
+ io.out(
722
+ ` ${mark(st.ok)} ${shortTx(st.stampRef)} bytes ${mark(st.stampRefOk && st.bodyOk)}` +
723
+ ` ranges ${mark(st.subtreesOk)} ${witness}${onChain}`,
724
+ );
725
+ for (const e of st.errors) io.out(paint(` - ${e}`, R));
726
+ }
727
+ }
728
+
700
729
  if (result.events.length > 0) {
701
730
  io.out("");
702
731
  io.out(paint(" Events", DIM));