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/verify.js ADDED
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+
3
+ // `vh verify <path>` — tamper check against the on-chain registry.
4
+ //
5
+ // The flow:
6
+ // 1. Recompute the content hash of the target path (file -> keccak256 of bytes;
7
+ // directory -> sorted-leaf Merkle root), using the exact same logic anchoring used
8
+ // (see cli/hash.js). The whole point: if a single byte of the file changed since it
9
+ // was anchored, this recomputed hash will differ.
10
+ // 2. Read ContributionRegistry.getRecord(hash) for that recomputed hash.
11
+ // * If a record exists for it -> the file is byte-for-byte what was anchored: MATCH.
12
+ // We report the recorded contributor and timestamp.
13
+ // * If getRecord reverts with NotAnchored (no record for this exact hash) -> either the
14
+ // content was never anchored, or it was anchored and has since been TAMPERED with so its
15
+ // hash no longer matches: MISMATCH.
16
+ //
17
+ // This is intentionally read-only: it needs only a provider (no signer, no key, no funds), and it
18
+ // never writes to the chain. Verification of a public, immutable record should never require a
19
+ // private key.
20
+
21
+ const { hashPath, hashGit } = require("./hash");
22
+ const { readReceipt, diffManifest } = require("./receipt");
23
+ const {
24
+ assertRegistry,
25
+ formatRegistryLine,
26
+ formatSkippedLine,
27
+ } = require("./registry");
28
+
29
+ const ARTIFACT = require("./core/registryArtifact");
30
+ const ABI = ARTIFACT.abi;
31
+
32
+ // Possible outcomes of a verify run.
33
+ const STATUS = Object.freeze({
34
+ MATCH: "MATCH", // recomputed hash is anchored on-chain
35
+ MISMATCH: "MISMATCH", // recomputed hash is NOT anchored (never anchored, or content was tampered)
36
+ });
37
+
38
+ /**
39
+ * Recompute the content hash for a filesystem path: a file hashes its keccak256 digest, a
40
+ * directory its sorted-leaf Merkle root — matching exactly what `vh anchor` would have stored. For a
41
+ * directory the per-file leaves are returned too (so a `--receipt` diff can localize a change).
42
+ *
43
+ * With `opts.git`, the root and leaves are recomputed over EXACTLY the files git tracks at `opts.ref`
44
+ * (default HEAD) — the SAME reproducible enumeration `vh anchor <dir> --git` used and `vh hash --git`
45
+ * defines (T-8.1). Untracked junk in the work tree is ignored, so the verdict depends only on the
46
+ * tracked content. The resolved commit oid + repo-relative scope are also returned for display (an
47
+ * untrusted provenance hint, never the verdict).
48
+ *
49
+ * @param {string} targetPath
50
+ * @param {{ git?: boolean, ref?: string }} [opts]
51
+ * @returns {{ contentHash: string, kind: "file"|"dir",
52
+ * leaves: Array<{path:string,contentHash:string,leaf:string}>|null,
53
+ * git: {commit:string,scope:string}|null }}
54
+ */
55
+ function contentHashForPath(targetPath, opts = {}) {
56
+ if (opts.git) {
57
+ const res = hashGit(targetPath, { ref: opts.ref });
58
+ const leaves = res.leaves.map((l) => ({
59
+ path: l.path,
60
+ contentHash: l.contentHash,
61
+ leaf: l.leaf,
62
+ }));
63
+ return {
64
+ contentHash: res.root,
65
+ kind: "dir",
66
+ leaves,
67
+ git: { commit: res.commit, scope: res.scope },
68
+ };
69
+ }
70
+ const res = hashPath(targetPath);
71
+ const leaves =
72
+ res.kind === "dir" && Array.isArray(res.leaves)
73
+ ? res.leaves.map((l) => ({ path: l.path, contentHash: l.contentHash, leaf: l.leaf }))
74
+ : null;
75
+ return { contentHash: res.root, kind: res.kind, leaves, git: null };
76
+ }
77
+
78
+ /**
79
+ * Decide whether a getRecord() failure means "no such record" (NotAnchored, an expected MISMATCH)
80
+ * versus a real, unexpected error (bad RPC, wrong address, network down) we must surface.
81
+ *
82
+ * The contract reverts with the custom error `NotAnchored(bytes32)` when a hash was never anchored.
83
+ * We also fall back to its 4-byte selector and a couple of generic revert-shaped signals, so this
84
+ * keeps working even if an RPC layer doesn't decode the named custom error for us.
85
+ */
86
+ function isNotAnchoredError(err, ethersLib, notAnchoredSelector) {
87
+ if (!err) return false;
88
+ // ethers v6 decodes known custom errors onto err.revert / err.errorName.
89
+ if (err.errorName === "NotAnchored") return true;
90
+ if (err.revert && err.revert.name === "NotAnchored") return true;
91
+
92
+ // Fall back to the raw revert data carrying the NotAnchored selector.
93
+ const data =
94
+ (err.data && (typeof err.data === "string" ? err.data : err.data.data)) ||
95
+ (err.info && err.info.error && err.info.error.data) ||
96
+ null;
97
+ if (typeof data === "string" && notAnchoredSelector && data.startsWith(notAnchoredSelector)) {
98
+ return true;
99
+ }
100
+
101
+ // Last-resort textual match (some providers only give a message).
102
+ const msg = String((err && err.message) || "");
103
+ return /NotAnchored/.test(msg);
104
+ }
105
+
106
+ /**
107
+ * Verify a path against the on-chain registry. Read-only: requires a provider, never a signer.
108
+ *
109
+ * The AUTHORITATIVE verdict is always re-deriving the content hash here and comparing it to the
110
+ * on-chain record (MATCH iff the recomputed hash is anchored). An optional `receiptPath` adds a
111
+ * convenience: for a directory it loads the receipt's per-file manifest and prints a precise
112
+ * ADDED/REMOVED/CHANGED diff so a MISMATCH localizes to specific files. The receipt is an UNTRUSTED
113
+ * hint (docs/TRUST-BOUNDARIES.md) — it only says *which* file diverged, never *whether* the content
114
+ * is valid. The diff is reported even on MATCH (it should be empty); a receipt for a different repo
115
+ * shows up as a fully-divergent diff, never a silent mislabel.
116
+ *
117
+ * @param {object} opts
118
+ * @param {string} opts.path path to a file or directory to verify
119
+ * @param {boolean}[opts.git] recompute the root over EXACTLY the git-tracked files (T-8.1)
120
+ * @param {string} [opts.ref] with git: which commit's tracked set (default HEAD)
121
+ * @param {string} opts.contractAddress deployed ContributionRegistry address to read from
122
+ * @param {object} opts.provider ethers v6 Provider (read-only RPC connection)
123
+ * @param {string} [opts.receiptPath] optional receipt whose manifest localizes a dir diff
124
+ * @param {object} [opts.ethers] ethers v6 module (defaults to the bundled one)
125
+ * @param {(s:string)=>void}[opts.log] sink for human output (defaults to process.stdout)
126
+ * @returns {Promise<{
127
+ * status: "MATCH"|"MISMATCH",
128
+ * contentHash: string,
129
+ * kind: "file"|"dir",
130
+ * path: string,
131
+ * contributor: string|null,
132
+ * timestamp: bigint|null,
133
+ * blockNumber: bigint|null,
134
+ * uri: string|null,
135
+ * manifestDiff: object|null, // present when a --receipt manifest was applied to a dir
136
+ * }>}
137
+ */
138
+ async function runVerify(opts) {
139
+ const ethersLib = opts.ethers || require("ethers");
140
+ const log = opts.log || ((s) => process.stdout.write(s));
141
+
142
+ const { path: targetPath, contractAddress, provider } = opts;
143
+ if (!targetPath) throw new Error("verify requires a <path>");
144
+ if (!contractAddress) {
145
+ throw new Error(
146
+ "no contract address: pass --contract <address> or set VH_CONTRACT in the environment"
147
+ );
148
+ }
149
+ if (!ethersLib.isAddress(contractAddress)) {
150
+ throw new Error(`invalid contract address: ${contractAddress}`);
151
+ }
152
+ if (!provider) {
153
+ throw new Error("no provider: pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL");
154
+ }
155
+
156
+ const { contentHash, kind, leaves, git } = contentHashForPath(targetPath, {
157
+ git: opts.git,
158
+ ref: opts.ref,
159
+ });
160
+
161
+ // T-11.2: authenticate the registry BEFORE any record read — no verdict is reported until we have
162
+ // confirmed there is a real verifyhash ContributionRegistry at this address (unless the caller
163
+ // explicitly, loudly opts out with skipIdentityCheck for a known not-yet-deployed/local-dev target).
164
+ let registryAuth = null;
165
+ if (!opts.skipIdentityCheck) {
166
+ registryAuth = await assertRegistry({ provider, contractAddress, ethers: ethersLib });
167
+ }
168
+
169
+ const iface = new ethersLib.Interface(ABI);
170
+ const notAnchoredSelector = iface.getError("NotAnchored").selector;
171
+
172
+ const contract = new ethersLib.Contract(
173
+ ethersLib.getAddress(contractAddress),
174
+ ABI,
175
+ provider
176
+ );
177
+
178
+ let record = null;
179
+ try {
180
+ record = await contract.getRecord(contentHash);
181
+ } catch (err) {
182
+ if (isNotAnchoredError(err, ethersLib, notAnchoredSelector)) {
183
+ record = null; // not anchored -> MISMATCH below
184
+ } else {
185
+ throw err; // genuine failure (network/address/etc.) — don't masquerade as a tamper result.
186
+ }
187
+ }
188
+
189
+ const result = {
190
+ contentHash,
191
+ kind,
192
+ path: targetPath,
193
+ git, // { commit, scope } when --git was used; null otherwise (untrusted provenance hint)
194
+ // T-11.2: the resolved registry identity (or null when the check was skipped). The human block and
195
+ // --json both surface this so a user can SEE the registry was authenticated before the verdict.
196
+ registry: registryAuth,
197
+ identitySkipped: Boolean(opts.skipIdentityCheck),
198
+ contributor: null,
199
+ authorBound: null,
200
+ timestamp: null,
201
+ blockNumber: null,
202
+ uri: null,
203
+ manifestDiff: null,
204
+ };
205
+
206
+ if (record === null) {
207
+ result.status = STATUS.MISMATCH;
208
+ } else {
209
+ result.status = STATUS.MATCH;
210
+ result.contributor = record.contributor;
211
+ result.authorBound = Boolean(record.authorBound);
212
+ result.timestamp = BigInt(record.timestamp);
213
+ result.blockNumber = BigInt(record.blockNumber);
214
+ result.uri = record.uri;
215
+ }
216
+
217
+ // Optional, UNTRUSTED localization: if a --receipt was given, diff its recorded manifest against
218
+ // the freshly-recomputed per-file leaves. This never changes the MATCH/MISMATCH verdict above
219
+ // (which is the authoritative re-derive-and-compare-to-chain check); it only says WHICH file moved.
220
+ if (opts.receiptPath) {
221
+ result.manifestDiff = _buildManifestDiff({
222
+ receiptPath: opts.receiptPath,
223
+ kind,
224
+ leaves,
225
+ contentHash,
226
+ });
227
+ }
228
+
229
+ log(formatVerify(result) + "\n");
230
+ return result;
231
+ }
232
+
233
+ /**
234
+ * Load the receipt at `receiptPath` and diff its manifest against the recomputed `leaves` for a
235
+ * directory target. Returns a structured diff (or an `error`/`note` object that formatVerify will
236
+ * render) — it never throws on a missing/foreign receipt, because the receipt is only an UNTRUSTED
237
+ * convenience and must not be able to break the authoritative verify.
238
+ */
239
+ function _buildManifestDiff({ receiptPath, kind, leaves, contentHash }) {
240
+ if (kind !== "dir") {
241
+ return { note: "--receipt manifest diff applies to a directory target only; ignored for a file." };
242
+ }
243
+ let receipt;
244
+ try {
245
+ receipt = readReceipt(receiptPath);
246
+ } catch (e) {
247
+ return { error: `could not read receipt: ${e.message}` };
248
+ }
249
+ if (!Array.isArray(receipt.manifest) || receipt.manifest.length === 0) {
250
+ return {
251
+ error:
252
+ "receipt has no manifest (it was written by an older build or for a file). " +
253
+ "Re-anchor with `vh anchor <dir> --receipt <p>` to record a manifest.",
254
+ };
255
+ }
256
+ // Cross-check that the receipt is even *about* this anchored hash, when it records one. A receipt
257
+ // for a different repo records a different contentHash; flagging it makes a foreign receipt show up
258
+ // as "different repo" rather than silently mislabeling unrelated files.
259
+ const receiptHashMismatch =
260
+ typeof receipt.contentHash === "string" &&
261
+ receipt.contentHash.toLowerCase() !== contentHash.toLowerCase();
262
+ const diff = diffManifest(receipt.manifest, leaves || []);
263
+ return { ...diff, receiptContentHash: receipt.contentHash || null, receiptHashMismatch };
264
+ }
265
+
266
+ /** Render a verify result as the human-readable block the CLI prints. */
267
+ function formatVerify(r) {
268
+ const lines = [
269
+ ` path: ${r.path} (${r.kind})`,
270
+ ` contentHash: ${r.contentHash}`,
271
+ ];
272
+ // T-11.2: the registry-authentication confirmation (or the loud skip warning) so the user can SEE
273
+ // the preflight ran before believing the verdict below.
274
+ if (r.identitySkipped) {
275
+ lines.push(formatSkippedLine());
276
+ } else if (r.registry) {
277
+ lines.push(formatRegistryLine(r.registry));
278
+ }
279
+ if (r.git) {
280
+ // Show WHICH commit's tracked set produced this root — an untrusted provenance hint, never the
281
+ // verdict (that is the MATCH/MISMATCH below, recomputed root vs the on-chain record).
282
+ lines.push(
283
+ ` git commit: ${r.git.commit} (untrusted provenance hint)`,
284
+ ` git scope: ${r.git.scope}`
285
+ );
286
+ }
287
+ lines.push(` result: ${r.status}`);
288
+ if (r.status === STATUS.MATCH) {
289
+ const ts = r.timestamp == null ? "(unknown)" : isoFromUnix(r.timestamp);
290
+ // Spell out exactly what `contributor` is allowed to mean for THIS record. A commit-reveal
291
+ // record (authorBound) is a front-running-resistant claim; a one-shot anchor is not.
292
+ const attribution = r.authorBound
293
+ ? "proven first claimant (commit-reveal, front-running-resistant)"
294
+ : "first anchorer only — NOT proven authorship (anyone could have anchored this hash)";
295
+ lines.push(
296
+ ` contributor: ${r.contributor}`,
297
+ ` attribution: ${attribution}`,
298
+ ` timestamp: ${r.timestamp} (${ts})`
299
+ );
300
+ if (r.uri) lines.push(` uri: ${r.uri}`);
301
+ } else {
302
+ lines.push(
303
+ " This content's hash is NOT anchored on-chain.",
304
+ " It was either never anchored, or it has been modified since it was anchored (tampered)."
305
+ );
306
+ }
307
+ if (r.manifestDiff) {
308
+ for (const line of formatManifestDiff(r.manifestDiff, r.status)) lines.push(line);
309
+ }
310
+ return lines.join("\n");
311
+ }
312
+
313
+ /**
314
+ * Render the optional --receipt manifest diff. Always leads with the trust caveat so a reader never
315
+ * mistakes the per-file localization for the authoritative verdict (which is the MATCH/MISMATCH above,
316
+ * derived from the on-chain record).
317
+ * @param {object} d the manifestDiff object built by _buildManifestDiff
318
+ * @param {string} status the authoritative MATCH/MISMATCH
319
+ * @returns {string[]} lines
320
+ */
321
+ function formatManifestDiff(d, status) {
322
+ const out = ["", " --- receipt manifest diff (UNTRUSTED hint) ---"];
323
+ out.push(
324
+ " NOTE: the receipt is an untrusted convenience. The authoritative verdict is the",
325
+ " MATCH/MISMATCH above (recomputed root vs the on-chain record). This diff only localizes",
326
+ " WHICH file diverged; it cannot make content valid or invalid on its own."
327
+ );
328
+ if (d.note) {
329
+ out.push(` ${d.note}`);
330
+ return out;
331
+ }
332
+ if (d.error) {
333
+ out.push(` receipt unusable: ${d.error}`);
334
+ return out;
335
+ }
336
+ if (d.receiptHashMismatch) {
337
+ out.push(
338
+ " WARNING: this receipt's recorded root does NOT match the recomputed root for this path.",
339
+ ` receipt root: ${d.receiptContentHash}`,
340
+ " The receipt is for a DIFFERENT directory snapshot (or a different repo). The per-file",
341
+ " diff below is between two unrelated manifests and should be read as fully divergent."
342
+ );
343
+ }
344
+ if (d.identical) {
345
+ out.push(" manifest: IDENTICAL — every file matches the receipt (no ADDED/REMOVED/CHANGED).");
346
+ return out;
347
+ }
348
+ out.push(
349
+ ` files: ${d.changed.length} CHANGED, ${d.added.length} ADDED, ${d.removed.length} REMOVED` +
350
+ ` (${d.unchanged.length} unchanged)`
351
+ );
352
+ for (const c of d.changed) {
353
+ out.push(` CHANGED ${c.path}`);
354
+ out.push(` old: ${c.oldContentHash}`);
355
+ out.push(` new: ${c.newContentHash}`);
356
+ }
357
+ for (const a of d.added) {
358
+ out.push(` ADDED ${a.path} (${a.contentHash}) present now, not in the receipt`);
359
+ }
360
+ for (const rm of d.removed) {
361
+ out.push(` REMOVED ${rm.path} (${rm.contentHash}) in the receipt, gone now`);
362
+ }
363
+ return out;
364
+ }
365
+
366
+ /** Format a unix-seconds bigint as an ISO-8601 UTC string for human display. */
367
+ function isoFromUnix(unixSeconds) {
368
+ try {
369
+ return new Date(Number(unixSeconds) * 1000).toISOString();
370
+ } catch (_) {
371
+ return "(unparseable)";
372
+ }
373
+ }
374
+
375
+ module.exports = {
376
+ runVerify,
377
+ formatVerify,
378
+ formatManifestDiff,
379
+ contentHashForPath,
380
+ isNotAnchoredError,
381
+ STATUS,
382
+ ABI,
383
+ };