verifyhash 0.1.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/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- package/trustledger/valueproof.js +350 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/core/agent-session.js — the PURE agent-session evidence core (T-68.1, EPIC-68 "AgentTrace").
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS IS
|
|
6
|
+
// A transport/filesystem-agnostic core that turns an ORDERED log of AI-agent session events
|
|
7
|
+
// (prompts, completions, tool calls/results, notes) into tamper-evident, selectively-REDACTABLE
|
|
8
|
+
// evidence. It provides:
|
|
9
|
+
//
|
|
10
|
+
// (a) a CANONICAL EVENT SCHEMA
|
|
11
|
+
// { seq, ts, actor, type, payload | payloadHash, redacted?, meta? }
|
|
12
|
+
// with `type` drawn from a CLOSED set (EVENT_TYPES) and STRICT validation: every failure
|
|
13
|
+
// is a NAMED reject `{ ok:false, reason, field? }` with a stable reason code (REASONS) —
|
|
14
|
+
// missing/extra/malformed fields, non-contiguous `seq`, non-string `ts`. Nothing here ever
|
|
15
|
+
// throws on hostile input.
|
|
16
|
+
//
|
|
17
|
+
// (b) the REDACTION-SAFE LEAF — the design decision that makes this evidentiary. Each event's
|
|
18
|
+
// Merkle leaf is computed over the canonical event with the payload represented by its
|
|
19
|
+
// HASH COMMITMENT:
|
|
20
|
+
// payloadHash = hashBytes(utf8(payload)) // cli/hash.js keccak256, verbatim
|
|
21
|
+
// leaf = hashBytes(utf8(JSON.stringify([
|
|
22
|
+
// LEAF_DOMAIN, seq, ts, actor, type, payloadHash, canonicalMetaJson|null ])))
|
|
23
|
+
// The payload bytes are NEVER in the leaf preimage — only their commitment is. So a FULL
|
|
24
|
+
// event (carrying `payload`) and its REDACTED twin (carrying only `payloadHash`, flagged
|
|
25
|
+
// `redacted: true`) derive the IDENTICAL leaf, and redacting ANY subset of a session's
|
|
26
|
+
// events changes NEITHER the leaves NOR the root. Verification recomputes `payloadHash`
|
|
27
|
+
// from `payload` when it is present (and cross-checks a carried `payloadHash` against it);
|
|
28
|
+
// when the payload is absent the well-formed commitment itself is what the tree binds.
|
|
29
|
+
// The fixed-position JSON array + JSON string escaping make the encoding unambiguous: no
|
|
30
|
+
// two distinct canonical events serialize to the same preimage.
|
|
31
|
+
//
|
|
32
|
+
// (c) the ORDERED LOG: sessionHead(events) -> { ok, size, root } via cli/journal-log.js
|
|
33
|
+
// treeHead over the event leaves REUSED VERBATIM (RFC-6962 0x00/0x01 domain separation,
|
|
34
|
+
// position-bound, NO sorting), plus proveEvent/verifyEvent (single-event inclusion against
|
|
35
|
+
// a head) and proveGrowth/verifyGrowth (append-only consistency between a mid-session
|
|
36
|
+
// checkpoint head and a later/final head) delegating to inclusionProof/verifyInclusion/
|
|
37
|
+
// consistencyProof/verifyConsistency VERBATIM. A sessionHead result doubles as the
|
|
38
|
+
// `{ size, root }` head object those verifiers bind sizes against.
|
|
39
|
+
//
|
|
40
|
+
// (d) redactEvent(event): the canonical redacted twin, with the round-trip invariant
|
|
41
|
+
// eventLeaf(redactEvent(e).event) === eventLeaf(e)
|
|
42
|
+
// so a packet holder can withhold any payload AFTER sealing without invalidating the head.
|
|
43
|
+
//
|
|
44
|
+
// TRUST BOUNDARY (honest, and carried into docs by T-68.4)
|
|
45
|
+
// - `ts` is SELF-ASSERTED metadata: this core records and binds the string but does NOT verify
|
|
46
|
+
// it against any clock (it has no clock). It proves "unaltered since sealed", never
|
|
47
|
+
// "happened at time T".
|
|
48
|
+
// - Garbage-in is out of scope: the head proves the LOG is intact and append-only, not that the
|
|
49
|
+
// log faithfully records what the agent actually did.
|
|
50
|
+
//
|
|
51
|
+
// PURITY (a hard acceptance criterion, statically guarded by the test)
|
|
52
|
+
// No fs / http / https / net / dns / child_process, no process.env, no clock, no randomness,
|
|
53
|
+
// no signing material. Requires ONLY:
|
|
54
|
+
// - `hashBytes` from cli/hash.js (the pure keccak over in-memory bytes — the ONE symbol
|
|
55
|
+
// imported; none of that module's file-walking helpers are referenced), REUSED not forked;
|
|
56
|
+
// - the five tree functions from cli/journal-log.js, REUSED not forked;
|
|
57
|
+
// - the pure byte helper `toUtf8Bytes` from ethers.
|
|
58
|
+
// Every exported function is TOTAL: hostile input yields a named `{ ok:false, reason }` verdict
|
|
59
|
+
// (or `null` from the leaf generator), never an exception, and results are fully deterministic.
|
|
60
|
+
|
|
61
|
+
const { hashBytes } = require("../hash");
|
|
62
|
+
const {
|
|
63
|
+
treeHead,
|
|
64
|
+
inclusionProof,
|
|
65
|
+
verifyInclusion,
|
|
66
|
+
consistencyProof,
|
|
67
|
+
verifyConsistency,
|
|
68
|
+
} = require("../journal-log");
|
|
69
|
+
const { toUtf8Bytes } = require("ethers");
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------------------------------
|
|
72
|
+
// Canonical schema constants.
|
|
73
|
+
// ---------------------------------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
// The CLOSED set of event types. A session log is a conversation between an agent and its tools;
|
|
76
|
+
// these five cover it. Extending the set is a schema version bump (change LEAF_DOMAIN too) — an
|
|
77
|
+
// unknown `type` is a NAMED reject, never silently accepted.
|
|
78
|
+
const EVENT_TYPES = Object.freeze(["prompt", "completion", "tool_call", "tool_result", "note"]);
|
|
79
|
+
|
|
80
|
+
// The exhaustive field set of a canonical event. Any other key is EVENT_UNKNOWN_FIELD: strictness
|
|
81
|
+
// here is what makes the leaf encoding total — every byte of an accepted event is either bound
|
|
82
|
+
// into the leaf (seq/ts/actor/type/payloadHash/meta) or committed by it (payload).
|
|
83
|
+
const EVENT_FIELDS = Object.freeze([
|
|
84
|
+
"seq",
|
|
85
|
+
"ts",
|
|
86
|
+
"actor",
|
|
87
|
+
"type",
|
|
88
|
+
"payload",
|
|
89
|
+
"payloadHash",
|
|
90
|
+
"redacted",
|
|
91
|
+
"meta",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
// Domain tag bound into every leaf preimage, so an agent-session leaf can never collide with any
|
|
95
|
+
// other artifact this project hashes. Bump the version if the encoding ever changes.
|
|
96
|
+
const LEAF_DOMAIN = "vh.agent-session/v1:event-leaf";
|
|
97
|
+
|
|
98
|
+
// Maximum nesting depth accepted for `meta`. The cap keeps canonicalization total on hostile
|
|
99
|
+
// input: a cyclic or absurdly deep object bottoms out at the cap and is REJECTED (EVENT_BAD_META)
|
|
100
|
+
// instead of overflowing the stack.
|
|
101
|
+
const META_MAX_DEPTH = 32;
|
|
102
|
+
|
|
103
|
+
// Maximum TOTAL number of values canonicalization may visit for one `meta`. The DEPTH cap alone
|
|
104
|
+
// stops cycles and deep-linear objects but NOT breadth blowup from SHARED references: a meta that
|
|
105
|
+
// reuses one child twice per level (`let n={leaf:1}; for(i<24) n={a:n,b:n};`) is O(24) objects in
|
|
106
|
+
// memory yet, without a budget, forces ~2^24 recursive visits (confirmed: OOM-kill / uncatchable
|
|
107
|
+
// SIGKILL, not a named verdict). A per-canonicalization work budget makes the cost O(budget)
|
|
108
|
+
// regardless of object-graph shape, so shared-DAG meta is REJECTED (EVENT_BAD_META), never a hang.
|
|
109
|
+
// Generous enough that any realistic JSON-shaped metadata passes; JSON text cannot even express
|
|
110
|
+
// sharing, so JSON.parse'd callers never approach it.
|
|
111
|
+
const META_MAX_NODES = 100000;
|
|
112
|
+
|
|
113
|
+
// Stable, named reason codes — the verdict contract callers (and the T-68.2 CLI) rely on.
|
|
114
|
+
const REASONS = Object.freeze({
|
|
115
|
+
EVENT_NOT_OBJECT: "EVENT_NOT_OBJECT",
|
|
116
|
+
EVENT_UNKNOWN_FIELD: "EVENT_UNKNOWN_FIELD",
|
|
117
|
+
EVENT_BAD_SEQ: "EVENT_BAD_SEQ",
|
|
118
|
+
EVENT_BAD_TS: "EVENT_BAD_TS",
|
|
119
|
+
EVENT_BAD_ACTOR: "EVENT_BAD_ACTOR",
|
|
120
|
+
EVENT_BAD_TYPE: "EVENT_BAD_TYPE",
|
|
121
|
+
EVENT_BAD_PAYLOAD: "EVENT_BAD_PAYLOAD",
|
|
122
|
+
EVENT_BAD_PAYLOAD_HASH: "EVENT_BAD_PAYLOAD_HASH",
|
|
123
|
+
EVENT_PAYLOAD_HASH_MISMATCH: "EVENT_PAYLOAD_HASH_MISMATCH",
|
|
124
|
+
EVENT_BAD_REDACTED_FLAG: "EVENT_BAD_REDACTED_FLAG",
|
|
125
|
+
EVENT_REDACTED_WITH_PAYLOAD: "EVENT_REDACTED_WITH_PAYLOAD",
|
|
126
|
+
EVENT_UNFLAGGED_REDACTION: "EVENT_UNFLAGGED_REDACTION",
|
|
127
|
+
EVENT_MISSING_PAYLOAD: "EVENT_MISSING_PAYLOAD",
|
|
128
|
+
EVENT_BAD_META: "EVENT_BAD_META",
|
|
129
|
+
SESSION_NOT_ARRAY: "SESSION_NOT_ARRAY",
|
|
130
|
+
SESSION_SEQ_NOT_CONTIGUOUS: "SESSION_SEQ_NOT_CONTIGUOUS",
|
|
131
|
+
INDEX_OUT_OF_RANGE: "INDEX_OUT_OF_RANGE",
|
|
132
|
+
PROOF_MALFORMED: "PROOF_MALFORMED",
|
|
133
|
+
PROOF_SEQ_MISMATCH: "PROOF_SEQ_MISMATCH",
|
|
134
|
+
EVENT_NOT_IN_HEAD: "EVENT_NOT_IN_HEAD",
|
|
135
|
+
GROWTH_RANGE: "GROWTH_RANGE",
|
|
136
|
+
GROWTH_NOT_APPEND_ONLY: "GROWTH_NOT_APPEND_ONLY",
|
|
137
|
+
HOSTILE_INPUT: "HOSTILE_INPUT",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const HEX32_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
141
|
+
|
|
142
|
+
function _isHex32(x) {
|
|
143
|
+
return typeof x === "string" && HEX32_RE.test(x);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A "plain" object: prototype is Object.prototype or null. Rejecting exotic objects (class
|
|
147
|
+
// instances, Maps, proxies-over-arrays, etc.) keeps canonicalization honest — what we hash is
|
|
148
|
+
// exactly the JSON-shaped data the caller could write to disk and read back.
|
|
149
|
+
function _isPlainObject(v) {
|
|
150
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
|
|
151
|
+
const proto = Object.getPrototypeOf(v);
|
|
152
|
+
return proto === Object.prototype || proto === null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------------------------------
|
|
156
|
+
// Canonical JSON — deterministic, total serialization for `meta`.
|
|
157
|
+
// - objects serialize with keys SORTED (UTF-16 code-unit order), so two semantically equal metas
|
|
158
|
+
// always hash identically regardless of insertion order;
|
|
159
|
+
// - only JSON-representable values are accepted: null, booleans, FINITE numbers, strings, arrays,
|
|
160
|
+
// plain objects. Anything else (undefined, functions, symbols, bigints, NaN/Infinity, class
|
|
161
|
+
// instances) -> null (reject);
|
|
162
|
+
// - depth is capped at META_MAX_DEPTH, which also terminates cycles -> null (reject);
|
|
163
|
+
// - a shared, mutable `budget` counts EVERY value visited across the whole traversal and caps it
|
|
164
|
+
// at META_MAX_NODES, so a shared-reference DAG (O(k) objects, ~2^k visits) is REJECTED instead
|
|
165
|
+
// of hanging/OOM-ing. Preferred over a WeakSet-on-path (catches cycles but not diamond fan-out)
|
|
166
|
+
// and over a never-removed identity Set (which would spuriously reject legitimate immutable
|
|
167
|
+
// sub-object reuse). Callers pass a fresh `{ n: 0 }` per `meta`.
|
|
168
|
+
// Returns the canonical JSON text, or null if the value is not canonicalizable (or blows the budget).
|
|
169
|
+
// ---------------------------------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
function _canonicalJson(value, depth, budget) {
|
|
172
|
+
if (depth > META_MAX_DEPTH) return null;
|
|
173
|
+
if (++budget.n > META_MAX_NODES) return null; // total-work budget: bounds shared-reference fan-out
|
|
174
|
+
if (value === null) return "null";
|
|
175
|
+
const t = typeof value;
|
|
176
|
+
if (t === "boolean") return value ? "true" : "false";
|
|
177
|
+
if (t === "number") return Number.isFinite(value) ? JSON.stringify(value) : null;
|
|
178
|
+
if (t === "string") return JSON.stringify(value);
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
const parts = [];
|
|
181
|
+
for (const item of value) {
|
|
182
|
+
const p = _canonicalJson(item, depth + 1, budget);
|
|
183
|
+
if (p === null) return null;
|
|
184
|
+
parts.push(p);
|
|
185
|
+
}
|
|
186
|
+
return "[" + parts.join(",") + "]";
|
|
187
|
+
}
|
|
188
|
+
if (_isPlainObject(value)) {
|
|
189
|
+
const keys = Object.keys(value).sort();
|
|
190
|
+
const parts = [];
|
|
191
|
+
for (const k of keys) {
|
|
192
|
+
const p = _canonicalJson(value[k], depth + 1, budget);
|
|
193
|
+
if (p === null) return null;
|
|
194
|
+
parts.push(JSON.stringify(k) + ":" + p);
|
|
195
|
+
}
|
|
196
|
+
return "{" + parts.join(",") + "}";
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------------------------------
|
|
202
|
+
// Payload commitment.
|
|
203
|
+
// ---------------------------------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The hash commitment of a payload: cli/hash.js `hashBytes` (keccak256, the primitive every other
|
|
207
|
+
* artifact in this project already trusts) over the UTF-8 bytes of the payload STRING. Payloads are
|
|
208
|
+
* strings by contract — a caller with structured data serializes it (deterministically, if they
|
|
209
|
+
* ever want to re-derive the commitment) BEFORE logging. TOTAL: non-string -> null, and a string
|
|
210
|
+
* that is not valid UTF-16 (a lone/unpaired surrogate — legal in JS, produced by truncated log
|
|
211
|
+
* fields or UTF-16 slicing) -> null too, since ethers' toUtf8Bytes THROWS INVALID_ARGUMENT on it.
|
|
212
|
+
* Never throws.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} payload
|
|
215
|
+
* @returns {string|null} 0x bytes32 (lowercase), or null if `payload` is not a UTF-8-encodable string.
|
|
216
|
+
*/
|
|
217
|
+
function payloadHash(payload) {
|
|
218
|
+
if (typeof payload !== "string") return null;
|
|
219
|
+
try {
|
|
220
|
+
return hashBytes(toUtf8Bytes(payload));
|
|
221
|
+
} catch (_) {
|
|
222
|
+
// Lone/unpaired UTF-16 surrogate: a legal JS string with no UTF-8 encoding. Return null so the
|
|
223
|
+
// function stays TOTAL, exactly like every other exported entry point in this module.
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------------------------------
|
|
229
|
+
// Event validation.
|
|
230
|
+
// ---------------------------------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* STRICT validation of one canonical event. Never throws; every failure is a named reject.
|
|
234
|
+
*
|
|
235
|
+
* Accepted shapes (exactly two):
|
|
236
|
+
* FULL: `payload` is a string; `redacted` absent or false; `payloadHash` optional but, if
|
|
237
|
+
* present, MUST equal the recomputed commitment (case-insensitively).
|
|
238
|
+
* REDACTED: `payload` absent; `payloadHash` is a 0x-bytes32 hex commitment; `redacted` MUST be
|
|
239
|
+
* exactly true (a missing payload without the explicit flag is EVENT_UNFLAGGED_REDACTION
|
|
240
|
+
* — redaction is always a declared act, never an accident).
|
|
241
|
+
* Common to both: `seq` a non-negative safe integer; `ts` any string (SELF-ASSERTED, untrusted —
|
|
242
|
+
* bound into the leaf but never interpreted); `actor` a non-empty string; `type` from EVENT_TYPES;
|
|
243
|
+
* `meta` (optional) any canonicalizable JSON value; NO other keys.
|
|
244
|
+
*
|
|
245
|
+
* @param {object} event
|
|
246
|
+
* @returns {{ ok: true, redacted: boolean, payloadHash: string, metaJson: string|null }
|
|
247
|
+
* | { ok: false, reason: string, field?: string }}
|
|
248
|
+
* On ok: `payloadHash` is the normalized (lowercase) commitment — recomputed from `payload`
|
|
249
|
+
* when present, taken from the carried commitment when redacted; `metaJson` is the
|
|
250
|
+
* canonical meta text (null when `meta` is absent).
|
|
251
|
+
*/
|
|
252
|
+
function validateEvent(event) {
|
|
253
|
+
try {
|
|
254
|
+
if (!_isPlainObject(event)) return { ok: false, reason: REASONS.EVENT_NOT_OBJECT };
|
|
255
|
+
for (const k of Object.keys(event)) {
|
|
256
|
+
if (!EVENT_FIELDS.includes(k)) {
|
|
257
|
+
return { ok: false, reason: REASONS.EVENT_UNKNOWN_FIELD, field: k };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!Number.isSafeInteger(event.seq) || event.seq < 0) {
|
|
261
|
+
return { ok: false, reason: REASONS.EVENT_BAD_SEQ, field: "seq" };
|
|
262
|
+
}
|
|
263
|
+
if (typeof event.ts !== "string") {
|
|
264
|
+
return { ok: false, reason: REASONS.EVENT_BAD_TS, field: "ts" };
|
|
265
|
+
}
|
|
266
|
+
if (typeof event.actor !== "string" || event.actor.length === 0) {
|
|
267
|
+
return { ok: false, reason: REASONS.EVENT_BAD_ACTOR, field: "actor" };
|
|
268
|
+
}
|
|
269
|
+
if (!EVENT_TYPES.includes(event.type)) {
|
|
270
|
+
return { ok: false, reason: REASONS.EVENT_BAD_TYPE, field: "type" };
|
|
271
|
+
}
|
|
272
|
+
const hasPayload = "payload" in event;
|
|
273
|
+
const hasHash = "payloadHash" in event;
|
|
274
|
+
if (hasPayload && typeof event.payload !== "string") {
|
|
275
|
+
return { ok: false, reason: REASONS.EVENT_BAD_PAYLOAD, field: "payload" };
|
|
276
|
+
}
|
|
277
|
+
if (hasHash && !_isHex32(event.payloadHash)) {
|
|
278
|
+
return { ok: false, reason: REASONS.EVENT_BAD_PAYLOAD_HASH, field: "payloadHash" };
|
|
279
|
+
}
|
|
280
|
+
if ("redacted" in event && typeof event.redacted !== "boolean") {
|
|
281
|
+
return { ok: false, reason: REASONS.EVENT_BAD_REDACTED_FLAG, field: "redacted" };
|
|
282
|
+
}
|
|
283
|
+
if (!hasPayload && !hasHash) {
|
|
284
|
+
return { ok: false, reason: REASONS.EVENT_MISSING_PAYLOAD, field: "payload" };
|
|
285
|
+
}
|
|
286
|
+
if (event.redacted === true && hasPayload) {
|
|
287
|
+
return { ok: false, reason: REASONS.EVENT_REDACTED_WITH_PAYLOAD, field: "redacted" };
|
|
288
|
+
}
|
|
289
|
+
if (event.redacted === true && !hasHash) {
|
|
290
|
+
return { ok: false, reason: REASONS.EVENT_BAD_PAYLOAD_HASH, field: "payloadHash" };
|
|
291
|
+
}
|
|
292
|
+
if (!hasPayload && event.redacted !== true) {
|
|
293
|
+
return { ok: false, reason: REASONS.EVENT_UNFLAGGED_REDACTION, field: "redacted" };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// The commitment: recomputed from the payload when present (and cross-checked against any
|
|
297
|
+
// carried payloadHash), taken from the carried commitment when redacted.
|
|
298
|
+
let commitment;
|
|
299
|
+
if (hasPayload) {
|
|
300
|
+
commitment = payloadHash(event.payload);
|
|
301
|
+
if (commitment === null) {
|
|
302
|
+
// A lone/unpaired UTF-16 surrogate is a legal JS string with no UTF-8 commitment: a
|
|
303
|
+
// SPECIFIC, named reject for the payload field rather than a generic HOSTILE_INPUT.
|
|
304
|
+
return { ok: false, reason: REASONS.EVENT_BAD_PAYLOAD, field: "payload" };
|
|
305
|
+
}
|
|
306
|
+
if (hasHash && commitment !== event.payloadHash.toLowerCase()) {
|
|
307
|
+
return { ok: false, reason: REASONS.EVENT_PAYLOAD_HASH_MISMATCH, field: "payloadHash" };
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
commitment = event.payloadHash.toLowerCase();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let metaJson = null;
|
|
314
|
+
if ("meta" in event) {
|
|
315
|
+
metaJson = _canonicalJson(event.meta, 0, { n: 0 });
|
|
316
|
+
if (metaJson === null) return { ok: false, reason: REASONS.EVENT_BAD_META, field: "meta" };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { ok: true, redacted: !hasPayload, payloadHash: commitment, metaJson };
|
|
320
|
+
} catch (_) {
|
|
321
|
+
// Hostile exotica (throwing getters, etc.) must never escape as an exception.
|
|
322
|
+
return { ok: false, reason: REASONS.HOSTILE_INPUT };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------------------------------
|
|
327
|
+
// The redaction-safe leaf.
|
|
328
|
+
// ---------------------------------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The Merkle LEAF VALUE of one canonical event — the redaction-safe commitment handed to
|
|
332
|
+
* cli/journal-log.js treeHead (which applies its own RFC-6962 0x00 leaf tag on top).
|
|
333
|
+
*
|
|
334
|
+
* The preimage is the fixed-position JSON array
|
|
335
|
+
* [ LEAF_DOMAIN, seq, ts, actor, type, payloadHash, canonicalMetaJson|null ]
|
|
336
|
+
* so every bound field edit changes the leaf, while the payload participates ONLY via its
|
|
337
|
+
* commitment: a full event and its redacted twin hash to the IDENTICAL leaf. The presentation-only
|
|
338
|
+
* `redacted` flag is deliberately NOT bound (it is derivable: payload absent <=> redacted). TOTAL:
|
|
339
|
+
* invalid event -> null (journal-log generator convention), never throws.
|
|
340
|
+
*
|
|
341
|
+
* @param {object} event a canonical event (full or redacted).
|
|
342
|
+
* @returns {string|null} 0x bytes32 leaf value, or null if the event does not validate.
|
|
343
|
+
*/
|
|
344
|
+
function eventLeaf(event) {
|
|
345
|
+
try {
|
|
346
|
+
const v = validateEvent(event);
|
|
347
|
+
if (!v.ok) return null;
|
|
348
|
+
const encoded = JSON.stringify([
|
|
349
|
+
LEAF_DOMAIN,
|
|
350
|
+
event.seq,
|
|
351
|
+
event.ts,
|
|
352
|
+
event.actor,
|
|
353
|
+
event.type,
|
|
354
|
+
v.payloadHash,
|
|
355
|
+
v.metaJson,
|
|
356
|
+
]);
|
|
357
|
+
return hashBytes(toUtf8Bytes(encoded));
|
|
358
|
+
} catch (_) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The canonical REDACTED TWIN of an event: payload dropped, its commitment carried, `redacted: true`
|
|
365
|
+
* declared, `meta` (when present) deep-copied in canonical form. Round-trip invariant (tested):
|
|
366
|
+
* eventLeaf(redactEvent(e).event) === eventLeaf(e)
|
|
367
|
+
* Idempotent: redacting an already-redacted event yields an equal twin. Never throws.
|
|
368
|
+
*
|
|
369
|
+
* @param {object} event a canonical event (full or redacted).
|
|
370
|
+
* @returns {{ ok: true, event: object } | { ok: false, reason: string, field?: string }}
|
|
371
|
+
*/
|
|
372
|
+
function redactEvent(event) {
|
|
373
|
+
try {
|
|
374
|
+
const v = validateEvent(event);
|
|
375
|
+
if (!v.ok) return v;
|
|
376
|
+
const twin = {
|
|
377
|
+
seq: event.seq,
|
|
378
|
+
ts: event.ts,
|
|
379
|
+
actor: event.actor,
|
|
380
|
+
type: event.type,
|
|
381
|
+
payloadHash: v.payloadHash,
|
|
382
|
+
redacted: true,
|
|
383
|
+
};
|
|
384
|
+
// Canonical deep copy: the twin never aliases caller-mutable state.
|
|
385
|
+
if (v.metaJson !== null) twin.meta = JSON.parse(v.metaJson);
|
|
386
|
+
return { ok: true, event: twin };
|
|
387
|
+
} catch (_) {
|
|
388
|
+
return { ok: false, reason: REASONS.HOSTILE_INPUT };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---------------------------------------------------------------------------------------------------
|
|
393
|
+
// The ordered session log.
|
|
394
|
+
// ---------------------------------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Validate a whole session: an ARRAY of canonical events whose `seq` values are CONTIGUOUS from 0
|
|
398
|
+
* (events[i].seq === i — `seq` is the tree position, which is what makes an inclusion proof bind an
|
|
399
|
+
* event to its place in the conversation). Named, LOCATED rejects; never throws.
|
|
400
|
+
*
|
|
401
|
+
* @param {object[]} events
|
|
402
|
+
* @returns {{ ok: true, size: number } | { ok: false, reason: string, index?: number, field?: string }}
|
|
403
|
+
*/
|
|
404
|
+
function validateSession(events) {
|
|
405
|
+
try {
|
|
406
|
+
if (!Array.isArray(events)) return { ok: false, reason: REASONS.SESSION_NOT_ARRAY };
|
|
407
|
+
for (let i = 0; i < events.length; i++) {
|
|
408
|
+
const v = validateEvent(events[i]);
|
|
409
|
+
if (!v.ok) return { ok: false, reason: v.reason, index: i, field: v.field };
|
|
410
|
+
if (events[i].seq !== i) {
|
|
411
|
+
return { ok: false, reason: REASONS.SESSION_SEQ_NOT_CONTIGUOUS, index: i };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { ok: true, size: events.length };
|
|
415
|
+
} catch (_) {
|
|
416
|
+
return { ok: false, reason: REASONS.HOSTILE_INPUT };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The session HEAD: cli/journal-log.js `treeHead` (REUSED VERBATIM — RFC-6962, position-bound,
|
|
422
|
+
* no sorting) over the ordered event leaves. Because leaves are redaction-safe, a fully or
|
|
423
|
+
* partially redacted session derives the IDENTICAL head as the full one.
|
|
424
|
+
*
|
|
425
|
+
* The ok-result is itself a valid `{ size, root }` head object, so it can be handed directly to
|
|
426
|
+
* verifyEvent/verifyGrowth (and to journal-log's own verifiers), which then BIND the size.
|
|
427
|
+
* An empty session is a legal (pre-first-event) checkpoint: { size: 0, root: EMPTY_ROOT }.
|
|
428
|
+
*
|
|
429
|
+
* @param {object[]} events
|
|
430
|
+
* @returns {{ ok: true, size: number, root: string }
|
|
431
|
+
* | { ok: false, reason: string, index?: number, field?: string }}
|
|
432
|
+
*/
|
|
433
|
+
function sessionHead(events) {
|
|
434
|
+
try {
|
|
435
|
+
const s = validateSession(events);
|
|
436
|
+
if (!s.ok) return s;
|
|
437
|
+
const head = treeHead(events.map((e) => eventLeaf(e)));
|
|
438
|
+
if (head.root === null) return { ok: false, reason: REASONS.HOSTILE_INPUT }; // unreachable post-validation
|
|
439
|
+
return { ok: true, size: head.size, root: head.root };
|
|
440
|
+
} catch (_) {
|
|
441
|
+
return { ok: false, reason: REASONS.HOSTILE_INPUT };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------------------------------
|
|
446
|
+
// Single-event inclusion: proveEvent -> verifyEvent.
|
|
447
|
+
// ---------------------------------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Build a disclosure proof for the event at `index`: the event itself (full or redacted, exactly as
|
|
451
|
+
* held — redact first for a redacted disclosure) plus the journal-log inclusion path. The raw leaf
|
|
452
|
+
* is deliberately NOT carried: verifyEvent must re-derive it from the disclosed event, so the proof
|
|
453
|
+
* is bound to the event DATA, never to a self-asserted hash.
|
|
454
|
+
*
|
|
455
|
+
* @param {object[]} events the full (or redacted-twin) session.
|
|
456
|
+
* @param {number} index 0 <= index < events.length.
|
|
457
|
+
* @returns {{ ok: true, proof: { event: object, inclusion: { leafIndex: number, treeSize: number, path: string[] } } }
|
|
458
|
+
* | { ok: false, reason: string, index?: number, field?: string }}
|
|
459
|
+
*/
|
|
460
|
+
function proveEvent(events, index) {
|
|
461
|
+
try {
|
|
462
|
+
const s = validateSession(events);
|
|
463
|
+
if (!s.ok) return s;
|
|
464
|
+
if (!Number.isInteger(index) || index < 0 || index >= events.length) {
|
|
465
|
+
return { ok: false, reason: REASONS.INDEX_OUT_OF_RANGE };
|
|
466
|
+
}
|
|
467
|
+
const ip = inclusionProof(events.map((e) => eventLeaf(e)), index);
|
|
468
|
+
if (ip === null) return { ok: false, reason: REASONS.HOSTILE_INPUT }; // unreachable post-validation
|
|
469
|
+
return {
|
|
470
|
+
ok: true,
|
|
471
|
+
proof: {
|
|
472
|
+
// Deep copy (events validate as JSON-shaped data), so the proof never aliases caller state.
|
|
473
|
+
event: JSON.parse(JSON.stringify(events[index])),
|
|
474
|
+
inclusion: { leafIndex: ip.leafIndex, treeSize: ip.treeSize, path: ip.path },
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
} catch (_) {
|
|
478
|
+
return { ok: false, reason: REASONS.HOSTILE_INPUT };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Verify a single-event disclosure against a TRUSTED head.
|
|
484
|
+
*
|
|
485
|
+
* Re-validates the disclosed event, recomputes `payloadHash` from `payload` when present (checking
|
|
486
|
+
* any carried commitment) or takes the commitment when redacted, re-derives the LEAF from that —
|
|
487
|
+
* never trusting a carried hash — checks `seq === leafIndex` (the event's claimed position IS its
|
|
488
|
+
* tree position), then delegates to cli/journal-log.js verifyInclusion VERBATIM. Passing the full
|
|
489
|
+
* `{ size, root }` head (e.g. a sessionHead result) also BINDS the tree size, so a proof replayed
|
|
490
|
+
* against a different-sized head is rejected outright. Never throws.
|
|
491
|
+
*
|
|
492
|
+
* @param {{ event: object, inclusion: { leafIndex: number, treeSize: number, path: string[] } }} proof
|
|
493
|
+
* @param {string|{size:number,root:string}} head trusted root, or full head (RECOMMENDED).
|
|
494
|
+
* @returns {{ ok: true, seq: number, redacted: boolean } | { ok: false, reason: string, field?: string }}
|
|
495
|
+
*/
|
|
496
|
+
function verifyEvent(proof, head) {
|
|
497
|
+
try {
|
|
498
|
+
if (!_isPlainObject(proof)) return { ok: false, reason: REASONS.PROOF_MALFORMED };
|
|
499
|
+
const v = validateEvent(proof.event);
|
|
500
|
+
if (!v.ok) return { ok: false, reason: v.reason, field: v.field };
|
|
501
|
+
const inc = proof.inclusion;
|
|
502
|
+
if (!_isPlainObject(inc)) return { ok: false, reason: REASONS.PROOF_MALFORMED };
|
|
503
|
+
if (proof.event.seq !== inc.leafIndex) {
|
|
504
|
+
return { ok: false, reason: REASONS.PROOF_SEQ_MISMATCH };
|
|
505
|
+
}
|
|
506
|
+
const leaf = eventLeaf(proof.event);
|
|
507
|
+
if (leaf === null) return { ok: false, reason: REASONS.PROOF_MALFORMED }; // unreachable post-validation
|
|
508
|
+
const included = verifyInclusion(
|
|
509
|
+
{ leaf, leafIndex: inc.leafIndex, treeSize: inc.treeSize, path: inc.path },
|
|
510
|
+
head
|
|
511
|
+
);
|
|
512
|
+
if (!included) return { ok: false, reason: REASONS.EVENT_NOT_IN_HEAD };
|
|
513
|
+
return { ok: true, seq: proof.event.seq, redacted: v.redacted };
|
|
514
|
+
} catch (_) {
|
|
515
|
+
return { ok: false, reason: REASONS.HOSTILE_INPUT };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------------------------------
|
|
520
|
+
// Append-only growth: proveGrowth -> verifyGrowth.
|
|
521
|
+
// ---------------------------------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Build the append-only consistency proof between the size-`firstSize` checkpoint prefix and the
|
|
525
|
+
* size-`secondSize` (default: full) prefix of `events`, delegating to cli/journal-log.js
|
|
526
|
+
* consistencyProof VERBATIM. 1 <= firstSize <= secondSize <= events.length.
|
|
527
|
+
*
|
|
528
|
+
* @param {object[]} events
|
|
529
|
+
* @param {number} firstSize the earlier checkpoint size (m).
|
|
530
|
+
* @param {number} [secondSize] the later size (n); defaults to events.length.
|
|
531
|
+
* @returns {{ ok: true, proof: { firstSize: number, secondSize: number, path: string[] } }
|
|
532
|
+
* | { ok: false, reason: string, index?: number, field?: string }}
|
|
533
|
+
*/
|
|
534
|
+
function proveGrowth(events, firstSize, secondSize) {
|
|
535
|
+
try {
|
|
536
|
+
const s = validateSession(events);
|
|
537
|
+
if (!s.ok) return s;
|
|
538
|
+
const n = secondSize === undefined ? events.length : secondSize;
|
|
539
|
+
if (
|
|
540
|
+
!Number.isInteger(firstSize) ||
|
|
541
|
+
!Number.isInteger(n) ||
|
|
542
|
+
firstSize < 1 ||
|
|
543
|
+
n < firstSize ||
|
|
544
|
+
n > events.length
|
|
545
|
+
) {
|
|
546
|
+
return { ok: false, reason: REASONS.GROWTH_RANGE };
|
|
547
|
+
}
|
|
548
|
+
const cp = consistencyProof(events.map((e) => eventLeaf(e)), firstSize, n);
|
|
549
|
+
if (cp === null) return { ok: false, reason: REASONS.HOSTILE_INPUT }; // unreachable post-validation
|
|
550
|
+
return { ok: true, proof: cp };
|
|
551
|
+
} catch (_) {
|
|
552
|
+
return { ok: false, reason: REASONS.HOSTILE_INPUT };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Verify that `laterHead` is an APPEND-ONLY extension of `earlierHead` — i.e. that no event at or
|
|
558
|
+
* before the checkpoint was rewritten, reordered, dropped or inserted between the two heads.
|
|
559
|
+
* Delegates to cli/journal-log.js verifyConsistency VERBATIM; passing full `{ size, root }` heads
|
|
560
|
+
* (e.g. sessionHead results — RECOMMENDED) also BINDS both sizes, so a proof lying about either
|
|
561
|
+
* size is rejected outright. Never throws.
|
|
562
|
+
*
|
|
563
|
+
* @param {string|{size:number,root:string}} earlierHead the checkpoint head (size m).
|
|
564
|
+
* @param {string|{size:number,root:string}} laterHead the later/final head (size n >= m).
|
|
565
|
+
* @param {{ firstSize: number, secondSize: number, path: string[] }} proof
|
|
566
|
+
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
567
|
+
*/
|
|
568
|
+
function verifyGrowth(earlierHead, laterHead, proof) {
|
|
569
|
+
try {
|
|
570
|
+
if (!_isPlainObject(proof)) return { ok: false, reason: REASONS.PROOF_MALFORMED };
|
|
571
|
+
const consistent = verifyConsistency(proof, earlierHead, laterHead);
|
|
572
|
+
if (!consistent) return { ok: false, reason: REASONS.GROWTH_NOT_APPEND_ONLY };
|
|
573
|
+
return { ok: true };
|
|
574
|
+
} catch (_) {
|
|
575
|
+
return { ok: false, reason: REASONS.HOSTILE_INPUT };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
module.exports = {
|
|
580
|
+
// Schema + verdict contract.
|
|
581
|
+
EVENT_TYPES,
|
|
582
|
+
EVENT_FIELDS,
|
|
583
|
+
LEAF_DOMAIN,
|
|
584
|
+
META_MAX_DEPTH,
|
|
585
|
+
META_MAX_NODES,
|
|
586
|
+
REASONS,
|
|
587
|
+
// The core operations.
|
|
588
|
+
payloadHash,
|
|
589
|
+
validateEvent,
|
|
590
|
+
eventLeaf,
|
|
591
|
+
redactEvent,
|
|
592
|
+
validateSession,
|
|
593
|
+
sessionHead,
|
|
594
|
+
proveEvent,
|
|
595
|
+
verifyEvent,
|
|
596
|
+
proveGrowth,
|
|
597
|
+
verifyGrowth,
|
|
598
|
+
};
|