@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/dist/case-file.js +6 -2
- package/dist/case.d.ts +51 -7
- package/dist/case.js +81 -16
- package/dist/cli.d.ts +8 -27
- package/dist/cli.js +184 -103
- package/dist/compose.js +6 -1
- package/dist/evidence.d.ts +36 -0
- package/dist/evidence.js +104 -3
- package/dist/file-match.d.ts +105 -0
- package/dist/file-match.js +374 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +24 -2
- package/dist/message-parts.d.ts +12 -6
- package/dist/message-parts.js +18 -10
- package/dist/verifier.d.ts +54 -2
- package/dist/verifier.js +195 -3
- package/dist/witness-cache.d.ts +85 -0
- package/dist/witness-cache.js +264 -0
- package/dist/witness-lookup.d.ts +112 -0
- package/dist/witness-lookup.js +300 -0
- package/package.json +1 -1
- package/src/case-file.ts +5 -2
- package/src/case.ts +101 -15
- package/src/cli.ts +191 -123
- package/src/compose.ts +8 -1
- package/src/evidence.ts +115 -2
- package/src/file-match.ts +461 -0
- package/src/index.ts +68 -0
- package/src/message-parts.ts +18 -10
- package/src/verifier.ts +190 -3
- package/src/witness-cache.ts +306 -0
- package/src/witness-lookup.ts +428 -0
- package/vectors/README.md +43 -3
- package/vectors/file-match.json +1808 -0
- package/vectors/jcs-reader.json +82 -3
- package/vectors/stamp-on-chain.json +18 -3
- package/vectors/witness-cache.json +1304 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanning/proof",
|
|
3
|
-
"version": "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
|
|
731
|
-
* printed in the scope sentence so a clean verdict
|
|
732
|
-
* `pack_undeclared` is a note only where the
|
|
733
|
-
* where it holds one, it is the exception
|
|
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
|
-
|
|
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
|
|
2002
|
-
* never head a case reading: they are
|
|
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
|
|
2007
|
-
"
|
|
2008
|
-
"
|
|
2009
|
-
"
|
|
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
|
-
|
|
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
|
}
|