@sanning/proof 0.11.1 → 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 +43 -8
- 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 +199 -43
- package/dist/case.js +522 -146
- package/dist/cli.d.ts +132 -1
- package/dist/cli.js +1140 -96
- 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/handoff.d.ts +16 -1
- package/dist/handoff.js +91 -16
- package/dist/index.d.ts +10 -7
- package/dist/index.js +15 -7
- 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 +651 -156
- package/src/cli.ts +1325 -100
- 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/handoff.ts +108 -17
- package/src/index.ts +60 -3
- 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 +56 -3
- package/vectors/case-file-index.json +668 -0
- package/vectors/case-split.json +1102 -0
- package/vectors/pack-grading.json +68 -4
- package/vectors/stamp-on-chain.json +3 -3
package/dist/evidence.js
CHANGED
|
@@ -21,13 +21,15 @@ import { deriveOperatorAddress, ed25519Verify, hexToBytes,
|
|
|
21
21
|
// body: two copies of one rule is how two kernels drift apart one edit at a
|
|
22
22
|
// time, which is the whole defect class this fix is about.
|
|
23
23
|
isHexField, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
24
|
+
import { isConformantHandoffRecord, isHandoffNamespace } from "./handoff.js";
|
|
24
25
|
import { leafHash, verifyInclusion } from "./merkle.js";
|
|
25
26
|
import { MAX_CANONICAL_DEPTH, contentHashes, exceedsDepth, jcs, jcsParse, verifyEnvelope, } from "./verifier.js";
|
|
26
27
|
import { inBodyContentOffered, sideContentEntry, } from "./side-content.js";
|
|
27
28
|
// Only the evidence-bundle major(s) this kernel verifies. A new major is a
|
|
28
29
|
// deliberate one-entry addition HERE — same fail-closed discipline as the
|
|
29
30
|
// envelope verifier's ACCEPTED_SPEC_MAJORS.
|
|
30
|
-
|
|
31
|
+
/** The accepted `sanning.evidence/v1` wrapper majors; exported in both kernels (P-01, 0.13.0). */
|
|
32
|
+
export const ACCEPTED_EVIDENCE_MAJORS = Object.freeze(["sanning.evidence/v1"]);
|
|
31
33
|
// The body_type this module knows how to verify structurally. An unknown
|
|
32
34
|
// body_type is not a hard failure of the WRAPPER (the signature + body_hash
|
|
33
35
|
// still verify), but its body cannot be re-derived, so the per-body verdict is
|
|
@@ -254,12 +256,16 @@ async function verifyBundle(bundle, options, exportSource) {
|
|
|
254
256
|
const b = bundle;
|
|
255
257
|
const specVersionOk = isEvidenceMajorAccepted(b.spec_version);
|
|
256
258
|
if (!specVersionOk) {
|
|
257
|
-
|
|
259
|
+
// `?? null`: an ABSENT field is spelled `null`, as the Python twin's
|
|
260
|
+
// `_json_str` spells it. `JSON.stringify(undefined)` is `undefined`, and
|
|
261
|
+
// the two kernels printed two different sentences for one bundle until
|
|
262
|
+
// `verify --json` compared them byte for byte (0.13.0).
|
|
263
|
+
return malformed(`unsupported evidence spec_version: ${JSON.stringify(b.spec_version ?? null)}`, false);
|
|
258
264
|
}
|
|
259
265
|
if (typeof b.signature_alg === "string" && b.signature_alg !== "ed25519") {
|
|
260
266
|
// The reference verifier implements ed25519 only (evidence-bundle §2 +
|
|
261
267
|
// decision #2); other algs are a deliberate future addition.
|
|
262
|
-
return malformed(`unsupported signature_alg: ${JSON.stringify(b.signature_alg)}`, true);
|
|
268
|
+
return malformed(`unsupported signature_alg: ${JSON.stringify(b.signature_alg ?? null)}`, true);
|
|
263
269
|
}
|
|
264
270
|
if (typeof b.public_key !== "string" || typeof b.signature !== "string") {
|
|
265
271
|
return malformed("evidence bundle is missing public_key/signature", true);
|
|
@@ -308,7 +314,7 @@ async function verifyBundle(bundle, options, exportSource) {
|
|
|
308
314
|
if (keyFormOk && signatureFormOk) {
|
|
309
315
|
try {
|
|
310
316
|
const { signature: _sig, co_signatures: _co, ...bundleForSig } = b;
|
|
311
|
-
signatureOk = await ed25519Verify(
|
|
317
|
+
signatureOk = await ed25519Verify(utf8(jcs(bundleForSig)), b.signature, b.public_key);
|
|
312
318
|
if (!signatureOk)
|
|
313
319
|
errors.push("wrapper Ed25519 signature verification failed");
|
|
314
320
|
}
|
|
@@ -524,7 +530,7 @@ function parseAnchorRef(raw, errors) {
|
|
|
524
530
|
}
|
|
525
531
|
const o = raw;
|
|
526
532
|
if (typeof o.kind !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(o.kind)) {
|
|
527
|
-
errors.push(`anchor_ref.kind is missing or malformed: ${JSON.stringify(o.kind)}`);
|
|
533
|
+
errors.push(`anchor_ref.kind is missing or malformed: ${JSON.stringify(o.kind ?? null)}`);
|
|
528
534
|
return { ref: null, malformed: true };
|
|
529
535
|
}
|
|
530
536
|
if (typeof o.id !== "string" || o.id === "") {
|
|
@@ -1076,21 +1082,62 @@ function isNonNegInt(n) {
|
|
|
1076
1082
|
async function verifyDisclosedContent(ev, eventId, eventBytes, sideContent) {
|
|
1077
1083
|
const evErrors = [];
|
|
1078
1084
|
let contentOk = null;
|
|
1085
|
+
// WHICH hash in the record is decided by the record's STRUCTURE
|
|
1086
|
+
// (evidence-bundle §5.1 v1.19, A20). A record `envelope-spec.md` §4.1
|
|
1087
|
+
// governs (`isHandoffNamespace`) commits to its message through
|
|
1088
|
+
// `event.message_hash`, SHA-256 of `JCS(message object)`, and has no
|
|
1089
|
+
// `content_hash`, so until A20 its disclosure bound to nothing and an honest
|
|
1090
|
+
// hand-off pack could never grade `verified`. For such a record the committed
|
|
1091
|
+
// hash is `event.message_hash` and nothing else: not a `content_hash` it also
|
|
1092
|
+
// carries, and not the envelope's. It binds only when the record is a
|
|
1093
|
+
// CONFORMANT hand-off record under the key that signed it
|
|
1094
|
+
// (`isConformantHandoffRecord`) and the value is 64 characters of lowercase
|
|
1095
|
+
// hex. Anything short of that commits nothing this reader will bind to, so
|
|
1096
|
+
// the reading is undetermined and never true: a record dressed as a hand-off
|
|
1097
|
+
// record gains no binding from the dress, and a record outside §4.1 keeps its
|
|
1098
|
+
// `content_hash` exactly as before, whatever `message_hash` field it writes.
|
|
1099
|
+
// The Python twin is `evidence._evaluate_content`.
|
|
1079
1100
|
const committed = [];
|
|
1080
1101
|
let recordContentHash;
|
|
1102
|
+
let governed = false;
|
|
1103
|
+
let handoffHash;
|
|
1081
1104
|
if (eventBytes !== undefined) {
|
|
1082
1105
|
try {
|
|
1083
|
-
const
|
|
1084
|
-
const
|
|
1106
|
+
const parsed = jcsParse(eventBytes);
|
|
1107
|
+
const record = parsed;
|
|
1108
|
+
const ch = record?.event?.content_hash;
|
|
1085
1109
|
if (typeof ch === "string" && ch)
|
|
1086
1110
|
recordContentHash = ch.toLowerCase();
|
|
1111
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1112
|
+
const envelope = ev.envelope;
|
|
1113
|
+
const held = {
|
|
1114
|
+
publicKey: envelope !== null && typeof envelope === "object" && !Array.isArray(envelope)
|
|
1115
|
+
? envelope.public_key
|
|
1116
|
+
: undefined,
|
|
1117
|
+
record: parsed,
|
|
1118
|
+
ref: "",
|
|
1119
|
+
};
|
|
1120
|
+
if (isHandoffNamespace(held)) {
|
|
1121
|
+
governed = true;
|
|
1122
|
+
const event = parsed.event;
|
|
1123
|
+
const mh = event !== null && typeof event === "object" && !Array.isArray(event)
|
|
1124
|
+
? event.message_hash
|
|
1125
|
+
: undefined;
|
|
1126
|
+
if (isConformantHandoffRecord(held) && isLowerHex(mh, 64))
|
|
1127
|
+
handoffHash = mh;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1087
1130
|
}
|
|
1088
1131
|
catch {
|
|
1089
1132
|
// record not JSON — the payload binding already reflects that; content
|
|
1090
1133
|
// stays undetermined rather than throwing.
|
|
1091
1134
|
}
|
|
1092
1135
|
}
|
|
1093
|
-
if (
|
|
1136
|
+
if (governed) {
|
|
1137
|
+
if (handoffHash !== undefined)
|
|
1138
|
+
committed.push(handoffHash);
|
|
1139
|
+
}
|
|
1140
|
+
else if (recordContentHash !== undefined) {
|
|
1094
1141
|
committed.push(recordContentHash);
|
|
1095
1142
|
}
|
|
1096
1143
|
else if (ev.envelope && typeof ev.envelope === "object") {
|
|
@@ -1112,15 +1159,20 @@ async function verifyDisclosedContent(ev, eventId, eventBytes, sideContent) {
|
|
|
1112
1159
|
if (disclosed === undefined || committed.length === 0) {
|
|
1113
1160
|
contentOk = null;
|
|
1114
1161
|
if (disclosed !== undefined && committed.length === 0) {
|
|
1115
|
-
evErrors.push(
|
|
1116
|
-
"
|
|
1162
|
+
evErrors.push(governed
|
|
1163
|
+
? "disclosed content present but this hand-off record commits no " +
|
|
1164
|
+
"message_hash a reader can bind to (not a conformant hand-off " +
|
|
1165
|
+
"record, or message_hash is not 64 lowercase hex) -- undetermined"
|
|
1166
|
+
: "disclosed content present but no committed content_hash to bind to " +
|
|
1167
|
+
"(record withheld or record has no event.content_hash) -- undetermined");
|
|
1117
1168
|
}
|
|
1118
1169
|
}
|
|
1119
1170
|
else {
|
|
1120
1171
|
const got = await sha256Hex(disclosed);
|
|
1121
1172
|
contentOk = committed.includes(got);
|
|
1122
1173
|
if (!contentOk) {
|
|
1123
|
-
|
|
1174
|
+
const fieldName = governed ? "message_hash" : "content_hash";
|
|
1175
|
+
evErrors.push(`disclosed content does not match the committed ${fieldName}: ` +
|
|
1124
1176
|
`sha256(disclosed)=${got} committed=${committed.join("|")}`);
|
|
1125
1177
|
}
|
|
1126
1178
|
}
|
|
@@ -1217,7 +1269,7 @@ async function verifyStampEntry(raw, seen) {
|
|
|
1217
1269
|
// two answers to one question inside one signature. The body_type is the
|
|
1218
1270
|
// reader's dispatch key and it said v1; honouring the other spelling is
|
|
1219
1271
|
// how a reader half-parses a shape it does not know.
|
|
1220
|
-
errors.push(`unsupported stamp_version ${JSON.stringify(body.stamp_version)} in a ` +
|
|
1272
|
+
errors.push(`unsupported stamp_version ${JSON.stringify(body.stamp_version ?? null)} in a ` +
|
|
1221
1273
|
`${STAMP_TRACE_BODY_TYPE} body (expected ${STAMP_BODY_VERSION})`);
|
|
1222
1274
|
return { result: base, witnessedHex };
|
|
1223
1275
|
}
|
|
@@ -1702,8 +1754,8 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1702
1754
|
* synthesis exists to drive `gatewayUnavailable`, which is what downgrades that
|
|
1703
1755
|
* run to `partial`. Here §5.2.4 already downgrades an unwitnessed stamp to
|
|
1704
1756
|
* `partial` on its own, with a better error. Copying the synthesis would add
|
|
1705
|
-
* nothing to the status
|
|
1706
|
-
*
|
|
1757
|
+
* nothing to the status, which is `partial` either way, and would add an
|
|
1758
|
+
* unreachable-gateway note for a network event that never happened, based on
|
|
1707
1759
|
* nothing but whether the caller happened to pass a gateway list.
|
|
1708
1760
|
*/
|
|
1709
1761
|
async function refetchStamps(stamps, witnessed, options, errors) {
|
|
@@ -2016,8 +2068,11 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
|
|
|
2016
2068
|
// step-5 erratum flagged in the slice report.
|
|
2017
2069
|
let verdictAgreementOk = false;
|
|
2018
2070
|
try {
|
|
2019
|
-
const
|
|
2020
|
-
const
|
|
2071
|
+
const cached = projectVerdictForAgreement(body.kernel_verdict);
|
|
2072
|
+
const fresh = projectVerdictForAgreement(verdict);
|
|
2073
|
+
toleratePreA20Content(cached, fresh, sourceEventsGoverned(inline));
|
|
2074
|
+
const cachedProjection = jcs(cached);
|
|
2075
|
+
const freshProjection = jcs(fresh);
|
|
2021
2076
|
verdictAgreementOk = cachedProjection === freshProjection;
|
|
2022
2077
|
if (!verdictAgreementOk) {
|
|
2023
2078
|
errors.push("recomputed verdict disagrees with the cached kernel_verdict " +
|
|
@@ -2046,8 +2101,8 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
|
|
|
2046
2101
|
status = "failed";
|
|
2047
2102
|
}
|
|
2048
2103
|
else if (sourceResult.status === "partial") {
|
|
2049
|
-
// Undetermined source (withheld record
|
|
2050
|
-
// exit 3
|
|
2104
|
+
// Undetermined source (a withheld record, or an unreachable gateway):
|
|
2105
|
+
// exit 3 either way since 0.13.0. Never a failure.
|
|
2051
2106
|
status = "partial";
|
|
2052
2107
|
}
|
|
2053
2108
|
else {
|
|
@@ -2193,7 +2248,7 @@ async function verifyAttestation(rec, sourceCheckpoints) {
|
|
|
2193
2248
|
if (rec.signature_alg !== "rsa-pss-sha256") {
|
|
2194
2249
|
return {
|
|
2195
2250
|
result,
|
|
2196
|
-
malformed: `unsupported attestation signature_alg: ${JSON.stringify(rec.signature_alg)}`,
|
|
2251
|
+
malformed: `unsupported attestation signature_alg: ${JSON.stringify(rec.signature_alg ?? null)}`,
|
|
2197
2252
|
};
|
|
2198
2253
|
}
|
|
2199
2254
|
const pub = rec.public_key;
|
|
@@ -2421,6 +2476,66 @@ function projectVerdictForAgreement(v) {
|
|
|
2421
2476
|
}),
|
|
2422
2477
|
};
|
|
2423
2478
|
}
|
|
2479
|
+
// Per event of an export's inline `sanning.anchor.trace/v3` source, in order:
|
|
2480
|
+
// whether `envelope-spec.md` §4.1 governs its record. Decoded the way the
|
|
2481
|
+
// content binding decodes it, so an event whose record is withheld, not hex or
|
|
2482
|
+
// not JCS is not governed here either. The Python twin is
|
|
2483
|
+
// `evidence._source_events_governed`.
|
|
2484
|
+
function sourceEventsGoverned(source) {
|
|
2485
|
+
if (source === null || typeof source !== "object" || Array.isArray(source))
|
|
2486
|
+
return [];
|
|
2487
|
+
const s = source;
|
|
2488
|
+
if (s.body_type !== ANCHOR_TRACE_BODY_TYPE)
|
|
2489
|
+
return [];
|
|
2490
|
+
const body = s.body;
|
|
2491
|
+
const events = body !== null && typeof body === "object" && !Array.isArray(body) ? body.events : undefined;
|
|
2492
|
+
if (!Array.isArray(events))
|
|
2493
|
+
return [];
|
|
2494
|
+
return events.map((raw) => {
|
|
2495
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
2496
|
+
return false;
|
|
2497
|
+
const ev = raw;
|
|
2498
|
+
if (typeof ev.event_bytes !== "string")
|
|
2499
|
+
return false;
|
|
2500
|
+
try {
|
|
2501
|
+
const parsed = jcsParse(hexToBytes(ev.event_bytes));
|
|
2502
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2503
|
+
return false;
|
|
2504
|
+
const envelope = ev.envelope;
|
|
2505
|
+
const held = {
|
|
2506
|
+
publicKey: envelope !== null && typeof envelope === "object" && !Array.isArray(envelope)
|
|
2507
|
+
? envelope.public_key
|
|
2508
|
+
: undefined,
|
|
2509
|
+
record: parsed,
|
|
2510
|
+
ref: "",
|
|
2511
|
+
};
|
|
2512
|
+
return isHandoffNamespace(held);
|
|
2513
|
+
}
|
|
2514
|
+
catch {
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
// The one difference §5 step 5 does not read as a disagreement
|
|
2520
|
+
// (evidence-export.md, evidence-bundle.md v1.19, A20): a cached `content_ok` of
|
|
2521
|
+
// `null` against a recomputed `true`, on an event whose record §4.1 governs. A
|
|
2522
|
+
// kernel before A20 could not bind a hand-off record's disclosed message, so an
|
|
2523
|
+
// export it composed claims less than the recompute proves, and reading that
|
|
2524
|
+
// as tamper failed an honest export. Only that direction: a cached `true`
|
|
2525
|
+
// against a recomputed `false` or `null`, `null` against `false`, and any
|
|
2526
|
+
// difference on a record outside §4.1 still disagree. Edits `fresh` in place
|
|
2527
|
+
// to match `cached` on those events. The Python twin is
|
|
2528
|
+
// `evidence._tolerate_pre_a20_content`.
|
|
2529
|
+
function toleratePreA20Content(cached, fresh, governed) {
|
|
2530
|
+
const n = Math.min(cached.events.length, fresh.events.length);
|
|
2531
|
+
for (let i = 0; i < n; i++) {
|
|
2532
|
+
if (governed[i] === true &&
|
|
2533
|
+
cached.events[i].content_ok === null &&
|
|
2534
|
+
fresh.events[i].content_ok === true) {
|
|
2535
|
+
fresh.events[i].content_ok = null;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2424
2539
|
function summarizeExport(status, source, attestations) {
|
|
2425
2540
|
const attOk = attestations.filter((a) => a.ok).length;
|
|
2426
2541
|
return (`Export ${status}: source bundle ${source.status} ` +
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const EXIT_VERIFIED = 0;
|
|
2
|
+
export declare const EXIT_FAILED = 1;
|
|
3
|
+
export declare const EXIT_MALFORMED = 2;
|
|
4
|
+
export declare const EXIT_UNDETERMINED = 3;
|
|
5
|
+
export declare const EXIT_MIXED = 4;
|
|
6
|
+
/**
|
|
7
|
+
* The exit code of each of the eight verdict words (`evidence-bundle.md` §3).
|
|
8
|
+
* `undetermined` is the name of 3, and never a word here.
|
|
9
|
+
*/
|
|
10
|
+
export declare const EXIT_CODES: Readonly<Record<string, number>>;
|
|
11
|
+
/** What every verb's help prints, identical in both commands. */
|
|
12
|
+
export declare const EXIT_CODES_HELP: string;
|
|
13
|
+
/**
|
|
14
|
+
* The exit code of a verdict word. A word outside the eight is a verdict
|
|
15
|
+
* nothing can render, so it exits 2, never 0.
|
|
16
|
+
*/
|
|
17
|
+
export declare function exitCodeFor(verdict: unknown): number;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// One exit-code table for every verb (0.13.0).
|
|
2
|
+
//
|
|
3
|
+
// Both commands, `npx @sanning/proof` and `sanning-proof`, map a verdict word to
|
|
4
|
+
// an exit code through this one table, for `verify`, `case` and `export` alike,
|
|
5
|
+
// and every verb's `--help` prints `EXIT_CODES_HELP` byte for byte. The twin is
|
|
6
|
+
// `src/sanning_proof/exit_codes.py`; the specification is `evidence-export.md`
|
|
7
|
+
// §5.
|
|
8
|
+
//
|
|
9
|
+
// Until 0.13.0 `verify` exited 0 on `partial` unless a gateway was unreachable,
|
|
10
|
+
// while `case` exited 3 on it, so a CI gate reading `verify`'s exit code read a
|
|
11
|
+
// stamp witnessed by nobody as verified. Every word that means "could not be
|
|
12
|
+
// settled" now exits 3, which is the code's name: undetermined.
|
|
13
|
+
export const EXIT_VERIFIED = 0;
|
|
14
|
+
export const EXIT_FAILED = 1;
|
|
15
|
+
export const EXIT_MALFORMED = 2;
|
|
16
|
+
export const EXIT_UNDETERMINED = 3;
|
|
17
|
+
export const EXIT_MIXED = 4;
|
|
18
|
+
/**
|
|
19
|
+
* The exit code of each of the eight verdict words (`evidence-bundle.md` §3).
|
|
20
|
+
* `undetermined` is the name of 3, and never a word here.
|
|
21
|
+
*/
|
|
22
|
+
export const EXIT_CODES = Object.freeze({
|
|
23
|
+
verified: EXIT_VERIFIED,
|
|
24
|
+
failed: EXIT_FAILED,
|
|
25
|
+
malformed: EXIT_MALFORMED,
|
|
26
|
+
partial: EXIT_UNDETERMINED,
|
|
27
|
+
pending: EXIT_UNDETERMINED,
|
|
28
|
+
incomplete: EXIT_UNDETERMINED,
|
|
29
|
+
not_found: EXIT_UNDETERMINED,
|
|
30
|
+
mixed: EXIT_MIXED,
|
|
31
|
+
});
|
|
32
|
+
/** What every verb's help prints, identical in both commands. */
|
|
33
|
+
export const EXIT_CODES_HELP = [
|
|
34
|
+
"Exit codes, one table for every verb:",
|
|
35
|
+
" 0 verified, or the export was written",
|
|
36
|
+
" 1 failed",
|
|
37
|
+
" 2 usage or malformed",
|
|
38
|
+
" 3 undetermined: partial, pending, incomplete or not_found",
|
|
39
|
+
" 4 mixed: more than one outcome, none failed; read the exceptions",
|
|
40
|
+
].join("\n");
|
|
41
|
+
/**
|
|
42
|
+
* The exit code of a verdict word. A word outside the eight is a verdict
|
|
43
|
+
* nothing can render, so it exits 2, never 0.
|
|
44
|
+
*/
|
|
45
|
+
export function exitCodeFor(verdict) {
|
|
46
|
+
if (typeof verdict === "string" && Object.prototype.hasOwnProperty.call(EXIT_CODES, verdict)) {
|
|
47
|
+
return EXIT_CODES[verdict];
|
|
48
|
+
}
|
|
49
|
+
return EXIT_MALFORMED;
|
|
50
|
+
}
|
package/dist/grading.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DeclarationCheck, DeclarationContentKind, DeclarationDeviationKind, DeclarationVerdict } from "./declaration.js";
|
|
2
2
|
import { type SideContentMap } from "./side-content.js";
|
|
3
|
-
export
|
|
3
|
+
export declare const PACK_VERDICTS: readonly ["verified", "partial", "pending", "not_found", "mixed", "failed", "malformed", "incomplete"];
|
|
4
|
+
export type PackVerdict = (typeof PACK_VERDICTS)[number];
|
|
4
5
|
/**
|
|
5
6
|
* The minimum this module reads out of a verification result.
|
|
6
7
|
*
|
|
@@ -12,9 +13,16 @@ export type PackVerdict = "verified" | "partial" | "pending" | "not_found" | "mi
|
|
|
12
13
|
*/
|
|
13
14
|
export interface GradeInput {
|
|
14
15
|
status: string | null | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* `eventId` and `payloadBindingOk` are read by rule version 6 alone (A22):
|
|
18
|
+
* an input that does not carry both is never withheld by its declaration,
|
|
19
|
+
* so an adapter written for rule version 5 keeps the stricter reading.
|
|
20
|
+
*/
|
|
15
21
|
events: {
|
|
16
22
|
ok: boolean;
|
|
17
23
|
contentOk: boolean | null;
|
|
24
|
+
eventId?: string;
|
|
25
|
+
payloadBindingOk?: boolean | null;
|
|
18
26
|
}[];
|
|
19
27
|
}
|
|
20
28
|
/**
|
|
@@ -56,13 +64,8 @@ export type CompletenessNote = {
|
|
|
56
64
|
presented: null;
|
|
57
65
|
kind: "unreadable";
|
|
58
66
|
};
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
* The pack and the UNSIGNED content handed in beside it do not line up
|
|
62
|
-
* (proof#147). A cause, so the pack cannot read clean over it and a reader
|
|
63
|
-
* is told which record; NOT a deviation, so it never reaches `failed`.
|
|
64
|
-
*/
|
|
65
|
-
| "declaration-content-dependent";
|
|
67
|
+
export declare const PACK_GRADE_CAUSE_KINDS: readonly ["records_omitted", "records_surplus", "leaf_count_unreadable", "no_disclosure", "missing_disclosure", "unreadable_disclosure", "unaccounted_disclosure", "declaration_deviation", "declaration_content_dependent"];
|
|
68
|
+
export type PackGradeCauseKind = (typeof PACK_GRADE_CAUSE_KINDS)[number];
|
|
66
69
|
/**
|
|
67
70
|
* One reason a pack is not `verified`, as data.
|
|
68
71
|
*
|
|
@@ -89,8 +92,8 @@ export interface PackGradeCause {
|
|
|
89
92
|
reason?: string;
|
|
90
93
|
/**
|
|
91
94
|
* Which deviation from the pack's own declaration, for
|
|
92
|
-
* `
|
|
93
|
-
* `
|
|
95
|
+
* `declaration_deviation`, or which content-dependent finding, for
|
|
96
|
+
* `declaration_content_dependent`.
|
|
94
97
|
*/
|
|
95
98
|
deviation?: DeclarationDeviationKind | DeclarationContentKind;
|
|
96
99
|
}
|
|
@@ -107,6 +110,17 @@ export interface PackGrade {
|
|
|
107
110
|
* check was passed, which is the pre-§5.4 call.
|
|
108
111
|
*/
|
|
109
112
|
declaration?: DeclarationVerdict;
|
|
113
|
+
/**
|
|
114
|
+
* Records WITHHELD BY THEIR DECLARATION (rule version 6, evidence-bundle
|
|
115
|
+
* 5.4.5 v1.20, A22): a readable declaration lists them in
|
|
116
|
+
* `declaredWithheld`, their `ok` is true, their `contentOk` is `null` and
|
|
117
|
+
* their `payloadBindingOk` is true. Never counted in `recordsVerified`: no
|
|
118
|
+
* byte of their content was hashed, and a surface MUST NOT print them as
|
|
119
|
+
* verified records.
|
|
120
|
+
*/
|
|
121
|
+
recordsWithheld: number;
|
|
122
|
+
/** Their `event_id`s, in bundle order. */
|
|
123
|
+
withheldEventIds: string[];
|
|
110
124
|
}
|
|
111
125
|
/**
|
|
112
126
|
* Account for the disclosed bytes a pack handed us against the records its
|
|
@@ -152,13 +166,14 @@ export declare function completenessNotes(bundle: unknown): CompletenessNote[];
|
|
|
152
166
|
* Every reason this pack is not `verified`, in the order a reader should hear
|
|
153
167
|
* them: what is missing from the whole, then what is missing from the parts.
|
|
154
168
|
*/
|
|
155
|
-
export declare function gradeCauses(account: DisclosureAccount, notes: readonly CompletenessNote[], declaration?: DeclarationCheck | null): PackGradeCause[];
|
|
169
|
+
export declare function gradeCauses(account: DisclosureAccount, notes: readonly CompletenessNote[], declaration?: DeclarationCheck | null, withheld?: readonly string[]): PackGradeCause[];
|
|
156
170
|
/**
|
|
157
171
|
* The verdict a surface may print, and the counts it may print beside it.
|
|
158
172
|
*
|
|
159
173
|
* The invariant, stated so a mutation test can kill it: `verified` is reachable
|
|
160
|
-
* ONLY when every record's supplied bytes bound to its own commitment
|
|
161
|
-
*
|
|
174
|
+
* ONLY when every record's supplied bytes bound to its own commitment, or
|
|
175
|
+
* (rule version 6) the record is withheld by its declaration, and nothing is
|
|
176
|
+
* missing, unreadable, surplus or omitted.
|
|
162
177
|
*
|
|
163
178
|
* Precedence: malformed > failed > incomplete > the kernel's own headline.
|
|
164
179
|
* A hard verdict wins — "the bytes are absent" is never reported as failure
|