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/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
|
+
};
|