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
package/cli/journal.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/journal.js — the pure, transport/filesystem-agnostic INTEGRITY-JOURNAL CORE (T-60.1).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS (EPIC-60, integrity OVER TIME)
|
|
6
|
+
// Every other surface in this project — CLI verify, verify-vh, serve-verify, the SDK, the GitHub
|
|
7
|
+
// Action — answers "do these exact bytes match this seal RIGHT NOW?" and then EXITS. This module is
|
|
8
|
+
// the structurally-new capability: an APPEND-ONLY, HASH-CHAINED journal of verify verdicts. Each run
|
|
9
|
+
// appends one entry; the log is ITSELF tamper-evident, so a deleted / edited / reordered / inserted
|
|
10
|
+
// past entry BREAKS the chain and `verifyJournal` LOCALIZES the first break. That is the "verified
|
|
11
|
+
// CONTINUOUSLY from date A to B, and here is the exact entry where one drifted" artifact a one-shot
|
|
12
|
+
// verify cannot produce.
|
|
13
|
+
//
|
|
14
|
+
// PURITY (a hard acceptance criterion)
|
|
15
|
+
// This file is TRANSPORT- and FILESYSTEM-agnostic: it does NO disk I/O, opens NO socket, and holds no
|
|
16
|
+
// signing material. It requires ONLY:
|
|
17
|
+
// - `hashBytes` (keccak256 of in-memory bytes) from cli/hash.js — the SAME hash the project already
|
|
18
|
+
// trusts for seals/Merkle roots, REUSED verbatim; NO new crypto is invented here.
|
|
19
|
+
// - `toUtf8Bytes` from ethers — a pure string-to-bytes encoder (NOT a network or signing primitive).
|
|
20
|
+
// A grep in test/journal.core.test.js asserts this file requires NONE of http/https/net/dns and does no
|
|
21
|
+
// signer/keyfile work at all.
|
|
22
|
+
//
|
|
23
|
+
// THE CHAIN (the exact transparency-log shape the project already uses for seals, reused)
|
|
24
|
+
// An entry is:
|
|
25
|
+
// { seq, prevHash, ts, artifact, verdict, entryHash }
|
|
26
|
+
// where:
|
|
27
|
+
// - seq : 0-based position in the journal (a genesis append is seq 0).
|
|
28
|
+
// - prevHash : the PRIOR entry's entryHash, or the documented GENESIS constant for seq 0.
|
|
29
|
+
// - ts : a SELF-ASSERTED wall-clock instant the caller supplies (an ISO string / number /
|
|
30
|
+
// anything JSON-serializable). The journal proves ORDERING + CONTINUITY of the
|
|
31
|
+
// verifier's OWN observations; it stays HONEST that `ts` is self-asserted until a
|
|
32
|
+
// trust-root signs/timestamps it — it NEVER claims "unaltered since date T" unqualified.
|
|
33
|
+
// - artifact : a caller-supplied label for WHAT was observed (e.g. a path / id). Stored verbatim.
|
|
34
|
+
// - verdict : the verify verdict this entry records, stored VERBATIM (deep-equal to the
|
|
35
|
+
// `verifyRequest` output it was built from — a test asserts this).
|
|
36
|
+
// - entryHash : hashBytes(canonical({ seq, prevHash, ts, artifact, verdict })). Because `prevHash`
|
|
37
|
+
// is folded in, each entryHash commits to the ENTIRE prefix before it — editing any
|
|
38
|
+
// past field, deleting/reordering/inserting an entry changes some downstream prevHash
|
|
39
|
+
// or entryHash and BREAKS the chain.
|
|
40
|
+
//
|
|
41
|
+
// The canonical serializer sorts object keys RECURSIVELY so the entryHash is independent of the key
|
|
42
|
+
// INSERTION order in the caller's `verdict`/`observation` objects (two logically-identical observations
|
|
43
|
+
// hash identically) while still being a total, injective encoding of the value.
|
|
44
|
+
|
|
45
|
+
const { toUtf8Bytes } = require("ethers");
|
|
46
|
+
const { hashBytes } = require("./hash");
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------------------------------
|
|
49
|
+
// Documented constants
|
|
50
|
+
// ---------------------------------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
// The GENESIS prevHash for seq 0. It is keccak256 of the fixed domain string below — a deterministic,
|
|
53
|
+
// documented, journal-specific constant (NOT a real prior entry's hash). Domain-separating it keeps a
|
|
54
|
+
// genesis prevHash from ever colliding with a real entryHash of some crafted entry.
|
|
55
|
+
const GENESIS_DOMAIN = "vh.integrity-journal/v1:genesis";
|
|
56
|
+
const GENESIS_PREV_HASH = hashBytes(toUtf8Bytes(GENESIS_DOMAIN));
|
|
57
|
+
|
|
58
|
+
// A schema/version tag folded into every entryHash so a v1 entry can never be replayed as a future
|
|
59
|
+
// v2 entry with a different meaning. Bump ONLY on a breaking change to the entry shape.
|
|
60
|
+
const JOURNAL_SCHEMA = "vh.integrity-journal/v1";
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------------------------------
|
|
63
|
+
// Canonical serialization — a recursive, key-sorted, deterministic JSON encoder (RFC-8785-style, kept
|
|
64
|
+
// minimal). PURE. This is what makes the entryHash byte-identical for logically-identical inputs.
|
|
65
|
+
// ---------------------------------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Deterministically serialize a JSON value: object keys sorted recursively, arrays order-preserved,
|
|
69
|
+
* NO insignificant whitespace. Rejects values JSON cannot faithfully round-trip (undefined, function,
|
|
70
|
+
* symbol, BigInt, non-finite number) so a malformed observation NEVER silently produces a stable hash
|
|
71
|
+
* that hides a lossy value — it throws, and appendEntry surfaces that as a clean error.
|
|
72
|
+
* @param {*} value
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
function canonicalize(value) {
|
|
76
|
+
return _canon(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _canon(value) {
|
|
80
|
+
if (value === null) return "null";
|
|
81
|
+
const t = typeof value;
|
|
82
|
+
if (t === "string") return JSON.stringify(value); // JSON.stringify escapes a string correctly + deterministically
|
|
83
|
+
if (t === "boolean") return value ? "true" : "false";
|
|
84
|
+
if (t === "number") {
|
|
85
|
+
if (!Number.isFinite(value)) {
|
|
86
|
+
throw new JournalError(`cannot canonicalize a non-finite number: ${String(value)}`);
|
|
87
|
+
}
|
|
88
|
+
// JSON's number grammar; V8 emits the shortest round-tripping form deterministically.
|
|
89
|
+
return JSON.stringify(value);
|
|
90
|
+
}
|
|
91
|
+
if (t === "bigint") {
|
|
92
|
+
throw new JournalError("cannot canonicalize a BigInt (not valid JSON)");
|
|
93
|
+
}
|
|
94
|
+
if (t === "undefined" || t === "function" || t === "symbol") {
|
|
95
|
+
throw new JournalError(`cannot canonicalize a value of type ${t} (not valid JSON)`);
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
return "[" + value.map((v) => _canon(v)).join(",") + "]";
|
|
99
|
+
}
|
|
100
|
+
if (t === "object") {
|
|
101
|
+
// Plain object: sort keys for a deterministic, insertion-order-independent encoding. A key whose
|
|
102
|
+
// value is undefined/function/symbol is DROPPED (matching JSON.stringify), so it cannot smuggle a
|
|
103
|
+
// non-JSON value into the hash.
|
|
104
|
+
const keys = Object.keys(value).sort();
|
|
105
|
+
const parts = [];
|
|
106
|
+
for (const k of keys) {
|
|
107
|
+
const v = value[k];
|
|
108
|
+
if (v === undefined || typeof v === "function" || typeof v === "symbol") continue;
|
|
109
|
+
parts.push(JSON.stringify(k) + ":" + _canon(v));
|
|
110
|
+
}
|
|
111
|
+
return "{" + parts.join(",") + "}";
|
|
112
|
+
}
|
|
113
|
+
// Unreachable for JSON values, but fail closed rather than emit something lossy.
|
|
114
|
+
throw new JournalError(`cannot canonicalize a value of type ${t}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------------------------------
|
|
118
|
+
// Errors — a named class so callers/tests can distinguish a journal-shape error from anything else.
|
|
119
|
+
// ---------------------------------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
class JournalError extends Error {
|
|
122
|
+
constructor(message) {
|
|
123
|
+
super(message);
|
|
124
|
+
this.name = "JournalError";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------------------------------
|
|
129
|
+
// entryHash — the chain link. hashBytes(canonical(preimage)) where the preimage folds in the schema
|
|
130
|
+
// tag, seq, prevHash, ts, artifact and verdict. PURE.
|
|
131
|
+
// ---------------------------------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Compute the canonical entryHash for the given entry fields. Deterministic: identical inputs ⇒
|
|
135
|
+
* byte-identical hash. Exposed so a verifier can RE-DERIVE it independently of how the entry was built.
|
|
136
|
+
* @param {object} fields { seq, prevHash, ts, artifact, verdict }
|
|
137
|
+
* @returns {string} 0x-prefixed keccak256
|
|
138
|
+
*/
|
|
139
|
+
function computeEntryHash(fields) {
|
|
140
|
+
const preimage = {
|
|
141
|
+
schema: JOURNAL_SCHEMA,
|
|
142
|
+
seq: fields.seq,
|
|
143
|
+
prevHash: fields.prevHash,
|
|
144
|
+
ts: fields.ts,
|
|
145
|
+
artifact: fields.artifact,
|
|
146
|
+
verdict: fields.verdict,
|
|
147
|
+
};
|
|
148
|
+
return hashBytes(toUtf8Bytes(canonicalize(preimage)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------------------------------
|
|
152
|
+
// appendEntry(priorEntry|null, observation) — build the NEXT entry. PURE (returns a new object; does
|
|
153
|
+
// NOT mutate priorEntry, does NO I/O).
|
|
154
|
+
// ---------------------------------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build the next journal entry that chains onto `priorEntry`.
|
|
158
|
+
*
|
|
159
|
+
* appendEntry(null, obs) -> seq 0, prevHash = GENESIS_PREV_HASH
|
|
160
|
+
* appendEntry(entryN, obs) -> seq N+1, prevHash = entryN.entryHash
|
|
161
|
+
*
|
|
162
|
+
* @param {object|null} priorEntry the previous entry, or null for the genesis (seq 0) append.
|
|
163
|
+
* @param {object} observation { verdict, artifact?, ts? }
|
|
164
|
+
* - verdict (required) the verify verdict to record. Stored VERBATIM
|
|
165
|
+
* (deep-equal to the verifyRequest output it was built from).
|
|
166
|
+
* - artifact (optional, default null) a label for what was observed.
|
|
167
|
+
* - ts (optional, default null) a SELF-ASSERTED timestamp.
|
|
168
|
+
* @returns {object} a NEW entry { seq, prevHash, ts, artifact, verdict, entryHash }.
|
|
169
|
+
* @throws {JournalError} on a malformed priorEntry/observation (never a silent bad chain).
|
|
170
|
+
*/
|
|
171
|
+
function appendEntry(priorEntry, observation) {
|
|
172
|
+
if (observation === null || typeof observation !== "object" || Array.isArray(observation)) {
|
|
173
|
+
throw new JournalError("observation must be an object { verdict, artifact?, ts? }");
|
|
174
|
+
}
|
|
175
|
+
if (!("verdict" in observation)) {
|
|
176
|
+
throw new JournalError("observation.verdict is required");
|
|
177
|
+
}
|
|
178
|
+
const verdict = observation.verdict;
|
|
179
|
+
if (verdict === undefined) {
|
|
180
|
+
throw new JournalError("observation.verdict is required (got undefined)");
|
|
181
|
+
}
|
|
182
|
+
// `artifact` and `ts` default to null (a stable, canonicalizable placeholder) when omitted.
|
|
183
|
+
const artifact = observation.artifact === undefined ? null : observation.artifact;
|
|
184
|
+
const ts = observation.ts === undefined ? null : observation.ts;
|
|
185
|
+
|
|
186
|
+
let seq;
|
|
187
|
+
let prevHash;
|
|
188
|
+
if (priorEntry === null || priorEntry === undefined) {
|
|
189
|
+
seq = 0;
|
|
190
|
+
prevHash = GENESIS_PREV_HASH;
|
|
191
|
+
} else {
|
|
192
|
+
_assertEntryShape(priorEntry, "priorEntry");
|
|
193
|
+
if (!Number.isInteger(priorEntry.seq) || priorEntry.seq < 0) {
|
|
194
|
+
throw new JournalError(`priorEntry.seq must be a non-negative integer, got ${String(priorEntry.seq)}`);
|
|
195
|
+
}
|
|
196
|
+
seq = priorEntry.seq + 1;
|
|
197
|
+
prevHash = priorEntry.entryHash;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const entry = {
|
|
201
|
+
seq,
|
|
202
|
+
prevHash,
|
|
203
|
+
ts,
|
|
204
|
+
artifact,
|
|
205
|
+
// Store the verdict VERBATIM. We deep-clone it so a later mutation of the caller's object cannot
|
|
206
|
+
// retroactively change what the journal recorded, while keeping it deep-equal to the input.
|
|
207
|
+
verdict: _deepCloneJson(verdict),
|
|
208
|
+
};
|
|
209
|
+
entry.entryHash = computeEntryHash(entry);
|
|
210
|
+
return entry;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------------------------------
|
|
214
|
+
// verifyJournal(entries[]) — walk the chain, LOCALIZING the first break. PURE. NEVER a false ok:true.
|
|
215
|
+
// ---------------------------------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Verify a full, ordered journal.
|
|
219
|
+
*
|
|
220
|
+
* @param {object[]} entries the journal entries in order (entries[0] must be the genesis, seq 0).
|
|
221
|
+
* @returns {object} on success: { ok:true, count:<n>, head:<last entryHash|GENESIS if empty> }
|
|
222
|
+
* on failure: { ok:false, brokenAt:<first broken index>, reason:<string> }
|
|
223
|
+
*
|
|
224
|
+
* A false positive is a security bug: any deviation — a wrong seq, a prevHash that does not match the
|
|
225
|
+
* previous entryHash, an entryHash that does not re-derive from the stored fields, a shape error, or a
|
|
226
|
+
* non-array input — yields ok:false with the FIRST offending index in `brokenAt`. It NEVER throws and
|
|
227
|
+
* NEVER returns ok:true for a tampered chain.
|
|
228
|
+
*/
|
|
229
|
+
function verifyJournal(entries) {
|
|
230
|
+
if (!Array.isArray(entries)) {
|
|
231
|
+
return { ok: false, brokenAt: 0, reason: "journal must be an array of entries" };
|
|
232
|
+
}
|
|
233
|
+
if (entries.length === 0) {
|
|
234
|
+
// An empty journal is vacuously consistent: no observations, nothing to contradict.
|
|
235
|
+
return { ok: true, count: 0, head: GENESIS_PREV_HASH };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let expectedPrevHash = GENESIS_PREV_HASH;
|
|
239
|
+
for (let i = 0; i < entries.length; i++) {
|
|
240
|
+
const e = entries[i];
|
|
241
|
+
|
|
242
|
+
// 1) Shape: a non-object / missing-field entry is the first break at i.
|
|
243
|
+
const shapeErr = _entryShapeError(e);
|
|
244
|
+
if (shapeErr) {
|
|
245
|
+
return { ok: false, brokenAt: i, reason: `entry ${i}: ${shapeErr}` };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 2) seq must equal its position — catches a reordering, a deletion, or an insertion that shifts seqs.
|
|
249
|
+
if (e.seq !== i) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
brokenAt: i,
|
|
253
|
+
reason: `entry ${i}: seq is ${JSON.stringify(e.seq)} but expected ${i} (reordered, deleted, or inserted)`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 3) prevHash must chain from the previous entry's entryHash (or GENESIS at seq 0).
|
|
258
|
+
if (e.prevHash !== expectedPrevHash) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
brokenAt: i,
|
|
262
|
+
reason:
|
|
263
|
+
i === 0
|
|
264
|
+
? `entry 0: prevHash ${JSON.stringify(e.prevHash)} is not the genesis constant`
|
|
265
|
+
: `entry ${i}: prevHash does not match entry ${i - 1}'s entryHash (chain broken)`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 4) entryHash must RE-DERIVE from the stored fields — catches an edit to verdict/ts/artifact/seq/prevHash.
|
|
270
|
+
const recomputed = computeEntryHash(e);
|
|
271
|
+
if (recomputed !== e.entryHash) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
brokenAt: i,
|
|
275
|
+
reason: `entry ${i}: entryHash does not match its contents (a field was edited or forged)`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
expectedPrevHash = e.entryHash;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { ok: true, count: entries.length, head: expectedPrevHash };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------------------------------
|
|
286
|
+
// Internal helpers
|
|
287
|
+
// ---------------------------------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
// Returns a human-readable reason string if `e` is not a well-shaped entry, else null. Used by
|
|
290
|
+
// verifyJournal so a malformed entry is a LOCALIZED break rather than a throw.
|
|
291
|
+
function _entryShapeError(e) {
|
|
292
|
+
if (e === null || typeof e !== "object" || Array.isArray(e)) {
|
|
293
|
+
return "not an object";
|
|
294
|
+
}
|
|
295
|
+
if (!Number.isInteger(e.seq) || e.seq < 0) {
|
|
296
|
+
return `seq must be a non-negative integer, got ${JSON.stringify(e.seq)}`;
|
|
297
|
+
}
|
|
298
|
+
if (typeof e.prevHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(e.prevHash)) {
|
|
299
|
+
return "prevHash must be a 0x-prefixed 32-byte hex string";
|
|
300
|
+
}
|
|
301
|
+
if (typeof e.entryHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(e.entryHash)) {
|
|
302
|
+
return "entryHash must be a 0x-prefixed 32-byte hex string";
|
|
303
|
+
}
|
|
304
|
+
if (!("verdict" in e)) {
|
|
305
|
+
return "verdict is missing";
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Throwing variant used by appendEntry on the priorEntry (a caller error, not a localized chain break).
|
|
311
|
+
function _assertEntryShape(e, label) {
|
|
312
|
+
const err = _entryShapeError(e);
|
|
313
|
+
if (err) throw new JournalError(`${label}: ${err}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Deep-clone a JSON value, detaching it from the caller's object while keeping it deep-equal to the
|
|
317
|
+
// input (a test asserts deepEqual). We route the clone through `canonicalize` FIRST: a non-JSON value
|
|
318
|
+
// (BigInt, function, non-finite number, symbol) throws a clean JournalError here — never a raw TypeError
|
|
319
|
+
// and never a silently-dropped field. The canonical string is a valid JSON encoding of the value, so
|
|
320
|
+
// JSON.parse reconstructs a faithful, key-sorted deep copy.
|
|
321
|
+
function _deepCloneJson(value) {
|
|
322
|
+
return JSON.parse(canonicalize(value === undefined ? null : value));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
appendEntry,
|
|
327
|
+
verifyJournal,
|
|
328
|
+
computeEntryHash,
|
|
329
|
+
canonicalize,
|
|
330
|
+
JournalError,
|
|
331
|
+
GENESIS_PREV_HASH,
|
|
332
|
+
GENESIS_DOMAIN,
|
|
333
|
+
JOURNAL_SCHEMA,
|
|
334
|
+
};
|