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.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. 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
+ };