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/vh.js
ADDED
|
@@ -0,0 +1,3927 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// verifyhash CLI entrypoint.
|
|
5
|
+
//
|
|
6
|
+
// Implemented commands:
|
|
7
|
+
// vh hash <path> Print the keccak256 of a file, or the sorted-leaf Merkle root of a
|
|
8
|
+
// directory (matching ContributionRegistry.verifyLeaf).
|
|
9
|
+
// vh anchor <path> [opts] Submit a file/dir's content hash on-chain via anchor().
|
|
10
|
+
// vh verify <path> [opts] Recompute a file/dir's hash, read it back from the registry, and
|
|
11
|
+
// report MATCH / MISMATCH (a one-byte edit flips it to MISMATCH).
|
|
12
|
+
// vh prove <file> [opts] Prove a single file belongs to an anchored repo root: build its
|
|
13
|
+
// Merkle proof and have the on-chain verifyLeaf accept/reject it.
|
|
14
|
+
|
|
15
|
+
const { hashPath, hashGit } = require("./hash");
|
|
16
|
+
const { runAnchor } = require("./anchor");
|
|
17
|
+
const { runVerify } = require("./verify");
|
|
18
|
+
const { runProve } = require("./prove");
|
|
19
|
+
const { runVerifyProof } = require("./proof");
|
|
20
|
+
const { runClaim, runCommit, runReveal } = require("./claim");
|
|
21
|
+
const { runList } = require("./list");
|
|
22
|
+
const { runShow } = require("./show");
|
|
23
|
+
const { runLineage } = require("./lineage");
|
|
24
|
+
const { runReputation } = require("./reputation");
|
|
25
|
+
const {
|
|
26
|
+
runDatasetBuild,
|
|
27
|
+
runDatasetVerify,
|
|
28
|
+
runDatasetDiff,
|
|
29
|
+
runDatasetSummary,
|
|
30
|
+
runDatasetReport,
|
|
31
|
+
runDatasetProve,
|
|
32
|
+
runDatasetVerifyProof,
|
|
33
|
+
runDatasetAttest,
|
|
34
|
+
runDatasetSign,
|
|
35
|
+
runDatasetVerifyAttest,
|
|
36
|
+
runDatasetCheck,
|
|
37
|
+
runDatasetTimestampRequest,
|
|
38
|
+
runDatasetTimestampWrap,
|
|
39
|
+
runDatasetVerifyTimestamp,
|
|
40
|
+
} = require("./dataset");
|
|
41
|
+
const {
|
|
42
|
+
runParcelBuild,
|
|
43
|
+
runParcelVerify,
|
|
44
|
+
runParcelAttest,
|
|
45
|
+
runParcelSign,
|
|
46
|
+
runParcelVerifyAttest,
|
|
47
|
+
runParcelTimestampRequest,
|
|
48
|
+
runParcelTimestampWrap,
|
|
49
|
+
runParcelVerifyTimestamp,
|
|
50
|
+
} = require("./parcel");
|
|
51
|
+
const { cmdTrust } = require("../trustledger/cli");
|
|
52
|
+
const { cmdEvidence } = require("./evidence");
|
|
53
|
+
const { cmdAgent } = require("./agent");
|
|
54
|
+
const { cmdIdentity } = require("./identity");
|
|
55
|
+
const { cmdRevocation } = require("./revocation");
|
|
56
|
+
const { cmdJournal } = require("./journal-cli");
|
|
57
|
+
const { cmdAnchorArtifact, cmdVerifyAnchored } = require("./anchor-artifact");
|
|
58
|
+
const serveVerifyHttp = require("./serve-verify-http");
|
|
59
|
+
const fulfillWebhookHttp = require("./fulfill-webhook-http");
|
|
60
|
+
|
|
61
|
+
function usage() {
|
|
62
|
+
return [
|
|
63
|
+
"vh — verifyhash CLI",
|
|
64
|
+
"",
|
|
65
|
+
"Usage:",
|
|
66
|
+
" vh hash <path> [--git] keccak256 of a file, or sorted-leaf Merkle root of a directory",
|
|
67
|
+
" (--git [--ref <ref>]: hash ONLY the files git tracks at that commit)",
|
|
68
|
+
" vh anchor <path> [opts] anchor a file/dir's content hash on-chain (FRONT-RUNNABLE)",
|
|
69
|
+
" vh claim <path> [opts] front-running-resistant attribution via commit-reveal (one-shot)",
|
|
70
|
+
" vh commit <path> [opts] commit-reveal step 1: commit + write a resumable claim receipt",
|
|
71
|
+
" vh reveal --receipt <p> commit-reveal step 2: resume from a receipt and reveal",
|
|
72
|
+
" vh anchor-artifact <sealed-file> --contract <addr> --rpc <url> (--key-env <VAR>|--key-file <p>) [--author-bound] [--uri <s>] [--out <receipt>] [--json] [--i-understand-mainnet] ANCHOR a sealed product artifact's ONE canonical digest on-chain (the T-70.1 closed table: evidence seal / agent-session packet / journal tree-head / trustledger seal / dataset|parcel attestation — each re-validated through its own shipped validator first) and emit the canonical vh-anchored-receipt@1 container binding digest + derivation + the read-back chain facts. Default is the ONE-SHOT anchor() (record NOT author-bound: first broadcaster wins); --author-bound runs the commit-reveal claim (D-1: front-run-resistant; the record reads back authorBound:true). FREE verb (no gate; gas is the caller's own). The signing key comes ONLY from --key-env/--key-file (read-used-discarded, never generated/persisted/logged); a non-testnet chainId refuses without --i-understand-mainnet (the EXISTING guard); --out writes the receipt to a caller-chosen path (never silently cwd; without --out it prints to stdout). Exit 0 anchored / 3 named reject (invalid artifact, or the registry's own revert e.g. AlreadyAnchored) / 2 usage / 1 IO-network-key",
|
|
73
|
+
" vh verify-anchored <receipt> <sealed-file> [--rpc <url> --contract <addr>] [--json] OFFLINE by default (NO key, NO network): strictly validate the anchored receipt and RECOMPUTE the artifact's digest through the same closed table — any deviation is a SPECIFIC named reject (digest-mismatch / kind-mismatch / how-mismatch / bad-receipt / the artifact's own named reject). With BOTH --rpc + --contract it ADDITIONALLY authenticates the registry FIRST (the EXISTING EPIC-11 identity probe: wrong address / non-registry / wrong chain each refuse) and re-checks the receipt's chain facts against the chain (contributor / authorBound / blockNumber / blockTime / txHash — each mismatch a named reject). It NEVER signs and needs NO key. Exit 0 ACCEPTED / 3 REJECTED / 2 usage / 1 IO — the SHARED 0/3 verify contract",
|
|
74
|
+
" vh verify <path> [opts] recompute the hash, read the registry, print MATCH / MISMATCH",
|
|
75
|
+
" vh prove <file> [opts] Merkle-prove a file against an anchored repo root via verifyLeaf",
|
|
76
|
+
" vh verify-proof <p> [opts] independently verify a portable proof artifact (offline + on-chain)",
|
|
77
|
+
" vh list [opts] enumerate the registry read-only (discovery + audit)",
|
|
78
|
+
" vh show <0xhash> [opts] look up ONE record by content hash (no local content needed)",
|
|
79
|
+
" vh lineage <0xhash> [opts] walk the parent chain UP from a record to its lineage root (read-only)",
|
|
80
|
+
" vh reputation <addr> [opts] verifiable, on-chain-derived contribution score for one address (read-only)",
|
|
81
|
+
" vh dataset build <dir> --out <p> tamper-evident dataset manifest (Merkle root + per-file leaves)",
|
|
82
|
+
" vh dataset verify <dir> --manifest <p> re-derive the root + per-file diff vs a manifest (OFFLINE)",
|
|
83
|
+
" vh dataset diff <manifestA> <manifestB> OFFLINE manifest-to-manifest change report (no tree/key/net)",
|
|
84
|
+
" vh dataset summary <manifest> OFFLINE provenance/license roll-up over a manifest (no tree/key/net)",
|
|
85
|
+
" vh dataset check <manifest> --policy <p> OFFLINE license/source policy gate (PASS/FAIL; CI-gateable)",
|
|
86
|
+
" vh dataset report <manifest> [--verify <dir>] [--policy <p>] ONE deterministic evidence document (combined CI gate)",
|
|
87
|
+
" vh dataset attest <manifest> [--out <p>] canonical UNSIGNED attestation payload (the signing-ready bytes)",
|
|
88
|
+
" vh dataset sign <manifest> --key-env <VAR>|--key-file <p> [--out <p>] sign with YOUR key -> signed container (offline)",
|
|
89
|
+
" vh dataset verify-attest <signed> [--manifest <m>] [--signer <addr>] OFFLINE verify a signed attestation (no key/net)",
|
|
90
|
+
" vh dataset timestamp-request <manifest> emit the SHA-256 digest your RFC-3161 TSA stamps (no key/net)",
|
|
91
|
+
" vh dataset timestamp-wrap <manifest> --token <p> wrap a TSA token -> verifiable timestamped container (no key/net)",
|
|
92
|
+
" vh dataset verify-timestamp <container> [--manifest <m>] OFFLINE verify an RFC-3161 timestamped attestation (no key/net)",
|
|
93
|
+
" vh dataset prove --file <p> --manifest <m> prove ONE file was a member of the dataset (OFFLINE)",
|
|
94
|
+
" vh dataset verify-proof <proof> fold a membership proof OFFLINE (no dataset, no key, no network)",
|
|
95
|
+
" vh parcel build <dir> --out <p> tamper-evident DELIVERY receipt (root + per-file leaves + untrusted parcel meta)",
|
|
96
|
+
" vh parcel verify <dir> --manifest <p> re-derive the root + per-file diff vs a parcel manifest (OFFLINE)",
|
|
97
|
+
" vh parcel attest <manifest> [--out <p>] canonical UNSIGNED parcel-attestation payload (the signing-ready bytes)",
|
|
98
|
+
" vh parcel sign <manifest> --key-env <VAR>|--key-file <p> [--out <p>] sign with YOUR key -> signed container (offline)",
|
|
99
|
+
" vh parcel verify-attest <signed> [--manifest <m>] [--signer <addr>] OFFLINE verify a signed parcel attestation (no key/net)",
|
|
100
|
+
" vh parcel timestamp-request <manifest> emit the SHA-256 digest your RFC-3161 TSA stamps (no key/net)",
|
|
101
|
+
" vh parcel timestamp-wrap <manifest> --token <p> wrap a TSA token -> verifiable timestamped container (no key/net)",
|
|
102
|
+
" vh parcel verify-timestamp <container> [--manifest <m>] OFFLINE verify an RFC-3161 timestamped parcel attestation (no key/net)",
|
|
103
|
+
" vh trust reconcile <bank> <ledger> <rentroll> [--out <dir>] [--seal [<file>]] [--license <f> --vendor <0xaddr>] three-way trust-account reconciliation -> dated audit packet (HTML+CSV; PASS/FAIL exit). FREE: baseline reconcile. PAID (require --license + --vendor): --state/--policy multi-state packs and --seal (tamper-evident seal)",
|
|
104
|
+
" vh trust license issue|verify ... issue a signed product license with a key YOU supply (read-used-discarded), or OFFLINE-verify one against --vendor (VALID/INVALID, exit 0/3). A valid license unlocks reconcile's paid surfaces",
|
|
105
|
+
" vh trust inspect <file> --as <bank|ledger|rentroll> read-only validator/preview: header + column map + sample + every bad row + a fix hint (writes nothing)",
|
|
106
|
+
" vh trust verify-seal <sealfile> [--dir <d>] [--inputs <d>] read-only OFFLINE seal verify (no key/net): ACCEPTED (0) only if EVERY sealed file re-derives, else REJECTED (3) with the per-file CHANGED/MISSING/UNEXPECTED list; sources sealed by basename resolve next to the seal (or --inputs <d>)",
|
|
107
|
+
" vh trust serve [--port <n>] [--host <h>] launch the LOCAL web front-door (default http://127.0.0.1:4173/): drop the three files in a browser, watch the balances tie out; files processed in-memory, nothing persisted server-side; exposing it is a HUMAN deploy step (never auto-deployed)",
|
|
108
|
+
" vh evidence seal <dir> [--out <p>] [--license <f> --vendor <0xaddr>] [--sign --key-env <VAR>|--key-file <p>] product-agnostic tamper-evident evidence packet (*.vhevidence.json) over the file set; default prints the seal + writes nothing. FREE: unsigned baseline seal of up to 25 files + verify. PAID (require --license + --vendor): --sign (signed-attestation wrap) and sealing > 25 files. Exit 0 ok / 3 seal-build-error / 2 usage / 1 IO",
|
|
109
|
+
" vh evidence verify <p> [--dir <d>] read-only, NO key: RE-DERIVE the root from the bytes referenced + report OK / which file CHANGED/MISSING/UNEXPECTED (the offline-recompute posture of `vh verify-seal`). On a SIGNED packet it no longer trusts the claimed signer: it REJECTS a forged signature OR labels a genuine one UNVERIFIED-for-pinning and points at `verify-signed`. Exit 0 OK / 3 REJECTED / 2 usage / 1 IO",
|
|
110
|
+
" vh evidence verify-signed <signed> [--dir <d>] [--signer <0xaddr>] [--revocations <f> --as-of <ISO>] OFFLINE/key-free/network-free: RECOVER the signer from a signed evidence packet (Check 1, always), (--signer) PIN it to an expected signer, (--dir) BIND the signature to YOUR bytes, (--revocations) check the signer was not REVOKED as of --as-of (default now); leads with the trust caveat + prints per-check PASS/FAIL. A forged/tampered/wrong-key signature, or a key revoked-before-as-of, is a clean REJECTED/REVOKED — never a silent pass. Exit 0 ACCEPTED / 3 REJECTED|REVOKED / 2 usage / 1 IO",
|
|
111
|
+
" vh evidence diff <packetA> <packetB> read-only, FREE, key-free, OFFLINE change report between TWO sealed evidence packets: leads with the CLAIMS-not-content TRUST line, prints IDENTICAL/DIFFERENT + per-file ADDED/REMOVED/CHANGED + a count line. Compares what each packet CLAIMS (no tree/key/net); a rename surfaces as REMOVED+ADDED; writes nothing; needs NO license. Exit 0 IDENTICAL / 3 DIFFERENT / 2 usage / 1 IO",
|
|
112
|
+
" vh evidence license fulfill --plan <id> --customer <name> [--paid-through <ISO>] [--catalog <f>] (--key-env <VAR>|--key-file <p>) [--issued <ISO>] [--license-id <id>] [--out <f>] MINT the signed *.vhevidence-license.json the paid surfaces accept: resolve <id> in the bundled DRAFT evidence plan catalog (or --catalog), copy that plan's entitlements VERBATIM, derive the window (--paid-through wins else the plan's term), sign with a HUMAN-provisioned key (EXACTLY ONE of --key-env/--key-file, read-used-discarded; the loop sets NO price). The minted license UNLOCKS `vh evidence seal --sign`. Exit 0 ok / 2 usage (unknown plan, bad window/date/catalog, key-source error) / 1 IO (fulfill is a PRODUCER: no exit-3 of its own; exit 3 is the downstream seal/verify GATE)",
|
|
113
|
+
" vh evidence go-live-preflight --binding <f> [--catalog <f>] [--secret-env <VAR>] (--key-env <VAR>|--key-file <p>) [--json] OFFLINE, dependency-free GO-LIVE CONFIG PREFLIGHT: VALIDATE the operator's OWN price->plan --binding + --catalog + vendor key end-to-end so a config typo cannot silently cause 'customer PAID, no license delivered'. For EVERY price it RESOLVES the mapped plan (an unmapped/duplicate/typo'd price is NAMED and non-zero, NEVER a silent default plan), MINTS a signed license with the vendor key, and confirms the delivered license PASSES the existing paid `vh evidence seal --sign` gate (a plan LACKING `evidence_signed` is caught -> FAIL, never PASS). With --secret-env it exercises your REAL webhook signing secret against a synthesized event (fail-closed: a forged event is rejected). Imports NO http/https/net/dns; the vendor key comes ONLY from --key-env/--key-file (held in memory, never written/logged); a throwaway workspace is removed on exit. --json emits the machine verdict. Exit 0 all-deliver / 2 config error / 3 a price would not deliver",
|
|
114
|
+
" vh agent seal <session.jsonl> [--out <p>] [--sign (--key-env <VAR>|--key-file <p>) --license <f> --vendor <0xaddr>] [--json] seal an ORDERED agent-session event log (JSONL: prompt/completion/tool_call/tool_result/note) into a tamper-evident, selectively-REDACTABLE *.vhagent.json packet: an RFC-6962-style Merkle head {size, root} over REDACTION-SAFE event leaves + the canonical event list + counts + the in-band trust note. FREE unsigned; --sign (a detached EIP-191 attestation over the HEAD, so ONE signature stays valid for every redacted copy) is the PAID DRAFT `agent_signed` capability, gated by the SAME offline evidence-license mechanism as `vh evidence seal --sign` (fail-closed). Exit 0 ok / 3 named reject or gate-fail / 2 usage / 1 IO",
|
|
115
|
+
" vh agent verify <packet> [--vendor <0xaddr>] read-only, NO key: RE-DERIVE every event leaf (recomputing each full payload's hash commitment; checking the carried commitment when redacted) + the root; a REJECT NAMES the first offending event seq; lists the withheld (redacted) seqs on ACCEPT. On a SIGNED packet it recovers the head signer and (--vendor) PINS it — a forged signature, a wrong pin, or a pinned-but-UNSIGNED packet is a clean REJECTED. Exit 0 ACCEPTED / 3 REJECTED / 2 usage / 1 IO",
|
|
116
|
+
" vh agent redact <packet> --seq <list> [--out <p>] emit a redacted copy that WITHHOLDS the named seqs' payloads behind their hash commitments — leaves + root UNCHANGED, so it STILL VERIFIES (and a carried head signature stays valid); the withheld seqs are listed. vh agent prove <packet> --seq <n> [--out <p>] / vh agent verify-proof <proof> [--root <hex>] disclose + check ONE event OFFLINE against the head (--root pins the head to one YOU trust). vh agent checkpoint <session.jsonl> [--out <p>] / vh agent verify-growth <earlier-head-or-packet> <later-packet> prove APPEND-ONLY growth between a mid-session checkpoint and a later/final packet (a rewritten past is REJECTED). All FREE, offline, key-less. Exit 0 ok|ACCEPTED / 3 REJECTED / 2 usage / 1 IO",
|
|
117
|
+
" vh agent commit-claim --repo <dir> [--ref <ref=HEAD>] --seq <n> [--ts <iso>] [--actor <s>] [--out <p>] [--json] bind a session to a git commit: derive the oid (cli/git.js resolveCommit) + the tracked-set work-tree root (the `vh hash --git` engine, hashGit) from YOUR repo and print/write ONE canonical JSONL claim event ready to append to the session log BEFORE `vh agent seal` (no --out => stdout is EXACTLY the line, note on stderr; --ts is SELF-ASSERTED metadata like every event ts). vh agent verify-commit <packet> --repo <dir> [--ref <ref=HEAD>] [--vendor <0xaddr>] the AUDITOR leg, from THEIR OWN clone: FIRST the FULL packet verification verbatim (signature/vendor-pin included — a tampered/forged packet never reaches the claim check), THEN re-resolve the oid + RECOMPUTE the root and ACCEPT only if a DISCLOSED claim matches; a REJECT names the check: packet-invalid / no-disclosed-claim / oid-mismatch / root-mismatch (=> check out the claimed commit in a CLEAN tree — hashGit reads work-tree bytes). CONTAINMENT, not causation. Both FREE, read-only, key-less. Exit 0 ok|ACCEPTED / 3 REJECTED / 2 usage / 1 IO",
|
|
118
|
+
" vh identity publish --address <0xaddr> --product-line <line> --claim <text> [--claim ...] --non-claim <text> [--non-claim ...] [--published-at <ISO>] (--key-env <VAR>|--key-file <p>) [--out <p>] MINT a signed producer IDENTITY CARD binding --address to the bounded --claim set it attests + the --non-claim set it explicitly does NOT. Signs with a HUMAN-provisioned key (EXACTLY ONE of --key-env/--key-file, read-used-discarded; the loop holds NO key) and MINTS ONLY when the key's address EQUALS --address (else hard-errors BEFORE writing). Default prints the card + writes nothing; --out writes a caller-chosen path (never cwd). Exit 0 ok / 2 usage / 1 IO",
|
|
119
|
+
" vh identity verify <card> [--signer <0xaddr>] [--revocations <f> --as-of <ISO>] OFFLINE/key-free/network-free: RECOVER the signer from a signed identity card, confirm the signature backs it AND the recovered signer IS the card's vendorAddress, OPTIONALLY pin --signer, OPTIONALLY check the vendor key was not REVOKED as of --as-of (default now), and print the claims/non-claims + per-check PASS/FAIL. Leads with the trust line. A forged/tampered/wrong-key card, a wrong --signer, or a key revoked-before-as-of is a clean REJECTED/REVOKED — never a silent pass. Exit 0 ACCEPTED / 3 REJECTED|REVOKED / 2 usage / 1 IO",
|
|
120
|
+
" vh revocation publish --address <0xaddr> --reason <reason> (--key-env <VAR>|--key-file <p>) [--superseded-by <0xaddr>] [--revoked-at <ISO>] [--out <p>] MINT a signed producer KEY REVOCATION marking --address REVOKED as of --revoked-at (default now) for --reason (one of [\"compromised\",\"retired\",\"rotated\",\"superseded\"]), OPTIONALLY naming a --superseded-by successor. Signs with a HUMAN-provisioned key (EXACTLY ONE of --key-env/--key-file, read-used-discarded; the loop holds NO key) and MINTS ONLY when that key's address EQUALS --address — a key revokes ITSELF; a third party cannot revoke a key it does not control (else it hard-errors BEFORE writing). Default prints the revocation + writes nothing; --out writes a caller-chosen path (never cwd). A revocation is a SIGNED CLAIM (revokedAt is self-asserted, NOT a trusted timestamp without P-3). Exit 0 ok / 2 usage / 1 IO",
|
|
121
|
+
" vh revocation verify <revocation> [--signer <0xaddr>] OFFLINE/key-free/network-free: RECOVER the signer from a signed key revocation, confirm the signature backs it AND the recovered signer IS the revocation's vendorAddress (a key revokes ITSELF), OPTIONALLY pin --signer, and print the reason/revokedAt/supersededBy + per-check PASS/FAIL. Leads with the trust line. A forged/tampered/wrong-key revocation, or a wrong --signer, is a clean REJECTED — never a silent pass. Exit 0 ACCEPTED / 3 REJECTED / 2 usage / 1 IO",
|
|
122
|
+
" vh journal append <artifact> --to <journalfile> [--dir <d>] [--ts <ISO>] VERIFY a *.vhevidence.json seal/signed container through the EXISTING composed verify path and record the verdict as ONE new, hash-chained line — STRICTLY ADDITIVELY (prior lines are never rewritten). Recording a REJECTED verdict is a successful append; the journal faithfully records what it saw. Integrity OVER TIME: a standing, tamper-evident record you re-run. Exit 0 appended / 2 usage / 1 IO",
|
|
123
|
+
" vh journal verify <journalfile> walk the on-disk hash-chain: a deleted/reordered/inserted/hand-edited past line BREAKS the chain and this LOCALIZES the first break — naming the drifted artifact + the seq where it drifted + brokenAt. The `ts` is SELF-ASSERTED (never claims \"unaltered since date T\" until a trust-root signs it). Exit 0 PASS (unbroken) / 3 BROKEN / 2 usage / 1 IO — the SHARED 0/3 verify contract",
|
|
124
|
+
" vh journal tree-head <journalfile> [--json] print the publishable Signed-Tree-Head-SHAPED commitment { size, root } — the RFC-6962 ORDERED Merkle head over the journal's entry hashes (cli/journal-log.js). Read-only, verify-only, NO key/network. The head is SELF-ASSERTED (the log holder's own commitment) — it does NOT prove \"existed at time T\" until a trust-root signs/timestamps it. Exit 0 head / 3 broken chain / 2 usage / 1 IO",
|
|
125
|
+
" vh journal prove-inclusion <journalfile> --seq <i> [--out <f>] [--json] emit a compact, SELF-CONTAINED inclusion-proof artifact { kind:\"vh-journal-inclusion\", leaf, seq, size, root, path[] } proving entry --seq is committed at its position under the current tree head — an O(log n) path a third party checks WITHOUT the log. Read-only (only --out is written). Exit 0 proved / 3 broken chain / 2 usage / 1 IO",
|
|
126
|
+
" vh journal prove-consistency <journalfile> --from <oldSize> [--out <f>] [--json] emit { kind:\"vh-journal-consistency\", first:{size,root}, second:{size,root}, proof[] } proving the CURRENT log is an APPEND-ONLY extension of its size---from prefix — the compact \"no history was rewritten\" guarantee a hash-chain alone cannot give a third party. Read-only (only --out is written). Exit 0 proved / 3 broken chain / 2 usage / 1 IO",
|
|
127
|
+
" vh journal check-proof <prooffile> [--json] the OFFLINE third-party AUDITOR command: read ONLY the proof artifact (NO journal, NO key, NO network) and verify it for its kind (inclusion or consistency). Hand an auditor a tree head + a proof file; they confirm inclusion/append-only-ness WITHOUT your log. ACCEPTED means the proof verifies against the head EMBEDDED in the artifact — compare that head against one you trust. Exit 0 ACCEPTED / 3 REJECTED / 2 usage / 1 IO — the SHARED 0/3 verify contract",
|
|
128
|
+
" vh serve-verify [--port <n>] [--host <h>] [--max-body <bytes>] launch a tiny loopback-only (default 127.0.0.1:4180) Node-core HTTP VERIFY server (ZERO new dependency). POST /verify a seal or signed container -> JSON verdict on a CI-mappable status (200 ACCEPTED / 422 REJECTED / 400 bad request / 413 over --max-body); GET /healthz -> { ok:true }. VERIFY-ONLY: it never signs, holds NO key, writes NO file. Binds loopback by default; exposing it publicly is a HUMAN deploy step (never auto-deployed). A verified seal is NOT a timestamp (P-3). Press Ctrl-C to stop; a bad flag exits 2, a bind failure exits 1",
|
|
129
|
+
" vh fulfill-webhook [--port <n>] [--host <h>] [--max-body <bytes>] [--tolerance <sec>] --secret-env <VAR> --binding <file> (--key-env <VAR>|--key-file <p>) --out <dir> [--catalog <f>] launch a tiny loopback-only (default 127.0.0.1:4190) Node-core HTTP FULFILLMENT webhook (ZERO new dependency): the DROP-IN that removes the human's last CODE step. POST /fulfill a Stripe-shaped paid event -> AUTHENTICATE it (HMAC over --secret-env, fail-closed), MAP its price to a plan via --binding, MINT the signed license the paid gate accepts, and DELIVER it IDEMPOTENTLY to --out (a re-delivered event returns the SAME licenseId, 200 { delivered, licenseId }; unsigned/forged/stale -> 401/400, unmappable -> 422, over --max-body -> 413). The vendor key (--key-env/--key-file) is held in memory only; the key/secret are NEVER written to disk or logs. Binds loopback by default; exposing it (real secret, real key, your domain+TLS) is a HUMAN deploy step. A license is an ACCESS credential, NOT a token (P-3). Press Ctrl-C to stop; a bad flag/config exits 2, a bind failure exits 1",
|
|
130
|
+
"",
|
|
131
|
+
"trust inspect options (read-only, writes NOTHING — the onboarding companion to reconcile):",
|
|
132
|
+
" --as <bank|ledger|rentroll> REQUIRED: which logical input <file> is (a malformed value is a usage error)",
|
|
133
|
+
" --bank-format <csv|ofx> with --as bank: force the bank file format (default: auto-detect)",
|
|
134
|
+
" --sample <n> echo at most n normalized sample records (date/signed-cents/kind/party/memo; default 5)",
|
|
135
|
+
" --json emit the full diagnostic report (header, column map, every error, hint) for piping",
|
|
136
|
+
" Runs the diagnostic ingest over ONE file and prints the detected header, the logical->header column map",
|
|
137
|
+
" (or \"(not found)\"), the OK/total parse count, a sample of normalized records, and EVERY failing row with",
|
|
138
|
+
" its number + reason. It ONLY checks the file PARSES into the normalized model — it does NOT reconcile or",
|
|
139
|
+
" attest (run `vh trust reconcile` for that). On a missing column or any bad row it prints an actionable hint",
|
|
140
|
+
" and exits 3; a fully-clean file exits 0. Bad --as is 2, an unreadable file is 1.",
|
|
141
|
+
"",
|
|
142
|
+
"trust reconcile options (the broker remains the responsible custodian; see the in-packet disclaimer):",
|
|
143
|
+
" --out <dir> write the dated HTML + CSV packet into THIS directory (created if absent;",
|
|
144
|
+
" never writes to cwd). Without --out, prints the summary + HTML to stdout.",
|
|
145
|
+
" --date <YYYY-MM-DD> report date (default: today); pin it for byte-reproducible output",
|
|
146
|
+
" --period <label> optional human label for the statement period (e.g. \"May 2026\")",
|
|
147
|
+
" --opening-bank <amt> opening bank balance (dollars, e.g. 1,234.56; default 0). With",
|
|
148
|
+
" --prior-close it OVERRIDES the seeded opening (override noted; a",
|
|
149
|
+
" chain-breaking override is flagged as a continuity break, not hidden)",
|
|
150
|
+
" --opening-book <amt> opening book balance (dollars; default 0); same override rule as --opening-bank",
|
|
151
|
+
" --prior-close <file> roll forward FROM a prior period's close.json: SEEDS the opening from its",
|
|
152
|
+
" ending and raises a CONTINUITY_BREAK if the roll-forward is not penny-exact",
|
|
153
|
+
" (a malformed close is a usage error, exit 2)",
|
|
154
|
+
" --emit-close <file> write THIS run's close.json to <file> (caller-named path only; never cwd) so",
|
|
155
|
+
" next month's --prior-close can chain from it",
|
|
156
|
+
" --seal [<file>] after the packet (and any --emit-close) is written, emit a TAMPER-EVIDENT",
|
|
157
|
+
" reconciliation seal binding the 3 source inputs + every emitted packet file",
|
|
158
|
+
" (default name under --out, or the caller-named <file>). REQUIRES --out.",
|
|
159
|
+
" The 3 source inputs are sealed by BASENAME so the binding travels with the",
|
|
160
|
+
" packet: ship each source NEXT TO the seal and the handoff verifies anywhere.",
|
|
161
|
+
" Verify it later, offline + read-only, with `vh trust verify-seal <sealfile>`.",
|
|
162
|
+
" --tolerance-cents <n> cents the three balances may differ and still tie out (default 0)",
|
|
163
|
+
" --bank-format <csv|ofx> force the bank file format (default: auto-detect)",
|
|
164
|
+
" --json emit the machine-readable packet model + exit-code contract",
|
|
165
|
+
" (exit: 0 PASS / 3 FAIL (does not tie out or an out-of-trust finding) / 2 usage / 1 IO)",
|
|
166
|
+
"",
|
|
167
|
+
"hash options:",
|
|
168
|
+
" --git hash EXACTLY the files git tracks (ignores untracked junk like",
|
|
169
|
+
" node_modules/, .env, build artifacts); <path> must be in a git repo",
|
|
170
|
+
" --ref <ref> with --git: which commit's tracked set to hash (default HEAD)",
|
|
171
|
+
"",
|
|
172
|
+
"anchor options (one-shot; contributor = 'first anchorer', NOT proven authorship):",
|
|
173
|
+
" --uri <uri> optional off-chain pointer stored with the hash (IPFS CID, URL)",
|
|
174
|
+
" --parent <0xhash> record an immutable predecessor edge to an ALREADY-anchored hash",
|
|
175
|
+
" (the lineage graph). Routes to anchorWithParent(); the parent must",
|
|
176
|
+
" already exist or the tx reverts UnknownParent. Omit it for a root.",
|
|
177
|
+
" A `parent` is only a CLAIMED predecessor: it proves neither content",
|
|
178
|
+
" ancestry nor any transfer of the parent's authorship.",
|
|
179
|
+
" --git anchor EXACTLY the files git tracks (ignores untracked junk); records",
|
|
180
|
+
" a `git` provenance hint (commit oid + scope) in the receipt",
|
|
181
|
+
" --ref <ref> with --git: which commit's tracked set to anchor (default HEAD)",
|
|
182
|
+
" --receipt <path> write an anchor receipt here (records a dir's per-file manifest",
|
|
183
|
+
" so `vh verify <dir> --receipt <p>` can localize WHICH file changed)",
|
|
184
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
185
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
186
|
+
" --dry-run print the tx that would be sent; needs no key, sends nothing",
|
|
187
|
+
" --i-understand-mainnet allow anchoring on a non-testnet chainId (DANGER: real funds)",
|
|
188
|
+
"",
|
|
189
|
+
"claim options (commit-reveal one-shot; contributor = proven first claimant, authorBound = true):",
|
|
190
|
+
" --uri <uri> optional off-chain pointer stored with the hash (IPFS CID, URL)",
|
|
191
|
+
" --parent <0xhash> record an immutable predecessor edge to an ALREADY-anchored hash",
|
|
192
|
+
" (routes the reveal leg to revealWithParent(); the parent must already",
|
|
193
|
+
" exist or it reverts UnknownParent). Works on the one-shot `vh claim`",
|
|
194
|
+
" AND on the resumable split: `vh commit --parent` persists the edge",
|
|
195
|
+
" into the receipt (v4) and `vh reveal` then records it — no reveal flag.",
|
|
196
|
+
" --git claim EXACTLY the files git tracks (records a `git` provenance hint)",
|
|
197
|
+
" --ref <ref> with --git: which commit's tracked set to claim (default HEAD)",
|
|
198
|
+
" --salt <0xhex> reuse a 32-byte salt (default: a fresh random one)",
|
|
199
|
+
" --receipt <path> persist a resumable claim receipt at this exact path (holds the SECRET",
|
|
200
|
+
" salt). WITHOUT it the one-shot claim persists NOTHING — use `vh commit`",
|
|
201
|
+
" for a durable, resumable receipt.",
|
|
202
|
+
" --receipt-dir <dir> persist the receipt into this directory under its default file name",
|
|
203
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
204
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
205
|
+
" --dry-run print the commit+reveal plan; needs no key, sends nothing",
|
|
206
|
+
" --i-understand-mainnet allow claiming on a non-testnet chainId (DANGER: real funds)",
|
|
207
|
+
"",
|
|
208
|
+
"commit options (step 1 of a resumable claim; writes a receipt, then commits):",
|
|
209
|
+
" --uri <uri> pointer recorded at reveal time (kept in the receipt until then)",
|
|
210
|
+
" --parent <0xhash> persist a predecessor edge to an ALREADY-anchored hash into the receipt;",
|
|
211
|
+
" the resumed `vh reveal` routes to revealWithParent() and records it (the",
|
|
212
|
+
" commit() tx itself carries no parent — the edge is recorded at reveal time).",
|
|
213
|
+
" A malformed/self-referential value hard-errors BEFORE any network call.",
|
|
214
|
+
" --git commit EXACTLY the files git tracks (records a `git` provenance hint)",
|
|
215
|
+
" --ref <ref> with --git: which commit's tracked set to commit (default HEAD)",
|
|
216
|
+
" --salt <0xhex> reuse a 32-byte salt (default: a fresh random one)",
|
|
217
|
+
" --receipt <path> write the claim receipt (holds the SECRET salt) at this exact path;",
|
|
218
|
+
" default <cwd>/<hashPrefix>.vhclaim.json — the EXACT file written is",
|
|
219
|
+
" always named in the success output so you can see/relocate/delete it",
|
|
220
|
+
" --receipt-dir <dir> write the receipt into this directory under its default file name",
|
|
221
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
222
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
223
|
+
" --i-understand-mainnet allow committing on a non-testnet chainId (DANGER: real funds)",
|
|
224
|
+
"",
|
|
225
|
+
"reveal options (step 2; resumes a prior commit from its receipt and reveals):",
|
|
226
|
+
" --receipt <path> REQUIRED: the receipt file written by `vh commit`. If the receipt",
|
|
227
|
+
" recorded a `--parent` it reveals via revealWithParent() (records the",
|
|
228
|
+
" lineage edge); otherwise it uses the legacy reveal(). No --parent flag.",
|
|
229
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
230
|
+
" --i-understand-mainnet allow revealing on a non-testnet chainId (DANGER: real funds)",
|
|
231
|
+
"",
|
|
232
|
+
"verify options:",
|
|
233
|
+
" --git recompute the root over EXACTLY the files git tracks (ignores",
|
|
234
|
+
" untracked junk); reproducible end-to-end against a fresh checkout",
|
|
235
|
+
" --ref <ref> with --git: which commit's tracked set to verify (default HEAD)",
|
|
236
|
+
" --receipt <path> UNTRUSTED hint: diff a dir against this receipt's manifest and print",
|
|
237
|
+
" ADDED/REMOVED/CHANGED per file (verdict still = root vs on-chain)",
|
|
238
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
239
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
240
|
+
" --skip-identity-check DANGER: skip authenticating the contract is a real verifyhash registry",
|
|
241
|
+
" (only for a KNOWN local/not-yet-deployed contract). The verdict is then",
|
|
242
|
+
" only as trustworthy as the RPC you pointed at. NEVER the default.",
|
|
243
|
+
"",
|
|
244
|
+
"prove options:",
|
|
245
|
+
" --root <dir> the repo root directory whose Merkle root <file> is proven against",
|
|
246
|
+
" --out <path> write a self-contained, portable proof artifact here (works on the",
|
|
247
|
+
" no-key --dry-run/build path); verify it later with `vh verify-proof`",
|
|
248
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
249
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
250
|
+
" --anchor anchor the repo root first (needs PRIVATE_KEY), then prove",
|
|
251
|
+
" --i-understand-mainnet allow --anchor on a non-testnet chainId (DANGER: real funds)",
|
|
252
|
+
" --dry-run build & print the proof only; needs no key and no network",
|
|
253
|
+
"",
|
|
254
|
+
"verify-proof options (read-only, NO key; needs only the artifact + an RPC URL — no repo):",
|
|
255
|
+
" <p> path to a proof artifact written by `vh prove --out <p>`",
|
|
256
|
+
" --contract <address> ContributionRegistry address (or the artifact's recorded address)",
|
|
257
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
258
|
+
" --json emit a machine-readable JSON object instead of the human block",
|
|
259
|
+
" --skip-identity-check DANGER: skip authenticating the contract + the artifact's chainId",
|
|
260
|
+
" cross-check (only for a KNOWN local/not-yet-deployed contract). NEVER",
|
|
261
|
+
" the default — the verdict is then only as trustworthy as the RPC.",
|
|
262
|
+
" Re-derives the leaf + re-folds the proof OFFLINE, then confirms the root is anchored on-chain.",
|
|
263
|
+
" Prints ACCEPTED only when the offline fold AND the on-chain checks all pass; else REJECTED /",
|
|
264
|
+
" NOT ANCHORED (non-zero exit). Proves SET-MEMBERSHIP in an anchored root, not authorship/uri.",
|
|
265
|
+
"",
|
|
266
|
+
"list options (read-only enumeration; provider only, never a signer/key):",
|
|
267
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
268
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
269
|
+
" --contributor <address> only records whose contributor is this address",
|
|
270
|
+
" --author-bound only commit-reveal records (authorBound = proven first claimant)",
|
|
271
|
+
" --limit <n> show at most n records (after --offset)",
|
|
272
|
+
" --offset <n> skip the first n (filtered) records",
|
|
273
|
+
" --json emit a machine-readable JSON envelope { registry, records }",
|
|
274
|
+
" --skip-identity-check DANGER: skip authenticating the contract is a real verifyhash registry",
|
|
275
|
+
" (only for a KNOWN local/not-yet-deployed contract). NEVER the default.",
|
|
276
|
+
"",
|
|
277
|
+
"show options (read-only lookup by hash; provider only, never a signer/key):",
|
|
278
|
+
" <0xhash> a 32-byte (0x + 64 hex) content hash, e.g. from `vh list`",
|
|
279
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
280
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
281
|
+
" --json emit a machine-readable JSON object instead of the human block",
|
|
282
|
+
" --skip-identity-check DANGER: skip authenticating the contract is a real verifyhash registry",
|
|
283
|
+
" (only for a KNOWN local/not-yet-deployed contract). NEVER the default.",
|
|
284
|
+
" NOTE: `show` proves only that the hash is on-chain; it does NOT re-derive content. To bind a",
|
|
285
|
+
" record to real bytes you must still run `vh verify <path>`. Exits non-zero if NOT ANCHORED.",
|
|
286
|
+
"",
|
|
287
|
+
"lineage options (read-only walk UP the parent chain; provider only, never a signer/key):",
|
|
288
|
+
" <0xhash> a 32-byte (0x + 64 hex) content hash to start the walk from",
|
|
289
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
290
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
291
|
+
" --max-depth <n> cap the walk at n ancestors (default 256); reaching the cap prints a",
|
|
292
|
+
" clear note instead of looping forever on a pathological chain",
|
|
293
|
+
" --json emit a machine-readable ordered ancestor array instead of the human block",
|
|
294
|
+
" --skip-identity-check DANGER: skip authenticating the contract is a real verifyhash registry",
|
|
295
|
+
" (only for a KNOWN local/not-yet-deployed contract). NEVER the default.",
|
|
296
|
+
" Walks child -> parent -> ... to the lineage root, printing each ancestor (contentHash, contributor,",
|
|
297
|
+
" attribution, timestamp+ISO, blockNumber, uri). A `parent` is only the CHILD author's CLAIMED",
|
|
298
|
+
" predecessor: it proves neither content ancestry nor a transfer of authorship. Exits non-zero if the",
|
|
299
|
+
" start hash is NOT ANCHORED.",
|
|
300
|
+
"",
|
|
301
|
+
"reputation options (read-only score for ONE address; provider only, never a signer/key):",
|
|
302
|
+
" <addr> a 20-byte (0x + 40 hex) contributor address, e.g. from `vh list`",
|
|
303
|
+
" --contract <address> ContributionRegistry address (or env VH_CONTRACT)",
|
|
304
|
+
" --rpc <url> JSON-RPC endpoint (or env VH_RPC_URL / AMOY_RPC_URL)",
|
|
305
|
+
" --json emit a machine-readable JSON object instead of the human block",
|
|
306
|
+
" --skip-identity-check DANGER: skip authenticating the contract is a real verifyhash registry",
|
|
307
|
+
" (only for a KNOWN local/not-yet-deployed contract). NEVER the default.",
|
|
308
|
+
" Reports total records + authorBound vs anchor-only + lineage-root vs revision breakdowns + the",
|
|
309
|
+
" earliest/latest block & timestamp. The score is a TRANSPARENT, on-chain-DERIVED aggregate — NOT a",
|
|
310
|
+
" token, NOT transferable. An anchor-only count is WEAKER (a plain anchor() is front-runnable), so the",
|
|
311
|
+
" breakdown reports authorBound and anchor-only SEPARATELY. It does NOT validate record CONTENT (run",
|
|
312
|
+
" `vh verify` for that). Exits non-zero if the address has NO contributions.",
|
|
313
|
+
"",
|
|
314
|
+
"dataset build options (tamper-evident dataset manifest; offline, NO key, NO network):",
|
|
315
|
+
" <dir> the dataset directory to manifest (walked recursively)",
|
|
316
|
+
" --out <path> REQUIRED: write the manifest JSON here (caller-chosen path; never cwd).",
|
|
317
|
+
" The exact absolute file written is named in the success output.",
|
|
318
|
+
" --hints <path> OPTIONAL: a JSON file { \"<relPath>\": { source, license } } of UNTRUSTED",
|
|
319
|
+
" per-file provenance hints. They are recorded labeled as untrusted and are",
|
|
320
|
+
" NOT bound into the Merkle root — editing them does not change the root.",
|
|
321
|
+
" --json emit a machine-readable { root, fileCount, out } object",
|
|
322
|
+
" Streams each file (a multi-GB dataset is hashed without loading all content into memory). The root",
|
|
323
|
+
" reuses the SAME path-bound Merkle convention as `vh hash <dir>` and the on-chain verifyLeaf — the",
|
|
324
|
+
" root commits to file NAMES and bytes, so any edit/rename/add/remove changes it.",
|
|
325
|
+
"",
|
|
326
|
+
"dataset verify options (OFFLINE re-derive + per-file diff; NO key, NO network):",
|
|
327
|
+
" <dir> the dataset directory to RE-DERIVE the root from (a fresh copy on disk)",
|
|
328
|
+
" --manifest <path> REQUIRED: a manifest written by `vh dataset build` (an UNTRUSTED hint).",
|
|
329
|
+
" --json emit a machine-readable { status, recomputedRoot, manifestRoot, ... }",
|
|
330
|
+
" The AUTHORITATIVE verdict is recomputed-root vs manifest-root — recomputed from the bytes on disk,",
|
|
331
|
+
" so a hand-edited manifest root cannot fake a MATCH. Prints a precise per-file ADDED/REMOVED/CHANGED",
|
|
332
|
+
" (old->new contentHash) diff (the SAME diff core as `vh verify --receipt`) to localize WHICH file",
|
|
333
|
+
" diverged; a rename shows as REMOVED+ADDED (the root commits to file names). Exit 0 MATCH, 3 MISMATCH.",
|
|
334
|
+
"",
|
|
335
|
+
"dataset diff options (OFFLINE manifest-to-manifest change report; NO tree, NO key, NO network):",
|
|
336
|
+
" <manifestA> REQUIRED: the BASELINE manifest (the 'from')",
|
|
337
|
+
" <manifestB> REQUIRED: the COMPARISON manifest (the 'to')",
|
|
338
|
+
" --json emit { rootA, rootB, rootsIdentical, identical, added, removed, changed, unchanged, counts }",
|
|
339
|
+
" Reads BOTH via the strict readManifest (a corrupt/foreign manifest is rejected) and diffs them by",
|
|
340
|
+
" REUSING the SAME diff core as `vh dataset verify`. ADDED = in B not A, REMOVED = in A not B,",
|
|
341
|
+
" CHANGED = same relPath/different content (old->new). A rename shows as REMOVED+ADDED (the path is",
|
|
342
|
+
" bound into the leaf). Compares what each manifest CLAIMS — it does NOT re-derive content (use",
|
|
343
|
+
" `vh dataset verify` against the live tree for that). The verdict/exit code is the CHANGE SET",
|
|
344
|
+
" (`identical`), NOT root-string equality. Exit 0 IDENTICAL, 3 DIFFERENT.",
|
|
345
|
+
"",
|
|
346
|
+
"dataset summary options (OFFLINE provenance/license roll-up; NO tree, NO key, NO network):",
|
|
347
|
+
" <manifest> REQUIRED: a manifest written by `vh dataset build`",
|
|
348
|
+
" --json emit { root, fileCount, licenses, sources, filesWithLicenseHint, filesWithSourceHint }",
|
|
349
|
+
" Reads the manifest via the strict readManifest (a corrupt/foreign manifest is rejected) and rolls",
|
|
350
|
+
" up the TRUSTED file set: total fileCount, the root, and license + source histograms (count of",
|
|
351
|
+
" files per CLAIMED value; files with no hint grouped under '(no license hint)' / '(no source hint)').",
|
|
352
|
+
" The file SET (relPath + content) is bound into the root and trustworthy; the {source, license}",
|
|
353
|
+
" hints are UNTRUSTED, self-asserted metadata NOT bound into the root — this counts what the dataset",
|
|
354
|
+
" CLAIMS, it does NOT verify any license/source is correct. '(no license hint)' means the manifest",
|
|
355
|
+
" asserts nothing, NOT that the file is unlicensed. Exit 0; usage error 2; corrupt/missing manifest 1.",
|
|
356
|
+
"",
|
|
357
|
+
"dataset check options (OFFLINE license/source policy gate; NO tree, NO key, NO network):",
|
|
358
|
+
" <manifest> REQUIRED: a manifest written by `vh dataset build`",
|
|
359
|
+
" --policy <path> REQUIRED: a versioned policy file. ALL rules OPTIONAL and combinable:",
|
|
360
|
+
" allowLicenses (allowlist), denyLicenses (denylist), allowSources,",
|
|
361
|
+
" denySources, requireLicense:true (every file MUST carry a license hint).",
|
|
362
|
+
" --json emit { verdict, fileCount, rulesEvaluated, violations:[{relPath,rule,value}] }",
|
|
363
|
+
" Reads the manifest AND policy strictly (a corrupt/foreign one is rejected) and evaluates the",
|
|
364
|
+
" manifest's TRUSTED file set against the policy in a PURE, deterministic function. Match semantics:",
|
|
365
|
+
" CASE-SENSITIVE EXACT string match on the hint value. A policy with NO rules trivially PASSes. The",
|
|
366
|
+
" {source, license} hints are UNTRUSTED, self-asserted metadata NOT bound into the root: a PASS means",
|
|
367
|
+
" the dataset's self-asserted hints satisfy this policy, NOT that the licenses are genuinely correct.",
|
|
368
|
+
" Violations are sorted by relPath then rule (byte-identical output across runs). A missing --policy",
|
|
369
|
+
" is a usage error. Exit 0 PASS, 3 FAIL; usage error 2; corrupt/missing manifest OR policy 1.",
|
|
370
|
+
"",
|
|
371
|
+
"dataset report options (ONE deterministic evidence document; OFFLINE; NO key, NO network):",
|
|
372
|
+
" <manifest> REQUIRED: a manifest written by `vh dataset build`",
|
|
373
|
+
" --verify <dir> OPTIONAL: re-derive the root from this live tree (REUSES dataset verify)",
|
|
374
|
+
" and embed the MATCH/MISMATCH verdict + per-file ADDED/REMOVED/CHANGED.",
|
|
375
|
+
" Without it, the report states plainly that NO live-tree verify was done.",
|
|
376
|
+
" --policy <path> OPTIONAL: evaluate the manifest against this policy (REUSES the SAME pure",
|
|
377
|
+
" evaluator as `vh dataset check`) and embed a 'Policy compliance' section",
|
|
378
|
+
" (verdict + rules evaluated + violating files: relPath/rule/value).",
|
|
379
|
+
" --out <path> write the report to this explicit path (caller-chosen; never cwd); the",
|
|
380
|
+
" exact file written is named. Without it the report prints to stdout.",
|
|
381
|
+
" --json emit { root, fileCount, licenses, sources, filesWithLicenseHint,",
|
|
382
|
+
" filesWithSourceHint, verify?, policy? } instead of the Markdown document",
|
|
383
|
+
" Reads the manifest strictly (a corrupt/foreign manifest is rejected) and CONSOLIDATES the dataset",
|
|
384
|
+
" identity (root + fileCount), the provenance/license roll-up (the SAME aggregation as `vh dataset",
|
|
385
|
+
" summary`), and the standing trust caveats into ONE document. Default human output is DETERMINISTIC",
|
|
386
|
+
" Markdown (byte-identical across runs over the same manifest + policy). It LEADS with the trust",
|
|
387
|
+
" posture and does NOT overclaim: it is NOT a timestamp ('unaltered since date T' needs a human-signed",
|
|
388
|
+
" step), and a policy PASS attests the dataset's UNTRUSTED self-asserted hints satisfy the policy, NOT",
|
|
389
|
+
" that the licenses are genuinely correct.",
|
|
390
|
+
" Exit (the report is a COMBINED CI gate — non-zero if ANY embedded gate fails, 0 only when all pass):",
|
|
391
|
+
" with --verify: 0 MATCH / 3 MISMATCH; with --policy: 0 PASS / 3 FAIL; with BOTH: 3 if EITHER fails,",
|
|
392
|
+
" 0 only when MATCH AND PASS; without either gate: 0 on a well-formed manifest. Usage error 2;",
|
|
393
|
+
" corrupt/missing manifest or policy (or bad --verify dir) 1.",
|
|
394
|
+
"",
|
|
395
|
+
"dataset attest options (canonical UNSIGNED attestation payload; OFFLINE; NO key, NO network):",
|
|
396
|
+
" <manifest> REQUIRED: a manifest written by `vh dataset build`",
|
|
397
|
+
" --out <path> write the canonical payload to this explicit path (caller-chosen; never",
|
|
398
|
+
" cwd); the exact file written is named. Without it, it prints to stdout.",
|
|
399
|
+
" --json emit the machine form — which IS the canonical, signable bytes",
|
|
400
|
+
" Reads the manifest strictly (a corrupt/foreign manifest is rejected) and emits a versioned,",
|
|
401
|
+
" strictly-validated, BYTE-DETERMINISTIC envelope committing to the dataset IDENTITY a signer signs:",
|
|
402
|
+
" the Merkle root, fileCount, and a canonical manifestDigest (keccak256 over a canonical serialization",
|
|
403
|
+
" of the committed file set — any edit to that set changes it). The envelope is marked `signed:false`",
|
|
404
|
+
" with a `signature:null` slot the human/timestamp step fills. This is the UNSIGNED payload: standing",
|
|
405
|
+
" up a real signing key / timestamp anchor is the human-owned trust-root (needs-human, P-3). Until a",
|
|
406
|
+
" signature is attached it proves only the same set-membership/identity the manifest already does —",
|
|
407
|
+
" NOT 'unaltered since date T'. Exit 0; usage error 2; corrupt/missing manifest 1.",
|
|
408
|
+
"",
|
|
409
|
+
"dataset sign options (sign the UNSIGNED attestation with YOUR key -> a signed container; OFFLINE; NO network):",
|
|
410
|
+
" <manifest> REQUIRED: a manifest written by `vh dataset build`",
|
|
411
|
+
" --key-env <VAR> read the signing private key from process.env[VAR] (EXACTLY ONE key source)",
|
|
412
|
+
" --key-file <path> read the signing private key from a file YOU created (EXACTLY ONE key source)",
|
|
413
|
+
" --out <path> write the signed container here (caller-chosen; never cwd). Without it the",
|
|
414
|
+
" signed bytes print to stdout. The signed container holds ONLY the PUBLIC",
|
|
415
|
+
" signer address + signature — NEVER the key.",
|
|
416
|
+
" --json emit { signed, signer, scheme, out, kind, container, note } (public only; NO",
|
|
417
|
+
" key). With NO --out, `container` carries the canonical signed bytes so --json",
|
|
418
|
+
" never drops the artifact (parity with `attest --json`); with --out it is null.",
|
|
419
|
+
" Builds the UNSIGNED payload EXACTLY as `vh dataset attest` does (same canonical bytes), constructs an",
|
|
420
|
+
" in-process ethers Wallet from YOUR key, signs (eip191-personal-sign), and wraps it WITHOUT editing the",
|
|
421
|
+
" payload. The key is read, used, and discarded — NEVER generated, persisted, or logged; the success",
|
|
422
|
+
" line states 'signed by <0xaddr>' so you can confirm WHICH key signed. EXACTLY ONE of --key-env/",
|
|
423
|
+
" --key-file is required: neither, both, a missing env var, an unreadable file, or a malformed/zero key",
|
|
424
|
+
" HARD-ERRORS BEFORE any signing (the message never includes the key). The output is accepted by",
|
|
425
|
+
" `vh dataset verify-attest` unchanged. This signs the dataset IDENTITY with the key YOU supplied — it",
|
|
426
|
+
" is NOT a trusted TIMESTAMP ('the signer says so', not 'existed by date T'; P-3). The key must be one",
|
|
427
|
+
" YOU provisioned outside this tool. Exit 0; usage error 2 (no/both key source, unknown flag); runtime 1.",
|
|
428
|
+
"",
|
|
429
|
+
"dataset verify-attest options (OFFLINE verify a SIGNED attestation; NO tree, NO provider, NO key, NO network):",
|
|
430
|
+
" <signed> REQUIRED: a signed-attestation container (the wrapped, signed T-17.1 artifact)",
|
|
431
|
+
" --manifest <path> OPTIONAL: bind the signature to YOUR dataset — recompute the canonical",
|
|
432
|
+
" attestation bytes from this manifest and require them byte-identical to the",
|
|
433
|
+
" signed payload (a binding mismatch REJECTS).",
|
|
434
|
+
" --signer <addr> OPTIONAL: pin the EXPECTED publisher — require the RECOVERED signer to equal",
|
|
435
|
+
" this address (so a buyer pins WHO must have signed, not just that someone did)",
|
|
436
|
+
" --revocations <path> OPTIONAL: a signed key-revocation file (one container or a JSON array). If the",
|
|
437
|
+
" publisher's key is REVOKED as of --as-of, the verdict is REVOKED (exit 3); a",
|
|
438
|
+
" later-dated or forged/invalid revocation never downgrades (forged ones warn).",
|
|
439
|
+
" --as-of <ISO> OPTIONAL (needs --revocations): the instant the revocation decision is made AS",
|
|
440
|
+
" OF (default: now). 'was this key trustworthy when this exhibit was sealed?'",
|
|
441
|
+
" --json emit a machine verdict { verdict, recoveredSigner, expectedSigner, checks, ... }",
|
|
442
|
+
" Reads the container strictly (a malformed/edited/foreign one is rejected), recovers the signing address",
|
|
443
|
+
" from the embedded canonical bytes + signature per the declared scheme (eip191-personal-sign), and",
|
|
444
|
+
" confirms it equals the container's `signer`. With --signer it also pins the expected publisher; with",
|
|
445
|
+
" --manifest it also confirms the signature binds the dataset you hold. Prints ACCEPTED only when EVERY",
|
|
446
|
+
" requested check passes, else REJECTED naming which failed. A valid signature proves the key-holder",
|
|
447
|
+
" vouched for this dataset IDENTITY — NOT a timestamp ('unaltered since date T' still needs P-3) and NOT",
|
|
448
|
+
" that the license/source hints are correct. Exit 0 ACCEPTED, 3 REJECTED/REVOKED; usage error 2; corrupt input 1.",
|
|
449
|
+
"",
|
|
450
|
+
"dataset prove options (OFFLINE set-membership of ONE file; NO key, NO network):",
|
|
451
|
+
" --file <path> REQUIRED: the single file to prove was a member of the dataset",
|
|
452
|
+
" --manifest <path> REQUIRED: a manifest written by `vh dataset build`",
|
|
453
|
+
" --out <path> write a self-contained proof artifact here (caller-chosen; never cwd).",
|
|
454
|
+
" Verify it later with `vh dataset verify-proof <p>` — no dataset needed.",
|
|
455
|
+
" --json emit a machine-readable { member, contentHash, relPath, root, ... }",
|
|
456
|
+
" Matches the file by CONTENT against the manifest's committed leaves and builds the Merkle proof",
|
|
457
|
+
" folding its leaf to the manifest root (the SAME construction as `vh prove`). A fabricated/altered",
|
|
458
|
+
" file is a clear NON-member (no artifact written). Exit 0 MEMBER, 3 NOT A MEMBER. Proves",
|
|
459
|
+
" SET-MEMBERSHIP only — NOT 'unaltered since date T', authorship, or licensing (a human-signed step).",
|
|
460
|
+
"",
|
|
461
|
+
"dataset verify-proof options (PURELY OFFLINE; NO dataset copy, NO manifest, NO key, NO network):",
|
|
462
|
+
" <proof> path to a proof artifact written by `vh dataset prove --out <p>`",
|
|
463
|
+
" --json emit a machine-readable { status, leafMatches, foldsToRoot, ... }",
|
|
464
|
+
" Folds the leaf through the proof to the recorded root (reuses the SAME recompute as verify-proof).",
|
|
465
|
+
" Prints CONFIRMED only when the leaf re-derives AND folds to the root; else REJECTED (non-zero exit).",
|
|
466
|
+
" Proves SET-MEMBERSHIP in the recorded root, NOT that the root is anchored on-chain (`vh verify-proof`",
|
|
467
|
+
" does the on-chain leg) nor 'unaltered since date T'. Exit 0 CONFIRMED, 3 REJECTED.",
|
|
468
|
+
"",
|
|
469
|
+
"parcel build options (tamper-evident DELIVERY receipt; offline, NO key, NO network):",
|
|
470
|
+
" <dir> the delivered directory to manifest (walked recursively)",
|
|
471
|
+
" --out <path> REQUIRED: where to write the parcel manifest (caller-chosen; never cwd)",
|
|
472
|
+
" --parcel-id <s> OPTIONAL untrusted self-asserted parcel identifier (NOT bound into the root)",
|
|
473
|
+
" --sender <s> OPTIONAL untrusted self-asserted sender (NOT bound into the root)",
|
|
474
|
+
" --recipient <s> OPTIONAL untrusted self-asserted recipient (NOT bound into the root)",
|
|
475
|
+
" --hints <path> OPTIONAL JSON of untrusted per-file {source,license} hints",
|
|
476
|
+
" --json emit { root, fileCount, out, parcel } instead of the human summary",
|
|
477
|
+
" Same Merkle root + per-file {relPath,contentHash,leaf} as a dataset manifest, PLUS an OPTIONAL,",
|
|
478
|
+
" UNTRUSTED `parcel` block (parcelId/sender/recipient) recorded as self-asserted metadata that is NOT",
|
|
479
|
+
" bound into the root. The receipt is NOT a trusted delivery timestamp — 'delivered ON date T' needs",
|
|
480
|
+
" the human-owned signing/timestamp trust-root (STRATEGY.md P-3). Exit 0; usage error 2; runtime 1.",
|
|
481
|
+
"",
|
|
482
|
+
"parcel verify options (OFFLINE re-derive + per-file diff; NO key, NO network):",
|
|
483
|
+
" <dir> the delivered directory to RE-DERIVE the root from (a fresh copy on disk)",
|
|
484
|
+
" --manifest <path> REQUIRED: a manifest written by `vh parcel build` (an UNTRUSTED hint)",
|
|
485
|
+
" --json emit { status, recomputedRoot, manifestRoot, parcel, diff } as JSON",
|
|
486
|
+
" Re-derives the root from disk and prints MATCH/MISMATCH + a precise per-file ADDED/REMOVED/CHANGED",
|
|
487
|
+
" diff (the SAME diff core as `vh dataset verify`). The AUTHORITATIVE verdict is recomputed-root vs",
|
|
488
|
+
" manifest-root; the untrusted `parcel` block plays NO part in it. Exit 0 MATCH, 3 MISMATCH (mirrors",
|
|
489
|
+
" `vh dataset verify` so all verify gates share ONE exit contract); usage 2; corrupt/missing manifest 1.",
|
|
490
|
+
"",
|
|
491
|
+
"parcel attest options (canonical UNSIGNED parcel-attestation payload; OFFLINE; NO key, NO network):",
|
|
492
|
+
" <manifest> REQUIRED: a manifest written by `vh parcel build`",
|
|
493
|
+
" --out <path> OPTIONAL: write the canonical bytes here (caller-chosen; never cwd)",
|
|
494
|
+
" --json emit the canonical machine form (which IS the same signable bytes)",
|
|
495
|
+
" Emits the deterministic, byte-canonical UNSIGNED attestation (root + fileCount + a canonical",
|
|
496
|
+
" manifestDigest over the delivered file SET) over the SAME core as `vh dataset attest`, with",
|
|
497
|
+
" `signed:false`. The UNTRUSTED `parcel` block is EXCLUDED. It is NOT a timestamp — attaching a real",
|
|
498
|
+
" signature is the human-owned signing/timestamp trust-root (STRATEGY.md P-3). Exit 0; usage 2; runtime 1.",
|
|
499
|
+
"",
|
|
500
|
+
"parcel sign options (sign the UNSIGNED parcel attestation with YOUR key -> a signed container; OFFLINE; NO network):",
|
|
501
|
+
" <manifest> REQUIRED: a manifest written by `vh parcel build`",
|
|
502
|
+
" --key-env <VAR> read the signing private key from process.env[VAR] (EXACTLY ONE key source)",
|
|
503
|
+
" --key-file <path> read the signing private key from a file YOU created (EXACTLY ONE key source)",
|
|
504
|
+
" --out <path> write the signed container here (caller-chosen; never cwd). Without it the",
|
|
505
|
+
" signed bytes print to stdout. The container holds ONLY the PUBLIC signer",
|
|
506
|
+
" address + signature — NEVER the key.",
|
|
507
|
+
" --json emit { signed, signer, scheme, out, kind, container, note } (public only; NO",
|
|
508
|
+
" key). With NO --out, `container` carries the canonical signed bytes so --json",
|
|
509
|
+
" never drops the artifact (parity with `attest --json`); with --out it is null.",
|
|
510
|
+
" Builds the UNSIGNED payload EXACTLY as `vh parcel attest` does, constructs an in-process ethers Wallet",
|
|
511
|
+
" from YOUR key, signs (eip191-personal-sign), and wraps it WITHOUT editing the payload. The key is read,",
|
|
512
|
+
" used, and discarded — NEVER generated, persisted, or logged; 'signed by <0xaddr>' confirms WHICH key",
|
|
513
|
+
" signed. EXACTLY ONE of --key-env/--key-file is required: neither, both, a missing env var, an unreadable",
|
|
514
|
+
" file, or a malformed/zero key HARD-ERRORS BEFORE any signing (the message never includes the key). The",
|
|
515
|
+
" output is accepted by `vh parcel verify-attest` unchanged. This signs the parcel IDENTITY with the key",
|
|
516
|
+
" YOU supplied — it is NOT a trusted delivery TIMESTAMP ('the signer says so'; P-3). The key must be one",
|
|
517
|
+
" YOU provisioned outside this tool. Exit 0; usage error 2 (no/both key source, unknown flag); runtime 1.",
|
|
518
|
+
"",
|
|
519
|
+
"parcel verify-attest options (OFFLINE verify a SIGNED parcel attestation; NO tree, NO provider, NO key, NO network):",
|
|
520
|
+
" <signed> REQUIRED: a signed parcel-attestation container",
|
|
521
|
+
" --manifest <path> OPTIONAL: bind the signature to YOUR parcel — recompute the canonical UNSIGNED",
|
|
522
|
+
" bytes from this manifest and require them byte-identical to the signed payload",
|
|
523
|
+
" --signer <addr> OPTIONAL: pin the expected SENDER (recovered signer must equal this address)",
|
|
524
|
+
" --revocations <path> OPTIONAL: a signed key-revocation file (one container or a JSON array). If the",
|
|
525
|
+
" sender's key is REVOKED as of --as-of, the verdict is REVOKED (exit 3); a",
|
|
526
|
+
" later-dated or forged/invalid revocation never downgrades (forged ones warn).",
|
|
527
|
+
" --as-of <ISO> OPTIONAL (needs --revocations): the instant the revocation decision is made AS",
|
|
528
|
+
" OF (default: now).",
|
|
529
|
+
" --json emit the machine-readable verdict (recovered signer + per-check booleans)",
|
|
530
|
+
" Recovers the signer over the SAME core as `vh dataset verify-attest`; the parcel signed-container kind",
|
|
531
|
+
" (verifyhash.parcel-attestation-signed) means a DATASET signed-container does NOT cross-verify. A valid",
|
|
532
|
+
" signature is NOT a delivery timestamp (STRATEGY.md P-3). Exit 0 ACCEPTED, 3 REJECTED/REVOKED; usage 2; runtime 1.",
|
|
533
|
+
"",
|
|
534
|
+
"serve-verify options (a tiny loopback-only Node-core HTTP VERIFY server; ZERO new dependency; verify-ONLY):",
|
|
535
|
+
" --port <n> TCP port to bind (0..65535; 0 = an OS-chosen ephemeral port). Default 4180.",
|
|
536
|
+
" --host <h> interface to bind. Default 127.0.0.1 (LOOPBACK) — a non-loopback interface is",
|
|
537
|
+
" NOT served by default; pass e.g. --host 0.0.0.0 to bind all interfaces (your",
|
|
538
|
+
" explicit, deliberate choice — exposing it is a HUMAN deploy step).",
|
|
539
|
+
" --max-body <bytes> reject a request body larger than this many bytes with HTTP 413 (default: the",
|
|
540
|
+
" core's own byte cap). Belt-and-braces against a hostile/accidental giant body.",
|
|
541
|
+
" Routes: POST /verify -> the SAME pure verify core as the SDK, wrapped as JSON over HTTP. Body is",
|
|
542
|
+
" { kind:\"verify-seal\", seal, entries:[{relPath,content,encoding}] } (UNSIGNED) OR",
|
|
543
|
+
" { kind:\"verify-signed-seal\", container, expectedSigner?, entries? } (SIGNED). It maps the",
|
|
544
|
+
" verdict to a CI-gateable status: 200 ACCEPTED / 422 REJECTED / 400 bad-or-malformed request",
|
|
545
|
+
" (invalid JSON, unknown kind, corrupt seal) / 413 body over --max-body. GET /healthz -> 200",
|
|
546
|
+
" { ok:true } for a liveness probe.",
|
|
547
|
+
" POSTURE (verify-only + loopback + P-3 + human-deploy): this server VERIFIES seals; it NEVER signs, holds",
|
|
548
|
+
" NO private key, and writes NO file. It binds loopback by default; exposing it publicly (your nginx/",
|
|
549
|
+
" Cloudflare, your domain, TLS) is an explicit HUMAN deploy step — it is NEVER auto-deployed. A verified",
|
|
550
|
+
" seal proves set-membership / that a signer vouched — NOT a trusted timestamp (\"sealed since date T\"",
|
|
551
|
+
" still needs the human-owned signing/timestamp trust-root, P-3). It LISTENS until killed (Ctrl-C); a bad",
|
|
552
|
+
" flag exits 2 BEFORE binding, a bind failure (EADDRINUSE / EACCES / bad --host) exits 1.",
|
|
553
|
+
"",
|
|
554
|
+
"fulfill-webhook options (a tiny loopback-only Node-core HTTP FULFILLMENT webhook; ZERO new dependency):",
|
|
555
|
+
" --secret-env <VAR> REQUIRED: env var holding the provider's webhook SIGNING SECRET (the HMAC key",
|
|
556
|
+
" the provider signs each delivery with). Read from process.env[VAR]; NEVER written.",
|
|
557
|
+
" --binding <file> REQUIRED: a validated price->plan binding JSON (kind vh-evidence-price-binding):",
|
|
558
|
+
" maps each (provider, priceId) onto ONE of YOUR evidence planIds. Unmapped price = 422.",
|
|
559
|
+
" --key-env <VAR> | --key-file <p> REQUIRED (EXACTLY ONE): the VENDOR signing key (read-used-held-in-memory,",
|
|
560
|
+
" NEVER written to disk or logs). It signs each delivered license; the loop sets NO price.",
|
|
561
|
+
" --out <dir> REQUIRED: an EXISTING directory the delivered *.vhlicense.json files are written to.",
|
|
562
|
+
" Never cwd; delivery is idempotent (keyed on the event) so a retry writes no duplicate.",
|
|
563
|
+
" --catalog <file> OPTIONAL evidence plan catalog (default: the bundled DRAFT). The binding's planIds",
|
|
564
|
+
" are checked against it; entitlements are copied from the resolved plan VERBATIM.",
|
|
565
|
+
" --port <n> TCP port to bind (0..65535; 0 = an OS-chosen ephemeral port). Default 4190.",
|
|
566
|
+
" --host <h> interface to bind. Default 127.0.0.1 (LOOPBACK) — a non-loopback interface is NOT",
|
|
567
|
+
" served by default; pass e.g. --host 0.0.0.0 to bind all interfaces (your explicit choice).",
|
|
568
|
+
" --max-body <bytes> reject a request body larger than this many bytes with HTTP 413 (default 256 KiB).",
|
|
569
|
+
" --tolerance <sec> signature replay window in seconds (default 300, Stripe's own). A t outside it = 401.",
|
|
570
|
+
" Route: POST /fulfill -> reads the RAW body, verifyProviderSignature (fail-closed: unsigned/forged/stale/",
|
|
571
|
+
" malformed = 401/400 with the localized reason, NOTHING delivered) -> parseEvidenceEvent ->",
|
|
572
|
+
" normalizeEvidenceEvent (via --binding) -> fulfillEvidenceOrder -> sign -> deliver. On success 200",
|
|
573
|
+
" { delivered, licenseId }; a re-delivered event returns the SAME licenseId (idempotent, no duplicate).",
|
|
574
|
+
" GET /healthz -> 200 { ok:true }.",
|
|
575
|
+
" POSTURE (fail-closed + loopback + access-credential + human-deploy): it AUTHENTICATES then DELIVERS a signed",
|
|
576
|
+
" license; it holds the vendor key IN MEMORY and writes NO key/secret to disk or logs, and makes NO outbound",
|
|
577
|
+
" request. It binds loopback by default; exposing it (your provider's real secret, your real key, your domain +",
|
|
578
|
+
" TLS) is an explicit HUMAN deploy step — NEVER auto-deployed. A delivered license is an ACCESS credential for",
|
|
579
|
+
" delivered software value — NOT a token/coin/NFT, not tradeable, and NOT a trusted timestamp (P-3). It LISTENS",
|
|
580
|
+
" until killed (Ctrl-C); a bad flag/config exits 2 BEFORE binding, a bind failure exits 1.",
|
|
581
|
+
"",
|
|
582
|
+
].join("\n");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Parse `hash` argv into { path, git, ref }. Takes exactly one positional <path>. `--git` scopes the
|
|
587
|
+
* hash to git-tracked files; `--ref <ref>` selects which commit's tracked set (only with `--git`).
|
|
588
|
+
* Throws on unknown/incomplete flags, a duplicate path, or `--ref` without `--git` (parser parity
|
|
589
|
+
* with the other commands) so a typo never silently changes what gets hashed.
|
|
590
|
+
*/
|
|
591
|
+
function parseHashArgs(argv) {
|
|
592
|
+
const opts = { path: undefined, git: false, ref: undefined };
|
|
593
|
+
for (let i = 0; i < argv.length; i++) {
|
|
594
|
+
const a = argv[i];
|
|
595
|
+
switch (a) {
|
|
596
|
+
case "--git":
|
|
597
|
+
opts.git = true;
|
|
598
|
+
break;
|
|
599
|
+
case "--ref":
|
|
600
|
+
opts.ref = argv[++i];
|
|
601
|
+
if (opts.ref === undefined) throw new Error("--ref requires a value");
|
|
602
|
+
break;
|
|
603
|
+
default:
|
|
604
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
605
|
+
if (opts.path !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
606
|
+
opts.path = a;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// --ref is meaningful only when scoping to git-tracked files; flag it rather than silently ignore.
|
|
610
|
+
if (opts.ref !== undefined && !opts.git) {
|
|
611
|
+
throw new Error("--ref requires --git (it selects which commit's tracked files to hash)");
|
|
612
|
+
}
|
|
613
|
+
return opts;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function cmdHash(argv) {
|
|
617
|
+
let opts;
|
|
618
|
+
try {
|
|
619
|
+
opts = parseHashArgs(argv);
|
|
620
|
+
} catch (e) {
|
|
621
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
622
|
+
return 2;
|
|
623
|
+
}
|
|
624
|
+
if (!opts.path) {
|
|
625
|
+
process.stderr.write("error: `vh hash` requires a <path>\n\n" + usage());
|
|
626
|
+
return 2;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// --git: hash EXACTLY the files git tracks (no filesystem walk, no untracked junk). Errors clearly
|
|
630
|
+
// on a non-git dir / unknown ref / zero tracked files — it never silently falls back to the walk.
|
|
631
|
+
if (opts.git) {
|
|
632
|
+
let result;
|
|
633
|
+
try {
|
|
634
|
+
result = hashGit(opts.path, { ref: opts.ref });
|
|
635
|
+
} catch (e) {
|
|
636
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
637
|
+
return 1;
|
|
638
|
+
}
|
|
639
|
+
// Print the root, then the resolved commit oid as a `# commit <oid>` comment so the snapshot is
|
|
640
|
+
// SELF-DESCRIBING: an operator running `--git --ref some-branch` can see WHICH commit produced
|
|
641
|
+
// this root (the whole point of a commit-pinned, reproducible snapshot). The comment leads with
|
|
642
|
+
// `#` so a downstream consumer of the line-oriented `<leaf> <path>` body can skip it trivially,
|
|
643
|
+
// and the root stays on line 1 — the human shape is otherwise byte-identical to the dir output.
|
|
644
|
+
process.stdout.write(result.root + "\n");
|
|
645
|
+
process.stdout.write(`# commit ${result.commit}\n`);
|
|
646
|
+
for (const { path: p, leaf } of result.leaves) {
|
|
647
|
+
process.stdout.write(`${leaf} ${p}\n`);
|
|
648
|
+
}
|
|
649
|
+
return 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let result;
|
|
653
|
+
try {
|
|
654
|
+
result = hashPath(opts.path);
|
|
655
|
+
} catch (e) {
|
|
656
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (result.kind === "file") {
|
|
661
|
+
process.stdout.write(result.root + "\n");
|
|
662
|
+
} else {
|
|
663
|
+
// Directory: print the root, then each file's path-bound leaf (what verifyLeaf consumes) for
|
|
664
|
+
// transparency. The root commits to file NAMES and content, so the leaf binds the path.
|
|
665
|
+
process.stdout.write(result.root + "\n");
|
|
666
|
+
for (const { path: p, leaf } of result.leaves) {
|
|
667
|
+
process.stdout.write(`${leaf} ${p}\n`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Parse `anchor` argv into { path, uri, contract, rpc, dryRun, iUnderstandMainnet }.
|
|
675
|
+
* Throws on unknown/incomplete flags so a typo never silently turns into a real submission.
|
|
676
|
+
*/
|
|
677
|
+
function parseAnchorArgs(argv) {
|
|
678
|
+
const opts = {
|
|
679
|
+
path: undefined,
|
|
680
|
+
uri: undefined,
|
|
681
|
+
parent: undefined,
|
|
682
|
+
receipt: undefined,
|
|
683
|
+
contract: undefined,
|
|
684
|
+
rpc: undefined,
|
|
685
|
+
git: false,
|
|
686
|
+
ref: undefined,
|
|
687
|
+
dryRun: false,
|
|
688
|
+
iUnderstandMainnet: false,
|
|
689
|
+
};
|
|
690
|
+
for (let i = 0; i < argv.length; i++) {
|
|
691
|
+
const a = argv[i];
|
|
692
|
+
switch (a) {
|
|
693
|
+
case "--dry-run":
|
|
694
|
+
opts.dryRun = true;
|
|
695
|
+
break;
|
|
696
|
+
case "--i-understand-mainnet":
|
|
697
|
+
opts.iUnderstandMainnet = true;
|
|
698
|
+
break;
|
|
699
|
+
case "--git":
|
|
700
|
+
opts.git = true;
|
|
701
|
+
break;
|
|
702
|
+
case "--ref":
|
|
703
|
+
opts.ref = argv[++i];
|
|
704
|
+
if (opts.ref === undefined) throw new Error("--ref requires a value");
|
|
705
|
+
break;
|
|
706
|
+
case "--uri":
|
|
707
|
+
opts.uri = argv[++i];
|
|
708
|
+
if (opts.uri === undefined) throw new Error("--uri requires a value");
|
|
709
|
+
break;
|
|
710
|
+
case "--parent":
|
|
711
|
+
opts.parent = argv[++i];
|
|
712
|
+
if (opts.parent === undefined) throw new Error("--parent requires a value");
|
|
713
|
+
break;
|
|
714
|
+
case "--receipt":
|
|
715
|
+
opts.receipt = argv[++i];
|
|
716
|
+
if (opts.receipt === undefined) throw new Error("--receipt requires a value");
|
|
717
|
+
break;
|
|
718
|
+
case "--contract":
|
|
719
|
+
opts.contract = argv[++i];
|
|
720
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
721
|
+
break;
|
|
722
|
+
case "--rpc":
|
|
723
|
+
opts.rpc = argv[++i];
|
|
724
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
725
|
+
break;
|
|
726
|
+
default:
|
|
727
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
728
|
+
if (opts.path !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
729
|
+
opts.path = a;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// --ref is meaningful only when scoping to git-tracked files (parser parity with `vh hash`).
|
|
733
|
+
if (opts.ref !== undefined && !opts.git) {
|
|
734
|
+
throw new Error("--ref requires --git (it selects which commit's tracked files to anchor)");
|
|
735
|
+
}
|
|
736
|
+
return opts;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function cmdAnchor(argv) {
|
|
740
|
+
let opts;
|
|
741
|
+
try {
|
|
742
|
+
opts = parseAnchorArgs(argv);
|
|
743
|
+
} catch (e) {
|
|
744
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
745
|
+
return 2;
|
|
746
|
+
}
|
|
747
|
+
if (!opts.path) {
|
|
748
|
+
process.stderr.write("error: `vh anchor` requires a <path>\n\n" + usage());
|
|
749
|
+
return 2;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const ethers = require("ethers");
|
|
753
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
754
|
+
|
|
755
|
+
// For a dry run we never construct a signer/provider: it must work with no key and no network.
|
|
756
|
+
if (opts.dryRun) {
|
|
757
|
+
try {
|
|
758
|
+
await runAnchor({
|
|
759
|
+
path: opts.path,
|
|
760
|
+
uri: opts.uri,
|
|
761
|
+
parent: opts.parent,
|
|
762
|
+
git: opts.git,
|
|
763
|
+
ref: opts.ref,
|
|
764
|
+
contractAddress,
|
|
765
|
+
receiptPath: opts.receipt,
|
|
766
|
+
dryRun: true,
|
|
767
|
+
ethers,
|
|
768
|
+
});
|
|
769
|
+
} catch (e) {
|
|
770
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
771
|
+
return 1;
|
|
772
|
+
}
|
|
773
|
+
return 0;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Real submission: build provider + signer from env/flags.
|
|
777
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
778
|
+
if (!rpcUrl) {
|
|
779
|
+
process.stderr.write(
|
|
780
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL " +
|
|
781
|
+
"(or use --dry-run to preview without a network)\n"
|
|
782
|
+
);
|
|
783
|
+
return 1;
|
|
784
|
+
}
|
|
785
|
+
const pk = process.env.PRIVATE_KEY;
|
|
786
|
+
if (!pk) {
|
|
787
|
+
process.stderr.write(
|
|
788
|
+
"error: no PRIVATE_KEY in the environment; cannot sign. Use --dry-run to preview.\n"
|
|
789
|
+
);
|
|
790
|
+
return 1;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
795
|
+
const signer = new ethers.Wallet(pk, provider);
|
|
796
|
+
await runAnchor({
|
|
797
|
+
path: opts.path,
|
|
798
|
+
uri: opts.uri,
|
|
799
|
+
parent: opts.parent,
|
|
800
|
+
git: opts.git,
|
|
801
|
+
ref: opts.ref,
|
|
802
|
+
contractAddress,
|
|
803
|
+
receiptPath: opts.receipt,
|
|
804
|
+
iUnderstandMainnet: opts.iUnderstandMainnet,
|
|
805
|
+
provider,
|
|
806
|
+
signer,
|
|
807
|
+
ethers,
|
|
808
|
+
});
|
|
809
|
+
} catch (e) {
|
|
810
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
811
|
+
return 1;
|
|
812
|
+
}
|
|
813
|
+
return 0;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Parse `claim`/`commit` argv into { path, uri, salt, receipt, contract, rpc, dryRun,
|
|
818
|
+
* iUnderstandMainnet }. Throws on unknown/incomplete flags so a typo never silently turns into a
|
|
819
|
+
* real submission. Both `vh claim` and `vh commit` take the same flags (commit ignores --dry-run).
|
|
820
|
+
*/
|
|
821
|
+
function parseClaimArgs(argv) {
|
|
822
|
+
const opts = {
|
|
823
|
+
path: undefined,
|
|
824
|
+
uri: undefined,
|
|
825
|
+
parent: undefined,
|
|
826
|
+
salt: undefined,
|
|
827
|
+
receipt: undefined,
|
|
828
|
+
receiptDir: undefined,
|
|
829
|
+
contract: undefined,
|
|
830
|
+
rpc: undefined,
|
|
831
|
+
git: false,
|
|
832
|
+
ref: undefined,
|
|
833
|
+
dryRun: false,
|
|
834
|
+
iUnderstandMainnet: false,
|
|
835
|
+
};
|
|
836
|
+
for (let i = 0; i < argv.length; i++) {
|
|
837
|
+
const a = argv[i];
|
|
838
|
+
switch (a) {
|
|
839
|
+
case "--dry-run":
|
|
840
|
+
opts.dryRun = true;
|
|
841
|
+
break;
|
|
842
|
+
case "--i-understand-mainnet":
|
|
843
|
+
opts.iUnderstandMainnet = true;
|
|
844
|
+
break;
|
|
845
|
+
case "--git":
|
|
846
|
+
opts.git = true;
|
|
847
|
+
break;
|
|
848
|
+
case "--ref":
|
|
849
|
+
opts.ref = argv[++i];
|
|
850
|
+
if (opts.ref === undefined) throw new Error("--ref requires a value");
|
|
851
|
+
break;
|
|
852
|
+
case "--uri":
|
|
853
|
+
opts.uri = argv[++i];
|
|
854
|
+
if (opts.uri === undefined) throw new Error("--uri requires a value");
|
|
855
|
+
break;
|
|
856
|
+
case "--parent":
|
|
857
|
+
opts.parent = argv[++i];
|
|
858
|
+
if (opts.parent === undefined) throw new Error("--parent requires a value");
|
|
859
|
+
break;
|
|
860
|
+
case "--salt":
|
|
861
|
+
opts.salt = argv[++i];
|
|
862
|
+
if (opts.salt === undefined) throw new Error("--salt requires a value");
|
|
863
|
+
break;
|
|
864
|
+
case "--receipt":
|
|
865
|
+
opts.receipt = argv[++i];
|
|
866
|
+
if (opts.receipt === undefined) throw new Error("--receipt requires a value");
|
|
867
|
+
break;
|
|
868
|
+
case "--receipt-dir":
|
|
869
|
+
opts.receiptDir = argv[++i];
|
|
870
|
+
if (opts.receiptDir === undefined) throw new Error("--receipt-dir requires a value");
|
|
871
|
+
break;
|
|
872
|
+
case "--contract":
|
|
873
|
+
opts.contract = argv[++i];
|
|
874
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
875
|
+
break;
|
|
876
|
+
case "--rpc":
|
|
877
|
+
opts.rpc = argv[++i];
|
|
878
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
879
|
+
break;
|
|
880
|
+
default:
|
|
881
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
882
|
+
if (opts.path !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
883
|
+
opts.path = a;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// --ref is meaningful only when scoping to git-tracked files (parser parity with `vh hash`).
|
|
887
|
+
if (opts.ref !== undefined && !opts.git) {
|
|
888
|
+
throw new Error("--ref requires --git (it selects which commit's tracked files to claim)");
|
|
889
|
+
}
|
|
890
|
+
// --receipt picks the exact file; --receipt-dir picks the folder. Asking for both is ambiguous, so
|
|
891
|
+
// hard-error rather than silently honor one (a fat-fingered combination must not pick a surprise path).
|
|
892
|
+
if (opts.receipt !== undefined && opts.receiptDir !== undefined) {
|
|
893
|
+
throw new Error("--receipt and --receipt-dir are mutually exclusive; pass at most one");
|
|
894
|
+
}
|
|
895
|
+
return opts;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Parse `reveal` argv into { receipt, rpc, iUnderstandMainnet }. `--receipt <path>` is required and
|
|
900
|
+
* carries everything reveal needs; there is no <path> positional. Throws on unknown/incomplete flags.
|
|
901
|
+
*/
|
|
902
|
+
function parseRevealArgs(argv) {
|
|
903
|
+
const opts = { receipt: undefined, rpc: undefined, iUnderstandMainnet: false };
|
|
904
|
+
for (let i = 0; i < argv.length; i++) {
|
|
905
|
+
const a = argv[i];
|
|
906
|
+
switch (a) {
|
|
907
|
+
case "--i-understand-mainnet":
|
|
908
|
+
opts.iUnderstandMainnet = true;
|
|
909
|
+
break;
|
|
910
|
+
case "--receipt":
|
|
911
|
+
opts.receipt = argv[++i];
|
|
912
|
+
if (opts.receipt === undefined) throw new Error("--receipt requires a value");
|
|
913
|
+
break;
|
|
914
|
+
case "--rpc":
|
|
915
|
+
opts.rpc = argv[++i];
|
|
916
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
917
|
+
break;
|
|
918
|
+
default:
|
|
919
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
920
|
+
throw new Error(`unexpected extra argument: ${a}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return opts;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function cmdClaim(argv) {
|
|
927
|
+
let opts;
|
|
928
|
+
try {
|
|
929
|
+
opts = parseClaimArgs(argv);
|
|
930
|
+
} catch (e) {
|
|
931
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
932
|
+
return 2;
|
|
933
|
+
}
|
|
934
|
+
if (!opts.path) {
|
|
935
|
+
process.stderr.write("error: `vh claim` requires a <path>\n\n" + usage());
|
|
936
|
+
return 2;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const ethers = require("ethers");
|
|
940
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
941
|
+
|
|
942
|
+
// Dry run: build the commit-reveal plan with no key and no network. We still need a committer
|
|
943
|
+
// address to compute the (sender-bound) commitment; allow VH_COMMITTER for previewing.
|
|
944
|
+
if (opts.dryRun) {
|
|
945
|
+
try {
|
|
946
|
+
await runClaim({
|
|
947
|
+
path: opts.path,
|
|
948
|
+
uri: opts.uri,
|
|
949
|
+
parent: opts.parent,
|
|
950
|
+
salt: opts.salt,
|
|
951
|
+
git: opts.git,
|
|
952
|
+
ref: opts.ref,
|
|
953
|
+
committer: process.env.VH_COMMITTER,
|
|
954
|
+
contractAddress,
|
|
955
|
+
dryRun: true,
|
|
956
|
+
ethers,
|
|
957
|
+
});
|
|
958
|
+
} catch (e) {
|
|
959
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
960
|
+
return 1;
|
|
961
|
+
}
|
|
962
|
+
return 0;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Real submission: build provider + signer from env/flags.
|
|
966
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
967
|
+
if (!rpcUrl) {
|
|
968
|
+
process.stderr.write(
|
|
969
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL " +
|
|
970
|
+
"(or use --dry-run to preview without a network)\n"
|
|
971
|
+
);
|
|
972
|
+
return 1;
|
|
973
|
+
}
|
|
974
|
+
const pk = process.env.PRIVATE_KEY;
|
|
975
|
+
if (!pk) {
|
|
976
|
+
process.stderr.write(
|
|
977
|
+
"error: no PRIVATE_KEY in the environment; cannot sign. Use --dry-run to preview.\n"
|
|
978
|
+
);
|
|
979
|
+
return 1;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
try {
|
|
983
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
984
|
+
const signer = new ethers.Wallet(pk, provider);
|
|
985
|
+
await runClaim({
|
|
986
|
+
path: opts.path,
|
|
987
|
+
uri: opts.uri,
|
|
988
|
+
parent: opts.parent,
|
|
989
|
+
salt: opts.salt,
|
|
990
|
+
git: opts.git,
|
|
991
|
+
ref: opts.ref,
|
|
992
|
+
receiptPath: opts.receipt,
|
|
993
|
+
receiptDir: opts.receiptDir,
|
|
994
|
+
contractAddress,
|
|
995
|
+
iUnderstandMainnet: opts.iUnderstandMainnet,
|
|
996
|
+
provider,
|
|
997
|
+
signer,
|
|
998
|
+
ethers,
|
|
999
|
+
});
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1002
|
+
return 1;
|
|
1003
|
+
}
|
|
1004
|
+
return 0;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function cmdCommit(argv) {
|
|
1008
|
+
let opts;
|
|
1009
|
+
try {
|
|
1010
|
+
opts = parseClaimArgs(argv);
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1013
|
+
return 2;
|
|
1014
|
+
}
|
|
1015
|
+
if (!opts.path) {
|
|
1016
|
+
process.stderr.write("error: `vh commit` requires a <path>\n\n" + usage());
|
|
1017
|
+
return 2;
|
|
1018
|
+
}
|
|
1019
|
+
// `commit` has no dry-run: it intentionally sends a real tx and writes a receipt. A typo'd
|
|
1020
|
+
// --dry-run should not silently no-op into nothing useful.
|
|
1021
|
+
if (opts.dryRun) {
|
|
1022
|
+
process.stderr.write(
|
|
1023
|
+
"error: `vh commit` has no --dry-run; use `vh claim --dry-run` to preview the plan\n"
|
|
1024
|
+
);
|
|
1025
|
+
return 2;
|
|
1026
|
+
}
|
|
1027
|
+
// The lineage edge (B-10.1) belongs on the REVEAL leg (revealWithParent), and the resumable receipt
|
|
1028
|
+
// schema (v4) now persists `parent` so a resumed `vh reveal` can record it. We thread `opts.parent`
|
|
1029
|
+
// into runCommit, which validates it up front via the SAME normalizeParent as `vh anchor --parent`:
|
|
1030
|
+
// a malformed/self-referential value hard-errors BEFORE any network call (a typo never silently
|
|
1031
|
+
// drops the edge), surfacing through the catch below with exit 1.
|
|
1032
|
+
|
|
1033
|
+
const ethers = require("ethers");
|
|
1034
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
1035
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1036
|
+
if (!rpcUrl) {
|
|
1037
|
+
process.stderr.write(
|
|
1038
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL\n"
|
|
1039
|
+
);
|
|
1040
|
+
return 1;
|
|
1041
|
+
}
|
|
1042
|
+
const pk = process.env.PRIVATE_KEY;
|
|
1043
|
+
if (!pk) {
|
|
1044
|
+
process.stderr.write(
|
|
1045
|
+
"error: no PRIVATE_KEY in the environment; cannot sign the commit.\n"
|
|
1046
|
+
);
|
|
1047
|
+
return 1;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
try {
|
|
1051
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1052
|
+
const signer = new ethers.Wallet(pk, provider);
|
|
1053
|
+
await runCommit({
|
|
1054
|
+
path: opts.path,
|
|
1055
|
+
uri: opts.uri,
|
|
1056
|
+
salt: opts.salt,
|
|
1057
|
+
parent: opts.parent, // B-10.1: persisted into the v4 receipt so `vh reveal` records the edge
|
|
1058
|
+
git: opts.git,
|
|
1059
|
+
ref: opts.ref,
|
|
1060
|
+
receiptPath: opts.receipt,
|
|
1061
|
+
receiptDir: opts.receiptDir,
|
|
1062
|
+
contractAddress,
|
|
1063
|
+
iUnderstandMainnet: opts.iUnderstandMainnet,
|
|
1064
|
+
provider,
|
|
1065
|
+
signer,
|
|
1066
|
+
ethers,
|
|
1067
|
+
});
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1070
|
+
return 1;
|
|
1071
|
+
}
|
|
1072
|
+
return 0;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async function cmdReveal(argv) {
|
|
1076
|
+
let opts;
|
|
1077
|
+
try {
|
|
1078
|
+
opts = parseRevealArgs(argv);
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1081
|
+
return 2;
|
|
1082
|
+
}
|
|
1083
|
+
if (!opts.receipt) {
|
|
1084
|
+
process.stderr.write("error: `vh reveal` requires --receipt <path>\n\n" + usage());
|
|
1085
|
+
return 2;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const ethers = require("ethers");
|
|
1089
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1090
|
+
if (!rpcUrl) {
|
|
1091
|
+
process.stderr.write(
|
|
1092
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL\n"
|
|
1093
|
+
);
|
|
1094
|
+
return 1;
|
|
1095
|
+
}
|
|
1096
|
+
const pk = process.env.PRIVATE_KEY;
|
|
1097
|
+
if (!pk) {
|
|
1098
|
+
process.stderr.write(
|
|
1099
|
+
"error: no PRIVATE_KEY in the environment; cannot sign the reveal.\n"
|
|
1100
|
+
);
|
|
1101
|
+
return 1;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1106
|
+
const signer = new ethers.Wallet(pk, provider);
|
|
1107
|
+
await runReveal({
|
|
1108
|
+
receiptPath: opts.receipt,
|
|
1109
|
+
iUnderstandMainnet: opts.iUnderstandMainnet,
|
|
1110
|
+
provider,
|
|
1111
|
+
signer,
|
|
1112
|
+
ethers,
|
|
1113
|
+
});
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1116
|
+
return 1;
|
|
1117
|
+
}
|
|
1118
|
+
return 0;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Parse `verify` argv into { path, contract, rpc }.
|
|
1123
|
+
* Throws on unknown/incomplete flags so a typo is never silently ignored.
|
|
1124
|
+
*/
|
|
1125
|
+
function parseVerifyArgs(argv) {
|
|
1126
|
+
const opts = {
|
|
1127
|
+
path: undefined,
|
|
1128
|
+
contract: undefined,
|
|
1129
|
+
rpc: undefined,
|
|
1130
|
+
receipt: undefined,
|
|
1131
|
+
git: false,
|
|
1132
|
+
ref: undefined,
|
|
1133
|
+
skipIdentityCheck: false,
|
|
1134
|
+
};
|
|
1135
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1136
|
+
const a = argv[i];
|
|
1137
|
+
switch (a) {
|
|
1138
|
+
case "--skip-identity-check":
|
|
1139
|
+
opts.skipIdentityCheck = true;
|
|
1140
|
+
break;
|
|
1141
|
+
case "--git":
|
|
1142
|
+
opts.git = true;
|
|
1143
|
+
break;
|
|
1144
|
+
case "--ref":
|
|
1145
|
+
opts.ref = argv[++i];
|
|
1146
|
+
if (opts.ref === undefined) throw new Error("--ref requires a value");
|
|
1147
|
+
break;
|
|
1148
|
+
case "--receipt":
|
|
1149
|
+
opts.receipt = argv[++i];
|
|
1150
|
+
if (opts.receipt === undefined) throw new Error("--receipt requires a value");
|
|
1151
|
+
break;
|
|
1152
|
+
case "--contract":
|
|
1153
|
+
opts.contract = argv[++i];
|
|
1154
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
1155
|
+
break;
|
|
1156
|
+
case "--rpc":
|
|
1157
|
+
opts.rpc = argv[++i];
|
|
1158
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
1159
|
+
break;
|
|
1160
|
+
default:
|
|
1161
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1162
|
+
if (opts.path !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1163
|
+
opts.path = a;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
// --ref is meaningful only when scoping to git-tracked files (parser parity with `vh hash`).
|
|
1167
|
+
if (opts.ref !== undefined && !opts.git) {
|
|
1168
|
+
throw new Error("--ref requires --git (it selects which commit's tracked files to verify)");
|
|
1169
|
+
}
|
|
1170
|
+
return opts;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function cmdVerify(argv) {
|
|
1174
|
+
let opts;
|
|
1175
|
+
try {
|
|
1176
|
+
opts = parseVerifyArgs(argv);
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1179
|
+
return 2;
|
|
1180
|
+
}
|
|
1181
|
+
if (!opts.path) {
|
|
1182
|
+
process.stderr.write("error: `vh verify` requires a <path>\n\n" + usage());
|
|
1183
|
+
return 2;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const ethers = require("ethers");
|
|
1187
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
1188
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1189
|
+
if (!rpcUrl) {
|
|
1190
|
+
process.stderr.write(
|
|
1191
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL\n"
|
|
1192
|
+
);
|
|
1193
|
+
return 1;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
let result;
|
|
1197
|
+
try {
|
|
1198
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1199
|
+
result = await runVerify({
|
|
1200
|
+
path: opts.path,
|
|
1201
|
+
git: opts.git,
|
|
1202
|
+
ref: opts.ref,
|
|
1203
|
+
contractAddress,
|
|
1204
|
+
receiptPath: opts.receipt,
|
|
1205
|
+
skipIdentityCheck: opts.skipIdentityCheck,
|
|
1206
|
+
provider,
|
|
1207
|
+
ethers,
|
|
1208
|
+
});
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1211
|
+
return 1;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Exit non-zero on a tamper/MISMATCH so scripts and CI can branch on it.
|
|
1215
|
+
return result.status === "MATCH" ? 0 : 3;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Parse `prove` argv into { file, root, contract, rpc, anchor, iUnderstandMainnet, dryRun }.
|
|
1220
|
+
* Throws on unknown/incomplete flags so a typo is never silently ignored.
|
|
1221
|
+
*/
|
|
1222
|
+
function parseProveArgs(argv) {
|
|
1223
|
+
const opts = {
|
|
1224
|
+
file: undefined,
|
|
1225
|
+
root: undefined,
|
|
1226
|
+
out: undefined,
|
|
1227
|
+
contract: undefined,
|
|
1228
|
+
rpc: undefined,
|
|
1229
|
+
anchor: false,
|
|
1230
|
+
iUnderstandMainnet: false,
|
|
1231
|
+
dryRun: false,
|
|
1232
|
+
};
|
|
1233
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1234
|
+
const a = argv[i];
|
|
1235
|
+
switch (a) {
|
|
1236
|
+
case "--dry-run":
|
|
1237
|
+
opts.dryRun = true;
|
|
1238
|
+
break;
|
|
1239
|
+
case "--anchor":
|
|
1240
|
+
opts.anchor = true;
|
|
1241
|
+
break;
|
|
1242
|
+
case "--i-understand-mainnet":
|
|
1243
|
+
opts.iUnderstandMainnet = true;
|
|
1244
|
+
break;
|
|
1245
|
+
case "--root":
|
|
1246
|
+
opts.root = argv[++i];
|
|
1247
|
+
if (opts.root === undefined) throw new Error("--root requires a value");
|
|
1248
|
+
break;
|
|
1249
|
+
case "--out":
|
|
1250
|
+
opts.out = argv[++i];
|
|
1251
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
1252
|
+
break;
|
|
1253
|
+
case "--contract":
|
|
1254
|
+
opts.contract = argv[++i];
|
|
1255
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
1256
|
+
break;
|
|
1257
|
+
case "--rpc":
|
|
1258
|
+
opts.rpc = argv[++i];
|
|
1259
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
1260
|
+
break;
|
|
1261
|
+
default:
|
|
1262
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1263
|
+
if (opts.file !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1264
|
+
opts.file = a;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return opts;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async function cmdProve(argv) {
|
|
1271
|
+
let opts;
|
|
1272
|
+
try {
|
|
1273
|
+
opts = parseProveArgs(argv);
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1276
|
+
return 2;
|
|
1277
|
+
}
|
|
1278
|
+
if (!opts.file) {
|
|
1279
|
+
process.stderr.write("error: `vh prove` requires a <file>\n\n" + usage());
|
|
1280
|
+
return 2;
|
|
1281
|
+
}
|
|
1282
|
+
if (!opts.root) {
|
|
1283
|
+
process.stderr.write("error: `vh prove` requires --root <dir> (the repo root)\n\n" + usage());
|
|
1284
|
+
return 2;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const ethers = require("ethers");
|
|
1288
|
+
|
|
1289
|
+
// Dry run: only builds & prints the proof (and writes the --out artifact if asked). No key, no
|
|
1290
|
+
// network — must work entirely offline. This is the no-key build path for `--out`.
|
|
1291
|
+
if (opts.dryRun) {
|
|
1292
|
+
try {
|
|
1293
|
+
await runProve({ file: opts.file, rootDir: opts.root, out: opts.out, dryRun: true, ethers });
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1296
|
+
return 1;
|
|
1297
|
+
}
|
|
1298
|
+
return 0;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
1302
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1303
|
+
if (!rpcUrl) {
|
|
1304
|
+
process.stderr.write(
|
|
1305
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL " +
|
|
1306
|
+
"(or use --dry-run to build the proof without a network)\n"
|
|
1307
|
+
);
|
|
1308
|
+
return 1;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
let result;
|
|
1312
|
+
try {
|
|
1313
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1314
|
+
// Only the --anchor path needs to sign; verifying a proof is read-only.
|
|
1315
|
+
let signer;
|
|
1316
|
+
if (opts.anchor) {
|
|
1317
|
+
const pk = process.env.PRIVATE_KEY;
|
|
1318
|
+
if (!pk) {
|
|
1319
|
+
process.stderr.write(
|
|
1320
|
+
"error: --anchor needs a PRIVATE_KEY in the environment to submit the root\n"
|
|
1321
|
+
);
|
|
1322
|
+
return 1;
|
|
1323
|
+
}
|
|
1324
|
+
signer = new ethers.Wallet(pk, provider);
|
|
1325
|
+
}
|
|
1326
|
+
result = await runProve({
|
|
1327
|
+
file: opts.file,
|
|
1328
|
+
rootDir: opts.root,
|
|
1329
|
+
out: opts.out,
|
|
1330
|
+
contractAddress,
|
|
1331
|
+
provider,
|
|
1332
|
+
signer,
|
|
1333
|
+
anchorFirst: opts.anchor,
|
|
1334
|
+
iUnderstandMainnet: opts.iUnderstandMainnet,
|
|
1335
|
+
ethers,
|
|
1336
|
+
});
|
|
1337
|
+
} catch (e) {
|
|
1338
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1339
|
+
return 1;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Exit non-zero when the on-chain verifyLeaf rejects the proof (tampered / not in the snapshot),
|
|
1343
|
+
// so scripts and CI can branch on it.
|
|
1344
|
+
return result.accepted ? 0 : 3;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Parse `verify-proof` argv into { artifact, contract, rpc, json }. Takes exactly one positional
|
|
1349
|
+
* <p> (the artifact path). Throws on unknown/incomplete flags or a duplicate/missing positional so a
|
|
1350
|
+
* typo never silently verifies the wrong file (parser parity with the other commands).
|
|
1351
|
+
*/
|
|
1352
|
+
function parseVerifyProofArgs(argv) {
|
|
1353
|
+
const opts = {
|
|
1354
|
+
artifact: undefined,
|
|
1355
|
+
contract: undefined,
|
|
1356
|
+
rpc: undefined,
|
|
1357
|
+
json: false,
|
|
1358
|
+
skipIdentityCheck: false,
|
|
1359
|
+
};
|
|
1360
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1361
|
+
const a = argv[i];
|
|
1362
|
+
switch (a) {
|
|
1363
|
+
case "--json":
|
|
1364
|
+
opts.json = true;
|
|
1365
|
+
break;
|
|
1366
|
+
case "--skip-identity-check":
|
|
1367
|
+
opts.skipIdentityCheck = true;
|
|
1368
|
+
break;
|
|
1369
|
+
case "--contract":
|
|
1370
|
+
opts.contract = argv[++i];
|
|
1371
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
1372
|
+
break;
|
|
1373
|
+
case "--rpc":
|
|
1374
|
+
opts.rpc = argv[++i];
|
|
1375
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
1376
|
+
break;
|
|
1377
|
+
default:
|
|
1378
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1379
|
+
if (opts.artifact !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1380
|
+
opts.artifact = a;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return opts;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
async function cmdVerifyProof(argv) {
|
|
1387
|
+
let opts;
|
|
1388
|
+
try {
|
|
1389
|
+
opts = parseVerifyProofArgs(argv);
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1392
|
+
return 2;
|
|
1393
|
+
}
|
|
1394
|
+
if (!opts.artifact) {
|
|
1395
|
+
process.stderr.write("error: `vh verify-proof` requires a <p> (proof artifact path)\n\n" + usage());
|
|
1396
|
+
return 2;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const ethers = require("ethers");
|
|
1400
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
1401
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1402
|
+
if (!rpcUrl) {
|
|
1403
|
+
process.stderr.write(
|
|
1404
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL " +
|
|
1405
|
+
"(verify-proof confirms the root is anchored on-chain)\n"
|
|
1406
|
+
);
|
|
1407
|
+
return 1;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
let result;
|
|
1411
|
+
try {
|
|
1412
|
+
// Read-only: provider only — `vh verify-proof` NEVER constructs a signer or touches a key.
|
|
1413
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1414
|
+
result = await runVerifyProof({
|
|
1415
|
+
artifactPath: opts.artifact,
|
|
1416
|
+
contractAddress,
|
|
1417
|
+
provider,
|
|
1418
|
+
json: opts.json,
|
|
1419
|
+
skipIdentityCheck: opts.skipIdentityCheck,
|
|
1420
|
+
ethers,
|
|
1421
|
+
});
|
|
1422
|
+
} catch (e) {
|
|
1423
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1424
|
+
return 1;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Exit 0 ONLY on ACCEPTED. A NOT ANCHORED root is exit 4 (mirrors `vh show`'s NOT ANCHORED), a
|
|
1428
|
+
// REJECTED proof is exit 3 (mirrors `vh verify`/`vh prove`), so scripts/CI can branch on each.
|
|
1429
|
+
if (result.status === "ACCEPTED") return 0;
|
|
1430
|
+
if (result.status === "NOT_ANCHORED") return 4;
|
|
1431
|
+
return 3;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Parse `list` argv into { contract, rpc, contributor, authorBound, limit, offset, json }.
|
|
1436
|
+
* `list` takes NO positional argument (it enumerates the whole registry). Throws on unknown or
|
|
1437
|
+
* incomplete flags so a typo never silently returns a wrong/empty list (parser parity with the
|
|
1438
|
+
* other commands). `--limit`/`--offset` must be non-negative integers.
|
|
1439
|
+
*/
|
|
1440
|
+
function parseListArgs(argv) {
|
|
1441
|
+
const opts = {
|
|
1442
|
+
contract: undefined,
|
|
1443
|
+
rpc: undefined,
|
|
1444
|
+
contributor: undefined,
|
|
1445
|
+
authorBound: false,
|
|
1446
|
+
limit: undefined,
|
|
1447
|
+
offset: undefined,
|
|
1448
|
+
json: false,
|
|
1449
|
+
skipIdentityCheck: false,
|
|
1450
|
+
};
|
|
1451
|
+
// Parse a flag value as a non-negative integer, hard-erroring on anything else.
|
|
1452
|
+
const intArg = (flag, raw) => {
|
|
1453
|
+
if (raw === undefined) throw new Error(`${flag} requires a value`);
|
|
1454
|
+
if (!/^\d+$/.test(raw)) throw new Error(`${flag} requires a non-negative integer, got: ${raw}`);
|
|
1455
|
+
return Number(raw);
|
|
1456
|
+
};
|
|
1457
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1458
|
+
const a = argv[i];
|
|
1459
|
+
switch (a) {
|
|
1460
|
+
case "--author-bound":
|
|
1461
|
+
opts.authorBound = true;
|
|
1462
|
+
break;
|
|
1463
|
+
case "--json":
|
|
1464
|
+
opts.json = true;
|
|
1465
|
+
break;
|
|
1466
|
+
case "--skip-identity-check":
|
|
1467
|
+
opts.skipIdentityCheck = true;
|
|
1468
|
+
break;
|
|
1469
|
+
case "--contract":
|
|
1470
|
+
opts.contract = argv[++i];
|
|
1471
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
1472
|
+
break;
|
|
1473
|
+
case "--rpc":
|
|
1474
|
+
opts.rpc = argv[++i];
|
|
1475
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
1476
|
+
break;
|
|
1477
|
+
case "--contributor":
|
|
1478
|
+
opts.contributor = argv[++i];
|
|
1479
|
+
if (opts.contributor === undefined) throw new Error("--contributor requires a value");
|
|
1480
|
+
break;
|
|
1481
|
+
case "--limit":
|
|
1482
|
+
opts.limit = intArg("--limit", argv[++i]);
|
|
1483
|
+
break;
|
|
1484
|
+
case "--offset":
|
|
1485
|
+
opts.offset = intArg("--offset", argv[++i]);
|
|
1486
|
+
break;
|
|
1487
|
+
default:
|
|
1488
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1489
|
+
throw new Error(`unexpected argument: ${a} (vh list takes no positional path)`);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return opts;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
async function cmdList(argv) {
|
|
1496
|
+
let opts;
|
|
1497
|
+
try {
|
|
1498
|
+
opts = parseListArgs(argv);
|
|
1499
|
+
} catch (e) {
|
|
1500
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1501
|
+
return 2;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const ethers = require("ethers");
|
|
1505
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
1506
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1507
|
+
if (!rpcUrl) {
|
|
1508
|
+
process.stderr.write(
|
|
1509
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL\n"
|
|
1510
|
+
);
|
|
1511
|
+
return 1;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
try {
|
|
1515
|
+
// Read-only: provider only — `vh list` NEVER constructs a signer or touches a key.
|
|
1516
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1517
|
+
await runList({
|
|
1518
|
+
contractAddress,
|
|
1519
|
+
provider,
|
|
1520
|
+
filters: {
|
|
1521
|
+
contributor: opts.contributor,
|
|
1522
|
+
authorBound: opts.authorBound,
|
|
1523
|
+
limit: opts.limit,
|
|
1524
|
+
offset: opts.offset,
|
|
1525
|
+
},
|
|
1526
|
+
json: opts.json,
|
|
1527
|
+
skipIdentityCheck: opts.skipIdentityCheck,
|
|
1528
|
+
ethers,
|
|
1529
|
+
});
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1532
|
+
return 1;
|
|
1533
|
+
}
|
|
1534
|
+
return 0;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* Parse `show` argv into { hash, contract, rpc, json }. Takes exactly one positional <0xhash>.
|
|
1539
|
+
* Throws on unknown/incomplete flags or a duplicate/missing hash so a typo never silently looks up
|
|
1540
|
+
* the wrong thing. The hash VALUE is shape-validated later (in runShow) so the same usage-grade error
|
|
1541
|
+
* fires whether the hash came from the CLI or a programmatic caller.
|
|
1542
|
+
*/
|
|
1543
|
+
function parseShowArgs(argv) {
|
|
1544
|
+
const opts = {
|
|
1545
|
+
hash: undefined,
|
|
1546
|
+
contract: undefined,
|
|
1547
|
+
rpc: undefined,
|
|
1548
|
+
json: false,
|
|
1549
|
+
skipIdentityCheck: false,
|
|
1550
|
+
};
|
|
1551
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1552
|
+
const a = argv[i];
|
|
1553
|
+
switch (a) {
|
|
1554
|
+
case "--json":
|
|
1555
|
+
opts.json = true;
|
|
1556
|
+
break;
|
|
1557
|
+
case "--skip-identity-check":
|
|
1558
|
+
opts.skipIdentityCheck = true;
|
|
1559
|
+
break;
|
|
1560
|
+
case "--contract":
|
|
1561
|
+
opts.contract = argv[++i];
|
|
1562
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
1563
|
+
break;
|
|
1564
|
+
case "--rpc":
|
|
1565
|
+
opts.rpc = argv[++i];
|
|
1566
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
1567
|
+
break;
|
|
1568
|
+
default:
|
|
1569
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1570
|
+
if (opts.hash !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1571
|
+
opts.hash = a;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return opts;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
async function cmdShow(argv) {
|
|
1578
|
+
let opts;
|
|
1579
|
+
try {
|
|
1580
|
+
opts = parseShowArgs(argv);
|
|
1581
|
+
} catch (e) {
|
|
1582
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1583
|
+
return 2;
|
|
1584
|
+
}
|
|
1585
|
+
if (!opts.hash) {
|
|
1586
|
+
process.stderr.write("error: `vh show` requires a <0xhash>\n\n" + usage());
|
|
1587
|
+
return 2;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const ethers = require("ethers");
|
|
1591
|
+
|
|
1592
|
+
// Validate the hash shape BEFORE building a provider or reading any env/network — a malformed/short
|
|
1593
|
+
// hash must hard-error with usage and never hit the network. We re-use runShow's normalizer (via a
|
|
1594
|
+
// dry, provider-less throw) by checking the shape here directly so the error precedes the RPC check.
|
|
1595
|
+
const { normalizeContentHash } = require("./show");
|
|
1596
|
+
try {
|
|
1597
|
+
normalizeContentHash(opts.hash, ethers);
|
|
1598
|
+
} catch (e) {
|
|
1599
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1600
|
+
return 2;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
1604
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1605
|
+
if (!rpcUrl) {
|
|
1606
|
+
process.stderr.write(
|
|
1607
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL\n"
|
|
1608
|
+
);
|
|
1609
|
+
return 1;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
let result;
|
|
1613
|
+
try {
|
|
1614
|
+
// Read-only: provider only — `vh show` NEVER constructs a signer or touches a key.
|
|
1615
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1616
|
+
result = await runShow({
|
|
1617
|
+
contentHash: opts.hash,
|
|
1618
|
+
contractAddress,
|
|
1619
|
+
provider,
|
|
1620
|
+
json: opts.json,
|
|
1621
|
+
skipIdentityCheck: opts.skipIdentityCheck,
|
|
1622
|
+
ethers,
|
|
1623
|
+
});
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1626
|
+
return 1;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Exit non-zero when the hash has no record so scripts/CI can branch on "NOT ANCHORED".
|
|
1630
|
+
return result.status === "ANCHORED" ? 0 : 4;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Parse `lineage` argv into { hash, contract, rpc, json, maxDepth }. Takes exactly one positional
|
|
1635
|
+
* <0xhash>. Throws on unknown/incomplete flags or a duplicate/missing hash so a typo never silently
|
|
1636
|
+
* walks the wrong thing (parser parity with `vh show`). The hash VALUE is shape-validated in runLineage
|
|
1637
|
+
* so the same usage-grade error fires whether the hash came from the CLI or a programmatic caller.
|
|
1638
|
+
* `--max-depth` must be a positive integer.
|
|
1639
|
+
*/
|
|
1640
|
+
function parseLineageArgs(argv) {
|
|
1641
|
+
const opts = {
|
|
1642
|
+
hash: undefined,
|
|
1643
|
+
contract: undefined,
|
|
1644
|
+
rpc: undefined,
|
|
1645
|
+
json: false,
|
|
1646
|
+
maxDepth: undefined,
|
|
1647
|
+
skipIdentityCheck: false,
|
|
1648
|
+
};
|
|
1649
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1650
|
+
const a = argv[i];
|
|
1651
|
+
switch (a) {
|
|
1652
|
+
case "--json":
|
|
1653
|
+
opts.json = true;
|
|
1654
|
+
break;
|
|
1655
|
+
case "--skip-identity-check":
|
|
1656
|
+
opts.skipIdentityCheck = true;
|
|
1657
|
+
break;
|
|
1658
|
+
case "--contract":
|
|
1659
|
+
opts.contract = argv[++i];
|
|
1660
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
1661
|
+
break;
|
|
1662
|
+
case "--rpc":
|
|
1663
|
+
opts.rpc = argv[++i];
|
|
1664
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
1665
|
+
break;
|
|
1666
|
+
case "--max-depth": {
|
|
1667
|
+
const raw = argv[++i];
|
|
1668
|
+
if (raw === undefined) throw new Error("--max-depth requires a value");
|
|
1669
|
+
// A positive-integer cap; reject a zero/negative/non-integer here so a typo never silently
|
|
1670
|
+
// changes how far the walk goes. (runLineage re-validates via normalizeMaxDepth for the
|
|
1671
|
+
// programmatic path; this keeps the CLI usage error early and consistent.)
|
|
1672
|
+
if (!/^\d+$/.test(raw) || Number(raw) < 1) {
|
|
1673
|
+
throw new Error(`--max-depth requires a positive integer, got: ${raw}`);
|
|
1674
|
+
}
|
|
1675
|
+
opts.maxDepth = Number(raw);
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
default:
|
|
1679
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1680
|
+
if (opts.hash !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1681
|
+
opts.hash = a;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return opts;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
async function cmdLineage(argv) {
|
|
1688
|
+
let opts;
|
|
1689
|
+
try {
|
|
1690
|
+
opts = parseLineageArgs(argv);
|
|
1691
|
+
} catch (e) {
|
|
1692
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1693
|
+
return 2;
|
|
1694
|
+
}
|
|
1695
|
+
if (!opts.hash) {
|
|
1696
|
+
process.stderr.write("error: `vh lineage` requires a <0xhash>\n\n" + usage());
|
|
1697
|
+
return 2;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const ethers = require("ethers");
|
|
1701
|
+
|
|
1702
|
+
// Validate the hash shape BEFORE building a provider or reading any env/network — a malformed/short
|
|
1703
|
+
// hash must hard-error with usage (exit 2) and never hit the network (parser parity with `vh show`).
|
|
1704
|
+
const { normalizeContentHash } = require("./show");
|
|
1705
|
+
try {
|
|
1706
|
+
normalizeContentHash(opts.hash, ethers);
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1709
|
+
return 2;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
1713
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1714
|
+
if (!rpcUrl) {
|
|
1715
|
+
process.stderr.write(
|
|
1716
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL\n"
|
|
1717
|
+
);
|
|
1718
|
+
return 1;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
let result;
|
|
1722
|
+
try {
|
|
1723
|
+
// Read-only: provider only — `vh lineage` NEVER constructs a signer or touches a key.
|
|
1724
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1725
|
+
result = await runLineage({
|
|
1726
|
+
contentHash: opts.hash,
|
|
1727
|
+
contractAddress,
|
|
1728
|
+
provider,
|
|
1729
|
+
maxDepth: opts.maxDepth,
|
|
1730
|
+
json: opts.json,
|
|
1731
|
+
skipIdentityCheck: opts.skipIdentityCheck,
|
|
1732
|
+
ethers,
|
|
1733
|
+
});
|
|
1734
|
+
} catch (e) {
|
|
1735
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1736
|
+
return 1;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Exit non-zero when the START hash has no record so scripts/CI can branch on "NOT ANCHORED" — the
|
|
1740
|
+
// same exit-4 contract `vh show` uses for a NOT ANCHORED hash, so the two read commands agree.
|
|
1741
|
+
return result.status === "WALKED" ? 0 : 4;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Parse `reputation` argv into { addr, contract, rpc, json, skipIdentityCheck }. Takes exactly one
|
|
1746
|
+
* positional <addr>. Throws on unknown/incomplete flags or a duplicate/missing addr so a typo never
|
|
1747
|
+
* silently scores the wrong (or no) address (parser parity with `vh show`/`vh lineage`). The addr VALUE
|
|
1748
|
+
* is shape-validated in runReputation so the same usage-grade error fires whether the addr came from
|
|
1749
|
+
* the CLI or a programmatic caller.
|
|
1750
|
+
*/
|
|
1751
|
+
function parseReputationArgs(argv) {
|
|
1752
|
+
const opts = {
|
|
1753
|
+
addr: undefined,
|
|
1754
|
+
contract: undefined,
|
|
1755
|
+
rpc: undefined,
|
|
1756
|
+
json: false,
|
|
1757
|
+
skipIdentityCheck: false,
|
|
1758
|
+
};
|
|
1759
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1760
|
+
const a = argv[i];
|
|
1761
|
+
switch (a) {
|
|
1762
|
+
case "--json":
|
|
1763
|
+
opts.json = true;
|
|
1764
|
+
break;
|
|
1765
|
+
case "--skip-identity-check":
|
|
1766
|
+
opts.skipIdentityCheck = true;
|
|
1767
|
+
break;
|
|
1768
|
+
case "--contract":
|
|
1769
|
+
opts.contract = argv[++i];
|
|
1770
|
+
if (opts.contract === undefined) throw new Error("--contract requires a value");
|
|
1771
|
+
break;
|
|
1772
|
+
case "--rpc":
|
|
1773
|
+
opts.rpc = argv[++i];
|
|
1774
|
+
if (opts.rpc === undefined) throw new Error("--rpc requires a value");
|
|
1775
|
+
break;
|
|
1776
|
+
default:
|
|
1777
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1778
|
+
if (opts.addr !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1779
|
+
opts.addr = a;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return opts;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
async function cmdReputation(argv) {
|
|
1786
|
+
let opts;
|
|
1787
|
+
try {
|
|
1788
|
+
opts = parseReputationArgs(argv);
|
|
1789
|
+
} catch (e) {
|
|
1790
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
1791
|
+
return 2;
|
|
1792
|
+
}
|
|
1793
|
+
if (!opts.addr) {
|
|
1794
|
+
process.stderr.write("error: `vh reputation` requires an <addr>\n\n" + usage());
|
|
1795
|
+
return 2;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const ethers = require("ethers");
|
|
1799
|
+
|
|
1800
|
+
// Validate the address shape BEFORE building a provider or reading any env/network — a malformed
|
|
1801
|
+
// address must hard-error with usage (exit 2) and never hit the network (parser parity with
|
|
1802
|
+
// `vh show`/`vh lineage`, which validate the hash shape first).
|
|
1803
|
+
if (!ethers.isAddress(opts.addr)) {
|
|
1804
|
+
process.stderr.write(
|
|
1805
|
+
`error: invalid address: ${opts.addr} (expected a 20-byte 0x-hex address)\n\n` + usage()
|
|
1806
|
+
);
|
|
1807
|
+
return 2;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
const contractAddress = opts.contract || process.env.VH_CONTRACT;
|
|
1811
|
+
const rpcUrl = opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL;
|
|
1812
|
+
if (!rpcUrl) {
|
|
1813
|
+
process.stderr.write(
|
|
1814
|
+
"error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL\n"
|
|
1815
|
+
);
|
|
1816
|
+
return 1;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
let result;
|
|
1820
|
+
try {
|
|
1821
|
+
// Read-only: provider only — `vh reputation` NEVER constructs a signer or touches a key.
|
|
1822
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
1823
|
+
result = await runReputation({
|
|
1824
|
+
address: opts.addr,
|
|
1825
|
+
contractAddress,
|
|
1826
|
+
provider,
|
|
1827
|
+
json: opts.json,
|
|
1828
|
+
skipIdentityCheck: opts.skipIdentityCheck,
|
|
1829
|
+
ethers,
|
|
1830
|
+
});
|
|
1831
|
+
} catch (e) {
|
|
1832
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
1833
|
+
return 1;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Exit non-zero when the address has NO contributions so scripts/CI can branch on "no contributions"
|
|
1837
|
+
// — the same not-found exit-4 contract `vh show`/`vh lineage` use, so the read commands agree.
|
|
1838
|
+
return result.total === 0 ? 4 : 0;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Parse `dataset build` argv into { dir, out, hints, json }. Takes exactly one positional <dir> and a
|
|
1843
|
+
* REQUIRED --out. Throws on unknown/incomplete flags or a duplicate/missing positional so a typo never
|
|
1844
|
+
* silently manifests the wrong tree or writes to a surprise path (parser parity with the other commands).
|
|
1845
|
+
*/
|
|
1846
|
+
function parseDatasetBuildArgs(argv) {
|
|
1847
|
+
const opts = { dir: undefined, out: undefined, hints: undefined, json: false };
|
|
1848
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1849
|
+
const a = argv[i];
|
|
1850
|
+
switch (a) {
|
|
1851
|
+
case "--json":
|
|
1852
|
+
opts.json = true;
|
|
1853
|
+
break;
|
|
1854
|
+
case "--out":
|
|
1855
|
+
opts.out = argv[++i];
|
|
1856
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
1857
|
+
break;
|
|
1858
|
+
case "--hints":
|
|
1859
|
+
opts.hints = argv[++i];
|
|
1860
|
+
if (opts.hints === undefined) throw new Error("--hints requires a value");
|
|
1861
|
+
break;
|
|
1862
|
+
default:
|
|
1863
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1864
|
+
if (opts.dir !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1865
|
+
opts.dir = a;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return opts;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* Parse `dataset verify` argv into { dir, manifest, json }. Takes exactly one positional <dir> and a
|
|
1873
|
+
* REQUIRED --manifest. Throws on unknown/incomplete flags or a duplicate/missing positional so a typo
|
|
1874
|
+
* never silently verifies the wrong tree or against a surprise manifest (parser parity with the others).
|
|
1875
|
+
*/
|
|
1876
|
+
function parseDatasetVerifyArgs(argv) {
|
|
1877
|
+
const opts = { dir: undefined, manifest: undefined, json: false };
|
|
1878
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1879
|
+
const a = argv[i];
|
|
1880
|
+
switch (a) {
|
|
1881
|
+
case "--json":
|
|
1882
|
+
opts.json = true;
|
|
1883
|
+
break;
|
|
1884
|
+
case "--manifest":
|
|
1885
|
+
opts.manifest = argv[++i];
|
|
1886
|
+
if (opts.manifest === undefined) throw new Error("--manifest requires a value");
|
|
1887
|
+
break;
|
|
1888
|
+
default:
|
|
1889
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1890
|
+
if (opts.dir !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1891
|
+
opts.dir = a;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return opts;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* Parse `dataset diff` argv into { manifestA, manifestB, json }. Takes EXACTLY two positional manifest
|
|
1899
|
+
* paths and an optional --json. Throws on a missing/third positional or an unknown flag, so a typo
|
|
1900
|
+
* never silently diffs the wrong pair (parser parity with the other dataset subcommands).
|
|
1901
|
+
*/
|
|
1902
|
+
function parseDatasetDiffArgs(argv) {
|
|
1903
|
+
const opts = { manifestA: undefined, manifestB: undefined, json: false };
|
|
1904
|
+
const positionals = [];
|
|
1905
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1906
|
+
const a = argv[i];
|
|
1907
|
+
switch (a) {
|
|
1908
|
+
case "--json":
|
|
1909
|
+
opts.json = true;
|
|
1910
|
+
break;
|
|
1911
|
+
default:
|
|
1912
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1913
|
+
positionals.push(a);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (positionals.length > 2) {
|
|
1917
|
+
throw new Error(
|
|
1918
|
+
`unexpected extra argument: ${positionals[2]} (vh dataset diff takes exactly two manifests)`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
opts.manifestA = positionals[0];
|
|
1922
|
+
opts.manifestB = positionals[1];
|
|
1923
|
+
return opts;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Parse `dataset summary` argv into { manifest, json }. Takes EXACTLY one positional manifest path and an
|
|
1928
|
+
* optional --json. Throws on a missing/extra positional or an unknown flag, so a typo never silently
|
|
1929
|
+
* summarizes the wrong (or no) manifest (parser parity with the other dataset subcommands).
|
|
1930
|
+
*/
|
|
1931
|
+
function parseDatasetSummaryArgs(argv) {
|
|
1932
|
+
const opts = { manifest: undefined, json: false };
|
|
1933
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1934
|
+
const a = argv[i];
|
|
1935
|
+
switch (a) {
|
|
1936
|
+
case "--json":
|
|
1937
|
+
opts.json = true;
|
|
1938
|
+
break;
|
|
1939
|
+
default:
|
|
1940
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1941
|
+
if (opts.manifest !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1942
|
+
opts.manifest = a;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return opts;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Parse `dataset check` argv into { manifest, policy, json }. Takes EXACTLY one positional manifest path,
|
|
1950
|
+
* a REQUIRED --policy <p>, and an optional --json. Throws on a missing/extra positional or an unknown/
|
|
1951
|
+
* incomplete flag, so a typo never silently checks the wrong (or no) manifest against a surprise policy
|
|
1952
|
+
* (parser parity with the other dataset subcommands). A missing --policy is enforced in cmdDatasetCheck.
|
|
1953
|
+
*/
|
|
1954
|
+
function parseDatasetCheckArgs(argv) {
|
|
1955
|
+
const opts = { manifest: undefined, policy: undefined, json: false };
|
|
1956
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1957
|
+
const a = argv[i];
|
|
1958
|
+
switch (a) {
|
|
1959
|
+
case "--json":
|
|
1960
|
+
opts.json = true;
|
|
1961
|
+
break;
|
|
1962
|
+
case "--policy":
|
|
1963
|
+
opts.policy = argv[++i];
|
|
1964
|
+
if (opts.policy === undefined) throw new Error("--policy requires a value");
|
|
1965
|
+
break;
|
|
1966
|
+
default:
|
|
1967
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
1968
|
+
if (opts.manifest !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
1969
|
+
opts.manifest = a;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return opts;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/**
|
|
1976
|
+
* Parse `dataset report` argv into { manifest, verifyDir, policy, out, json }. Takes EXACTLY one
|
|
1977
|
+
* positional manifest path, an optional --verify <dir>, an optional --policy <p>, an optional --out <p>,
|
|
1978
|
+
* and an optional --json. Throws on a missing/extra positional or an unknown/incomplete flag, so a typo
|
|
1979
|
+
* never silently reports the wrong (or no) manifest, verifies a surprise tree, or checks a surprise
|
|
1980
|
+
* policy (parser parity with the other dataset subcommands).
|
|
1981
|
+
*/
|
|
1982
|
+
function parseDatasetReportArgs(argv) {
|
|
1983
|
+
const opts = { manifest: undefined, verifyDir: undefined, policy: undefined, out: undefined, json: false };
|
|
1984
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1985
|
+
const a = argv[i];
|
|
1986
|
+
switch (a) {
|
|
1987
|
+
case "--json":
|
|
1988
|
+
opts.json = true;
|
|
1989
|
+
break;
|
|
1990
|
+
case "--verify":
|
|
1991
|
+
opts.verifyDir = argv[++i];
|
|
1992
|
+
if (opts.verifyDir === undefined) throw new Error("--verify requires a value");
|
|
1993
|
+
break;
|
|
1994
|
+
case "--policy":
|
|
1995
|
+
opts.policy = argv[++i];
|
|
1996
|
+
if (opts.policy === undefined) throw new Error("--policy requires a value");
|
|
1997
|
+
break;
|
|
1998
|
+
case "--out":
|
|
1999
|
+
opts.out = argv[++i];
|
|
2000
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
2001
|
+
break;
|
|
2002
|
+
default:
|
|
2003
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2004
|
+
if (opts.manifest !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2005
|
+
opts.manifest = a;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return opts;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/**
|
|
2012
|
+
* Parse `dataset attest` argv into { manifest, out, json }. Takes EXACTLY one positional manifest path,
|
|
2013
|
+
* an optional --out <p>, and an optional --json. Throws on a missing/extra positional or an unknown/
|
|
2014
|
+
* incomplete flag, so a typo never silently attests the wrong (or no) manifest or writes to a surprise
|
|
2015
|
+
* path (parser parity with the other dataset subcommands).
|
|
2016
|
+
*/
|
|
2017
|
+
function parseDatasetAttestArgs(argv) {
|
|
2018
|
+
const opts = { manifest: undefined, out: undefined, json: false };
|
|
2019
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2020
|
+
const a = argv[i];
|
|
2021
|
+
switch (a) {
|
|
2022
|
+
case "--json":
|
|
2023
|
+
opts.json = true;
|
|
2024
|
+
break;
|
|
2025
|
+
case "--out":
|
|
2026
|
+
opts.out = argv[++i];
|
|
2027
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
2028
|
+
break;
|
|
2029
|
+
default:
|
|
2030
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2031
|
+
if (opts.manifest !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2032
|
+
opts.manifest = a;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return opts;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Parse `dataset sign`/`parcel sign` argv into { manifest, keyEnv, keyFile, out, json }. Takes EXACTLY one
|
|
2040
|
+
* positional manifest path, EXACTLY ONE of --key-env <VAR> / --key-file <path>, an optional --out <p>, and
|
|
2041
|
+
* an optional --json. Throws on a missing/extra positional or an unknown/incomplete flag (parser parity with
|
|
2042
|
+
* the other dataset/parcel subcommands). The neither/both key-source check is the value layer's job
|
|
2043
|
+
* (loadSigningWallet), so the SAME error is produced whether the command is run via the CLI or programmatically.
|
|
2044
|
+
*/
|
|
2045
|
+
function parseSignArgs(argv) {
|
|
2046
|
+
const opts = { manifest: undefined, keyEnv: undefined, keyFile: undefined, out: undefined, json: false };
|
|
2047
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2048
|
+
const a = argv[i];
|
|
2049
|
+
switch (a) {
|
|
2050
|
+
case "--json":
|
|
2051
|
+
opts.json = true;
|
|
2052
|
+
break;
|
|
2053
|
+
case "--key-env":
|
|
2054
|
+
opts.keyEnv = argv[++i];
|
|
2055
|
+
if (opts.keyEnv === undefined) throw new Error("--key-env requires a value");
|
|
2056
|
+
break;
|
|
2057
|
+
case "--key-file":
|
|
2058
|
+
opts.keyFile = argv[++i];
|
|
2059
|
+
if (opts.keyFile === undefined) throw new Error("--key-file requires a value");
|
|
2060
|
+
break;
|
|
2061
|
+
case "--out":
|
|
2062
|
+
opts.out = argv[++i];
|
|
2063
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
2064
|
+
break;
|
|
2065
|
+
default:
|
|
2066
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2067
|
+
if (opts.manifest !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2068
|
+
opts.manifest = a;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return opts;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Shared up-front shape validation for the OPTIONAL recipient-side trust-decision flags (--revocations /
|
|
2075
|
+
// --as-of, EPIC-51 / T-51.2) on the dataset + parcel verify-attest commands. Returns null when fine, else a
|
|
2076
|
+
// usage-error message (so the caller emits exit 2). A malformed --as-of is a usage error here (never a
|
|
2077
|
+
// runtime throw mid-verify); --as-of without --revocations is a usage error (the flag would silently do
|
|
2078
|
+
// nothing). The canonical-instant check mirrors the trust-asof core's grammar.
|
|
2079
|
+
function validateVerifyAsOfFlags(opts) {
|
|
2080
|
+
if (opts.asOf !== undefined && !opts.revocations) {
|
|
2081
|
+
return "--as-of requires --revocations (it pins the instant the revocation decision is made AS OF)";
|
|
2082
|
+
}
|
|
2083
|
+
if (opts.asOf !== undefined) {
|
|
2084
|
+
const re = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
2085
|
+
const ms = Date.parse(opts.asOf);
|
|
2086
|
+
if (
|
|
2087
|
+
typeof opts.asOf !== "string" ||
|
|
2088
|
+
!re.test(opts.asOf) ||
|
|
2089
|
+
Number.isNaN(ms) ||
|
|
2090
|
+
new Date(ms).toISOString() !== opts.asOf
|
|
2091
|
+
) {
|
|
2092
|
+
return `invalid --as-of: ${opts.asOf} (expected a canonical ISO-8601 UTC instant, e.g. 2026-06-01T00:00:00.000Z)`;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
return null;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Parse `dataset verify-attest` argv into { signed, manifest, signer, revocations, asOf, json }. Takes
|
|
2100
|
+
* EXACTLY one positional <signed> container path, an optional --manifest <m>, an optional --signer <addr>, an
|
|
2101
|
+
* optional --revocations <f> / --as-of <ISO>, and an optional --json. Throws on a missing/extra positional or
|
|
2102
|
+
* an unknown/incomplete flag, so a typo never silently verifies the wrong (or no) container, binds a surprise
|
|
2103
|
+
* manifest, or pins a surprise signer (parser parity with the other dataset subcommands).
|
|
2104
|
+
*/
|
|
2105
|
+
function parseDatasetVerifyAttestArgs(argv) {
|
|
2106
|
+
const opts = {
|
|
2107
|
+
signed: undefined,
|
|
2108
|
+
manifest: undefined,
|
|
2109
|
+
signer: undefined,
|
|
2110
|
+
revocations: undefined,
|
|
2111
|
+
asOf: undefined,
|
|
2112
|
+
json: false,
|
|
2113
|
+
};
|
|
2114
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2115
|
+
const a = argv[i];
|
|
2116
|
+
switch (a) {
|
|
2117
|
+
case "--json":
|
|
2118
|
+
opts.json = true;
|
|
2119
|
+
break;
|
|
2120
|
+
case "--manifest":
|
|
2121
|
+
opts.manifest = argv[++i];
|
|
2122
|
+
if (opts.manifest === undefined) throw new Error("--manifest requires a value");
|
|
2123
|
+
break;
|
|
2124
|
+
case "--signer":
|
|
2125
|
+
opts.signer = argv[++i];
|
|
2126
|
+
if (opts.signer === undefined) throw new Error("--signer requires a value");
|
|
2127
|
+
break;
|
|
2128
|
+
case "--revocations":
|
|
2129
|
+
opts.revocations = argv[++i];
|
|
2130
|
+
if (opts.revocations === undefined) throw new Error("--revocations requires a value");
|
|
2131
|
+
break;
|
|
2132
|
+
case "--as-of":
|
|
2133
|
+
opts.asOf = argv[++i];
|
|
2134
|
+
if (opts.asOf === undefined) throw new Error("--as-of requires a value");
|
|
2135
|
+
break;
|
|
2136
|
+
default:
|
|
2137
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2138
|
+
if (opts.signed !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2139
|
+
opts.signed = a;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return opts;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/**
|
|
2146
|
+
* Parse `dataset prove` argv into { file, manifest, out, json }. Takes NO positional (the file is the
|
|
2147
|
+
* REQUIRED --file flag, the manifest the REQUIRED --manifest flag), so a stray positional hard-errors —
|
|
2148
|
+
* a typo never silently proves the wrong file or writes to a surprise path (parser parity with the others).
|
|
2149
|
+
*/
|
|
2150
|
+
function parseDatasetProveArgs(argv) {
|
|
2151
|
+
const opts = { file: undefined, manifest: undefined, out: undefined, json: false };
|
|
2152
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2153
|
+
const a = argv[i];
|
|
2154
|
+
switch (a) {
|
|
2155
|
+
case "--json":
|
|
2156
|
+
opts.json = true;
|
|
2157
|
+
break;
|
|
2158
|
+
case "--file":
|
|
2159
|
+
opts.file = argv[++i];
|
|
2160
|
+
if (opts.file === undefined) throw new Error("--file requires a value");
|
|
2161
|
+
break;
|
|
2162
|
+
case "--manifest":
|
|
2163
|
+
opts.manifest = argv[++i];
|
|
2164
|
+
if (opts.manifest === undefined) throw new Error("--manifest requires a value");
|
|
2165
|
+
break;
|
|
2166
|
+
case "--out":
|
|
2167
|
+
opts.out = argv[++i];
|
|
2168
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
2169
|
+
break;
|
|
2170
|
+
default:
|
|
2171
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2172
|
+
throw new Error(`unexpected argument: ${a} (vh dataset prove takes --file/--manifest, no positional)`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return opts;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* Parse `dataset verify-proof` argv into { artifact, json }. Takes exactly one positional <proof> (the
|
|
2180
|
+
* artifact path). Throws on unknown/incomplete flags or a duplicate/missing positional (parser parity).
|
|
2181
|
+
*/
|
|
2182
|
+
function parseDatasetVerifyProofArgs(argv) {
|
|
2183
|
+
const opts = { artifact: undefined, json: false };
|
|
2184
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2185
|
+
const a = argv[i];
|
|
2186
|
+
switch (a) {
|
|
2187
|
+
case "--json":
|
|
2188
|
+
opts.json = true;
|
|
2189
|
+
break;
|
|
2190
|
+
default:
|
|
2191
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2192
|
+
if (opts.artifact !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2193
|
+
opts.artifact = a;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return opts;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* Parse `dataset timestamp-request`/`parcel timestamp-request` argv into { manifest, out, json }. Takes
|
|
2201
|
+
* EXACTLY one positional manifest path, an optional --out <p>, and an optional --json. Throws on a
|
|
2202
|
+
* missing/extra positional or an unknown/incomplete flag (parser parity with `attest`).
|
|
2203
|
+
*/
|
|
2204
|
+
function parseTimestampRequestArgs(argv) {
|
|
2205
|
+
const opts = { manifest: undefined, out: undefined, json: false };
|
|
2206
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2207
|
+
const a = argv[i];
|
|
2208
|
+
switch (a) {
|
|
2209
|
+
case "--json":
|
|
2210
|
+
opts.json = true;
|
|
2211
|
+
break;
|
|
2212
|
+
case "--out":
|
|
2213
|
+
opts.out = argv[++i];
|
|
2214
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
2215
|
+
break;
|
|
2216
|
+
default:
|
|
2217
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2218
|
+
if (opts.manifest !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2219
|
+
opts.manifest = a;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return opts;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Parse `dataset timestamp-wrap`/`parcel timestamp-wrap` argv into { manifest, token, out, json }. Takes
|
|
2227
|
+
* EXACTLY one positional manifest path, a REQUIRED --token <path|base64>, an optional --out <p>, and an
|
|
2228
|
+
* optional --json. Throws on a missing/extra positional or an unknown/incomplete flag (parser parity).
|
|
2229
|
+
*/
|
|
2230
|
+
function parseTimestampWrapArgs(argv) {
|
|
2231
|
+
const opts = { manifest: undefined, token: undefined, out: undefined, json: false };
|
|
2232
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2233
|
+
const a = argv[i];
|
|
2234
|
+
switch (a) {
|
|
2235
|
+
case "--json":
|
|
2236
|
+
opts.json = true;
|
|
2237
|
+
break;
|
|
2238
|
+
case "--token":
|
|
2239
|
+
opts.token = argv[++i];
|
|
2240
|
+
if (opts.token === undefined) throw new Error("--token requires a value");
|
|
2241
|
+
break;
|
|
2242
|
+
case "--out":
|
|
2243
|
+
opts.out = argv[++i];
|
|
2244
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
2245
|
+
break;
|
|
2246
|
+
default:
|
|
2247
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2248
|
+
if (opts.manifest !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2249
|
+
opts.manifest = a;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
return opts;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* Parse `dataset verify-timestamp`/`parcel verify-timestamp` argv into { container, manifest, json }. Takes
|
|
2257
|
+
* EXACTLY one positional <container> path, an optional --manifest <m>, and an optional --json. Throws on a
|
|
2258
|
+
* missing/extra positional or an unknown/incomplete flag, so a typo never silently verifies the wrong (or
|
|
2259
|
+
* no) container or binds a surprise manifest (parser parity with `verify-attest`/the other subcommands).
|
|
2260
|
+
*/
|
|
2261
|
+
function parseVerifyTimestampArgs(argv) {
|
|
2262
|
+
const opts = { container: undefined, manifest: undefined, json: false };
|
|
2263
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2264
|
+
const a = argv[i];
|
|
2265
|
+
switch (a) {
|
|
2266
|
+
case "--json":
|
|
2267
|
+
opts.json = true;
|
|
2268
|
+
break;
|
|
2269
|
+
case "--manifest":
|
|
2270
|
+
opts.manifest = argv[++i];
|
|
2271
|
+
if (opts.manifest === undefined) throw new Error("--manifest requires a value");
|
|
2272
|
+
break;
|
|
2273
|
+
default:
|
|
2274
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2275
|
+
if (opts.container !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2276
|
+
opts.container = a;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return opts;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
/**
|
|
2283
|
+
* Parse `parcel build` argv into { dir, out, parcelId, sender, recipient, hints, json }. Takes exactly one
|
|
2284
|
+
* positional <dir>, a REQUIRED --out, and OPTIONAL untrusted parcel-metadata flags. Throws on unknown/
|
|
2285
|
+
* incomplete flags or a duplicate/missing positional so a typo never silently builds the wrong tree
|
|
2286
|
+
* (parser parity with the dataset subcommands).
|
|
2287
|
+
*/
|
|
2288
|
+
function parseParcelBuildArgs(argv) {
|
|
2289
|
+
const opts = {
|
|
2290
|
+
dir: undefined,
|
|
2291
|
+
out: undefined,
|
|
2292
|
+
parcelId: undefined,
|
|
2293
|
+
sender: undefined,
|
|
2294
|
+
recipient: undefined,
|
|
2295
|
+
hints: undefined,
|
|
2296
|
+
json: false,
|
|
2297
|
+
};
|
|
2298
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2299
|
+
const a = argv[i];
|
|
2300
|
+
switch (a) {
|
|
2301
|
+
case "--json":
|
|
2302
|
+
opts.json = true;
|
|
2303
|
+
break;
|
|
2304
|
+
case "--out":
|
|
2305
|
+
opts.out = argv[++i];
|
|
2306
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
2307
|
+
break;
|
|
2308
|
+
case "--parcel-id":
|
|
2309
|
+
opts.parcelId = argv[++i];
|
|
2310
|
+
if (opts.parcelId === undefined) throw new Error("--parcel-id requires a value");
|
|
2311
|
+
break;
|
|
2312
|
+
case "--sender":
|
|
2313
|
+
opts.sender = argv[++i];
|
|
2314
|
+
if (opts.sender === undefined) throw new Error("--sender requires a value");
|
|
2315
|
+
break;
|
|
2316
|
+
case "--recipient":
|
|
2317
|
+
opts.recipient = argv[++i];
|
|
2318
|
+
if (opts.recipient === undefined) throw new Error("--recipient requires a value");
|
|
2319
|
+
break;
|
|
2320
|
+
case "--hints":
|
|
2321
|
+
opts.hints = argv[++i];
|
|
2322
|
+
if (opts.hints === undefined) throw new Error("--hints requires a value");
|
|
2323
|
+
break;
|
|
2324
|
+
default:
|
|
2325
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2326
|
+
if (opts.dir !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2327
|
+
opts.dir = a;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return opts;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
/**
|
|
2334
|
+
* Parse `parcel verify` argv into { dir, manifest, json }. Takes exactly one positional <dir> and a
|
|
2335
|
+
* REQUIRED --manifest. Throws on unknown/incomplete flags or a duplicate/missing positional so a typo
|
|
2336
|
+
* never silently verifies the wrong tree or against a surprise manifest (parser parity).
|
|
2337
|
+
*/
|
|
2338
|
+
function parseParcelVerifyArgs(argv) {
|
|
2339
|
+
const opts = { dir: undefined, manifest: undefined, json: false };
|
|
2340
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2341
|
+
const a = argv[i];
|
|
2342
|
+
switch (a) {
|
|
2343
|
+
case "--json":
|
|
2344
|
+
opts.json = true;
|
|
2345
|
+
break;
|
|
2346
|
+
case "--manifest":
|
|
2347
|
+
opts.manifest = argv[++i];
|
|
2348
|
+
if (opts.manifest === undefined) throw new Error("--manifest requires a value");
|
|
2349
|
+
break;
|
|
2350
|
+
default:
|
|
2351
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2352
|
+
if (opts.dir !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2353
|
+
opts.dir = a;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return opts;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* Parse `parcel attest` argv into { manifest, out, json }. Takes EXACTLY one positional manifest path, an
|
|
2361
|
+
* optional --out <p>, and an optional --json. Throws on a missing/extra positional or an unknown/incomplete
|
|
2362
|
+
* flag (parser parity with `dataset attest`).
|
|
2363
|
+
*/
|
|
2364
|
+
function parseParcelAttestArgs(argv) {
|
|
2365
|
+
const opts = { manifest: undefined, out: undefined, json: false };
|
|
2366
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2367
|
+
const a = argv[i];
|
|
2368
|
+
switch (a) {
|
|
2369
|
+
case "--json":
|
|
2370
|
+
opts.json = true;
|
|
2371
|
+
break;
|
|
2372
|
+
case "--out":
|
|
2373
|
+
opts.out = argv[++i];
|
|
2374
|
+
if (opts.out === undefined) throw new Error("--out requires a value");
|
|
2375
|
+
break;
|
|
2376
|
+
default:
|
|
2377
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2378
|
+
if (opts.manifest !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2379
|
+
opts.manifest = a;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return opts;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* Parse `parcel verify-attest` argv into { signed, manifest, signer, json }. Takes EXACTLY one positional
|
|
2387
|
+
* <signed> container path, an optional --manifest <m>, an optional --signer <addr>, and an optional --json.
|
|
2388
|
+
* Throws on a missing/extra positional or an unknown/incomplete flag (parser parity with `dataset
|
|
2389
|
+
* verify-attest`).
|
|
2390
|
+
*/
|
|
2391
|
+
function parseParcelVerifyAttestArgs(argv) {
|
|
2392
|
+
const opts = {
|
|
2393
|
+
signed: undefined,
|
|
2394
|
+
manifest: undefined,
|
|
2395
|
+
signer: undefined,
|
|
2396
|
+
revocations: undefined,
|
|
2397
|
+
asOf: undefined,
|
|
2398
|
+
json: false,
|
|
2399
|
+
};
|
|
2400
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2401
|
+
const a = argv[i];
|
|
2402
|
+
switch (a) {
|
|
2403
|
+
case "--json":
|
|
2404
|
+
opts.json = true;
|
|
2405
|
+
break;
|
|
2406
|
+
case "--manifest":
|
|
2407
|
+
opts.manifest = argv[++i];
|
|
2408
|
+
if (opts.manifest === undefined) throw new Error("--manifest requires a value");
|
|
2409
|
+
break;
|
|
2410
|
+
case "--signer":
|
|
2411
|
+
opts.signer = argv[++i];
|
|
2412
|
+
if (opts.signer === undefined) throw new Error("--signer requires a value");
|
|
2413
|
+
break;
|
|
2414
|
+
case "--revocations":
|
|
2415
|
+
opts.revocations = argv[++i];
|
|
2416
|
+
if (opts.revocations === undefined) throw new Error("--revocations requires a value");
|
|
2417
|
+
break;
|
|
2418
|
+
case "--as-of":
|
|
2419
|
+
opts.asOf = argv[++i];
|
|
2420
|
+
if (opts.asOf === undefined) throw new Error("--as-of requires a value");
|
|
2421
|
+
break;
|
|
2422
|
+
default:
|
|
2423
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
2424
|
+
if (opts.signed !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
2425
|
+
opts.signed = a;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return opts;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
function cmdDataset(argv) {
|
|
2432
|
+
const [sub, ...rest] = argv;
|
|
2433
|
+
if (sub === "verify") {
|
|
2434
|
+
return cmdDatasetVerify(rest);
|
|
2435
|
+
}
|
|
2436
|
+
if (sub === "diff") {
|
|
2437
|
+
return cmdDatasetDiff(rest);
|
|
2438
|
+
}
|
|
2439
|
+
if (sub === "summary") {
|
|
2440
|
+
return cmdDatasetSummary(rest);
|
|
2441
|
+
}
|
|
2442
|
+
if (sub === "check") {
|
|
2443
|
+
return cmdDatasetCheck(rest);
|
|
2444
|
+
}
|
|
2445
|
+
if (sub === "report") {
|
|
2446
|
+
return cmdDatasetReport(rest);
|
|
2447
|
+
}
|
|
2448
|
+
if (sub === "attest") {
|
|
2449
|
+
return cmdDatasetAttest(rest);
|
|
2450
|
+
}
|
|
2451
|
+
if (sub === "sign") {
|
|
2452
|
+
return cmdDatasetSign(rest);
|
|
2453
|
+
}
|
|
2454
|
+
if (sub === "verify-attest") {
|
|
2455
|
+
return cmdDatasetVerifyAttest(rest);
|
|
2456
|
+
}
|
|
2457
|
+
if (sub === "timestamp-request") {
|
|
2458
|
+
return cmdDatasetTimestampRequest(rest);
|
|
2459
|
+
}
|
|
2460
|
+
if (sub === "timestamp-wrap") {
|
|
2461
|
+
return cmdDatasetTimestampWrap(rest);
|
|
2462
|
+
}
|
|
2463
|
+
if (sub === "verify-timestamp") {
|
|
2464
|
+
return cmdDatasetVerifyTimestamp(rest);
|
|
2465
|
+
}
|
|
2466
|
+
if (sub === "prove") {
|
|
2467
|
+
return cmdDatasetProve(rest);
|
|
2468
|
+
}
|
|
2469
|
+
if (sub === "verify-proof") {
|
|
2470
|
+
return cmdDatasetVerifyProof(rest);
|
|
2471
|
+
}
|
|
2472
|
+
if (sub !== "build") {
|
|
2473
|
+
process.stderr.write(
|
|
2474
|
+
`error: unknown dataset subcommand: ${sub === undefined ? "(none)" : sub} ` +
|
|
2475
|
+
`(expected: build | verify | diff | summary | check | report | attest | sign | verify-attest | timestamp-request | timestamp-wrap | verify-timestamp | prove | verify-proof)\n\n` + usage()
|
|
2476
|
+
);
|
|
2477
|
+
return 2;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
let opts;
|
|
2481
|
+
try {
|
|
2482
|
+
opts = parseDatasetBuildArgs(rest);
|
|
2483
|
+
} catch (e) {
|
|
2484
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2485
|
+
return 2;
|
|
2486
|
+
}
|
|
2487
|
+
if (!opts.dir) {
|
|
2488
|
+
process.stderr.write("error: `vh dataset build` requires a <dir>\n\n" + usage());
|
|
2489
|
+
return 2;
|
|
2490
|
+
}
|
|
2491
|
+
if (!opts.out) {
|
|
2492
|
+
process.stderr.write("error: `vh dataset build` requires --out <path>\n\n" + usage());
|
|
2493
|
+
return 2;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// Optional untrusted hints: read + parse the JSON file BEFORE walking the tree so a malformed hints
|
|
2497
|
+
// file hard-errors early (and never half-writes a manifest). dataset.js validates that every hinted
|
|
2498
|
+
// path exists in the tree.
|
|
2499
|
+
let hints;
|
|
2500
|
+
if (opts.hints !== undefined) {
|
|
2501
|
+
const fs = require("fs");
|
|
2502
|
+
let raw;
|
|
2503
|
+
try {
|
|
2504
|
+
raw = fs.readFileSync(opts.hints, "utf8");
|
|
2505
|
+
} catch (e) {
|
|
2506
|
+
process.stderr.write(`error: cannot read --hints file ${opts.hints}: ${e.message}\n`);
|
|
2507
|
+
return 1;
|
|
2508
|
+
}
|
|
2509
|
+
try {
|
|
2510
|
+
hints = JSON.parse(raw);
|
|
2511
|
+
} catch (e) {
|
|
2512
|
+
process.stderr.write(`error: --hints file ${opts.hints} is not valid JSON: ${e.message}\n`);
|
|
2513
|
+
return 1;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
try {
|
|
2518
|
+
runDatasetBuild({ dir: opts.dir, out: opts.out, hints, json: opts.json });
|
|
2519
|
+
} catch (e) {
|
|
2520
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2521
|
+
return 1;
|
|
2522
|
+
}
|
|
2523
|
+
return 0;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
/**
|
|
2527
|
+
* `vh dataset verify <dir> --manifest <p>` — re-derive the dataset root from a FRESH copy on disk and
|
|
2528
|
+
* compare it to the manifest's (UNTRUSTED) recorded root, plus a precise per-file diff. OFFLINE: no
|
|
2529
|
+
* provider, no key, no network. Exit 0 on MATCH, 3 on MISMATCH (so scripts/CI can branch like
|
|
2530
|
+
* `vh verify`), 2 on a usage error, 1 on a runtime error (missing/corrupt manifest, bad dir).
|
|
2531
|
+
*/
|
|
2532
|
+
function cmdDatasetVerify(argv) {
|
|
2533
|
+
let opts;
|
|
2534
|
+
try {
|
|
2535
|
+
opts = parseDatasetVerifyArgs(argv);
|
|
2536
|
+
} catch (e) {
|
|
2537
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2538
|
+
return 2;
|
|
2539
|
+
}
|
|
2540
|
+
if (!opts.dir) {
|
|
2541
|
+
process.stderr.write("error: `vh dataset verify` requires a <dir>\n\n" + usage());
|
|
2542
|
+
return 2;
|
|
2543
|
+
}
|
|
2544
|
+
if (!opts.manifest) {
|
|
2545
|
+
process.stderr.write("error: `vh dataset verify` requires --manifest <path>\n\n" + usage());
|
|
2546
|
+
return 2;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
let result;
|
|
2550
|
+
try {
|
|
2551
|
+
result = runDatasetVerify({ dir: opts.dir, manifest: opts.manifest, json: opts.json });
|
|
2552
|
+
} catch (e) {
|
|
2553
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2554
|
+
return 1;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// Exit non-zero on a tamper/MISMATCH so scripts and CI can branch on it (mirrors `vh verify`).
|
|
2558
|
+
return result.status === "MATCH" ? 0 : 3;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
/**
|
|
2562
|
+
* `vh dataset diff <manifestA> <manifestB> [--json]` — OFFLINE manifest-to-manifest change report.
|
|
2563
|
+
* Reads BOTH manifests strictly and reuses the SAME diff core as `vh dataset verify`. PURELY OFFLINE:
|
|
2564
|
+
* no tree, no provider, no key, no network. Exit 0 when the manifests are IDENTICAL, 3 when they
|
|
2565
|
+
* DIFFER (so CI can branch — "fail the pipeline if the training set changed"), 2 on a usage error, 1
|
|
2566
|
+
* on a runtime error (missing/corrupt manifest).
|
|
2567
|
+
*/
|
|
2568
|
+
function cmdDatasetDiff(argv) {
|
|
2569
|
+
let opts;
|
|
2570
|
+
try {
|
|
2571
|
+
opts = parseDatasetDiffArgs(argv);
|
|
2572
|
+
} catch (e) {
|
|
2573
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2574
|
+
return 2;
|
|
2575
|
+
}
|
|
2576
|
+
if (!opts.manifestA || !opts.manifestB) {
|
|
2577
|
+
process.stderr.write(
|
|
2578
|
+
"error: `vh dataset diff` requires exactly two manifest paths <manifestA> <manifestB>\n\n" +
|
|
2579
|
+
usage()
|
|
2580
|
+
);
|
|
2581
|
+
return 2;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
let result;
|
|
2585
|
+
try {
|
|
2586
|
+
result = runDatasetDiff({
|
|
2587
|
+
manifestA: opts.manifestA,
|
|
2588
|
+
manifestB: opts.manifestB,
|
|
2589
|
+
json: opts.json,
|
|
2590
|
+
});
|
|
2591
|
+
} catch (e) {
|
|
2592
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2593
|
+
return 1;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
// Exit non-zero when the manifests DIFFER so CI can branch (mirrors the dataset family's MISMATCH).
|
|
2597
|
+
// The verdict is the CHANGE SET (`identical`), not raw root-string equality, so the exit code can
|
|
2598
|
+
// never disagree with the printed/JSON changeset — a hand-edited `root` whose leaves are unchanged
|
|
2599
|
+
// still exits 0 (IDENTICAL), matching its empty changeset.
|
|
2600
|
+
return result.identical ? 0 : 3;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/**
|
|
2604
|
+
* `vh dataset summary <manifest> [--json]` — OFFLINE provenance/license roll-up over a manifest. Reads
|
|
2605
|
+
* the manifest strictly and aggregates the (UNTRUSTED) per-file {source, license} hints into histograms,
|
|
2606
|
+
* leading with the trust caveat that this counts CLAIMS, not verified facts. PURELY OFFLINE: no tree, no
|
|
2607
|
+
* provider, no key, no network. Exit 0 on success, 2 on a usage error, 1 on a runtime error (missing or
|
|
2608
|
+
* corrupt manifest).
|
|
2609
|
+
*/
|
|
2610
|
+
function cmdDatasetSummary(argv) {
|
|
2611
|
+
let opts;
|
|
2612
|
+
try {
|
|
2613
|
+
opts = parseDatasetSummaryArgs(argv);
|
|
2614
|
+
} catch (e) {
|
|
2615
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2616
|
+
return 2;
|
|
2617
|
+
}
|
|
2618
|
+
if (!opts.manifest) {
|
|
2619
|
+
process.stderr.write("error: `vh dataset summary` requires a <manifest>\n\n" + usage());
|
|
2620
|
+
return 2;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
try {
|
|
2624
|
+
runDatasetSummary({ manifest: opts.manifest, json: opts.json });
|
|
2625
|
+
} catch (e) {
|
|
2626
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2627
|
+
return 1;
|
|
2628
|
+
}
|
|
2629
|
+
return 0;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
/**
|
|
2633
|
+
* `vh dataset check <manifest> --policy <p> [--json]` — OFFLINE license/source policy gate. Reads the
|
|
2634
|
+
* manifest AND policy strictly (a corrupt/foreign one is rejected) and evaluates the manifest's TRUSTED
|
|
2635
|
+
* file set against the policy in a PURE, deterministic function. PURELY OFFLINE: no tree, no provider, no
|
|
2636
|
+
* key, no network. Exit 0 PASS, 3 FAIL (mirrors the dataset family's data-divergence convention so all
|
|
2637
|
+
* dataset gates use the same 0/3 contract), 2 on a usage error (missing/extra positional, missing
|
|
2638
|
+
* --policy, unknown flag), 1 on a runtime error (missing/corrupt manifest OR policy). A missing --policy
|
|
2639
|
+
* is a usage error (2), NOT a silent PASS.
|
|
2640
|
+
*/
|
|
2641
|
+
function cmdDatasetCheck(argv) {
|
|
2642
|
+
let opts;
|
|
2643
|
+
try {
|
|
2644
|
+
opts = parseDatasetCheckArgs(argv);
|
|
2645
|
+
} catch (e) {
|
|
2646
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2647
|
+
return 2;
|
|
2648
|
+
}
|
|
2649
|
+
if (!opts.manifest) {
|
|
2650
|
+
process.stderr.write("error: `vh dataset check` requires a <manifest>\n\n" + usage());
|
|
2651
|
+
return 2;
|
|
2652
|
+
}
|
|
2653
|
+
// A missing --policy is a USAGE error (2), never a silent PASS: a gate with no policy must not pass.
|
|
2654
|
+
if (!opts.policy) {
|
|
2655
|
+
process.stderr.write("error: `vh dataset check` requires --policy <path>\n\n" + usage());
|
|
2656
|
+
return 2;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
let result;
|
|
2660
|
+
try {
|
|
2661
|
+
result = runDatasetCheck({ manifest: opts.manifest, policy: opts.policy, json: opts.json });
|
|
2662
|
+
} catch (e) {
|
|
2663
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2664
|
+
return 1;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// Exit non-zero on FAIL so CI can gate (mirrors the dataset family's 0/3 data-divergence convention).
|
|
2668
|
+
return result.verdict === "PASS" ? 0 : 3;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
/**
|
|
2672
|
+
* `vh dataset report <manifest> [--verify <dir>] [--policy <p>] [--json] [--out <p>]` — ONE
|
|
2673
|
+
* self-contained, deterministic evidence document. Reads the manifest strictly, consolidates the dataset
|
|
2674
|
+
* identity + the provenance/license roll-up (REUSES the SAME aggregation as `vh dataset summary`) + the
|
|
2675
|
+
* trust caveats, OPTIONALLY embeds a live-tree verification verdict (REUSES `runDatasetVerify`), and
|
|
2676
|
+
* OPTIONALLY embeds a Policy compliance verdict (REUSES the SAME pure `evaluatePolicy` as `vh dataset
|
|
2677
|
+
* check` — the report verdict can never diverge from `vh dataset check`'s). PURELY OFFLINE for the
|
|
2678
|
+
* manifest-only path (no tree/provider/key/network); `--verify` adds an offline live-tree re-derive.
|
|
2679
|
+
*
|
|
2680
|
+
* EXIT CODES — the report is a COMBINED CI gate (non-zero whenever ANY embedded gate fails, 0 only when
|
|
2681
|
+
* all pass):
|
|
2682
|
+
* - WITH --verify: 0 on MATCH, 3 on MISMATCH (mirrors `vh dataset verify`).
|
|
2683
|
+
* - WITH --policy: 0 on PASS, 3 on FAIL (mirrors `vh dataset check`).
|
|
2684
|
+
* - WITH BOTH: 3 if EITHER the verify is MISMATCH OR the policy is FAIL; 0 only when MATCH AND PASS.
|
|
2685
|
+
* - WITHOUT either gate: 0 on a well-formed manifest.
|
|
2686
|
+
* - 2 on a usage error; 1 on a runtime error (missing/corrupt manifest or policy, or a bad --verify dir).
|
|
2687
|
+
*/
|
|
2688
|
+
function cmdDatasetReport(argv) {
|
|
2689
|
+
let opts;
|
|
2690
|
+
try {
|
|
2691
|
+
opts = parseDatasetReportArgs(argv);
|
|
2692
|
+
} catch (e) {
|
|
2693
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2694
|
+
return 2;
|
|
2695
|
+
}
|
|
2696
|
+
if (!opts.manifest) {
|
|
2697
|
+
process.stderr.write("error: `vh dataset report` requires a <manifest>\n\n" + usage());
|
|
2698
|
+
return 2;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
let result;
|
|
2702
|
+
try {
|
|
2703
|
+
result = runDatasetReport({
|
|
2704
|
+
manifest: opts.manifest,
|
|
2705
|
+
verifyDir: opts.verifyDir,
|
|
2706
|
+
policy: opts.policy,
|
|
2707
|
+
out: opts.out,
|
|
2708
|
+
json: opts.json,
|
|
2709
|
+
});
|
|
2710
|
+
} catch (e) {
|
|
2711
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2712
|
+
return 1;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// COMBINED gate: the report is non-zero whenever ANY embedded gate fails, and 0 only when all pass.
|
|
2716
|
+
// --verify => fail on MISMATCH (mirrors `vh dataset verify`).
|
|
2717
|
+
// --policy => fail on FAIL (mirrors `vh dataset check`).
|
|
2718
|
+
// With BOTH, either failure yields exit 3; with NEITHER, a well-formed manifest is exit 0.
|
|
2719
|
+
if (result.verifyStatus === "MISMATCH" || result.policyVerdict === "FAIL") return 3;
|
|
2720
|
+
return 0;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
/**
|
|
2724
|
+
* `vh dataset attest <manifest> [--out <p>] [--json]` — emit the canonical, byte-deterministic UNSIGNED
|
|
2725
|
+
* attestation payload the human signing/timestamp trust-root (P-3) will sign. Reads the manifest
|
|
2726
|
+
* strictly and commits to the dataset identity (root + fileCount + canonical manifestDigest) plus the
|
|
2727
|
+
* standing trust caveat, with explicit `signed:false`/`signature:null` markers. PURELY OFFLINE: no tree,
|
|
2728
|
+
* no provider, no key, no network. Exit 0 on success, 2 on a usage error, 1 on a runtime error
|
|
2729
|
+
* (missing/corrupt manifest).
|
|
2730
|
+
*/
|
|
2731
|
+
function cmdDatasetAttest(argv) {
|
|
2732
|
+
let opts;
|
|
2733
|
+
try {
|
|
2734
|
+
opts = parseDatasetAttestArgs(argv);
|
|
2735
|
+
} catch (e) {
|
|
2736
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2737
|
+
return 2;
|
|
2738
|
+
}
|
|
2739
|
+
if (!opts.manifest) {
|
|
2740
|
+
process.stderr.write("error: `vh dataset attest` requires a <manifest>\n\n" + usage());
|
|
2741
|
+
return 2;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
try {
|
|
2745
|
+
runDatasetAttest({ manifest: opts.manifest, out: opts.out, json: opts.json });
|
|
2746
|
+
} catch (e) {
|
|
2747
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2748
|
+
return 1;
|
|
2749
|
+
}
|
|
2750
|
+
return 0;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* `vh dataset sign <manifest> --key-env <VAR> | --key-file <path> [--out <p>] [--json]` — sign the UNSIGNED
|
|
2755
|
+
* dataset attestation with a HUMAN-supplied key and emit the SIGNED container. The key is read, used to
|
|
2756
|
+
* build an in-process ethers Wallet, used to sign, and discarded — NEVER generated, persisted, or logged;
|
|
2757
|
+
* success/`--json` output prints ONLY the PUBLIC signer address, the output path, and the scheme. PURELY
|
|
2758
|
+
* OFFLINE (EIP-191 personal_sign; no provider, no network). The output is accepted by `vh dataset
|
|
2759
|
+
* verify-attest` unchanged.
|
|
2760
|
+
*
|
|
2761
|
+
* EXIT CODES: 0 success; 2 on a usage error (missing/extra positional, unknown/incomplete flag, NEITHER or
|
|
2762
|
+
* BOTH of --key-env/--key-file — the source must be unambiguous BEFORE we touch a key); 1 on a runtime
|
|
2763
|
+
* error (a missing env var, an unreadable key file, a malformed/zero key, or a corrupt/missing manifest).
|
|
2764
|
+
* No error message ever includes the key material.
|
|
2765
|
+
*/
|
|
2766
|
+
async function cmdDatasetSign(argv) {
|
|
2767
|
+
let opts;
|
|
2768
|
+
try {
|
|
2769
|
+
opts = parseSignArgs(argv);
|
|
2770
|
+
} catch (e) {
|
|
2771
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2772
|
+
return 2;
|
|
2773
|
+
}
|
|
2774
|
+
if (!opts.manifest) {
|
|
2775
|
+
process.stderr.write("error: `vh dataset sign` requires a <manifest>\n\n" + usage());
|
|
2776
|
+
return 2;
|
|
2777
|
+
}
|
|
2778
|
+
// The key SOURCE must be unambiguous — EXACTLY ONE of --key-env/--key-file — and that is a USAGE error
|
|
2779
|
+
// (exit 2), checked BEFORE any key is read. (A present-but-bad key value is a RUNTIME error, exit 1,
|
|
2780
|
+
// surfaced from runDatasetSign's loadSigningWallet below — never echoing the key.)
|
|
2781
|
+
const hasEnv = opts.keyEnv !== undefined;
|
|
2782
|
+
const hasFile = opts.keyFile !== undefined;
|
|
2783
|
+
if (!hasEnv && !hasFile) {
|
|
2784
|
+
process.stderr.write(
|
|
2785
|
+
"error: `vh dataset sign` requires EXACTLY ONE signing-key source: --key-env <VAR> or " +
|
|
2786
|
+
"--key-file <path>\n\n" + usage()
|
|
2787
|
+
);
|
|
2788
|
+
return 2;
|
|
2789
|
+
}
|
|
2790
|
+
if (hasEnv && hasFile) {
|
|
2791
|
+
process.stderr.write(
|
|
2792
|
+
"error: --key-env and --key-file are mutually exclusive; pass EXACTLY ONE signing-key source\n\n" +
|
|
2793
|
+
usage()
|
|
2794
|
+
);
|
|
2795
|
+
return 2;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
try {
|
|
2799
|
+
await runDatasetSign({
|
|
2800
|
+
manifest: opts.manifest,
|
|
2801
|
+
keyEnv: opts.keyEnv,
|
|
2802
|
+
keyFile: opts.keyFile,
|
|
2803
|
+
out: opts.out,
|
|
2804
|
+
json: opts.json,
|
|
2805
|
+
});
|
|
2806
|
+
} catch (e) {
|
|
2807
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2808
|
+
return 1;
|
|
2809
|
+
}
|
|
2810
|
+
return 0;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2814
|
+
* `vh dataset verify-attest <signed> [--manifest <m>] [--signer <addr>] [--json]` — OFFLINE verify a
|
|
2815
|
+
* SIGNED attestation container. Reads the container strictly, recovers the signer from the embedded
|
|
2816
|
+
* canonical bytes + signature, and confirms it equals the container's `signer`; with --signer it pins the
|
|
2817
|
+
* expected publisher; with --manifest it confirms the signature binds the buyer's own dataset. PURELY
|
|
2818
|
+
* OFFLINE: no tree, no provider, no key, no network. Exit 0 ACCEPTED, 3 REJECTED (mirrors the dataset
|
|
2819
|
+
* family's 0/3 data-divergence convention so a buyer's CI can gate), 2 on a usage error (missing/extra
|
|
2820
|
+
* positional, unknown flag, malformed --signer), 1 on a runtime error (missing/corrupt container or manifest).
|
|
2821
|
+
*/
|
|
2822
|
+
function cmdDatasetVerifyAttest(argv) {
|
|
2823
|
+
let opts;
|
|
2824
|
+
try {
|
|
2825
|
+
opts = parseDatasetVerifyAttestArgs(argv);
|
|
2826
|
+
} catch (e) {
|
|
2827
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2828
|
+
return 2;
|
|
2829
|
+
}
|
|
2830
|
+
if (!opts.signed) {
|
|
2831
|
+
process.stderr.write(
|
|
2832
|
+
"error: `vh dataset verify-attest` requires a <signed> (signed attestation container path)\n\n" +
|
|
2833
|
+
usage()
|
|
2834
|
+
);
|
|
2835
|
+
return 2;
|
|
2836
|
+
}
|
|
2837
|
+
// Validate the --signer address SHAPE up front (when given) so a malformed expected publisher is a
|
|
2838
|
+
// usage error (2), never a runtime throw mid-verify (parser parity with `vh show`/`vh reputation`,
|
|
2839
|
+
// which validate the address/hash shape before doing any work). PURELY OFFLINE — no network here either.
|
|
2840
|
+
if (opts.signer !== undefined) {
|
|
2841
|
+
const ethers = require("ethers");
|
|
2842
|
+
if (!ethers.isAddress(opts.signer)) {
|
|
2843
|
+
process.stderr.write(
|
|
2844
|
+
`error: invalid --signer address: ${opts.signer} (expected a 20-byte 0x-hex address)\n\n` +
|
|
2845
|
+
usage()
|
|
2846
|
+
);
|
|
2847
|
+
return 2;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
// Validate the --as-of SHAPE up front (when given) so a malformed decision instant is a usage error (2),
|
|
2851
|
+
// never a runtime throw mid-verify. --as-of only matters under --revocations; pinning it without
|
|
2852
|
+
// --revocations is a usage error (the flag would silently do nothing otherwise).
|
|
2853
|
+
{
|
|
2854
|
+
const asOfErr = validateVerifyAsOfFlags(opts);
|
|
2855
|
+
if (asOfErr) {
|
|
2856
|
+
process.stderr.write(`error: ${asOfErr}\n\n` + usage());
|
|
2857
|
+
return 2;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
let result;
|
|
2862
|
+
try {
|
|
2863
|
+
result = runDatasetVerifyAttest({
|
|
2864
|
+
signed: opts.signed,
|
|
2865
|
+
manifest: opts.manifest,
|
|
2866
|
+
signer: opts.signer,
|
|
2867
|
+
revocations: opts.revocations,
|
|
2868
|
+
asOf: opts.asOf,
|
|
2869
|
+
json: opts.json,
|
|
2870
|
+
});
|
|
2871
|
+
} catch (e) {
|
|
2872
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2873
|
+
return 1;
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// Exit non-zero on REJECTED so a buyer's CI can gate (mirrors the dataset family's 0/3 convention).
|
|
2877
|
+
return result.accepted ? 0 : 3;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
/**
|
|
2881
|
+
* `vh dataset timestamp-request <manifest> [--out <p>] [--json]` — emit the SHA-256 digest of the canonical
|
|
2882
|
+
* UNSIGNED attestation bytes (the messageImprint a human submits to their RFC-3161 TSA), plus a recipe for
|
|
2883
|
+
* producing the token. PURELY OFFLINE: NO key, NO network. Exit 0 success, 2 usage error, 1 runtime error.
|
|
2884
|
+
*/
|
|
2885
|
+
function cmdDatasetTimestampRequest(argv) {
|
|
2886
|
+
let opts;
|
|
2887
|
+
try {
|
|
2888
|
+
opts = parseTimestampRequestArgs(argv);
|
|
2889
|
+
} catch (e) {
|
|
2890
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2891
|
+
return 2;
|
|
2892
|
+
}
|
|
2893
|
+
if (!opts.manifest) {
|
|
2894
|
+
process.stderr.write("error: `vh dataset timestamp-request` requires a <manifest>\n\n" + usage());
|
|
2895
|
+
return 2;
|
|
2896
|
+
}
|
|
2897
|
+
try {
|
|
2898
|
+
runDatasetTimestampRequest({ manifest: opts.manifest, out: opts.out, json: opts.json });
|
|
2899
|
+
} catch (e) {
|
|
2900
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2901
|
+
return 1;
|
|
2902
|
+
}
|
|
2903
|
+
return 0;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
/**
|
|
2907
|
+
* `vh dataset timestamp-wrap <manifest> --token <path|base64> [--out <p>] [--json]` — wrap the RFC-3161
|
|
2908
|
+
* token the human obtained from their TSA into a verifiable `*-attestation-timestamped` container, binding
|
|
2909
|
+
* it to the re-derived canonical SHA-256 digest. ERRORS CLEARLY (exit 1) if the token does not bind the
|
|
2910
|
+
* digest. PURELY OFFLINE: NO key, NO network. Exit 0 success, 2 usage error (missing manifest/--token,
|
|
2911
|
+
* unknown/incomplete flag), 1 runtime error (corrupt manifest, unparseable/non-binding token).
|
|
2912
|
+
*/
|
|
2913
|
+
function cmdDatasetTimestampWrap(argv) {
|
|
2914
|
+
let opts;
|
|
2915
|
+
try {
|
|
2916
|
+
opts = parseTimestampWrapArgs(argv);
|
|
2917
|
+
} catch (e) {
|
|
2918
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2919
|
+
return 2;
|
|
2920
|
+
}
|
|
2921
|
+
if (!opts.manifest) {
|
|
2922
|
+
process.stderr.write("error: `vh dataset timestamp-wrap` requires a <manifest>\n\n" + usage());
|
|
2923
|
+
return 2;
|
|
2924
|
+
}
|
|
2925
|
+
if (!opts.token) {
|
|
2926
|
+
process.stderr.write(
|
|
2927
|
+
"error: `vh dataset timestamp-wrap` requires --token <path|base64> (the RFC-3161 token from your TSA)\n\n" +
|
|
2928
|
+
usage()
|
|
2929
|
+
);
|
|
2930
|
+
return 2;
|
|
2931
|
+
}
|
|
2932
|
+
try {
|
|
2933
|
+
runDatasetTimestampWrap({
|
|
2934
|
+
manifest: opts.manifest,
|
|
2935
|
+
token: opts.token,
|
|
2936
|
+
out: opts.out,
|
|
2937
|
+
json: opts.json,
|
|
2938
|
+
});
|
|
2939
|
+
} catch (e) {
|
|
2940
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2941
|
+
return 1;
|
|
2942
|
+
}
|
|
2943
|
+
return 0;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
/**
|
|
2947
|
+
* `vh dataset verify-timestamp <container> [--manifest <m>] [--json]` — the OFFLINE independent-timestamp
|
|
2948
|
+
* verifier. Reads the timestamped container, re-derives the canonical bytes from the embedded UNSIGNED
|
|
2949
|
+
* payload, confirms digest == sha256(bytes), parses the RFC-3161 token, and confirms it BINDS that digest;
|
|
2950
|
+
* with --manifest it ALSO requires the embedded attestation to be byte-identical to the buyer's own
|
|
2951
|
+
* re-derived canonical bytes. Prints ACCEPTED (with the asserted genTime / TSA serial / policy OID) or
|
|
2952
|
+
* REJECTED naming which check failed. PURELY OFFLINE: no tree, no provider, no key, no network. Exit 0
|
|
2953
|
+
* ACCEPTED, 3 REJECTED (mirrors the family's 0/3 convention so a buyer's CI can gate), 2 on a usage error,
|
|
2954
|
+
* 1 on a runtime error (missing/corrupt container or manifest).
|
|
2955
|
+
*/
|
|
2956
|
+
function cmdDatasetVerifyTimestamp(argv) {
|
|
2957
|
+
let opts;
|
|
2958
|
+
try {
|
|
2959
|
+
opts = parseVerifyTimestampArgs(argv);
|
|
2960
|
+
} catch (e) {
|
|
2961
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2962
|
+
return 2;
|
|
2963
|
+
}
|
|
2964
|
+
if (!opts.container) {
|
|
2965
|
+
process.stderr.write(
|
|
2966
|
+
"error: `vh dataset verify-timestamp` requires a <container> (timestamped attestation path)\n\n" +
|
|
2967
|
+
usage()
|
|
2968
|
+
);
|
|
2969
|
+
return 2;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
let result;
|
|
2973
|
+
try {
|
|
2974
|
+
result = runDatasetVerifyTimestamp({
|
|
2975
|
+
container: opts.container,
|
|
2976
|
+
manifest: opts.manifest,
|
|
2977
|
+
json: opts.json,
|
|
2978
|
+
});
|
|
2979
|
+
} catch (e) {
|
|
2980
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
2981
|
+
return 1;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// Exit non-zero on REJECTED so a buyer's CI can gate (mirrors the family's 0/3 convention).
|
|
2985
|
+
return result.accepted ? 0 : 3;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
/**
|
|
2989
|
+
* `vh dataset prove --file <p> --manifest <m> [--out <p>] [--json]` — build an OFFLINE set-membership
|
|
2990
|
+
* proof that ONE file was a member of the manifest's dataset. NO key, NO network. Exit 0 on MEMBER, 3
|
|
2991
|
+
* on NOT A MEMBER (so scripts/CI can branch), 2 on a usage error, 1 on a runtime error.
|
|
2992
|
+
*/
|
|
2993
|
+
function cmdDatasetProve(argv) {
|
|
2994
|
+
let opts;
|
|
2995
|
+
try {
|
|
2996
|
+
opts = parseDatasetProveArgs(argv);
|
|
2997
|
+
} catch (e) {
|
|
2998
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
2999
|
+
return 2;
|
|
3000
|
+
}
|
|
3001
|
+
if (!opts.file) {
|
|
3002
|
+
process.stderr.write("error: `vh dataset prove` requires --file <path>\n\n" + usage());
|
|
3003
|
+
return 2;
|
|
3004
|
+
}
|
|
3005
|
+
if (!opts.manifest) {
|
|
3006
|
+
process.stderr.write("error: `vh dataset prove` requires --manifest <path>\n\n" + usage());
|
|
3007
|
+
return 2;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
let result;
|
|
3011
|
+
try {
|
|
3012
|
+
result = runDatasetProve({
|
|
3013
|
+
file: opts.file,
|
|
3014
|
+
manifest: opts.manifest,
|
|
3015
|
+
out: opts.out,
|
|
3016
|
+
json: opts.json,
|
|
3017
|
+
});
|
|
3018
|
+
} catch (e) {
|
|
3019
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3020
|
+
return 1;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// Exit non-zero when the file is NOT a member so scripts/CI can branch (mirrors `vh verify`/MISMATCH).
|
|
3024
|
+
return result.member ? 0 : 3;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
/**
|
|
3028
|
+
* `vh dataset verify-proof <proof> [--json]` — fold a membership proof artifact PURELY OFFLINE (no
|
|
3029
|
+
* dataset, no manifest, no key, no network). Exit 0 on CONFIRMED, 3 on REJECTED (mirrors `vh verify`),
|
|
3030
|
+
* 2 on a usage error, 1 on a runtime error (missing/corrupt artifact).
|
|
3031
|
+
*/
|
|
3032
|
+
function cmdDatasetVerifyProof(argv) {
|
|
3033
|
+
let opts;
|
|
3034
|
+
try {
|
|
3035
|
+
opts = parseDatasetVerifyProofArgs(argv);
|
|
3036
|
+
} catch (e) {
|
|
3037
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3038
|
+
return 2;
|
|
3039
|
+
}
|
|
3040
|
+
if (!opts.artifact) {
|
|
3041
|
+
process.stderr.write(
|
|
3042
|
+
"error: `vh dataset verify-proof` requires a <proof> (artifact path)\n\n" + usage()
|
|
3043
|
+
);
|
|
3044
|
+
return 2;
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
let result;
|
|
3048
|
+
try {
|
|
3049
|
+
result = runDatasetVerifyProof({ artifact: opts.artifact, json: opts.json });
|
|
3050
|
+
} catch (e) {
|
|
3051
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3052
|
+
return 1;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
// Exit non-zero on REJECTED so scripts/CI can branch (mirrors `vh verify`/MISMATCH).
|
|
3056
|
+
return result.status === "CONFIRMED" ? 0 : 3;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
/**
|
|
3060
|
+
* `vh parcel <subcommand>` — ProofParcel: tamper-evident B2B data-DELIVERY receipts over the shared
|
|
3061
|
+
* provenance core. Dispatches build/verify; an unknown/missing subcommand hard-errors with usage (exit 2,
|
|
3062
|
+
* parser parity with `vh dataset`).
|
|
3063
|
+
*/
|
|
3064
|
+
function cmdParcel(argv) {
|
|
3065
|
+
const [sub, ...rest] = argv;
|
|
3066
|
+
if (sub === "verify") {
|
|
3067
|
+
return cmdParcelVerify(rest);
|
|
3068
|
+
}
|
|
3069
|
+
if (sub === "attest") {
|
|
3070
|
+
return cmdParcelAttest(rest);
|
|
3071
|
+
}
|
|
3072
|
+
if (sub === "sign") {
|
|
3073
|
+
return cmdParcelSign(rest);
|
|
3074
|
+
}
|
|
3075
|
+
if (sub === "verify-attest") {
|
|
3076
|
+
return cmdParcelVerifyAttest(rest);
|
|
3077
|
+
}
|
|
3078
|
+
if (sub === "timestamp-request") {
|
|
3079
|
+
return cmdParcelTimestampRequest(rest);
|
|
3080
|
+
}
|
|
3081
|
+
if (sub === "timestamp-wrap") {
|
|
3082
|
+
return cmdParcelTimestampWrap(rest);
|
|
3083
|
+
}
|
|
3084
|
+
if (sub === "verify-timestamp") {
|
|
3085
|
+
return cmdParcelVerifyTimestamp(rest);
|
|
3086
|
+
}
|
|
3087
|
+
if (sub !== "build") {
|
|
3088
|
+
process.stderr.write(
|
|
3089
|
+
`error: unknown parcel subcommand: ${sub === undefined ? "(none)" : sub} ` +
|
|
3090
|
+
`(expected: build | verify | attest | sign | verify-attest | timestamp-request | timestamp-wrap | verify-timestamp)\n\n` + usage()
|
|
3091
|
+
);
|
|
3092
|
+
return 2;
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
let opts;
|
|
3096
|
+
try {
|
|
3097
|
+
opts = parseParcelBuildArgs(rest);
|
|
3098
|
+
} catch (e) {
|
|
3099
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3100
|
+
return 2;
|
|
3101
|
+
}
|
|
3102
|
+
if (!opts.dir) {
|
|
3103
|
+
process.stderr.write("error: `vh parcel build` requires a <dir>\n\n" + usage());
|
|
3104
|
+
return 2;
|
|
3105
|
+
}
|
|
3106
|
+
if (!opts.out) {
|
|
3107
|
+
process.stderr.write("error: `vh parcel build` requires --out <path>\n\n" + usage());
|
|
3108
|
+
return 2;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
// Optional untrusted per-file hints: read + parse the JSON file BEFORE walking the tree so a malformed
|
|
3112
|
+
// hints file hard-errors early (and never half-writes a manifest). parcel.js validates every hinted
|
|
3113
|
+
// path exists in the tree.
|
|
3114
|
+
let hints;
|
|
3115
|
+
if (opts.hints !== undefined) {
|
|
3116
|
+
const fs = require("fs");
|
|
3117
|
+
let raw;
|
|
3118
|
+
try {
|
|
3119
|
+
raw = fs.readFileSync(opts.hints, "utf8");
|
|
3120
|
+
} catch (e) {
|
|
3121
|
+
process.stderr.write(`error: cannot read --hints file ${opts.hints}: ${e.message}\n`);
|
|
3122
|
+
return 1;
|
|
3123
|
+
}
|
|
3124
|
+
try {
|
|
3125
|
+
hints = JSON.parse(raw);
|
|
3126
|
+
} catch (e) {
|
|
3127
|
+
process.stderr.write(`error: --hints file ${opts.hints} is not valid JSON: ${e.message}\n`);
|
|
3128
|
+
return 1;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
// Assemble the OPTIONAL, UNTRUSTED parcel block from the dedicated flags (omitting absent fields so an
|
|
3133
|
+
// empty block never litters the manifest). runParcelBuild records it as self-asserted metadata only.
|
|
3134
|
+
const parcel = {};
|
|
3135
|
+
if (opts.parcelId !== undefined) parcel.parcelId = opts.parcelId;
|
|
3136
|
+
if (opts.sender !== undefined) parcel.sender = opts.sender;
|
|
3137
|
+
if (opts.recipient !== undefined) parcel.recipient = opts.recipient;
|
|
3138
|
+
|
|
3139
|
+
try {
|
|
3140
|
+
runParcelBuild({
|
|
3141
|
+
dir: opts.dir,
|
|
3142
|
+
out: opts.out,
|
|
3143
|
+
hints,
|
|
3144
|
+
parcel: Object.keys(parcel).length > 0 ? parcel : undefined,
|
|
3145
|
+
json: opts.json,
|
|
3146
|
+
});
|
|
3147
|
+
} catch (e) {
|
|
3148
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3149
|
+
return 1;
|
|
3150
|
+
}
|
|
3151
|
+
return 0;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
/**
|
|
3155
|
+
* `vh parcel verify <dir> --manifest <p>` — re-derive the parcel root from a FRESH copy on disk and
|
|
3156
|
+
* compare it to the manifest's (UNTRUSTED) recorded root, plus a precise per-file diff. OFFLINE: no
|
|
3157
|
+
* provider, no key, no network. Exit 0 on MATCH, 3 on MISMATCH (mirrors `vh dataset verify` so all verify
|
|
3158
|
+
* gates share ONE exit contract), 2 on a usage error, 1 on a runtime error (missing/corrupt/foreign
|
|
3159
|
+
* manifest, bad dir).
|
|
3160
|
+
*/
|
|
3161
|
+
function cmdParcelVerify(argv) {
|
|
3162
|
+
let opts;
|
|
3163
|
+
try {
|
|
3164
|
+
opts = parseParcelVerifyArgs(argv);
|
|
3165
|
+
} catch (e) {
|
|
3166
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3167
|
+
return 2;
|
|
3168
|
+
}
|
|
3169
|
+
if (!opts.dir) {
|
|
3170
|
+
process.stderr.write("error: `vh parcel verify` requires a <dir>\n\n" + usage());
|
|
3171
|
+
return 2;
|
|
3172
|
+
}
|
|
3173
|
+
if (!opts.manifest) {
|
|
3174
|
+
process.stderr.write("error: `vh parcel verify` requires --manifest <path>\n\n" + usage());
|
|
3175
|
+
return 2;
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
let result;
|
|
3179
|
+
try {
|
|
3180
|
+
result = runParcelVerify({ dir: opts.dir, manifest: opts.manifest, json: opts.json });
|
|
3181
|
+
} catch (e) {
|
|
3182
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3183
|
+
return 1;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
// Exit non-zero on a tamper/MISMATCH so scripts and CI can branch on it (mirrors `vh dataset verify`).
|
|
3187
|
+
return result.status === "MATCH" ? 0 : 3;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
/**
|
|
3191
|
+
* `vh parcel attest <manifest> [--out <p>] [--json]` — emit the canonical, byte-deterministic UNSIGNED
|
|
3192
|
+
* parcel-attestation payload a human signing/timestamp trust-root (P-3) will sign. Reads the parcel
|
|
3193
|
+
* manifest strictly and commits to the parcel identity (root + fileCount + canonical manifestDigest) plus
|
|
3194
|
+
* the standing trust caveat, with explicit `signed:false`/`signature:null` markers. PURELY OFFLINE: no
|
|
3195
|
+
* tree, no provider, no key, no network. Exit 0 on success, 2 on a usage error, 1 on a runtime error.
|
|
3196
|
+
*/
|
|
3197
|
+
function cmdParcelAttest(argv) {
|
|
3198
|
+
let opts;
|
|
3199
|
+
try {
|
|
3200
|
+
opts = parseParcelAttestArgs(argv);
|
|
3201
|
+
} catch (e) {
|
|
3202
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3203
|
+
return 2;
|
|
3204
|
+
}
|
|
3205
|
+
if (!opts.manifest) {
|
|
3206
|
+
process.stderr.write("error: `vh parcel attest` requires a <manifest>\n\n" + usage());
|
|
3207
|
+
return 2;
|
|
3208
|
+
}
|
|
3209
|
+
try {
|
|
3210
|
+
runParcelAttest({ manifest: opts.manifest, out: opts.out, json: opts.json });
|
|
3211
|
+
} catch (e) {
|
|
3212
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3213
|
+
return 1;
|
|
3214
|
+
}
|
|
3215
|
+
return 0;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
/**
|
|
3219
|
+
* `vh parcel sign <manifest> --key-env <VAR> | --key-file <path> [--out <p>] [--json]` — sign the UNSIGNED
|
|
3220
|
+
* parcel attestation with a HUMAN-supplied key and emit the SIGNED container (the THIN parcel parallel to
|
|
3221
|
+
* `vh dataset sign`). The key is read, used to build an in-process ethers Wallet, used to sign, and
|
|
3222
|
+
* discarded — NEVER generated, persisted, or logged; output prints ONLY the PUBLIC signer address, the
|
|
3223
|
+
* output path, and the scheme. PURELY OFFLINE. The output is accepted by `vh parcel verify-attest` unchanged.
|
|
3224
|
+
*
|
|
3225
|
+
* EXIT CODES: 0 success; 2 on a usage error (missing/extra positional, unknown/incomplete flag, NEITHER or
|
|
3226
|
+
* BOTH of --key-env/--key-file); 1 on a runtime error (missing env var, unreadable key file, malformed/zero
|
|
3227
|
+
* key, corrupt/missing manifest). No error message ever includes the key material.
|
|
3228
|
+
*/
|
|
3229
|
+
async function cmdParcelSign(argv) {
|
|
3230
|
+
let opts;
|
|
3231
|
+
try {
|
|
3232
|
+
opts = parseSignArgs(argv);
|
|
3233
|
+
} catch (e) {
|
|
3234
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3235
|
+
return 2;
|
|
3236
|
+
}
|
|
3237
|
+
if (!opts.manifest) {
|
|
3238
|
+
process.stderr.write("error: `vh parcel sign` requires a <manifest>\n\n" + usage());
|
|
3239
|
+
return 2;
|
|
3240
|
+
}
|
|
3241
|
+
const hasEnv = opts.keyEnv !== undefined;
|
|
3242
|
+
const hasFile = opts.keyFile !== undefined;
|
|
3243
|
+
if (!hasEnv && !hasFile) {
|
|
3244
|
+
process.stderr.write(
|
|
3245
|
+
"error: `vh parcel sign` requires EXACTLY ONE signing-key source: --key-env <VAR> or " +
|
|
3246
|
+
"--key-file <path>\n\n" + usage()
|
|
3247
|
+
);
|
|
3248
|
+
return 2;
|
|
3249
|
+
}
|
|
3250
|
+
if (hasEnv && hasFile) {
|
|
3251
|
+
process.stderr.write(
|
|
3252
|
+
"error: --key-env and --key-file are mutually exclusive; pass EXACTLY ONE signing-key source\n\n" +
|
|
3253
|
+
usage()
|
|
3254
|
+
);
|
|
3255
|
+
return 2;
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
try {
|
|
3259
|
+
await runParcelSign({
|
|
3260
|
+
manifest: opts.manifest,
|
|
3261
|
+
keyEnv: opts.keyEnv,
|
|
3262
|
+
keyFile: opts.keyFile,
|
|
3263
|
+
out: opts.out,
|
|
3264
|
+
json: opts.json,
|
|
3265
|
+
});
|
|
3266
|
+
} catch (e) {
|
|
3267
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3268
|
+
return 1;
|
|
3269
|
+
}
|
|
3270
|
+
return 0;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
/**
|
|
3274
|
+
* `vh parcel verify-attest <signed> [--manifest <m>] [--signer <addr>] [--json]` — OFFLINE verify a SIGNED
|
|
3275
|
+
* parcel-attestation container. Reads the container strictly, recovers the signer from the embedded
|
|
3276
|
+
* canonical bytes + signature, and confirms it equals the container's `signer`; with --signer it pins the
|
|
3277
|
+
* expected sender; with --manifest it confirms the signature binds the recipient's own parcel. PURELY
|
|
3278
|
+
* OFFLINE: no tree, no provider, no key, no network. Exit 0 ACCEPTED, 3 REJECTED (mirrors the family's 0/3
|
|
3279
|
+
* convention so a recipient's CI can gate), 2 on a usage error, 1 on a runtime error.
|
|
3280
|
+
*/
|
|
3281
|
+
function cmdParcelVerifyAttest(argv) {
|
|
3282
|
+
let opts;
|
|
3283
|
+
try {
|
|
3284
|
+
opts = parseParcelVerifyAttestArgs(argv);
|
|
3285
|
+
} catch (e) {
|
|
3286
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3287
|
+
return 2;
|
|
3288
|
+
}
|
|
3289
|
+
if (!opts.signed) {
|
|
3290
|
+
process.stderr.write(
|
|
3291
|
+
"error: `vh parcel verify-attest` requires a <signed> (signed attestation container path)\n\n" +
|
|
3292
|
+
usage()
|
|
3293
|
+
);
|
|
3294
|
+
return 2;
|
|
3295
|
+
}
|
|
3296
|
+
// Validate the --signer address SHAPE up front (when given) so a malformed expected sender is a usage
|
|
3297
|
+
// error (2), never a runtime throw mid-verify. PURELY OFFLINE — no network here either.
|
|
3298
|
+
if (opts.signer !== undefined) {
|
|
3299
|
+
const ethers = require("ethers");
|
|
3300
|
+
if (!ethers.isAddress(opts.signer)) {
|
|
3301
|
+
process.stderr.write(
|
|
3302
|
+
`error: invalid --signer address: ${opts.signer} (expected a 20-byte 0x-hex address)\n\n` +
|
|
3303
|
+
usage()
|
|
3304
|
+
);
|
|
3305
|
+
return 2;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
// Validate the --as-of SHAPE up front (when given) so a malformed decision instant is a usage error (2),
|
|
3309
|
+
// never a runtime throw mid-verify. --as-of only matters under --revocations.
|
|
3310
|
+
{
|
|
3311
|
+
const asOfErr = validateVerifyAsOfFlags(opts);
|
|
3312
|
+
if (asOfErr) {
|
|
3313
|
+
process.stderr.write(`error: ${asOfErr}\n\n` + usage());
|
|
3314
|
+
return 2;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
let result;
|
|
3319
|
+
try {
|
|
3320
|
+
result = runParcelVerifyAttest({
|
|
3321
|
+
signed: opts.signed,
|
|
3322
|
+
manifest: opts.manifest,
|
|
3323
|
+
signer: opts.signer,
|
|
3324
|
+
revocations: opts.revocations,
|
|
3325
|
+
asOf: opts.asOf,
|
|
3326
|
+
json: opts.json,
|
|
3327
|
+
});
|
|
3328
|
+
} catch (e) {
|
|
3329
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3330
|
+
return 1;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
// Exit non-zero on REJECTED so a recipient's CI can gate (mirrors the family's 0/3 convention).
|
|
3334
|
+
return result.accepted ? 0 : 3;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
/**
|
|
3338
|
+
* `vh parcel timestamp-request <manifest> [--out <p>] [--json]` — emit the SHA-256 digest of the canonical
|
|
3339
|
+
* UNSIGNED parcel-attestation bytes (the messageImprint a human submits to their RFC-3161 TSA), plus a
|
|
3340
|
+
* recipe. PURELY OFFLINE: NO key, NO network. Exit 0 success, 2 usage error, 1 runtime error.
|
|
3341
|
+
*/
|
|
3342
|
+
function cmdParcelTimestampRequest(argv) {
|
|
3343
|
+
let opts;
|
|
3344
|
+
try {
|
|
3345
|
+
opts = parseTimestampRequestArgs(argv);
|
|
3346
|
+
} catch (e) {
|
|
3347
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3348
|
+
return 2;
|
|
3349
|
+
}
|
|
3350
|
+
if (!opts.manifest) {
|
|
3351
|
+
process.stderr.write("error: `vh parcel timestamp-request` requires a <manifest>\n\n" + usage());
|
|
3352
|
+
return 2;
|
|
3353
|
+
}
|
|
3354
|
+
try {
|
|
3355
|
+
runParcelTimestampRequest({ manifest: opts.manifest, out: opts.out, json: opts.json });
|
|
3356
|
+
} catch (e) {
|
|
3357
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3358
|
+
return 1;
|
|
3359
|
+
}
|
|
3360
|
+
return 0;
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
/**
|
|
3364
|
+
* `vh parcel timestamp-wrap <manifest> --token <path|base64> [--out <p>] [--json]` — wrap the RFC-3161 token
|
|
3365
|
+
* the human obtained from their TSA into a verifiable `parcel-attestation-timestamped` container, binding it
|
|
3366
|
+
* to the re-derived canonical SHA-256 digest. ERRORS CLEARLY (exit 1) if the token does not bind the digest.
|
|
3367
|
+
* PURELY OFFLINE: NO key, NO network. Exit 0 success, 2 usage error, 1 runtime error.
|
|
3368
|
+
*/
|
|
3369
|
+
function cmdParcelTimestampWrap(argv) {
|
|
3370
|
+
let opts;
|
|
3371
|
+
try {
|
|
3372
|
+
opts = parseTimestampWrapArgs(argv);
|
|
3373
|
+
} catch (e) {
|
|
3374
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3375
|
+
return 2;
|
|
3376
|
+
}
|
|
3377
|
+
if (!opts.manifest) {
|
|
3378
|
+
process.stderr.write("error: `vh parcel timestamp-wrap` requires a <manifest>\n\n" + usage());
|
|
3379
|
+
return 2;
|
|
3380
|
+
}
|
|
3381
|
+
if (!opts.token) {
|
|
3382
|
+
process.stderr.write(
|
|
3383
|
+
"error: `vh parcel timestamp-wrap` requires --token <path|base64> (the RFC-3161 token from your TSA)\n\n" +
|
|
3384
|
+
usage()
|
|
3385
|
+
);
|
|
3386
|
+
return 2;
|
|
3387
|
+
}
|
|
3388
|
+
try {
|
|
3389
|
+
runParcelTimestampWrap({
|
|
3390
|
+
manifest: opts.manifest,
|
|
3391
|
+
token: opts.token,
|
|
3392
|
+
out: opts.out,
|
|
3393
|
+
json: opts.json,
|
|
3394
|
+
});
|
|
3395
|
+
} catch (e) {
|
|
3396
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3397
|
+
return 1;
|
|
3398
|
+
}
|
|
3399
|
+
return 0;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
/**
|
|
3403
|
+
* `vh parcel verify-timestamp <container> [--manifest <m>] [--json]` — the OFFLINE independent-timestamp
|
|
3404
|
+
* verifier for ProofParcel. THIN parallel to `vh dataset verify-timestamp`: reads the timestamped parcel
|
|
3405
|
+
* container, confirms digest == sha256(canonical bytes) + the RFC-3161 token binds it, and (with --manifest)
|
|
3406
|
+
* binds the timestamp to the recipient's own parcel. PURELY OFFLINE: no tree, no provider, no key, no
|
|
3407
|
+
* network. Exit 0 ACCEPTED, 3 REJECTED (mirrors the family's 0/3 convention), 2 usage error, 1 runtime error.
|
|
3408
|
+
*/
|
|
3409
|
+
function cmdParcelVerifyTimestamp(argv) {
|
|
3410
|
+
let opts;
|
|
3411
|
+
try {
|
|
3412
|
+
opts = parseVerifyTimestampArgs(argv);
|
|
3413
|
+
} catch (e) {
|
|
3414
|
+
process.stderr.write(`error: ${e.message}\n\n` + usage());
|
|
3415
|
+
return 2;
|
|
3416
|
+
}
|
|
3417
|
+
if (!opts.container) {
|
|
3418
|
+
process.stderr.write(
|
|
3419
|
+
"error: `vh parcel verify-timestamp` requires a <container> (timestamped attestation path)\n\n" +
|
|
3420
|
+
usage()
|
|
3421
|
+
);
|
|
3422
|
+
return 2;
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
let result;
|
|
3426
|
+
try {
|
|
3427
|
+
result = runParcelVerifyTimestamp({
|
|
3428
|
+
container: opts.container,
|
|
3429
|
+
manifest: opts.manifest,
|
|
3430
|
+
json: opts.json,
|
|
3431
|
+
});
|
|
3432
|
+
} catch (e) {
|
|
3433
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
3434
|
+
return 1;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// Exit non-zero on REJECTED so a recipient's CI can gate (mirrors the family's 0/3 convention).
|
|
3438
|
+
return result.accepted ? 0 : 3;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// ---------------------------------------------------------------------------
|
|
3442
|
+
// `vh serve-verify [--port <n>] [--host <h>] [--max-body <bytes>]` (T-59.2)
|
|
3443
|
+
// ---------------------------------------------------------------------------
|
|
3444
|
+
//
|
|
3445
|
+
// A tiny, dependency-free (Node-core `http` ONLY) loopback-only HTTP VERIFY server. It fronts the PURE
|
|
3446
|
+
// `verifyRequest` core (cli/serve-verify.js) — POST /verify a seal / signed container, get a JSON verdict
|
|
3447
|
+
// on a CI-mappable status (200 ACCEPTED / 422 REJECTED / 400 bad request); GET /healthz -> { ok:true }.
|
|
3448
|
+
//
|
|
3449
|
+
// VERIFY-ONLY: it never signs, holds NO private key, and writes NO file (the whole request path is I/O-free
|
|
3450
|
+
// but for the socket). It binds LOOPBACK (127.0.0.1) by default — a non-loopback interface is NOT served
|
|
3451
|
+
// unless the operator explicitly passes --host. Exposing it publicly is a HUMAN deploy step (never
|
|
3452
|
+
// auto-deployed). A verified seal is NOT a timestamp (P-3). The transport + banner live in
|
|
3453
|
+
// cli/serve-verify-http.js; this is only the CLI plumbing (parse flags, bind, print, propagate exit codes).
|
|
3454
|
+
|
|
3455
|
+
// Parse `serve-verify` argv into { port, host, maxBody }. Flags only (no positionals). A --port must be an
|
|
3456
|
+
// integer in 0..65535 (0 = OS-chosen ephemeral port); a --max-body must be a positive integer. An unknown
|
|
3457
|
+
// flag, a positional, or a bad numeric value is a USAGE error (exit 2), never silently coerced.
|
|
3458
|
+
function parseServeVerifyArgs(argv) {
|
|
3459
|
+
const opts = { port: undefined, host: undefined, maxBody: undefined };
|
|
3460
|
+
const positionals = [];
|
|
3461
|
+
const intFlag = (raw, flag, { allowZero }) => {
|
|
3462
|
+
const s = String(raw == null ? "" : raw);
|
|
3463
|
+
if (!/^\d+$/.test(s)) throw new Error(`${flag} must be a non-negative integer (got "${raw}")`);
|
|
3464
|
+
const n = Number(s);
|
|
3465
|
+
if (!allowZero && n === 0) throw new Error(`${flag} must be a positive integer (got "${raw}")`);
|
|
3466
|
+
return n;
|
|
3467
|
+
};
|
|
3468
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3469
|
+
const a = argv[i];
|
|
3470
|
+
switch (a) {
|
|
3471
|
+
case "--port": {
|
|
3472
|
+
const raw = argv[++i];
|
|
3473
|
+
if (raw === undefined) throw new Error("--port requires a value");
|
|
3474
|
+
const n = intFlag(raw, "--port", { allowZero: true });
|
|
3475
|
+
if (n > 65535) throw new Error(`--port must be in 0..65535 (got "${raw}")`);
|
|
3476
|
+
opts.port = n;
|
|
3477
|
+
break;
|
|
3478
|
+
}
|
|
3479
|
+
case "--host":
|
|
3480
|
+
opts.host = argv[++i];
|
|
3481
|
+
if (opts.host === undefined) throw new Error("--host requires a value");
|
|
3482
|
+
break;
|
|
3483
|
+
case "--max-body": {
|
|
3484
|
+
const raw = argv[++i];
|
|
3485
|
+
if (raw === undefined) throw new Error("--max-body requires a value");
|
|
3486
|
+
opts.maxBody = intFlag(raw, "--max-body", { allowZero: false });
|
|
3487
|
+
break;
|
|
3488
|
+
}
|
|
3489
|
+
default:
|
|
3490
|
+
if (a && a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
3491
|
+
positionals.push(a);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
if (positionals.length > 0) {
|
|
3495
|
+
throw new Error(`unexpected argument: ${positionals[0]} (serve-verify takes no positionals)`);
|
|
3496
|
+
}
|
|
3497
|
+
return opts;
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
// runServeVerify binds the server and prints the banner. It does NOT block; it resolves to
|
|
3501
|
+
// { code, server, url } once listening (or on a bind failure). `io` is injectable so a test can supply a
|
|
3502
|
+
// createServer / write sink and confirm the wiring; the default builds + listens on a real loopback socket.
|
|
3503
|
+
function runServeVerify(opts, io = {}) {
|
|
3504
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
3505
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
3506
|
+
|
|
3507
|
+
const port = opts.port == null ? serveVerifyHttp.DEFAULT_PORT : opts.port;
|
|
3508
|
+
const host = opts.host == null ? serveVerifyHttp.DEFAULT_HOST : opts.host;
|
|
3509
|
+
const maxBodyBytes = opts.maxBody == null ? serveVerifyHttp.DEFAULT_MAX_BODY_BYTES : opts.maxBody;
|
|
3510
|
+
|
|
3511
|
+
const srv = (io.createServer || serveVerifyHttp.createServer)({ maxBodyBytes });
|
|
3512
|
+
|
|
3513
|
+
return new Promise((resolve) => {
|
|
3514
|
+
let settled = false;
|
|
3515
|
+
// Surface a bind failure (EADDRINUSE / EACCES / bad --host) as a clear IO error AND resolve with code 1
|
|
3516
|
+
// so the failure propagates to the process exit code (without this resolve the Promise would hang; the
|
|
3517
|
+
// failed server holds no handles, so Node would exit 0 on its own — collapsing the IO(1) failure class).
|
|
3518
|
+
srv.on("error", (e) => {
|
|
3519
|
+
if (settled) return;
|
|
3520
|
+
settled = true;
|
|
3521
|
+
writeErr(`error: cannot start vh serve-verify: ${e.message}\n`);
|
|
3522
|
+
resolve({ code: 1, server: srv, url: null, error: e });
|
|
3523
|
+
});
|
|
3524
|
+
srv.listen(port, host, () => {
|
|
3525
|
+
if (settled) return;
|
|
3526
|
+
settled = true;
|
|
3527
|
+
// When --port 0 was given the OS chose the actual port; report the real one.
|
|
3528
|
+
const bound = srv.address();
|
|
3529
|
+
const realPort = bound && typeof bound === "object" ? bound.port : port;
|
|
3530
|
+
const url = `http://${host}:${realPort}/`;
|
|
3531
|
+
write(serveVerifyHttp.banner(url, host));
|
|
3532
|
+
resolve({ code: 0, server: srv, url });
|
|
3533
|
+
});
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
// cmdServeVerify: parse argv, then bind + print. The dispatcher awaits a PLAIN exit code, so this resolves
|
|
3538
|
+
// to a NUMBER: a bad flag resolves immediately to 2 (usage) BEFORE binding; a bind failure resolves to 1
|
|
3539
|
+
// (IO); on success it binds, prints the banner, and returns a Promise that NEVER resolves — the open socket
|
|
3540
|
+
// keeps the event loop alive so the door stays up until the operator kills it (Ctrl-C), like a normal
|
|
3541
|
+
// server. Tests call `runServeVerify` directly (which resolves with the live { server } handle) so they can
|
|
3542
|
+
// hit it and close it; the bind-failure path is exercised through cmdServeVerify to assert the exit code.
|
|
3543
|
+
function cmdServeVerify(argv, io = {}) {
|
|
3544
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
3545
|
+
let opts;
|
|
3546
|
+
try {
|
|
3547
|
+
opts = parseServeVerifyArgs(argv);
|
|
3548
|
+
} catch (e) {
|
|
3549
|
+
writeErr(`error: ${e.message}\n\n` + usage());
|
|
3550
|
+
return Promise.resolve(2);
|
|
3551
|
+
}
|
|
3552
|
+
return runServeVerify(opts, io).then((res) => {
|
|
3553
|
+
if (res.code !== 0) return res.code;
|
|
3554
|
+
return new Promise(() => {});
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
// ---------------------------------------------------------------------------
|
|
3559
|
+
// `vh fulfill-webhook ... --secret-env <VAR> --binding <file> (--key-env|--key-file) --out <dir>` (T-62.2)
|
|
3560
|
+
// ---------------------------------------------------------------------------
|
|
3561
|
+
//
|
|
3562
|
+
// A tiny, dependency-free (Node-core `http` ONLY) loopback-only HTTP FULFILLMENT webhook. It wires the pure
|
|
3563
|
+
// intake core (cli/core/fulfill-intake.js) to the shipped evidence license fulfiller: on each authenticated
|
|
3564
|
+
// POST it MINTS the signed license `vh evidence license fulfill` would mint and DELIVERS it to --out,
|
|
3565
|
+
// idempotently. This is the CLI plumbing (parse flags, load the key/secret/binding/catalog, bind, print,
|
|
3566
|
+
// propagate exit codes); the transport + fulfillment live in cli/fulfill-webhook-http.js.
|
|
3567
|
+
//
|
|
3568
|
+
// KEY / SECRET HYGIENE: the signing secret comes ONLY from --secret-env and the vendor key ONLY from EXACTLY
|
|
3569
|
+
// ONE of --key-env/--key-file (via the SAME read-used-discarded loadSigningWallet the sign path uses). The
|
|
3570
|
+
// raw key string exists only long enough to build an in-process Wallet; neither the key nor the secret is
|
|
3571
|
+
// ever written to disk or logged (error messages name only the SOURCE var/path, never the material).
|
|
3572
|
+
|
|
3573
|
+
// Parse `fulfill-webhook` argv. Flags only (no positionals). A --port must be an integer in 0..65535 (0 =
|
|
3574
|
+
// ephemeral); --max-body / --tolerance are validated numerics. An unknown flag, a positional, or a bad
|
|
3575
|
+
// numeric value is a USAGE error, never silently coerced. Required-flag presence is checked in run (so a
|
|
3576
|
+
// missing --secret-env/--binding/--out/key-source is a clean usage error with the full flag list).
|
|
3577
|
+
function parseFulfillWebhookArgs(argv) {
|
|
3578
|
+
const opts = {
|
|
3579
|
+
port: undefined,
|
|
3580
|
+
host: undefined,
|
|
3581
|
+
maxBody: undefined,
|
|
3582
|
+
tolerance: undefined,
|
|
3583
|
+
secretEnv: undefined,
|
|
3584
|
+
binding: undefined,
|
|
3585
|
+
keyEnv: undefined,
|
|
3586
|
+
keyFile: undefined,
|
|
3587
|
+
out: undefined,
|
|
3588
|
+
catalog: undefined,
|
|
3589
|
+
};
|
|
3590
|
+
const positionals = [];
|
|
3591
|
+
const intFlag = (raw, flag, { allowZero }) => {
|
|
3592
|
+
const s = String(raw == null ? "" : raw);
|
|
3593
|
+
if (!/^\d+$/.test(s)) throw new Error(`${flag} must be a non-negative integer (got "${raw}")`);
|
|
3594
|
+
const n = Number(s);
|
|
3595
|
+
if (!allowZero && n === 0) throw new Error(`${flag} must be a positive integer (got "${raw}")`);
|
|
3596
|
+
return n;
|
|
3597
|
+
};
|
|
3598
|
+
const need = (flag, i) => {
|
|
3599
|
+
const v = argv[i];
|
|
3600
|
+
if (v === undefined) throw new Error(`${flag} requires a value`);
|
|
3601
|
+
return v;
|
|
3602
|
+
};
|
|
3603
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3604
|
+
const a = argv[i];
|
|
3605
|
+
switch (a) {
|
|
3606
|
+
case "--port": {
|
|
3607
|
+
const n = intFlag(need("--port", ++i), "--port", { allowZero: true });
|
|
3608
|
+
if (n > 65535) throw new Error(`--port must be in 0..65535 (got "${argv[i]}")`);
|
|
3609
|
+
opts.port = n;
|
|
3610
|
+
break;
|
|
3611
|
+
}
|
|
3612
|
+
case "--host":
|
|
3613
|
+
opts.host = need("--host", ++i);
|
|
3614
|
+
break;
|
|
3615
|
+
case "--max-body":
|
|
3616
|
+
opts.maxBody = intFlag(need("--max-body", ++i), "--max-body", { allowZero: false });
|
|
3617
|
+
break;
|
|
3618
|
+
case "--tolerance":
|
|
3619
|
+
opts.tolerance = intFlag(need("--tolerance", ++i), "--tolerance", { allowZero: true });
|
|
3620
|
+
break;
|
|
3621
|
+
case "--secret-env":
|
|
3622
|
+
opts.secretEnv = need("--secret-env", ++i);
|
|
3623
|
+
break;
|
|
3624
|
+
case "--binding":
|
|
3625
|
+
opts.binding = need("--binding", ++i);
|
|
3626
|
+
break;
|
|
3627
|
+
case "--key-env":
|
|
3628
|
+
opts.keyEnv = need("--key-env", ++i);
|
|
3629
|
+
break;
|
|
3630
|
+
case "--key-file":
|
|
3631
|
+
opts.keyFile = need("--key-file", ++i);
|
|
3632
|
+
break;
|
|
3633
|
+
case "--out":
|
|
3634
|
+
opts.out = need("--out", ++i);
|
|
3635
|
+
break;
|
|
3636
|
+
case "--catalog":
|
|
3637
|
+
opts.catalog = need("--catalog", ++i);
|
|
3638
|
+
break;
|
|
3639
|
+
default:
|
|
3640
|
+
if (a && a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
3641
|
+
positionals.push(a);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
if (positionals.length > 0) {
|
|
3645
|
+
throw new Error(`unexpected argument: ${positionals[0]} (fulfill-webhook takes no positionals)`);
|
|
3646
|
+
}
|
|
3647
|
+
return opts;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
// runFulfillWebhook: validate config (required flags, secret, binding, catalog, key, --out dir), build the
|
|
3651
|
+
// handler config, then bind + print. Resolves to { code, server, url } once listening (or on a config/bind
|
|
3652
|
+
// failure). ALL config errors resolve BEFORE any socket is bound. `io` is injectable so a test can supply a
|
|
3653
|
+
// createServer / write sinks / an injected `now` clock and confirm the wiring; the default builds + listens
|
|
3654
|
+
// on a real loopback socket. On a config error it resolves { code: 2 }; on a bind error { code: 1 }.
|
|
3655
|
+
function runFulfillWebhook(opts, io = {}) {
|
|
3656
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
3657
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
3658
|
+
|
|
3659
|
+
const fs = require("fs");
|
|
3660
|
+
const path = require("path");
|
|
3661
|
+
const coreAttestation = require("./core/attestation");
|
|
3662
|
+
const evidencePlans = require("./core/evidence-plans");
|
|
3663
|
+
const fulfillIntake = require("./core/fulfill-intake");
|
|
3664
|
+
const evidence = require("./evidence");
|
|
3665
|
+
|
|
3666
|
+
// ---- required flags (a clear, key-free message per missing one) ----------
|
|
3667
|
+
for (const [flag, val] of [
|
|
3668
|
+
["--secret-env", opts.secretEnv],
|
|
3669
|
+
["--binding", opts.binding],
|
|
3670
|
+
["--out", opts.out],
|
|
3671
|
+
]) {
|
|
3672
|
+
if (val == null) {
|
|
3673
|
+
writeErr(`error: \`vh fulfill-webhook\` requires ${flag}\n`);
|
|
3674
|
+
return Promise.resolve({ code: 2 });
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
// ---- the signing SECRET (from --secret-env only; name the VAR, never the value) ----
|
|
3679
|
+
const secret = process.env[opts.secretEnv];
|
|
3680
|
+
if (secret === undefined || secret === "") {
|
|
3681
|
+
writeErr(`error: environment variable ${opts.secretEnv} is not set (or empty); it must hold the webhook signing secret\n`);
|
|
3682
|
+
return Promise.resolve({ code: 2 });
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
// ---- the plan catalog (bundled DRAFT by default) + the price binding -------
|
|
3686
|
+
let catalog;
|
|
3687
|
+
try {
|
|
3688
|
+
const catalogPath = opts.catalog != null ? path.resolve(opts.catalog) : evidence.BUNDLED_EVIDENCE_CATALOG;
|
|
3689
|
+
catalog = evidencePlans.validateEvidencePlanCatalog(JSON.parse(fs.readFileSync(catalogPath, "utf8")));
|
|
3690
|
+
} catch (e) {
|
|
3691
|
+
writeErr(`error: cannot load evidence plan catalog: ${e.message}\n`);
|
|
3692
|
+
return Promise.resolve({ code: 2 });
|
|
3693
|
+
}
|
|
3694
|
+
let binding;
|
|
3695
|
+
try {
|
|
3696
|
+
binding = fulfillIntake.validateEvidencePriceBinding(
|
|
3697
|
+
JSON.parse(fs.readFileSync(path.resolve(opts.binding), "utf8")),
|
|
3698
|
+
catalog
|
|
3699
|
+
);
|
|
3700
|
+
} catch (e) {
|
|
3701
|
+
writeErr(`error: cannot load --binding ${opts.binding}: ${e.message}\n`);
|
|
3702
|
+
return Promise.resolve({ code: 2 });
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// ---- the VENDOR key (EXACTLY ONE of --key-env/--key-file; read-used-held-in-memory, never persisted) ----
|
|
3706
|
+
let wallet;
|
|
3707
|
+
try {
|
|
3708
|
+
({ wallet } = coreAttestation.loadSigningWallet({ keyEnv: opts.keyEnv, keyFile: opts.keyFile }));
|
|
3709
|
+
} catch (e) {
|
|
3710
|
+
writeErr(`error: ${e.message}\n`);
|
|
3711
|
+
return Promise.resolve({ code: 2 });
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
// ---- --out must be an EXISTING directory (never cwd; fail loudly on a typo) ----
|
|
3715
|
+
const outDir = path.resolve(opts.out);
|
|
3716
|
+
try {
|
|
3717
|
+
if (!fs.statSync(outDir).isDirectory()) throw new Error("not a directory");
|
|
3718
|
+
} catch (_e) {
|
|
3719
|
+
writeErr(`error: --out ${opts.out} must be an existing directory (create it first; the loop never writes to cwd)\n`);
|
|
3720
|
+
return Promise.resolve({ code: 2 });
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
const port = opts.port == null ? fulfillWebhookHttp.DEFAULT_PORT : opts.port;
|
|
3724
|
+
const host = opts.host == null ? fulfillWebhookHttp.DEFAULT_HOST : opts.host;
|
|
3725
|
+
const maxBodyBytes = opts.maxBody == null ? fulfillWebhookHttp.DEFAULT_MAX_BODY_BYTES : opts.maxBody;
|
|
3726
|
+
|
|
3727
|
+
const handlerOpts = {
|
|
3728
|
+
wallet,
|
|
3729
|
+
secret,
|
|
3730
|
+
binding,
|
|
3731
|
+
catalog,
|
|
3732
|
+
outDir,
|
|
3733
|
+
maxBodyBytes,
|
|
3734
|
+
toleranceSec: opts.tolerance,
|
|
3735
|
+
now: io.now, // injectable clock (ms); the http layer defaults to Date.now
|
|
3736
|
+
};
|
|
3737
|
+
|
|
3738
|
+
let srv;
|
|
3739
|
+
try {
|
|
3740
|
+
srv = (io.createServer || fulfillWebhookHttp.createServer)(handlerOpts);
|
|
3741
|
+
} catch (e) {
|
|
3742
|
+
// A programmer/config error building the handler (never leaks the key/secret).
|
|
3743
|
+
writeErr(`error: cannot start vh fulfill-webhook: ${e.message}\n`);
|
|
3744
|
+
return Promise.resolve({ code: 2 });
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
return new Promise((resolve) => {
|
|
3748
|
+
let settled = false;
|
|
3749
|
+
srv.on("error", (e) => {
|
|
3750
|
+
if (settled) return;
|
|
3751
|
+
settled = true;
|
|
3752
|
+
writeErr(`error: cannot start vh fulfill-webhook: ${e.message}\n`);
|
|
3753
|
+
resolve({ code: 1, server: srv, url: null, error: e });
|
|
3754
|
+
});
|
|
3755
|
+
srv.listen(port, host, () => {
|
|
3756
|
+
if (settled) return;
|
|
3757
|
+
settled = true;
|
|
3758
|
+
const bound = srv.address();
|
|
3759
|
+
const realPort = bound && typeof bound === "object" ? bound.port : port;
|
|
3760
|
+
const url = `http://${host}:${realPort}/`;
|
|
3761
|
+
write(fulfillWebhookHttp.banner(url, host, outDir));
|
|
3762
|
+
resolve({ code: 0, server: srv, url });
|
|
3763
|
+
});
|
|
3764
|
+
});
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
// cmdFulfillWebhook: parse argv, then validate + bind + print. Resolves to a NUMBER: a bad flag or config
|
|
3768
|
+
// resolves to 2 (usage) BEFORE binding; a bind failure resolves to 1 (IO); on success it binds, prints the
|
|
3769
|
+
// banner, and returns a Promise that NEVER resolves — the open socket keeps the door up until Ctrl-C. Tests
|
|
3770
|
+
// call `runFulfillWebhook` directly (it resolves with the live { server } handle) so they can hit + close it.
|
|
3771
|
+
function cmdFulfillWebhook(argv, io = {}) {
|
|
3772
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
3773
|
+
let opts;
|
|
3774
|
+
try {
|
|
3775
|
+
opts = parseFulfillWebhookArgs(argv);
|
|
3776
|
+
} catch (e) {
|
|
3777
|
+
writeErr(`error: ${e.message}\n\n` + usage());
|
|
3778
|
+
return Promise.resolve(2);
|
|
3779
|
+
}
|
|
3780
|
+
return runFulfillWebhook(opts, io).then((res) => {
|
|
3781
|
+
if (res.code !== 0) return res.code;
|
|
3782
|
+
return new Promise(() => {});
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
async function main(argv) {
|
|
3787
|
+
const [cmd, ...rest] = argv;
|
|
3788
|
+
switch (cmd) {
|
|
3789
|
+
case "hash":
|
|
3790
|
+
return cmdHash(rest);
|
|
3791
|
+
case "anchor":
|
|
3792
|
+
return cmdAnchor(rest);
|
|
3793
|
+
case "anchor-artifact":
|
|
3794
|
+
return cmdAnchorArtifact(rest);
|
|
3795
|
+
case "verify-anchored":
|
|
3796
|
+
return cmdVerifyAnchored(rest);
|
|
3797
|
+
case "claim":
|
|
3798
|
+
return cmdClaim(rest);
|
|
3799
|
+
case "commit":
|
|
3800
|
+
return cmdCommit(rest);
|
|
3801
|
+
case "reveal":
|
|
3802
|
+
return cmdReveal(rest);
|
|
3803
|
+
case "verify":
|
|
3804
|
+
return cmdVerify(rest);
|
|
3805
|
+
case "prove":
|
|
3806
|
+
return cmdProve(rest);
|
|
3807
|
+
case "verify-proof":
|
|
3808
|
+
return cmdVerifyProof(rest);
|
|
3809
|
+
case "list":
|
|
3810
|
+
return cmdList(rest);
|
|
3811
|
+
case "show":
|
|
3812
|
+
return cmdShow(rest);
|
|
3813
|
+
case "lineage":
|
|
3814
|
+
return cmdLineage(rest);
|
|
3815
|
+
case "reputation":
|
|
3816
|
+
return cmdReputation(rest);
|
|
3817
|
+
case "dataset":
|
|
3818
|
+
return cmdDataset(rest);
|
|
3819
|
+
case "parcel":
|
|
3820
|
+
return cmdParcel(rest);
|
|
3821
|
+
case "trust":
|
|
3822
|
+
return cmdTrust(rest);
|
|
3823
|
+
case "evidence":
|
|
3824
|
+
return cmdEvidence(rest);
|
|
3825
|
+
case "agent":
|
|
3826
|
+
return cmdAgent(rest);
|
|
3827
|
+
case "identity":
|
|
3828
|
+
return cmdIdentity(rest);
|
|
3829
|
+
case "revocation":
|
|
3830
|
+
return cmdRevocation(rest);
|
|
3831
|
+
case "journal":
|
|
3832
|
+
return cmdJournal(rest);
|
|
3833
|
+
case "serve-verify":
|
|
3834
|
+
return cmdServeVerify(rest);
|
|
3835
|
+
case "fulfill-webhook":
|
|
3836
|
+
return cmdFulfillWebhook(rest);
|
|
3837
|
+
case undefined:
|
|
3838
|
+
case "-h":
|
|
3839
|
+
case "--help":
|
|
3840
|
+
case "help":
|
|
3841
|
+
process.stdout.write(usage());
|
|
3842
|
+
return 0;
|
|
3843
|
+
default:
|
|
3844
|
+
process.stderr.write(`error: unknown command: ${cmd}\n\n` + usage());
|
|
3845
|
+
return 2;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
if (require.main === module) {
|
|
3850
|
+
Promise.resolve(main(process.argv.slice(2))).then((code) => process.exit(code));
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
module.exports = {
|
|
3854
|
+
main,
|
|
3855
|
+
cmdHash,
|
|
3856
|
+
cmdAnchor,
|
|
3857
|
+
cmdClaim,
|
|
3858
|
+
cmdCommit,
|
|
3859
|
+
cmdReveal,
|
|
3860
|
+
cmdVerify,
|
|
3861
|
+
cmdProve,
|
|
3862
|
+
cmdVerifyProof,
|
|
3863
|
+
cmdList,
|
|
3864
|
+
cmdShow,
|
|
3865
|
+
cmdLineage,
|
|
3866
|
+
cmdReputation,
|
|
3867
|
+
cmdDataset,
|
|
3868
|
+
cmdDatasetVerify,
|
|
3869
|
+
cmdDatasetDiff,
|
|
3870
|
+
cmdDatasetSummary,
|
|
3871
|
+
cmdDatasetCheck,
|
|
3872
|
+
cmdDatasetReport,
|
|
3873
|
+
cmdDatasetAttest,
|
|
3874
|
+
cmdDatasetSign,
|
|
3875
|
+
cmdDatasetVerifyAttest,
|
|
3876
|
+
cmdDatasetVerifyTimestamp,
|
|
3877
|
+
cmdDatasetProve,
|
|
3878
|
+
cmdDatasetVerifyProof,
|
|
3879
|
+
cmdParcel,
|
|
3880
|
+
cmdParcelVerify,
|
|
3881
|
+
cmdParcelAttest,
|
|
3882
|
+
cmdParcelSign,
|
|
3883
|
+
cmdParcelVerifyAttest,
|
|
3884
|
+
cmdParcelVerifyTimestamp,
|
|
3885
|
+
cmdTrust,
|
|
3886
|
+
cmdEvidence,
|
|
3887
|
+
cmdAgent,
|
|
3888
|
+
cmdIdentity,
|
|
3889
|
+
cmdRevocation,
|
|
3890
|
+
cmdJournal,
|
|
3891
|
+
cmdAnchorArtifact,
|
|
3892
|
+
cmdVerifyAnchored,
|
|
3893
|
+
cmdServeVerify,
|
|
3894
|
+
runServeVerify,
|
|
3895
|
+
parseServeVerifyArgs,
|
|
3896
|
+
cmdFulfillWebhook,
|
|
3897
|
+
runFulfillWebhook,
|
|
3898
|
+
parseFulfillWebhookArgs,
|
|
3899
|
+
parseVerifyTimestampArgs,
|
|
3900
|
+
parseParcelBuildArgs,
|
|
3901
|
+
parseParcelVerifyArgs,
|
|
3902
|
+
parseParcelAttestArgs,
|
|
3903
|
+
parseParcelVerifyAttestArgs,
|
|
3904
|
+
parseSignArgs,
|
|
3905
|
+
parseDatasetBuildArgs,
|
|
3906
|
+
parseDatasetVerifyArgs,
|
|
3907
|
+
parseDatasetDiffArgs,
|
|
3908
|
+
parseDatasetSummaryArgs,
|
|
3909
|
+
parseDatasetCheckArgs,
|
|
3910
|
+
parseDatasetReportArgs,
|
|
3911
|
+
parseDatasetAttestArgs,
|
|
3912
|
+
parseDatasetVerifyAttestArgs,
|
|
3913
|
+
parseDatasetProveArgs,
|
|
3914
|
+
parseDatasetVerifyProofArgs,
|
|
3915
|
+
parseHashArgs,
|
|
3916
|
+
parseAnchorArgs,
|
|
3917
|
+
parseClaimArgs,
|
|
3918
|
+
parseRevealArgs,
|
|
3919
|
+
parseVerifyArgs,
|
|
3920
|
+
parseProveArgs,
|
|
3921
|
+
parseVerifyProofArgs,
|
|
3922
|
+
parseListArgs,
|
|
3923
|
+
parseShowArgs,
|
|
3924
|
+
parseLineageArgs,
|
|
3925
|
+
parseReputationArgs,
|
|
3926
|
+
usage,
|
|
3927
|
+
};
|