@sanning/proof 0.14.0 → 0.16.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/cli.d.ts +0 -42
- package/dist/cli.js +9 -159
- package/dist/declaration.d.ts +1 -1
- package/dist/declaration.js +182 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/witness-lookup.d.ts +112 -0
- package/dist/witness-lookup.js +300 -0
- package/package.json +2 -2
- package/src/cli.ts +7 -209
- package/src/declaration.ts +186 -3
- package/src/index.ts +25 -0
- package/src/witness-lookup.ts +428 -0
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { type WitnessLine } from "./case.js";
|
|
3
|
-
import { type CachedWitnessTime, type WitnessTimeAnswer, type WitnessTimeCheck } from "./witness-cache.js";
|
|
4
2
|
interface Cli {
|
|
5
3
|
out: (s: string) => void;
|
|
6
4
|
err: (s: string) => void;
|
|
@@ -106,46 +104,6 @@ export declare function readFileEntry(entry: TreeEntry, key: string): Promise<Ui
|
|
|
106
104
|
*/
|
|
107
105
|
export declare function treeFromArguments(paths: readonly string[]): Promise<Map<string, TreeEntry>>;
|
|
108
106
|
export declare const CASE_USAGE: string;
|
|
109
|
-
interface OnChainRow {
|
|
110
|
-
ref: string;
|
|
111
|
-
witnesses: number;
|
|
112
|
-
confirmed: number;
|
|
113
|
-
mismatched: number;
|
|
114
|
-
unreachable: number;
|
|
115
|
-
not_checked: number;
|
|
116
|
-
/** Present only where the pack caches a witness time (5.6.6, v1.25, A30). */
|
|
117
|
-
witness_times?: WitnessTimeRow[];
|
|
118
|
-
}
|
|
119
|
-
interface WitnessTimeRow {
|
|
120
|
-
stamp_ref: string;
|
|
121
|
-
cached: {
|
|
122
|
-
block_height: number;
|
|
123
|
-
block_time: number;
|
|
124
|
-
};
|
|
125
|
-
outcome: WitnessTimeCheck["outcome"];
|
|
126
|
-
answers: {
|
|
127
|
-
gateway: string;
|
|
128
|
-
outcome: WitnessTimeAnswer["outcome"];
|
|
129
|
-
block_height: number | null;
|
|
130
|
-
block_time: number | null;
|
|
131
|
-
}[];
|
|
132
|
-
}
|
|
133
|
-
interface OnChainHalf {
|
|
134
|
-
/** Present only where a lookup ran: each gateway that answered, in the order given. */
|
|
135
|
-
answered?: string[];
|
|
136
|
-
checked: boolean;
|
|
137
|
-
gateways: string[];
|
|
138
|
-
packs: OnChainRow[];
|
|
139
|
-
verdict: "not_checked" | "confirmed" | "failed" | "undetermined";
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* `cached` is every witness time the packs cache (5.6.6, v1.25, A30). Where no
|
|
143
|
-
* lookup ran, each is printed labelled not checked. Where one ran, each time
|
|
144
|
-
* the public record matched is printed as the record's, each time no lookup
|
|
145
|
-
* settled stays labelled not checked, and a time the record contradicts is the
|
|
146
|
-
* reason the line reads `differs`.
|
|
147
|
-
*/
|
|
148
|
-
export declare function witnessLine(onChain: OnChainHalf, cached?: readonly CachedWitnessTime[]): WitnessLine;
|
|
149
107
|
/**
|
|
150
108
|
* The case verdict's code, moved only by an on-chain half that was asked for:
|
|
151
109
|
* a witness that does not match is a failure whatever the reading said, and an
|
package/dist/cli.js
CHANGED
|
@@ -48,7 +48,7 @@ import { basename, join, resolve } from "node:path";
|
|
|
48
48
|
import { fileURLToPath } from "node:url";
|
|
49
49
|
import { isAgentProofSpec, verifyAgentProof } from "./agent-proof.js";
|
|
50
50
|
import { CHAIN_READING_SCOPE, readChains, renderChainFinding } from "./chain.js";
|
|
51
|
-
import { VERDICT_DISPLAY_TITLES,
|
|
51
|
+
import { VERDICT_DISPLAY_TITLES, caseReliance, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
|
|
52
52
|
import { CASE_FILE_INDEX_NAME, jsonDepthExceeds, readCaseFileIndex, renderCaseFileFinding, } from "./case-file.js";
|
|
53
53
|
import { ZipReadError, readZip } from "./zip-reader.js";
|
|
54
54
|
import { composeExport } from "./compose.js";
|
|
@@ -56,7 +56,13 @@ import { isHexField, sha256Hex, utf8 } from "./crypto.js";
|
|
|
56
56
|
import { verifyEvidenceBundle } from "./evidence.js";
|
|
57
57
|
import { DuplicateKeyError, parseRefusingDuplicates } from "./verifier.js";
|
|
58
58
|
import { fileMatchToJson, matchCaseFiles, matchFiles, renderFileMatch } from "./file-match.js";
|
|
59
|
-
import { WITNESS_CACHE_NAME,
|
|
59
|
+
import { WITNESS_CACHE_NAME, readWitnessCache, } from "./witness-cache.js";
|
|
60
|
+
// The public-record (witness) lookup this command runs with `--gateways`:
|
|
61
|
+
// moved to its own pure module (proof#200) so a consumer that is not this CLI,
|
|
62
|
+
// the verify page, can call the SAME implementation. `onChainHalf` and
|
|
63
|
+
// `witnessLine` are this command's own two building blocks, unchanged; see
|
|
64
|
+
// `./witness-lookup.ts` for the exported entry point, `lookupPublicRecord`.
|
|
65
|
+
import { onChainHalf, witnessLine } from "./witness-lookup.js";
|
|
60
66
|
import { checkDeclaration, renderDeclarationSummary, renderNote } from "./declaration.js";
|
|
61
67
|
import { completenessNotes, gradePack, renderCause } from "./grading.js";
|
|
62
68
|
import { EXIT_CODES_HELP, EXIT_FAILED, EXIT_MALFORMED, EXIT_UNDETERMINED, EXIT_VERIFIED, exitCodeFor, } from "./exit-codes.js";
|
|
@@ -1744,162 +1750,6 @@ function sortKeys(v) {
|
|
|
1744
1750
|
function jsonText(v) {
|
|
1745
1751
|
return JSON.stringify(sortKeys(v), null, 2);
|
|
1746
1752
|
}
|
|
1747
|
-
function witnessTimeRow(t) {
|
|
1748
|
-
return {
|
|
1749
|
-
stamp_ref: t.cached.stampRef,
|
|
1750
|
-
cached: { block_height: t.cached.blockHeight, block_time: t.cached.blockTime },
|
|
1751
|
-
outcome: t.outcome,
|
|
1752
|
-
answers: t.answers.map((a) => ({
|
|
1753
|
-
gateway: a.gateway,
|
|
1754
|
-
outcome: a.outcome,
|
|
1755
|
-
block_height: a.blockHeight,
|
|
1756
|
-
block_time: a.blockTime,
|
|
1757
|
-
})),
|
|
1758
|
-
};
|
|
1759
|
-
}
|
|
1760
|
-
// Each pack's witnesses, re-fetched through `verifyEvidenceBundle`. Outside the
|
|
1761
|
-
// case reading on purpose: `readCase` makes no network call, and neither does
|
|
1762
|
-
// the verify page's case reading, so the case verdict stays the one both
|
|
1763
|
-
// surfaces compute. This adds the half the reading names as not checked.
|
|
1764
|
-
async function onChainHalf(packs, gateways, io) {
|
|
1765
|
-
if (gateways.length === 0)
|
|
1766
|
-
return { checked: false, gateways: [], packs: [], verdict: "not_checked" };
|
|
1767
|
-
const rows = [];
|
|
1768
|
-
const answered = new Set();
|
|
1769
|
-
for (const [ref, bundle, cached] of packs) {
|
|
1770
|
-
const result = await verifyEvidenceBundle(bundle, {
|
|
1771
|
-
gateways,
|
|
1772
|
-
...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}),
|
|
1773
|
-
});
|
|
1774
|
-
const witnesses = [...result.checkpoints, ...(result.stamps ?? [])];
|
|
1775
|
-
const row = {
|
|
1776
|
-
ref,
|
|
1777
|
-
witnesses: witnesses.length,
|
|
1778
|
-
confirmed: 0,
|
|
1779
|
-
mismatched: 0,
|
|
1780
|
-
unreachable: 0,
|
|
1781
|
-
not_checked: 0,
|
|
1782
|
-
};
|
|
1783
|
-
for (const w of witnesses) {
|
|
1784
|
-
const rollup = w.onChain?.rollup ?? null;
|
|
1785
|
-
if (rollup === "confirm")
|
|
1786
|
-
row.confirmed++;
|
|
1787
|
-
else if (rollup === "mismatch")
|
|
1788
|
-
row.mismatched++;
|
|
1789
|
-
else if (rollup === "unreachable")
|
|
1790
|
-
row.unreachable++;
|
|
1791
|
-
else
|
|
1792
|
-
row.not_checked++;
|
|
1793
|
-
}
|
|
1794
|
-
// The witness times the pack's witness-cache.json gives, looked up where
|
|
1795
|
-
// it gives any (5.6.6, v1.25, A30). A pack with no cache gets no key and
|
|
1796
|
-
// no request.
|
|
1797
|
-
const times = await checkWitnessCache(cached, gateways, io.fetchImpl ?? fetch);
|
|
1798
|
-
if (times.length > 0)
|
|
1799
|
-
row.witness_times = times.map(witnessTimeRow);
|
|
1800
|
-
rows.push(row);
|
|
1801
|
-
for (const w of witnesses) {
|
|
1802
|
-
for (const outcome of w.onChain?.perGateway ?? []) {
|
|
1803
|
-
if (outcome.outcome === "confirm" || outcome.outcome === "mismatch") {
|
|
1804
|
-
answered.add(outcome.gateway);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
for (const t of times) {
|
|
1809
|
-
for (const a of t.answers) {
|
|
1810
|
-
if (a.outcome === "matches" || a.outcome === "differs")
|
|
1811
|
-
answered.add(a.gateway);
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
const verdict = rows.some((r) => r.mismatched > 0 || (r.witness_times ?? []).some((t) => t.outcome === "differs"))
|
|
1816
|
-
? "failed"
|
|
1817
|
-
: rows.some((r) => r.witnesses === 0 || r.unreachable > 0 || r.not_checked > 0)
|
|
1818
|
-
? "undetermined"
|
|
1819
|
-
: "confirmed";
|
|
1820
|
-
return {
|
|
1821
|
-
answered: gateways.filter((g) => answered.has(g)),
|
|
1822
|
-
checked: true,
|
|
1823
|
-
gateways: [...gateways],
|
|
1824
|
-
packs: rows,
|
|
1825
|
-
verdict,
|
|
1826
|
-
};
|
|
1827
|
-
}
|
|
1828
|
-
/**
|
|
1829
|
-
* The witness line (evidence-bundle 5.5.6 v1.20, A24, D7) from the on-chain
|
|
1830
|
-
* half: `confirmed` is `confirmed`, `not_checked` and `undetermined` are
|
|
1831
|
-
* `not_checked` with the reason, and `failed` is `differs`. It never moves the
|
|
1832
|
-
* headline.
|
|
1833
|
-
*/
|
|
1834
|
-
// Distinct `[blockHeight, blockTime]` pairs, in time order.
|
|
1835
|
-
function distinctTimes(times) {
|
|
1836
|
-
const seen = new Map();
|
|
1837
|
-
for (const [h, t] of times)
|
|
1838
|
-
seen.set(`${h}:${t}`, [h, t]);
|
|
1839
|
-
return [...seen.values()].sort((a, b) => a[1] - b[1] || a[0] - b[0]);
|
|
1840
|
-
}
|
|
1841
|
-
// Only the keys that carry something: a line with no cached or recorded time
|
|
1842
|
-
// is the line 0.13.0 printed, field for field.
|
|
1843
|
-
function withTimes(line, cached, recorded) {
|
|
1844
|
-
return {
|
|
1845
|
-
...line,
|
|
1846
|
-
...(cached.length > 0 ? { cached } : {}),
|
|
1847
|
-
...(recorded.length > 0 ? { recorded } : {}),
|
|
1848
|
-
};
|
|
1849
|
-
}
|
|
1850
|
-
/**
|
|
1851
|
-
* `cached` is every witness time the packs cache (5.6.6, v1.25, A30). Where no
|
|
1852
|
-
* lookup ran, each is printed labelled not checked. Where one ran, each time
|
|
1853
|
-
* the public record matched is printed as the record's, each time no lookup
|
|
1854
|
-
* settled stays labelled not checked, and a time the record contradicts is the
|
|
1855
|
-
* reason the line reads `differs`.
|
|
1856
|
-
*/
|
|
1857
|
-
export function witnessLine(onChain, cached = []) {
|
|
1858
|
-
if (!onChain.checked) {
|
|
1859
|
-
return withTimes({
|
|
1860
|
-
state: WITNESS_NOT_LOOKED_UP.state,
|
|
1861
|
-
reasons: [...WITNESS_NOT_LOOKED_UP.reasons],
|
|
1862
|
-
services: [...WITNESS_NOT_LOOKED_UP.services],
|
|
1863
|
-
}, distinctTimes(cached.map((c) => [c.blockHeight, c.blockTime])), []);
|
|
1864
|
-
}
|
|
1865
|
-
const services = [...(onChain.answered ?? [])];
|
|
1866
|
-
const rows = onChain.packs;
|
|
1867
|
-
const times = rows.flatMap((r) => r.witness_times ?? []);
|
|
1868
|
-
const unsettled = distinctTimes(times
|
|
1869
|
-
.filter((t) => t.outcome === "not_checked")
|
|
1870
|
-
.map((t) => [t.cached.block_height, t.cached.block_time]));
|
|
1871
|
-
const recorded = distinctTimes(times
|
|
1872
|
-
.filter((t) => t.outcome === "matches")
|
|
1873
|
-
.flatMap((t) => t.answers.filter((a) => a.outcome === "matches"))
|
|
1874
|
-
.map((a) => [a.block_height, a.block_time]));
|
|
1875
|
-
const contradicted = [];
|
|
1876
|
-
for (const t of times) {
|
|
1877
|
-
if (t.outcome !== "differs")
|
|
1878
|
-
continue;
|
|
1879
|
-
const a = t.answers.find((x) => x.outcome === "differs");
|
|
1880
|
-
const reason = `a pack dates the witness ${renderWitnessTime(t.cached.block_height, t.cached.block_time)}` +
|
|
1881
|
-
`, and the public record dates it ${renderWitnessTime(a.block_height, a.block_time)}`;
|
|
1882
|
-
if (!contradicted.includes(reason))
|
|
1883
|
-
contradicted.push(reason);
|
|
1884
|
-
}
|
|
1885
|
-
if (onChain.verdict === "failed") {
|
|
1886
|
-
return withTimes({ state: "differs", reasons: contradicted, services }, unsettled, recorded);
|
|
1887
|
-
}
|
|
1888
|
-
if (onChain.verdict === "confirmed") {
|
|
1889
|
-
return withTimes({ state: "confirmed", reasons: [], services }, unsettled, recorded);
|
|
1890
|
-
}
|
|
1891
|
-
const reasons = [];
|
|
1892
|
-
if (rows.some((r) => r.witnesses === 0))
|
|
1893
|
-
reasons.push("a pack names no witness");
|
|
1894
|
-
if (rows.some((r) => r.unreachable > 0)) {
|
|
1895
|
-
reasons.push("a lookup did not settle: the service could not be reached, or it " +
|
|
1896
|
-
"holds no such witness yet");
|
|
1897
|
-
}
|
|
1898
|
-
if (rows.some((r) => r.not_checked > 0)) {
|
|
1899
|
-
reasons.push("no lookup is made for a witness kind a pack names");
|
|
1900
|
-
}
|
|
1901
|
-
return withTimes({ state: "not_checked", reasons, services }, unsettled, recorded);
|
|
1902
|
-
}
|
|
1903
1753
|
/**
|
|
1904
1754
|
* The case verdict's code, moved only by an on-chain half that was asked for:
|
|
1905
1755
|
* a witness that does not match is a failure whatever the reading said, and an
|
|
@@ -2137,7 +1987,7 @@ async function caseCommand(argv, io) {
|
|
|
2137
1987
|
for (const [p, bundle] of bundles) {
|
|
2138
1988
|
caches.push(readWitnessCache(bundle, await witnessCacheBytes(tree, split, p)));
|
|
2139
1989
|
}
|
|
2140
|
-
const onChain = await onChainHalf(bundles.map(([p, bundle], i) => [p.ref, bundle, caches[i]]), gateways, io);
|
|
1990
|
+
const onChain = await onChainHalf(bundles.map(([p, bundle], i) => [p.ref, bundle, caches[i]]), gateways, { ...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}) });
|
|
2141
1991
|
const witness = witnessLine(onChain, caches.flat());
|
|
2142
1992
|
const code = caseExitCode(reading.verdict, onChain.verdict);
|
|
2143
1993
|
const nil = (v) => (v === undefined ? null : v);
|
package/dist/declaration.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface DeclarationContentFinding {
|
|
|
60
60
|
eventId?: string;
|
|
61
61
|
}
|
|
62
62
|
/** Everything this check reports that is NOT a deviation, as a closed set. */
|
|
63
|
-
export declare const DECLARATION_NOTE_KINDS: readonly ["disclosure_did_not_travel", "pair_record_withheld", "role_step_withheld"];
|
|
63
|
+
export declare const DECLARATION_NOTE_KINDS: readonly ["disclosure_did_not_travel", "pair_record_withheld", "role_step_withheld", "other_declaration_held", "other_declaration_withheld"];
|
|
64
64
|
export type DeclarationNoteKind = (typeof DECLARATION_NOTE_KINDS)[number];
|
|
65
65
|
/**
|
|
66
66
|
* One named non-deviation finding, as data.
|
package/dist/declaration.js
CHANGED
|
@@ -130,10 +130,10 @@
|
|
|
130
130
|
// The kernel's one reader for bytes a producer signed: strict UTF-8, no leading
|
|
131
131
|
// byte-order mark, no non-finite literal. `jcs_loads` in
|
|
132
132
|
// `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
|
|
133
|
-
import { hexToBytes } from "./crypto.js";
|
|
134
|
-
import { jcsParse } from "./verifier.js";
|
|
133
|
+
import { hexToBytes, utf8 } from "./crypto.js";
|
|
134
|
+
import { jcs, jcsParse } from "./verifier.js";
|
|
135
135
|
import { isConformantHandoffRecord, isHandoffNamespace } from "./handoff.js";
|
|
136
|
-
import { hasSideContent, inBodyContentOffered } from "./side-content.js";
|
|
136
|
+
import { hasSideContent, inBodyContentOffered, sideContentEntry, } from "./side-content.js";
|
|
137
137
|
/** The first declaration schema, which every earlier reader knows. */
|
|
138
138
|
export const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
139
139
|
/**
|
|
@@ -255,6 +255,23 @@ export const DECLARATION_NOTE_KINDS = [
|
|
|
255
255
|
* because its payload binding is undetermined.
|
|
256
256
|
*/
|
|
257
257
|
"role_step_withheld",
|
|
258
|
+
/**
|
|
259
|
+
* Rule 10 (v1.29, A36). A record whose signed `event_type` is `declaration`
|
|
260
|
+
* and whose content HANDED IN to this reader is exactly `JCS(object)` of a
|
|
261
|
+
* declaration object of a version this reader knows, carrying exactly that
|
|
262
|
+
* version's keys, and not this pack's own. The note names the content handed
|
|
263
|
+
* in, never the record: whether those bytes bind to the record's
|
|
264
|
+
* `content_hash` is the bundle verifier's check, and an accuser can hand in
|
|
265
|
+
* declaration-shaped bytes for an honest record. What the pack holds is what
|
|
266
|
+
* the declaration in its body says, so this moves no verdict.
|
|
267
|
+
*/
|
|
268
|
+
"other_declaration_held",
|
|
269
|
+
/**
|
|
270
|
+
* Rule 10. A record whose signed `event_type` is `declaration` and whose
|
|
271
|
+
* content did not reach this reader from either channel, so whether it is a
|
|
272
|
+
* declaration other than this pack's own is UNKNOWN.
|
|
273
|
+
*/
|
|
274
|
+
"other_declaration_withheld",
|
|
258
275
|
];
|
|
259
276
|
/**
|
|
260
277
|
* The record committed inside one `events[]` entry, or `null`.
|
|
@@ -500,6 +517,155 @@ function roleReading(p, role, claimed) {
|
|
|
500
517
|
}
|
|
501
518
|
return null;
|
|
502
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* The `event_type` a declaration record is signed under: a reserved
|
|
522
|
+
* single-segment name, and the one `@sanning/anchor`'s `bundle` anchors a
|
|
523
|
+
* declaration as.
|
|
524
|
+
*/
|
|
525
|
+
const DECLARATION_EVENT_TYPE = "declaration";
|
|
526
|
+
/**
|
|
527
|
+
* The keys a declaration object carries, under every version this reader
|
|
528
|
+
* knows: exactly these, no more and no fewer (5.4.1).
|
|
529
|
+
*/
|
|
530
|
+
const DECLARATION_KEYS = ["declaration_version", "case_id", "agents", "period", "records", "handoffs"];
|
|
531
|
+
function hasExactKeys(value, keys) {
|
|
532
|
+
if (!isObject(value))
|
|
533
|
+
return false;
|
|
534
|
+
const have = Object.keys(value);
|
|
535
|
+
return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(value, k));
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Whether `obj` is shaped like a declaration this reader knows: one of
|
|
539
|
+
* `DECLARATION_VERSIONS` compared exactly, exactly the keys that version
|
|
540
|
+
* defines, and the value types this module reads.
|
|
541
|
+
*
|
|
542
|
+
* It is the shape test `@sanning/anchor`'s `isDeclarationShape` makes, and it
|
|
543
|
+
* checks shape only, not the 5.4.1 rules `readDeclaration` grades: a stale
|
|
544
|
+
* declaration is somebody else's claim, and this reader does not grade it, it
|
|
545
|
+
* names it.
|
|
546
|
+
*/
|
|
547
|
+
function isDeclarationShape(obj) {
|
|
548
|
+
if (!isObject(obj))
|
|
549
|
+
return false;
|
|
550
|
+
const version = obj["declaration_version"];
|
|
551
|
+
if (version !== DECLARATION_VERSION && version !== DECLARATION_VERSION_V2)
|
|
552
|
+
return false;
|
|
553
|
+
if (!hasExactKeys(obj, DECLARATION_KEYS))
|
|
554
|
+
return false;
|
|
555
|
+
if (!isId(obj["case_id"]))
|
|
556
|
+
return false;
|
|
557
|
+
const agents = obj["agents"];
|
|
558
|
+
if (!Array.isArray(agents) || !agents.every((a) => typeof a === "string"))
|
|
559
|
+
return false;
|
|
560
|
+
const period = obj["period"];
|
|
561
|
+
if (!hasExactKeys(period, ["start", "end"]))
|
|
562
|
+
return false;
|
|
563
|
+
if (typeof period["start"] !== "string" || typeof period["end"] !== "string")
|
|
564
|
+
return false;
|
|
565
|
+
const recordKeys = version === DECLARATION_VERSION_V2 ? ["event_id", "disclosure", "role"] : ["event_id", "disclosure"];
|
|
566
|
+
const records = obj["records"];
|
|
567
|
+
if (!Array.isArray(records))
|
|
568
|
+
return false;
|
|
569
|
+
for (const r of records) {
|
|
570
|
+
if (!hasExactKeys(r, recordKeys) || typeof r["event_id"] !== "string")
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
const handoffs = obj["handoffs"];
|
|
574
|
+
if (!Array.isArray(handoffs))
|
|
575
|
+
return false;
|
|
576
|
+
return handoffs.every((h) => hasExactKeys(h, ["sent_event_id", "received_event_id"]));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* The content bytes disclosed for one record, or `null` where none are usable:
|
|
580
|
+
* `events[].content` first, then the side-content map, chosen the way the
|
|
581
|
+
* bundle verifier chooses the bytes it binds.
|
|
582
|
+
*/
|
|
583
|
+
function disclosedBytes(ev, p, content) {
|
|
584
|
+
const inBody = isObject(ev) ? ev["content"] : undefined;
|
|
585
|
+
if (typeof inBody === "string") {
|
|
586
|
+
try {
|
|
587
|
+
return hexToBytes(inBody);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// falls through to the side channel, as the bundle verifier does
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (!p.nameable)
|
|
594
|
+
return null;
|
|
595
|
+
const side = sideContentEntry(content, p.eventId).value;
|
|
596
|
+
if (side instanceof Uint8Array)
|
|
597
|
+
return side;
|
|
598
|
+
if (typeof side === "string") {
|
|
599
|
+
try {
|
|
600
|
+
return hexToBytes(side);
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
function sameBytes(a, b) {
|
|
609
|
+
if (a.length !== b.length)
|
|
610
|
+
return false;
|
|
611
|
+
for (let i = 0; i < a.length; i++)
|
|
612
|
+
if (a[i] !== b[i])
|
|
613
|
+
return false;
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Whether `raw` is exactly `JCS(object)` of a declaration-shaped object.
|
|
618
|
+
* Canonical bytes, not merely parseable ones: a declaration is anchored as
|
|
619
|
+
* `JCS(declaration)`, and nothing an agent writes by accident.
|
|
620
|
+
*/
|
|
621
|
+
function isDeclarationBytes(raw) {
|
|
622
|
+
try {
|
|
623
|
+
const obj = jcsParse(raw);
|
|
624
|
+
return isDeclarationShape(obj) && sameBytes(utf8(jcs(obj)), raw);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Rule 10: every record that is a declaration other than the pack's own, in
|
|
632
|
+
* bundle order.
|
|
633
|
+
*
|
|
634
|
+
* The record's `event_type` is read from its signed `event_bytes`, so a record
|
|
635
|
+
* whose bytes the pack withholds is not named: the reader cannot see what it
|
|
636
|
+
* was signed as. Its content is read from either channel; a record whose
|
|
637
|
+
* content is byte for byte `JCS` of the pack's own declaration is the pack's
|
|
638
|
+
* own and is never named.
|
|
639
|
+
*/
|
|
640
|
+
function otherDeclarationNotes(bundle, pack, declaration, content) {
|
|
641
|
+
const events = pack.length > 0 ? (bodyOf(bundle)?.events ?? []) : [];
|
|
642
|
+
let own = null;
|
|
643
|
+
try {
|
|
644
|
+
own = utf8(jcs(declaration));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
own = null;
|
|
648
|
+
}
|
|
649
|
+
const notes = [];
|
|
650
|
+
pack.forEach((p, i) => {
|
|
651
|
+
if (p.record === null || p.record.event_type !== DECLARATION_EVENT_TYPE)
|
|
652
|
+
return;
|
|
653
|
+
const raw = disclosedBytes(events[i], p, content);
|
|
654
|
+
if (raw === null) {
|
|
655
|
+
// Offered and unusable is not withheld: something arrived, it is not a
|
|
656
|
+
// declaration, and the bundle verifier names what is wrong with it.
|
|
657
|
+
// Nothing arrived is the reading the reader cannot settle.
|
|
658
|
+
if (!p.disclosed)
|
|
659
|
+
notes.push({ kind: "other_declaration_withheld", eventId: p.eventId });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (own !== null && sameBytes(raw, own))
|
|
663
|
+
return;
|
|
664
|
+
if (isDeclarationBytes(raw))
|
|
665
|
+
notes.push({ kind: "other_declaration_held", eventId: p.eventId });
|
|
666
|
+
});
|
|
667
|
+
return notes;
|
|
668
|
+
}
|
|
503
669
|
/**
|
|
504
670
|
* Check a pack against the declaration inside its body.
|
|
505
671
|
*
|
|
@@ -719,6 +885,11 @@ export function checkDeclaration(bundle, content) {
|
|
|
719
885
|
deviations.push({ kind, eventId });
|
|
720
886
|
}
|
|
721
887
|
}
|
|
888
|
+
// Rule 10 (5.4.2 v1.29, A36): content handed in for a record signed
|
|
889
|
+
// `declaration` that is a declaration other than this pack's own. Notes only, in bundle order, after every other note, and
|
|
890
|
+
// never a deviation: the pack holds what its own declaration says whatever
|
|
891
|
+
// one of its records says.
|
|
892
|
+
notes.push(...otherDeclarationNotes(bundle, pack, declaration, content));
|
|
722
893
|
// A proven deviation wins: it is a mismatch between what the pack claimed
|
|
723
894
|
// and what its own signed body carries, and a failing check always wins.
|
|
724
895
|
// With no deviation and a content-dependent finding, the pack lines up with
|
|
@@ -838,6 +1009,14 @@ export function renderNote(note) {
|
|
|
838
1009
|
case "role_step_withheld":
|
|
839
1010
|
return (`${note.eventId} is signed as a step and this pack withholds its ` +
|
|
840
1011
|
"record bytes, so whether it is one is UNKNOWN");
|
|
1012
|
+
case "other_declaration_held":
|
|
1013
|
+
return (`the content handed in for ${note.eventId} is a declaration that is ` +
|
|
1014
|
+
"not this pack's own: it says what some pack holds, and what this " +
|
|
1015
|
+
"pack holds is what the declaration in its body says");
|
|
1016
|
+
case "other_declaration_withheld":
|
|
1017
|
+
return (`${note.eventId} is signed as a declaration and no content for it ` +
|
|
1018
|
+
"reached this verifier, so whether it is a declaration other than " +
|
|
1019
|
+
"this pack's own is UNKNOWN");
|
|
841
1020
|
}
|
|
842
1021
|
}
|
|
843
1022
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type { DeclarationCheck, DeclarationClaims, DeclarationContentFinding, De
|
|
|
11
11
|
export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_NOTE_KINDS, CASE_RELIANCE, CASE_RELIANCE_HANDOFF_CLAUSE, CASE_VERDICTS, CONTENT_WITHHELD_BY_DECLARATION, CO_SIGNATURES_PRESENT, VERDICT_DISPLAY_TITLES, WITNESS_NOT_LOOKED_UP, WITNESS_STATES, WITNESS_TIME_CACHED, WITNESS_TIME_RECORDED, caseReliance, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
|
|
12
12
|
export { MAX_BLOCK_TIME, MAX_WITNESS_CACHE_DEPTH, WITNESS_CACHE_NAME, WITNESS_CACHE_VERSION, checkWitnessCache, formatBlockTime, readWitnessCache, renderWitnessTime, witnessTimeQuery, } from "./witness-cache.js";
|
|
13
13
|
export type { CachedWitnessTime, WitnessTimeAnswer, WitnessTimeCheck } from "./witness-cache.js";
|
|
14
|
+
export { lookupPublicRecord, onChainHalf, witnessLine } from "./witness-lookup.js";
|
|
15
|
+
export type { OnChainHalf, OnChainRow, PublicRecordPack, PublicRecordResult, PublicRecordState, WitnessLookupOptions, WitnessTimeRow, } from "./witness-lookup.js";
|
|
14
16
|
export type { CaseException, CaseExceptionKind, CaseNote, CaseNoteKind, CasePack, CasePackReading, CaseReading, CaseRecordReading, CaseVerdict, WitnessLine, WitnessState, } from "./case.js";
|
|
15
17
|
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";
|
|
16
18
|
export type { CaseFileFinding, CaseFileFindingKind, CaseFileIndexReading, } from "./case-file.js";
|
package/dist/index.js
CHANGED
|
@@ -89,6 +89,21 @@ export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_
|
|
|
89
89
|
// The witnessed-time cache (evidence-bundle 5.6.6, v1.25, A30): read it, look it
|
|
90
90
|
// up, print it. Never an input to a verdict.
|
|
91
91
|
export { MAX_BLOCK_TIME, MAX_WITNESS_CACHE_DEPTH, WITNESS_CACHE_NAME, WITNESS_CACHE_VERSION, checkWitnessCache, formatBlockTime, readWitnessCache, renderWitnessTime, witnessTimeQuery, } from "./witness-cache.js";
|
|
92
|
+
// The public-record (witness) lookup the CLI runs with `--gateways`
|
|
93
|
+
// (src/witness-lookup.ts, proof#200): moved out of the CLI so a consumer
|
|
94
|
+
// that is not the CLI, the verify page, calls the SAME implementation
|
|
95
|
+
// rather than keeping its own copy (verify-page#70). Pure kernel scope: the
|
|
96
|
+
// one network call lives in `lookupPublicRecord` and `onChainHalf`, runs
|
|
97
|
+
// only when a caller hands in a gateway, and goes through `options.fetchImpl`
|
|
98
|
+
// where a caller supplies one (a browser page's own `fetch`) or the ambient
|
|
99
|
+
// global `fetch` otherwise, so this export runs in a browser exactly as it
|
|
100
|
+
// runs in Node. `onChainHalf` and `witnessLine` are the CLI's own unchanged
|
|
101
|
+
// building blocks; `lookupPublicRecord` is the new one-call, four-state entry
|
|
102
|
+
// point a page wants, additive over both. `confirmed` from
|
|
103
|
+
// `lookupPublicRecord` is a claim about the BYTES only, never about whether
|
|
104
|
+
// the pack itself verifies, and a caller must never show it apart from the
|
|
105
|
+
// pack's own verdict.
|
|
106
|
+
export { lookupPublicRecord, onChainHalf, witnessLine } from "./witness-lookup.js";
|
|
92
107
|
// The case file's index (src/case-file.ts, evidence-bundle.md §5.6, A25):
|
|
93
108
|
// compared with the drop it describes, with NO verdict contribution. The index
|
|
94
109
|
// is never an input to `readCase`. Pure: bytes and a file list in.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { WitnessLine, WitnessState } from "./case.js";
|
|
2
|
+
import { type CachedWitnessTime, type WitnessTimeAnswer, type WitnessTimeCheck } from "./witness-cache.js";
|
|
3
|
+
/** The seam every network call in this module goes through: used where given, else the ambient global `fetch`. */
|
|
4
|
+
export interface WitnessLookupOptions {
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
}
|
|
7
|
+
export interface OnChainRow {
|
|
8
|
+
ref: string;
|
|
9
|
+
witnesses: number;
|
|
10
|
+
confirmed: number;
|
|
11
|
+
mismatched: number;
|
|
12
|
+
unreachable: number;
|
|
13
|
+
not_checked: number;
|
|
14
|
+
/** Present only where the pack caches a witness time (5.6.6, v1.25, A30). */
|
|
15
|
+
witness_times?: WitnessTimeRow[];
|
|
16
|
+
}
|
|
17
|
+
export interface WitnessTimeRow {
|
|
18
|
+
stamp_ref: string;
|
|
19
|
+
cached: {
|
|
20
|
+
block_height: number;
|
|
21
|
+
block_time: number;
|
|
22
|
+
};
|
|
23
|
+
outcome: WitnessTimeCheck["outcome"];
|
|
24
|
+
answers: {
|
|
25
|
+
gateway: string;
|
|
26
|
+
outcome: WitnessTimeAnswer["outcome"];
|
|
27
|
+
block_height: number | null;
|
|
28
|
+
block_time: number | null;
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
export interface OnChainHalf {
|
|
32
|
+
/** Present only where a lookup ran: each gateway that answered, in the order given. */
|
|
33
|
+
answered?: string[];
|
|
34
|
+
checked: boolean;
|
|
35
|
+
gateways: string[];
|
|
36
|
+
packs: OnChainRow[];
|
|
37
|
+
verdict: "not_checked" | "confirmed" | "failed" | "undetermined";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Each pack's witnesses, re-fetched through `verifyEvidenceBundle`. Outside
|
|
41
|
+
* the case reading on purpose: `readCase` makes no network call, and neither
|
|
42
|
+
* does the verify page's case reading, so the case verdict stays the one both
|
|
43
|
+
* surfaces compute. This adds the half the reading names as not checked.
|
|
44
|
+
*
|
|
45
|
+
* Moved from `cli.ts` unchanged, apart from taking `fetchImpl` directly
|
|
46
|
+
* instead of a `Cli` stream (`io.fetchImpl ?? fetch`, the same fallback the
|
|
47
|
+
* CLI already applied at its own call site).
|
|
48
|
+
*/
|
|
49
|
+
export declare function onChainHalf(packs: readonly (readonly [string, unknown, readonly CachedWitnessTime[]])[], gateways: readonly string[], options?: WitnessLookupOptions): Promise<OnChainHalf>;
|
|
50
|
+
/**
|
|
51
|
+
* `cached` is every witness time the packs cache (5.6.6, v1.25, A30). Where no
|
|
52
|
+
* lookup ran, each is printed labelled not checked. Where one ran, each time
|
|
53
|
+
* the public record matched is printed as the record's, each time no lookup
|
|
54
|
+
* settled stays labelled not checked, and a time the record contradicts is the
|
|
55
|
+
* reason the line reads `differs`.
|
|
56
|
+
*
|
|
57
|
+
* Moved from `cli.ts` unchanged: same inputs, same three states
|
|
58
|
+
* (`WitnessState`), same rendering. The CLI's `case` command still calls this
|
|
59
|
+
* exact function, so its witness line is unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export declare function witnessLine(onChain: OnChainHalf, cached?: readonly CachedWitnessTime[]): WitnessLine;
|
|
62
|
+
/** The four states this export's result can carry. The CLI's own `WitnessState` plus `no_public_record`. */
|
|
63
|
+
export type PublicRecordState = WitnessState | "no_public_record";
|
|
64
|
+
/**
|
|
65
|
+
* One pack, as this lookup needs it: the parsed `bundle.json` and, where the
|
|
66
|
+
* pack carries one, the RAW BYTES of its `witness-cache.json`, read as part
|
|
67
|
+
* of the pack, per proof#200's acceptance criteria, rather than as a
|
|
68
|
+
* separately-loaded side file. `null` and `undefined` both mean "this pack has
|
|
69
|
+
* no witness-cache.json", the same reading `readWitnessCache` gives a missing
|
|
70
|
+
* file.
|
|
71
|
+
*/
|
|
72
|
+
export interface PublicRecordPack {
|
|
73
|
+
/** How this pack is named in the result; carries no meaning beyond that. */
|
|
74
|
+
ref: string;
|
|
75
|
+
bundle: unknown;
|
|
76
|
+
witnessCache?: Uint8Array | null;
|
|
77
|
+
}
|
|
78
|
+
export interface PublicRecordResult {
|
|
79
|
+
state: PublicRecordState;
|
|
80
|
+
reasons: string[];
|
|
81
|
+
services: string[];
|
|
82
|
+
/** Witness times a pack caches that no lookup settled (5.6.6, v1.25, A30). */
|
|
83
|
+
cached?: [number, number][];
|
|
84
|
+
/** Witness times the public record returned that matched a pack's cache. */
|
|
85
|
+
recorded?: [number, number][];
|
|
86
|
+
/** The line as both the CLI and a page print it, one string, kernel words. */
|
|
87
|
+
rendered: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The public-record lookup the CLI runs with `--gateways`, exported so a
|
|
91
|
+
* consumer that is not the CLI can run the SAME lookup rather than copy it
|
|
92
|
+
* (proof#200; the copy this closes is verify-page#70's `src/lib/verify.ts`).
|
|
93
|
+
*
|
|
94
|
+
* `packs` is every pack in the drop being read; `gateways` is empty for an
|
|
95
|
+
* offline reading, which never makes a network call and always answers
|
|
96
|
+
* `not_checked` or `no_public_record`. `options.fetchImpl` is the one seam a
|
|
97
|
+
* network call goes through, a browser page supplies its own `fetchImpl`;
|
|
98
|
+
* with none given this falls back to the ambient global `fetch`, so a
|
|
99
|
+
* browser page and a test can each supply their own and Node's `fetch` is
|
|
100
|
+
* never assumed where a caller passed something else.
|
|
101
|
+
*
|
|
102
|
+
* `state` is `confirmed` only about the BYTES: the public record holds what a
|
|
103
|
+
* pack names. It says nothing about whether the pack itself verifies (its
|
|
104
|
+
* wrapper signature, its `body_hash`), and a caller must never show
|
|
105
|
+
* `confirmed` apart from the pack's own verdict (proof#201).
|
|
106
|
+
*
|
|
107
|
+
* Never throws. Every input, including a malformed pack, a non-string
|
|
108
|
+
* gateway, or a `fetchImpl` that itself misbehaves, is read as untrusted, and
|
|
109
|
+
* anything this function or what it calls cannot make sense of settles as
|
|
110
|
+
* `not_checked` rather than escape as an exception.
|
|
111
|
+
*/
|
|
112
|
+
export declare function lookupPublicRecord(packs: readonly PublicRecordPack[], gateways: readonly string[], options?: WitnessLookupOptions): Promise<PublicRecordResult>;
|