@sanning/proof 0.13.0 → 0.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanning/proof",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
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 and sanning.events/v2. Conformance-gated byte-for-byte against the test-vectors corpus.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/case-file.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  * network. How the files were listed and hashed is the caller's.
26
26
  */
27
27
 
28
- import { jcsParse } from "./verifier.js";
28
+ import { DuplicateKeyError, jcsParse } from "./verifier.js";
29
29
 
30
30
  /** The index's dispatch key (5.6.2). An absent or unknown value makes the index unreadable. */
31
31
  export const CASE_FILE_VERSION = "sanning.case-file/v1";
@@ -237,7 +237,10 @@ function readIndex(raw: Uint8Array): ReadIndex {
237
237
  let value: unknown;
238
238
  try {
239
239
  value = jcsParse(raw);
240
- } catch {
240
+ } catch (e) {
241
+ // Named, because an index that lists one pack twice under one member name
242
+ // reads as whichever copy came last (I-JSON, proof#144).
243
+ if (e instanceof DuplicateKeyError) throw new Unreadable(e.message);
241
244
  throw new Unreadable("not readable as JSON");
242
245
  }
243
246
  if (!isObject(value)) throw new Unreadable("not a JSON object");
package/src/case.ts CHANGED
@@ -260,7 +260,7 @@ import {
260
260
  readHandoffs,
261
261
  recordIsMalformed,
262
262
  } from "./handoff.js";
263
- import { verifyEvidenceBundle } from "./evidence.js";
263
+ import { coSignaturesPresent, verifyEvidenceBundle } from "./evidence.js";
264
264
  import type { EvidenceBundleResult } from "./evidence.js";
265
265
  import {
266
266
  type PackGrade,
@@ -271,6 +271,7 @@ import {
271
271
  gradePack,
272
272
  } from "./grading.js";
273
273
  import type { SideContentMap } from "./side-content.js";
274
+ import { renderWitnessTime } from "./witness-cache.js";
274
275
 
275
276
  // ---- The vocabulary ---------------------------------------------------------
276
277
 
@@ -727,13 +728,25 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
727
728
  export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
728
729
 
729
730
  /**
730
- * The two NOTEs (5.5.3 v1.20): findings on the reading with no contribution,
731
- * printed in the scope sentence so a clean verdict never hides either.
732
- * `pack_undeclared` is a note only where the reading holds no hand-off record;
733
- * where it holds one, it is the exception of that name.
731
+ * The three NOTEs (5.5.3 v1.20, extended proof#148): findings on the reading
732
+ * with no contribution, printed in the scope sentence so a clean verdict
733
+ * never hides any of them. `pack_undeclared` is a note only where the
734
+ * reading holds no hand-off record; where it holds one, it is the exception
735
+ * of that name.
734
736
  */
735
737
  export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
736
- export const CASE_NOTE_KINDS = [PACK_UNDECLARED, CONTENT_WITHHELD_BY_DECLARATION] as const;
738
+ /**
739
+ * A pack's wrapper carries a present `co_signatures` (evidence-bundle.md §2,
740
+ * decided by Phil 2026-09-24 on proof#148). The field verifies nothing and
741
+ * sits outside the wrapper's signed scope, so the case reading names it
742
+ * rather than presenting it as evidence, and it contributes no verdict.
743
+ */
744
+ export const CO_SIGNATURES_PRESENT = "co_signatures_present";
745
+ export const CASE_NOTE_KINDS = [
746
+ PACK_UNDECLARED,
747
+ CONTENT_WITHHELD_BY_DECLARATION,
748
+ CO_SIGNATURES_PRESENT,
749
+ ] as const;
737
750
  export type CaseNoteKind = (typeof CASE_NOTE_KINDS)[number];
738
751
 
739
752
  /**
@@ -1330,6 +1343,16 @@ export async function readCase(
1330
1343
  }
1331
1344
  holderKeys.add(holder);
1332
1345
 
1346
+ if (coSignaturesPresent(pack.bundle)) {
1347
+ // A NOTE, with no contribution (5.5.3 v1.20, proof#148): the wrapper's
1348
+ // co_signatures verifies nothing and sits outside its signed scope, so
1349
+ // the case reading names it rather than moving anything on its account.
1350
+ caseNotes.push({
1351
+ position,
1352
+ note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref },
1353
+ });
1354
+ }
1355
+
1333
1356
  const bodyType = bodyTypeOf(pack.bundle);
1334
1357
  const records = packRecords(pack.bundle, pack.content);
1335
1358
  // The per-record gradings below are joined to `records` BY INDEX, so a
@@ -1939,6 +1962,11 @@ export function renderCaseNote(note: CaseNote): string {
1939
1962
  );
1940
1963
  case PACK_UNDECLARED:
1941
1964
  return `${note.packRef} does not say what it holds`;
1965
+ case CO_SIGNATURES_PRESENT:
1966
+ return (
1967
+ `${note.packRef} carries unverified co-signatures in its wrapper; ` +
1968
+ "they are not part of the evidence"
1969
+ );
1942
1970
  default:
1943
1971
  return (note as CaseNote).kind;
1944
1972
  }
@@ -1998,17 +2026,18 @@ export const VERDICT_DISPLAY_TITLES: Readonly<Record<string, string>> = {
1998
2026
 
1999
2027
  /**
2000
2028
  * What a reader may rely on, for each of the six words a case reading reaches
2001
- * (5.5.6 v1.20, A26), the same for every drop. `pending` and `not_found`
2002
- * never head a case reading: they are witness states.
2029
+ * (5.5.6 v1.20, A26; the `verified` text at v1.28, A35), the same for every
2030
+ * drop. `pending` and `not_found` never head a case reading: they are
2031
+ * witness states. `verified` is the base sentence, printed alone where the
2032
+ * drop holds no hand-off record; a caller reads `caseReliance` for
2033
+ * `verified`, never this mapping directly.
2003
2034
  */
2004
2035
  export const CASE_RELIANCE: Readonly<Record<CaseVerdict, string>> = {
2005
2036
  verified:
2006
- "Every pack in this drop is genuine and unaltered and agrees with its " +
2007
- "own signed declaration. Every hand-off with both sides here is " +
2008
- "intact. Every other record is one its holder signed as a step. " +
2009
- "Nothing here could not be checked. This was read offline, on this " +
2010
- "device. It does not say these packs are everything the parties " +
2011
- "recorded.",
2037
+ "Every pack in this drop is genuine, unaltered, and matches its own " +
2038
+ "signed declaration. Every record in it was signed by its holder's " +
2039
+ "key. This was read offline, on this device. It does not say this " +
2040
+ "drop holds everything that was recorded.",
2012
2041
  partial:
2013
2042
  "Nothing here was proven wrong, and a check could not be completed: a " +
2014
2043
  "pack names no witness, or a pack in a hand-off reading does not say " +
@@ -2031,6 +2060,37 @@ export const CASE_RELIANCE: Readonly<Record<CaseVerdict, string>> = {
2031
2060
  "exception before relying on any of it.",
2032
2061
  };
2033
2062
 
2063
+ /**
2064
+ * The clause a `verified` drop that holds a hand-off record gains, inserted
2065
+ * after `CASE_RELIANCE`'s first sentence (5.5.6, A35). Never printed alone,
2066
+ * and never where `handoffRecords` is zero: most drops hold one agent's
2067
+ * pack and no hand-off to say anything about.
2068
+ */
2069
+ export const CASE_RELIANCE_HANDOFF_CLAUSE =
2070
+ "Every hand-off with both sides in this drop is intact.";
2071
+
2072
+ /**
2073
+ * What a reader may rely on for one case reading (5.5.6 v1.28, A35), byte
2074
+ * for byte the text `case_reliance`'s Python sibling returns.
2075
+ *
2076
+ * For every word but `verified` this is `CASE_RELIANCE` unchanged. For
2077
+ * `verified`, the hand-off clause is inserted after the first sentence
2078
+ * where `handoffRecords` is not zero, and left out where it is zero: the
2079
+ * base sentence never claims a hand-off the drop does not hold.
2080
+ */
2081
+ export function caseReliance(verdict: CaseVerdict, handoffRecords: number): string {
2082
+ if (verdict === "verified" && handoffRecords > 0) {
2083
+ return (
2084
+ "Every pack in this drop is genuine, unaltered, and matches its own " +
2085
+ "signed declaration. Every hand-off with both sides in this drop is " +
2086
+ "intact. Every record in it was signed by its holder's key. This " +
2087
+ "was read offline, on this device. It does not say this drop holds " +
2088
+ "everything that was recorded."
2089
+ );
2090
+ }
2091
+ return CASE_RELIANCE[verdict];
2092
+ }
2093
+
2034
2094
  /** The three witness states, and how each is printed (5.5.6, D7). No other. */
2035
2095
  export const WITNESS_STATES = {
2036
2096
  confirmed: "Confirmed on the public record",
@@ -2050,6 +2110,18 @@ export interface WitnessLine {
2050
2110
  state: WitnessState;
2051
2111
  reasons: string[];
2052
2112
  services: string[];
2113
+ /**
2114
+ * Witness times a pack caches that no lookup settled, as `[blockHeight,
2115
+ * blockTime]`, distinct and in time order (5.6.6, v1.25, A30). Printed labelled
2116
+ * not checked, and never as a time a reader may rely on. Absent reads as none.
2117
+ */
2118
+ cached?: [number, number][];
2119
+ /**
2120
+ * Witness times the public record returned where they matched a pack's
2121
+ * cache, distinct and in time order. The gateway's answer, named with the
2122
+ * service, never the cache's value. Absent reads as none.
2123
+ */
2124
+ recorded?: [number, number][];
2053
2125
  }
2054
2126
 
2055
2127
  /** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
@@ -2060,9 +2132,23 @@ export const WITNESS_NOT_LOOKED_UP: Readonly<WitnessLine> = Object.freeze({
2060
2132
  });
2061
2133
 
2062
2134
  /** The witness line as both kernels print it. */
2135
+ /** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
2136
+ export const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
2137
+ /** How a witness time the public record returned is printed where it matched the cache. */
2138
+ export const WITNESS_TIME_RECORDED = "the public record dates the witness";
2139
+
2140
+ function witnessTimes(times: readonly [number, number][]): string {
2141
+ return times.map(([h, t]) => renderWitnessTime(h, t)).join(", ");
2142
+ }
2143
+
2063
2144
  export function renderWitnessLine(line: WitnessLine): string {
2064
2145
  let text = `Witness: ${WITNESS_STATES[line.state]}`;
2065
- if (line.reasons.length > 0) text += `: ${line.reasons.join("; ")}`;
2146
+ const parts = [...line.reasons];
2147
+ if (line.cached && line.cached.length > 0) parts.push(`${WITNESS_TIME_CACHED}: ${witnessTimes(line.cached)}`);
2148
+ if (line.recorded && line.recorded.length > 0) {
2149
+ parts.push(`${WITNESS_TIME_RECORDED}: ${witnessTimes(line.recorded)}`);
2150
+ }
2151
+ if (parts.length > 0) text += `: ${parts.join("; ")}`;
2066
2152
  if (line.services.length > 0) text += ` (answered: ${line.services.join(", ")})`;
2067
2153
  return text;
2068
2154
  }