verifyhash 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. package/trustledger/valueproof.js +350 -0
@@ -0,0 +1,519 @@
1
+ # Evidence Packets (`vh evidence`)
2
+
3
+ A **product-agnostic, license-gated, tamper-evident evidence packet** for any directory of files.
4
+ `vh evidence seal <dir>` binds the whole file set into ONE content-addressed `*.vhevidence.json` packet;
5
+ `vh evidence verify <p>` re-derives the root from the bytes you hold and localizes any tamper to the exact
6
+ file. It is the **second vertical** on verifyhash's shared provenance core (after DataLedger and
7
+ ProofParcel), and it ships its **own** sellable license product.
8
+
9
+ > **Trust boundary (the output leads with this):** the seal proves **TAMPER-EVIDENCE +
10
+ > OFFLINE-RECOMPUTE**, **NOT a trusted timestamp**. "Sealed at time T" still rides the human-owned
11
+ > signing/timestamp trust-root (`needs-human`, **P-3** in [`STRATEGY.md`](../STRATEGY.md)). The packet is
12
+ > an **UNTRUSTED transport container** — `verify` re-derives the root from the bytes referenced; it never
13
+ > trusts the packet's own stored hashes. `verify` checks the **CONTENT, not the signer** — to prove **WHO**
14
+ > signed a signed packet, use `verify-signed` (it recovers the signer from the cryptography, never the
15
+ > claimed label). See [`docs/TRUST-BOUNDARIES.md`](TRUST-BOUNDARIES.md).
16
+
17
+ ## What it is for
18
+
19
+ Any time you hand someone a folder of files and later need to prove **"this is the EXACT set of files I
20
+ gave you, byte-for-byte unaltered"** — incident-response evidence bundles, audit work-paper folders,
21
+ QA/release artifact sets, a contract's exhibit pack, a dataset hand-off. A text editor can silently
22
+ rewrite one byte and nothing detects it; an evidence packet detects it and **names the file**.
23
+
24
+ It is deliberately **generic**: unlike the TrustLedger reconciliation seal, the evidence packet binds NO
25
+ domain verdict/role/period — it commits to the file SET and nothing else. That is what makes it reusable
26
+ across products.
27
+
28
+ ## Commands
29
+
30
+ ```
31
+ vh evidence seal <dir> [--out <p>] [--license <f> --vendor <0xaddr>] [--sign --key-env <VAR>|--key-file <p>] [--json]
32
+ vh evidence verify <p> [--dir <d>] [--json]
33
+ vh evidence verify-signed <signed> [--dir <d>] [--signer <0xaddr>] [--json]
34
+ vh evidence diff <p1> <p2> [--json]
35
+ ```
36
+
37
+ - `seal` walks `<dir>` (reusing the SAME path-bound enumeration as `vh hash <dir>` / `vh dataset build`),
38
+ builds the packet over [`cli/core/packetseal.js`](../cli/core/packetseal.js), and **either prints the
39
+ seal to stdout (default — writes NOTHING) or writes it to `--out <p>`**. It NEVER writes to cwd without
40
+ `--out`. Exit: **0** ok / **3** seal-build-error / **2** usage / **1** IO.
41
+ - `verify` is **read-only, NO key**. It re-derives the **content root** from the bytes referenced and reports
42
+ **OK** or exactly which file **CHANGED / MISSING / UNEXPECTED**. Files resolve relative to `--dir` (if given)
43
+ else the packet file's own directory (the packet stores relPaths relative to the sealed `<dir>`, so the
44
+ portable hand-off ships the files next to the packet). Exit: **0** OK / **3** REJECTED / **2** usage /
45
+ **1** IO — the SAME offline-recompute posture as `vh verify-seal` / `vh verify-proof`. **`verify` checks
46
+ the CONTENT, not the signer.** On a SIGNED packet it never reports the claimed signer as trusted: it
47
+ RECOVERS the signer from the bytes + signature and either **REJECTS a forged signature** OR labels a
48
+ genuine one **UNVERIFIED-for-pinning**, pointing you at `verify-signed` — it does **NOT** pin the signer to
49
+ anyone you trust. To prove **WHO** signed, run `verify-signed` (below).
50
+ - `verify-signed` is the **recipient's "prove WHO signed this" step** — the trust check the **paid signed
51
+ surface exists to enable**, and the command that ACTUALLY checks a signed packet's signer. It is
52
+ **OFFLINE / key-free / network-free** and **recover-not-trust**: it RECOVERS the public signer address from
53
+ the embedded canonical seal bytes + signature (**Check 1, ALWAYS**), it never trusts the container's
54
+ claimed `signer` label. **`--signer <0xaddr>` PINS** the recovered signer to an expected publisher (Check 2),
55
+ and **`--dir <d>` BINDS** the signature to YOUR OWN bytes by recomputing the canonical seal from that
56
+ directory (Check 3). The verdict is **ACCEPTED** only when every requested check passes; a
57
+ forged / tampered / wrong-key signature, a wrong `--signer`, or a wrong `--dir` is a clean **REJECTED** —
58
+ **NEVER a silent pass**. It leads with the trust caveat and prints each check **PASS / FAIL / [skip]**.
59
+ Exit: **0** ACCEPTED / **3** REJECTED / **2** usage / **1** IO (mirrors `vh dataset verify-attest`).
60
+ - `diff` is the **recipient-side** companion to `verify`: it compares TWO already-sealed packets and reports
61
+ what **ADDED / REMOVED / CHANGED** between them, OFFLINE, with **no directory and no key**. It is
62
+ **read-only, FREE, key-free** — a diff produces no new sealed/signed artifact, so there is nothing to gate.
63
+ Exit: **0** IDENTICAL / **3** DIFFERENT / **2** usage / **1** IO — the SAME exit contract as `vh dataset diff`.
64
+ A `diff` compares what each packet **CLAIMS**; it does **NOT** re-derive content from bytes (to confirm a
65
+ packet still matches a real directory, run `vh evidence verify <p> --dir <d>`). It changes no `seal`/`verify`
66
+ behavior.
67
+
68
+ ## Free vs paid
69
+
70
+ | Surface | Tier | Gate |
71
+ | --- | --- | --- |
72
+ | Unsigned baseline seal of up to **25 files** + `verify` + `verify-signed` + `diff` | **FREE** | none — try before you buy |
73
+ | `--sign` (wrap the seal in a signed attestation) | **PAID** | `evidence_signed` |
74
+ | Sealing **more than 25 files** in one packet | **PAID** | `evidence_unlimited` |
75
+
76
+ `verify-signed` is the FREE, key-free **recipient** side of the PAID `--sign` surface: the operator pays to
77
+ PRODUCE a signed packet, and any recipient runs `verify-signed` to PROVE who signed it — no license, no
78
+ vendor, nothing to gate (a recipient checking a signature mints no new artifact). The trust the paid signed
79
+ surface sells is only realized when the recipient runs `verify-signed` to recover + pin + bind the signer.
80
+
81
+ The free tier stays fully open so a buyer can evaluate the product end-to-end. A paid surface REQUIRES a
82
+ valid `--license <f> --vendor <0xaddr>`, verified **OFFLINE** via [`cli/core/license.js`](../cli/core/license.js)
83
+ against the **evidence-product** entitlement table (`kind: vh-evidence-license` — a **separate** product
84
+ from `trustledger-license`). The gate reuses the **same `verifyLicense` / named-reject posture** as the
85
+ TrustLedger CLI: a missing/expired/`wrong_issuer`/under-entitled license is a hard refuse that **never
86
+ silently downgrades to a free run**, and the packet is never written when the gate fails.
87
+
88
+ ## The evidence-packet schema (every field UNTRUSTED transport)
89
+
90
+ A bare packet (`*.vhevidence.json`):
91
+
92
+ ```json
93
+ {
94
+ "kind": "vh.evidence-seal",
95
+ "schemaVersion": 1,
96
+ "note": "This evidence seal is TAMPER-EVIDENT + OFFLINE-RECOMPUTABLE, NOT a trusted timestamp. …",
97
+ "root": "0x…32-byte…",
98
+ "fileCount": 3,
99
+ "files": [
100
+ { "relPath": "a.txt", "contentHash": "0x…", "leaf": "0x…" },
101
+ { "relPath": "sub/b.bin", "contentHash": "0x…", "leaf": "0x…" }
102
+ ]
103
+ }
104
+ ```
105
+
106
+ | Field | Meaning | Trust |
107
+ | --- | --- | --- |
108
+ | `kind` | `vh.evidence-seal` (generic; bare) or `vh.evidence-seal-signed` (signed wrap) | identity discriminator; `verify` rejects a foreign/edited kind |
109
+ | `schemaVersion` | format version (`1`) | rejected if unsupported |
110
+ | `note` | the standing trust caveat carried in-band | must match the standing note (caveat can't drift) |
111
+ | `root` | Merkle root over every `(relPath, content)` pair | **UNTRUSTED** — `verify` RE-DERIVES it from the bytes and compares |
112
+ | `fileCount` | number of sealed files | must equal `files.length` |
113
+ | `files[]` | per-file `{ relPath, contentHash, leaf }`, sorted by `relPath` | UNTRUSTED — each `leaf` must equal `pathLeaf(relPath, contentHash)`, and the whole list must re-fold to `root` |
114
+
115
+ **Everything in the packet is untrusted transport.** Verification is authoritative by **re-computing** the
116
+ per-file content hashes and the root from the bytes you supply; the stored hashes are merely the
117
+ EXPECTATION it checks against. A hand-edited `root` (or a leaf, or a `contentHash`) is caught two ways:
118
+ the per-file leaf must be internally self-consistent, AND the whole set must re-fold to `root`. The
119
+ `root` uses the **exact same path-bound, domain-separated Merkle convention** as `vh hash <dir>` and the
120
+ on-chain `verifyLeaf` — no new crypto, no second hashing scheme.
121
+
122
+ A **signed** packet (`kind: vh.evidence-seal-signed`, the paid `evidence_signed` surface) wraps the EXACT
123
+ canonical bare-seal bytes in `attestation` and attaches a detached EIP-191 `signature` — the SAME
124
+ signed-attestation envelope ([`cli/core/attestation.js`](../cli/core/attestation.js)) the dataset/parcel
125
+ products use. The signature is **untrusted transport too**: the container's claimed `signer` is just a label
126
+ until `vh evidence verify-signed` RECOVERS the public address from the bytes + signature and confirms it.
127
+ The recovered signer proves **WHO vouched**, NOT **WHEN**:
128
+
129
+ > **Signer-vouch, NOT a timestamp (P-3).** A valid signature proves the HOLDER OF `signer`'s key vouched for
130
+ > THIS evidence seal (the embedded root + the full set of (relPath, content) pairs). It does NOT by itself
131
+ > prove a trustworthy TIMESTAMP: "sealed/vouched since a date T" still needs the human-owned signing/timestamp
132
+ > trust-root (needs-human, P-3). It is NOT a legal opinion.
133
+
134
+ ## Worked example: seal → hand over packet → verify
135
+
136
+ ```
137
+ # 1. Seal a folder. Without --out, the seal prints to stdout and NOTHING is written.
138
+ # Write the packet NEXT TO the files so the hand-off is portable.
139
+ $ vh evidence seal ./evidence-bundle --out ./evidence-bundle/bundle.vhevidence.json
140
+ This evidence seal is TAMPER-EVIDENT + OFFLINE-RECOMPUTABLE, NOT a trusted timestamp. …
141
+ sealed 3 files into an evidence packet — root 0xe393…b6f1
142
+ written: /abs/evidence-bundle/bundle.vhevidence.json
143
+
144
+ # 2. Hand the WHOLE folder (files + bundle.vhevidence.json) to the other party.
145
+
146
+ # 3. They verify offline, no key — files resolve next to the packet by default:
147
+ $ vh evidence verify ./evidence-bundle/bundle.vhevidence.json
148
+
149
+ root matches: yes
150
+ files: 3 matched, 0 changed, 0 missing, 0 unexpected
151
+ OK — every sealed file re-derives byte-for-byte and the root matches. # exit 0
152
+
153
+ # 4. If ANY file was altered in transit, verify names it and exits non-zero:
154
+ $ vh evidence verify ./evidence-bundle/bundle.vhevidence.json
155
+ REJECTED — the files do NOT match the packet:
156
+ CHANGED report.pdf: sealed 0x… != on-disk 0x… # exit 3
157
+ ```
158
+
159
+ ## Proving WHO signed: `vh evidence verify-signed`
160
+
161
+ `verify` answers **"are these the exact bytes that were sealed?"** — the content check. It does **NOT** answer
162
+ **"who signed it?"**: on a signed packet `verify` recovers the signer only to flag a forgery or call a genuine
163
+ signer **UNVERIFIED-for-pinning**; it never pins the signer to anyone you trust. The recipient's
164
+ **"prove WHO signed this"** step — the trust check the paid `--sign` surface exists to enable — is a separate
165
+ command, `verify-signed`:
166
+
167
+ ```
168
+ # Operator (key provisioned outside the loop) seals + signs, gated by an evidence license:
169
+ $ vh evidence seal ./bundle --out ./bundle/b.vhevidence.json \
170
+ --sign --key-env EV_OP_KEY --license evidence.vhlicense.json --vendor 0x<evidence-vendor>
171
+ … signed by: 0x<operator>
172
+
173
+ # The recipient PROVES who signed it — recover (always) + pin (--signer) + bind (--dir), all OFFLINE/key-free:
174
+ $ vh evidence verify-signed ./bundle/b.vhevidence.json --signer 0x<operator> --dir ./bundle
175
+ TRUST: A valid signature proves the HOLDER OF `signer`'s key vouched for THIS evidence seal … # caveat first
176
+ verify-signed: ACCEPTED
177
+ recovered signer: 0x<operator> (from the embedded canonical seal bytes + signature)
178
+ [PASS] signature recovers to the claimed signer
179
+ [PASS] recovered signer matches the expected signer (0x<operator>)
180
+ [PASS] the signature binds YOUR directory …
181
+ ACCEPTED: every requested check passed. # exit 0
182
+
183
+ # A WRONG --signer (or a forged/tampered signature, or a --dir that doesn't match) is a clean REJECTED:
184
+ $ vh evidence verify-signed ./bundle/b.vhevidence.json --signer 0x<someone-else>
185
+
186
+ REJECTED: failed check(s): signerMatchesExpected. # exit 3
187
+ ```
188
+
189
+ **The boundary in one line.** `verify` = does the CONTENT match the seal? (re-derive the root from bytes).
190
+ `verify-signed` = does a TRUSTED signer vouch for it? (recover the signer, then `--signer` to pin and `--dir`
191
+ to bind). Use `verify` when you only hold the files; use `verify-signed` when the packet is signed and you
192
+ need to prove the signer. `verify-signed` is **recover-not-trust**: it never believes the claimed `signer`
193
+ label — it derives the address from the cryptography and (with `--signer`) checks it against the publisher
194
+ you expected.
195
+
196
+ > **Signer-vouch, NOT a timestamp (P-3).** A valid signature proves the HOLDER OF `signer`'s key vouched for
197
+ > THIS evidence seal (the embedded root + the full set of (relPath, content) pairs). It does NOT by itself
198
+ > prove a trustworthy TIMESTAMP: "sealed/vouched since a date T" still needs the human-owned signing/timestamp
199
+ > trust-root (needs-human, P-3). It is NOT a legal opinion.
200
+
201
+ ### Was the signing key still good? `--revocations <f> [--as-of <ISO>]`
202
+
203
+ A genuine signature proves *who* signed — but a key can be **compromised, rotated, or retired** after it
204
+ signed. `verify-signed` lets the recipient ask the only question that then matters — **"was that key
205
+ trustworthy AS OF the instant this exhibit was sealed?"** — by passing the vendor's signed
206
+ [**key revocation(s)**](KEY-LIFECYCLE.md):
207
+
208
+ ```
209
+ # An exhibit signed under a key the vendor later revoked-BEFORE your as-of instant downgrades to REVOKED:
210
+ $ vh evidence verify-signed ./bundle/b.vhevidence.json --signer 0x<operator> --dir ./bundle \
211
+ --revocations ./operator.vhrevocation.json --as-of 2026-07-01T00:00:00.000Z
212
+
213
+ revocation check (as of 2026-07-01T00:00:00.000Z):
214
+ [REVOKED] the signing key (0x<operator>) was REVOKED as of 2026-06-26T00:00:00.000Z (reason: rotated) … This artifact is NOT trustworthy as of 2026-07-01T00:00:00.000Z.
215
+ REJECTED: … # exit 3
216
+ ```
217
+
218
+ `--revocations` is **strictly optional and non-loosening**: with NO `--revocations` the verdict + exit code
219
+ are **byte-for-byte** what they are today. A revocation can ONLY turn an ACCEPTED into a **REVOKED**, never
220
+ the reverse; a revocation dated AFTER your `--as-of` keeps the ACCEPTED verdict with an informational
221
+ "later-revoked" note (the exhibit WAS signed while the key was good); and a **forged / tampered /
222
+ third-party** revocation is **IGNORED with a warning**, never trusted to downgrade. Remember the boundary:
223
+ a revocation is a **signed CLAIM** by the key-holder (`revokedAt` is self-asserted), **NOT** a trusted
224
+ wall-clock timestamp without P-3, so `--as-of` is **recipient-chosen evidence, not an oracle**. The
225
+ producer side (`vh revocation publish`) and the full key-lifecycle story:
226
+ [`docs/KEY-LIFECYCLE.md`](KEY-LIFECYCLE.md).
227
+
228
+ ## What changed between two hand-offs? `vh evidence diff`
229
+
230
+ `diff` is the **recipient-side** companion to `verify`. You were handed the **v1** packet of a folder, and
231
+ later the **v2** packet of the next hand-off. To see exactly what moved between them, run the diff over the
232
+ two **portable artifacts** — no directory, no key, no network:
233
+
234
+ ```
235
+ $ vh evidence diff ./v1.vhevidence.json ./v2.vhevidence.json
236
+ TRUST: this compares what each evidence packet CLAIMS — it does NOT re-derive content (there is no directory). …
237
+ (run `vh evidence verify <packet> --dir <d>` against the live tree to re-derive a root from bytes).
238
+
239
+ files: DIFFERENT
240
+ ADDED new.txt …
241
+ REMOVED old.txt …
242
+ CHANGED report.pdf old: 0x… -> new: 0x…
243
+ +1 / -1 / ~1 / 2 unchanged # exit 3 (DIFFERENT)
244
+ ```
245
+
246
+ - **What it reports.** `vh evidence diff v1 v2` reports **ADDED / REMOVED / CHANGED** purely from the two
247
+ sealed packets, OFFLINE, with **no directory and no key**. The change set is directional: `v1` is the
248
+ baseline, `v2` is the comparison. Exit **0** when the two packets are IDENTICAL, **3** when DIFFERENT (the
249
+ SAME exit contract as `vh dataset diff`), **2** usage, **1** IO.
250
+ - **A rename shows as REMOVED + ADDED.** The relPath is bound into each leaf, so moving `old.txt` to
251
+ `new.txt` (even with byte-identical content) surfaces as **REMOVED(old) + ADDED(new)**, never a single
252
+ CHANGED.
253
+ - **It compares CLAIMS, NOT content.** A diff compares what each packet **CLAIMS** — it does **NOT** re-derive
254
+ content from bytes (there is no directory to read). To confirm a packet still matches a **real directory**
255
+ byte-for-byte, run `vh evidence verify <p> --dir <d>` — that is the bytes-level check. `diff` changes no
256
+ `seal`/`verify` behavior; it is a purely additive read.
257
+ - **`diff` is FREE / key-free.** It produces no new sealed/signed artifact, so there is **nothing to gate**:
258
+ no `--license`, no `--vendor`, no entitlement check. A recipient can run it on any two packets they hold —
259
+ one more fully-open surface in the free-tier funnel (P-7) that a buyer can evaluate before paying for the
260
+ signed/unlimited paid tiers.
261
+
262
+ > **Trust boundary (unchanged):** the seal proves **TAMPER-EVIDENCE + OFFLINE-RECOMPUTE**, **NOT a trusted
263
+ > timestamp**. "Sealed at time T" still rides the human-owned signing/timestamp trust-root (`needs-human`,
264
+ > **P-3** in [`STRATEGY.md`](../STRATEGY.md)). A diff inherits this boundary: it tells you what the two
265
+ > packets CLAIM differs, it does not prove WHEN either was sealed.
266
+
267
+ ### Gate the change in CI: `vh evidence diff … --policy <f>`
268
+
269
+ A bare diff answers *what changed*; a pipeline needs *is this change ALLOWED?* — and a non-zero exit when it
270
+ is not. Pass a small **drift policy** and the exit code becomes the **policy verdict**: a DIFFERENT-but-
271
+ **permitted** change PASSes (exit **0**), a **disallowed** change FAILs (exit **3**). The verdict is computed
272
+ from the *same* change set the diff prints, so it can never disagree with the body.
273
+
274
+ ```
275
+ $ vh evidence diff ./v1.vhevidence.json ./v2.vhevidence.json --policy ./drift.json
276
+
277
+ files: DIFFERENT
278
+ ADDED new-exhibit.pdf …
279
+ ## drift policy
280
+ verdict: PASS (rules evaluated: 2)
281
+ PASS — every change between A and B is permitted by this policy. # exit 0 (gate PASS)
282
+ ```
283
+
284
+ A drift policy is a JSON object `{ "kind": "vh.evidence-drift-policy", "schemaVersion": 1, … }` with any
285
+ combination of these **optional** rules (a policy with no rules trivially PASSes):
286
+
287
+ - `"noAdded": true` / `"noRemoved": true` / `"noChanged": true` — forbid *any* ADD / REMOVE / edit.
288
+ `noRemoved` is the load-bearing **chain-of-custody** guard: an evidence packet that LOSES a file is
289
+ suspicious. `noRemoved` + `noChanged` together enforce an **append-only** evidence trail.
290
+ - `"allowChangePaths": ["src", …]` — a CHANGED file **outside** every allowed POSIX prefix violates
291
+ (e.g. only files under `src/` may be edited). The match is **segment-aware**: `src` matches `src/x` and
292
+ `src`, never `srcfoo`.
293
+ - `"frozenPaths": ["legal", …]` — a file under a frozen prefix that is CHANGED **or** REMOVED violates
294
+ (those paths may be neither edited nor deleted); **adding** a new file under a frozen prefix is allowed.
295
+
296
+ A rename is REMOVED(old) + ADDED(new) in the change set, so it is gated as a remove + an add — never a silent
297
+ edit. `--policy --json` carries a `drift` block `{ verdict, rulesEvaluated, violations[] }` (each violation is
298
+ `{ relPath, rule, change }`), so a CI consumer reads the verdict and the exact offending files from the same
299
+ object as the change set. The gate is **OFFLINE / key-free / FREE** like the diff itself — it reuses the pure
300
+ `diffEvidence` change set verbatim and adds no crypto. It mirrors `vh dataset check`'s policy gate, so the two
301
+ read identically.
302
+
303
+ ## How it reuses the shared cores
304
+
305
+ The evidence product is a **thin adapter** — it re-implements no crypto:
306
+
307
+ - **Seal** → [`cli/core/packetseal.js`](../cli/core/packetseal.js), the generic packet-seal core
308
+ (`buildSeal`/`validateSeal`/`verifySeal`). The evidence adapter supplies only its `kind` and uses the
309
+ core with **no header** (the optional verdict/role binding seam stays unused — that's the trust-reconcile
310
+ vocabulary the evidence product deliberately omits). TrustLedger's reconciliation seal uses the SAME
311
+ core *with* a header; the machinery is identical.
312
+ - **File enumeration** → `cli/hash.js › listFiles`, the SAME recursive walk `vh hash <dir>` and
313
+ `vh dataset build` use. relPaths are POSIX-normalized and relative to the sealed `<dir>`.
314
+ - **License** → [`cli/core/license.js`](../cli/core/license.js), the generic signed-entitlement engine.
315
+ The evidence adapter supplies its OWN `kind` (`vh-evidence-license`) + closed entitlement table; the
316
+ core does all the crypto via the shared attestation envelope. `verifyLicense` re-derives the signer,
317
+ pins it to `--vendor`, checks the window, and localizes the reject reason (`wrong_issuer`/`expired`/…).
318
+ - **Signed wrap** → [`cli/core/attestation.js`](../cli/core/attestation.js), the same EIP-191
319
+ signed-attestation envelope as the dataset/parcel/seal products.
320
+
321
+ ## Issue a license per sale: `vh evidence license fulfill`
322
+
323
+ The paid surfaces (`--sign`, sealing > 25 files) only unlock for a holder of a valid `*.vhevidence-license.json`.
324
+ Minting one **by hand** for every sale does not scale: a human at a terminal would have to remember the **exact**
325
+ entitlement flags a tier grants and **hand-compute** the expiry. That is error-prone (a typo grants the wrong tier,
326
+ a mis-keyed expiry drifts) and **un-automatable** — a billing provider's *payment-succeeded* event carries a
327
+ **`planId`** and a **paid-through date**, not a comma-list of entitlement flags. **`vh evidence license fulfill`**
328
+ + the **evidence plan catalog** close that gap: they turn "issue the right evidence license" into **one
329
+ deterministic command** a billing webhook can drive, with **no hand-authored entitlement list**. This is the
330
+ seller's **"issue a license per sale"** step — the self-serve fulfillment seam that makes an evidence sale
331
+ machine-driven, NOT a human hand-crafting entitlement flags.
332
+
333
+ > **Boundary (VERBATIM — read this first).** The loop ships **ONLY** the catalog **schema** + the order→license
334
+ > **mapping** + **ephemeral test keys**. It **NEVER** sets a price, holds a real key, runs a payment processor,
335
+ > or takes a real payment. **Provisioning the evidence vendor key, setting the PRICE/term column in the catalog,
336
+ > and wiring the actual webhook/billing remain HUMAN-owned outward steps** (STRATEGY.md › P-7 steps 1–2). A plan
337
+ > is an **ACCESS DESCRIPTION** for delivered software value — which paid evidence features a subscription unlocks
338
+ > and for how long — **NOT a token, NOT tradeable, NOT an appreciating asset**, and the catalog makes
339
+ > **NO claim of regulatory compliance**. The actual subscription agreement governs.
340
+
341
+ > **Trust boundary (unchanged).** Fulfilling a license mints an **ACCESS credential**, NOT a trusted timestamp.
342
+ > A minted license proves the holder paid for the named evidence features; it does **NOT** prove **WHEN** any
343
+ > packet was sealed — "sealed at time T" still rides the human-owned signing/timestamp trust-root (`needs-human`,
344
+ > **P-3** in [`STRATEGY.md`](../STRATEGY.md)). The license is verified the SAME way every evidence artifact is —
345
+ > `verifyLicense` RE-DERIVES the signer from the bytes + signature and pins it to `--vendor`; the container's
346
+ > claimed `vendor` is UNTRUSTED transport until then.
347
+
348
+ ### The evidence plan catalog (a DRAFT the human prices)
349
+
350
+ A plan catalog is a single, **versioned, strictly-validated** JSON file. [`cli/core/evidence-plans.js`](../cli/core/evidence-plans.js)
351
+ is the source of truth (pure `validateEvidencePlanCatalog` / `getEvidencePlan` / `fulfillEvidenceOrder`, **no I/O,
352
+ no clock, no key**). It is the **one** machine-readable mapping `planId → { entitlements, termDays, displayName }`
353
+ over the **CLOSED** evidence entitlement table — so an unknown entitlement or a duplicate plan is a **hard build
354
+ error**, never a silent mis-grant. Every field:
355
+
356
+ | Field | Required | Type | Meaning |
357
+ | --- | --- | --- | --- |
358
+ | `kind` | **yes** | string `"vh-evidence-plan-catalog"` | Fixes the artifact type, **disjoint** from a license/seal AND from the `trustledger-plan-catalog` kind. A wrong/missing `kind` is a hard `EvidencePlanCatalogError`. |
359
+ | `schemaVersion` | **yes** | integer (currently **1**) | Pins the catalog shape. Any unsupported version is a hard error — never coerced. |
360
+ | `plans` | **yes** | non-empty array | The plan list. Emitted in `planId`-sorted order, deterministically. |
361
+ | `plans[].planId` | **yes** | non-empty string | The plan id a billing `planId` resolves against. **Duplicate ids are rejected.** |
362
+ | `plans[].displayName` | **yes** | non-empty string | A human label for the tier (shown, not enforced). |
363
+ | `plans[].entitlements` | **yes** | non-empty array of **known** flags | The paid features this plan unlocks — drawn **ONLY** from the **closed evidence entitlement table** (`evidence_signed`, `evidence_unlimited`). An unknown or duplicate flag is a hard error. This is what `fulfill` copies into the license **verbatim**. |
364
+ | `plans[].termDays` | **yes** | **positive integer** | The subscription term in days. When an order omits an explicit `--paid-through`, `expiresAt = issuedAt + termDays` days. A non-integer or non-positive term is rejected (never rounded/coerced). |
365
+
366
+ > **The catalog is a DRAFT the HUMAN prices.** The bundled catalog is a **DRAFT skeleton**: it ships the
367
+ > `planId → entitlements/term/displayName` mapping, but **the PRICE and your real term are YOURS to set** (P-7
368
+ > step 2). Editing the catalog (a data file in this validated schema) is exactly that narrow human step — no
369
+ > engine change is needed. The shipped `_DRAFT` string is ignored by the engine and exists only to keep the
370
+ > access-description posture attached to the file itself.
371
+
372
+ **The closed entitlement table.** The set of entitlement flags a plan may grant is **exactly** the evidence
373
+ license CFG's closed table (`cli/evidence.js › LICENSE_CFG`), derived via the SAME core
374
+ `entitlementFlags(cfg)` helper the license **gate** uses — never a hard-coded copy — so the catalog and the gate
375
+ that honors a license can **never drift**. The closed table:
376
+
377
+ | Entitlement flag | Unlocks |
378
+ | --- | --- |
379
+ | `evidence_signed` | wrap the seal in a signed attestation (`vh evidence seal --sign`) |
380
+ | `evidence_unlimited` | seal **more than 25 files** (above the free `SAMPLE_LIMIT`) in one packet |
381
+
382
+ A flag outside that table is a **hard reject** at catalog-validation time — the evidence catalog can never grant a
383
+ TrustLedger entitlement (nor vice-versa); the two products are **DISJOINT**.
384
+
385
+ ### The bundled draft skeleton
386
+
387
+ The catalog `fulfill` resolves against when you pass **no** `--catalog` is the bundled draft
388
+ (`cli/core/fixtures/evidence-plans/baseline.json`), read from **this package's own** fixtures dir — never the
389
+ caller's cwd. Its draft plans:
390
+
391
+ | `planId` | `displayName` | entitlements | `termDays` |
392
+ | --- | --- | --- | --- |
393
+ | `evidence-signed-monthly` | Evidence Signed (monthly) — DRAFT | `evidence_signed` | `30` |
394
+ | `evidence-pro-annual` | Evidence Pro (annual) — DRAFT | `evidence_signed`, `evidence_unlimited` | `365` |
395
+
396
+ These are a **skeleton to copy**: keep/rename the plans, set **your** `termDays`, and attach **your** price
397
+ out-of-band. Point `--catalog <file>` at your own catalog to override the bundle entirely.
398
+
399
+ ### `vh evidence license fulfill` (the one-command shape)
400
+
401
+ ```
402
+ vh evidence license fulfill --plan <planId> --customer <name> [--paid-through <ISO>] [--catalog <file>]
403
+ (--key-env <VAR> | --key-file <path>)
404
+ [--issued <ISO>] [--license-id <id>] [--out <file>] [--json]
405
+ ```
406
+
407
+ `fulfill` looks the `planId` up in the catalog, copies that plan's **entitlements VERBATIM** (never re-typed),
408
+ derives the window (`--paid-through`, else `issuedAt + termDays`), and mints the **SAME** signed
409
+ `*.vhevidence-license.json` the existing `verifyLicense` gate accepts byte-for-byte — so it **UNLOCKS**
410
+ `vh evidence seal --sign` (and the > 25-file `evidence_unlimited` surface) end-to-end. The order→license mapping
411
+ (`fulfillEvidenceOrder`) is **pure + deterministic**: the same `{ plan, customer, paidThrough, issuedAt }` + the
412
+ same catalog yields a **byte-identical** license.
413
+
414
+ - **The key-source rule.** The vendor key is read **EXACTLY ONE** of `--key-env <VAR>` / `--key-file <path>` and is
415
+ **read-used-discarded** — the **same** posture as `vh evidence seal --sign` / `vh dataset sign`. The loop
416
+ **never holds** a key; **only the PUBLIC vendor address is echoed**, never the key. Neither/both/missing/malformed
417
+ key sources hard-error (exit `2`) with a **key-free** message.
418
+ - An **unknown plan**, a `--paid-through` **at or before** `issuedAt`, a **malformed** `--issued`/`--paid-through`,
419
+ or a **malformed `--catalog`** file is a **usage error (exit `2`)** — a named reject, never a silent mis-grant,
420
+ and **no file is written** on failure.
421
+ - With `--out <file>` the signed container is written to **that** path (and **only** there — never cwd); without
422
+ `--out` it streams to stdout. `--json` round-trips the public summary (`vendor`, `entitlements`, `issuedAt`,
423
+ `expiresAt`, …) so a webhook handler can script it. Exit: **0** ok / **2** usage (unknown plan, bad
424
+ window/date, bad `--catalog`, key-source error) / **1** IO — `fulfill` is a **producer**: it has **no** exit-3
425
+ "gate-fail" path of its own. The exit-**3** in the evidence family belongs to the **downstream consumer gate**
426
+ (`vh evidence seal --sign` / `verify` / `verify-signed` / `diff`), which is where a webhook handler keys
427
+ retry/alert logic for a *rejected* license — never on `fulfill`, which surfaces a fulfillment reject (typo'd
428
+ plan, bad window) as a named **exit 2**, distinct from a genuine IO fault (**exit 1**).
429
+
430
+ ### The worked flow: `payment-succeeded` webhook → `fulfill` → deliver `*.vhevidence-license.json`
431
+
432
+ A billing provider's *payment-succeeded / renewed* webhook fires with a `planId` and a paid-through date. The
433
+ handler authenticates the webhook signature (the provider's own SDK + the provider's signing secret — a
434
+ HUMAN-owned secret the loop **never holds**), then runs **one** `vh evidence license fulfill` call and delivers
435
+ the minted license to the paying customer:
436
+
437
+ ```
438
+ # Your webhook handler, AFTER authenticating the provider's signature, runs ONE command per sale:
439
+ $ vh evidence license fulfill \
440
+ --plan evidence-pro-annual --customer "Acme Co" \
441
+ --paid-through 2027-06-01T00:00:00.000Z \
442
+ --key-env EVIDENCE_VENDOR_KEY \
443
+ --out ./out/acme.vhevidence-license.json
444
+ fulfilled evidence license for plan evidence-pro-annual by vendor 0x<evidence-vendor>
445
+ entitlements: evidence_signed, evidence_unlimited
446
+ written: /abs/out/acme.vhevidence-license.json # exit 0
447
+
448
+ # Deliver acme.vhevidence-license.json to the paying customer. They run the paid surface OFFLINE,
449
+ # pinning your PUBLISHED vendor address — no per-sale terminal step for you:
450
+ $ vh evidence seal ./bundle --out ./bundle/b.vhevidence.json \
451
+ --sign --key-env ACME_OP_KEY \
452
+ --license ./acme.vhevidence-license.json --vendor 0x<evidence-vendor> # unlocked by the minted license
453
+ ```
454
+
455
+ The per-sale work collapses to **no terminal step per sale**: a renewal re-runs the **same** deterministic
456
+ command with a new `--paid-through`, mints a fresh license, and delivers it — the same machine-driven seam a
457
+ renewal webhook drives. The loop ships the **catalog + the mapping + the fulfill command + ephemeral test keys**;
458
+ **provisioning the vendor key, setting the price/term column, and wiring the actual webhook/billing remain
459
+ HUMAN-owned outward steps** (STRATEGY.md › P-7 steps 1–2). NO new human gate is introduced — the fulfillment
460
+ command automates the *mechanism* of an existing P-7 step, it does not add one.
461
+
462
+ ## Reference self-serve fulfillment webhook: `vh fulfill-webhook`
463
+
464
+ The worked flow above still asks the human to **write** the webhook handler — the code that authenticates the
465
+ provider's signature, maps the price to a plan, and shells out to `vh evidence license fulfill`. **`vh
466
+ fulfill-webhook`** ships **that handler**, tested, as a tiny loopback-only Node-core HTTP server (**ZERO new
467
+ dependency**), so the human's **last CODE step becomes a config step**: run it, point your billing provider's
468
+ webhook at it, and every paid event delivers a license — no handler to author.
469
+
470
+ It wires the pure **fulfillment-intake core** ([`cli/core/fulfill-intake.js`](../cli/core/fulfill-intake.js))
471
+ to the fulfiller: on each POST it runs `verifyProviderSignature` → `parseEvidenceEvent` →
472
+ `normalizeEvidenceEvent` → `fulfillEvidenceOrder` → `evidence.buildLicense`, reusing every seam **verbatim**.
473
+
474
+ ```
475
+ vh fulfill-webhook [--port <n>] [--host <h>] [--max-body <bytes>] [--tolerance <sec>] \
476
+ --secret-env <VAR> --binding <file> (--key-env <VAR> | --key-file <p>) \
477
+ --out <dir> [--catalog <file>]
478
+ ```
479
+
480
+ - **`--secret-env <VAR>`** — the env var holding the provider's **webhook signing secret** (the HMAC key it
481
+ signs each delivery with). Read from `process.env[VAR]`; **never written to disk or logs**.
482
+ - **`--binding <file>`** — a validated **price→plan binding** (`kind: vh-evidence-price-binding`) mapping each
483
+ `(provider, priceId)` onto one of **your** evidence `planId`s. An unmapped price is a NAMED **422**, never a
484
+ silent default plan.
485
+ - **`--key-env <VAR>` | `--key-file <p>`** — **EXACTLY ONE**: the **vendor signing key**. It is
486
+ read-used-**held-in-memory** to sign each delivered license and is **NEVER written to disk or logs** (the
487
+ same `loadSigningWallet` read the sign path uses; the loop sets **no price**).
488
+ - **`--out <dir>`** — an **existing** directory the delivered `*.vhlicense.json` files are written to (**never
489
+ cwd**). Delivery is **idempotent**, keyed on the event, so an at-least-once retry writes **no duplicate**.
490
+ - **`--catalog <file>`** — OPTIONAL evidence plan catalog (default: the bundled **DRAFT**). Entitlements are
491
+ copied from the resolved plan **verbatim**.
492
+
493
+ **On each `POST /fulfill`:** it reads the RAW body (bounded by `--max-body` → **413**), **authenticates** it
494
+ with `verifyProviderSignature` (**fail-closed**: an **unsigned** request is **401**, a **malformed** signature
495
+ header is **400**, a **forged** signature or **stale/replayed** timestamp is **401** — each with the localized
496
+ reason, delivering **NOTHING**), maps its price to a plan via `--binding`, mints the signed license the paid
497
+ gate accepts, and **delivers** it. On success it responds **`200 { delivered, licenseId }`**; a **re-delivered
498
+ event returns the SAME `licenseId`** (idempotent, no duplicate). An authenticated event that maps to no plan
499
+ is **422**. `GET /healthz` → `200 { ok:true }`.
500
+
501
+ It **binds loopback (127.0.0.1) by default** — a non-loopback interface is not served unless you pass
502
+ `--host` — makes **no outbound network request**, holds the vendor key **in memory only**, and writes
503
+ **neither the key nor the secret** to disk or logs.
504
+
505
+ > **Boundary (VERBATIM — read this first).**
506
+ >
507
+ > The loop ships this reference handler and its OFFLINE tests (a synthetic signing secret and an ephemeral `Wallet.createRandom()` vendor key); provisioning the REAL provider webhook secret, the REAL vendor key, and DEPLOYING the endpoint behind your own URL/TLS remain the human-owned steps.
508
+ >
509
+ > A delivered license is an ACCESS credential for delivered software value — NOT a token/coin/NFT, and not tradeable.
510
+
511
+ ## Going to market
512
+
513
+ Standing up the evidence vendor keypair, the price, and the first design partner are **human steps** —
514
+ see **P-7 (needs-human)** in [`STRATEGY.md`](../STRATEGY.md). The loop builds and locally tests; it never
515
+ holds a vendor key, never sets a price, and never deploys.
516
+
517
+
518
+ ---
519
+ <sub>© 2026 verifyhash.com · Licensed under Apache-2.0 (SPDX-License-Identifier: Apache-2.0) — see the [LICENSE](https://verifyhash.com/LICENSE) and [NOTICE](https://verifyhash.com/NOTICE) served with this file.</sub>
@@ -0,0 +1,66 @@
1
+ # Go live: the first dollar
2
+
3
+ This is the **decision-ready** path from "the loop built a sellable evidence product" to "a paying customer
4
+ receives a license." Everything the loop can do is **already built, tested, and green**; what remains are the
5
+ **human-owned outward steps** (provision a key, set a price, deploy) that the guardrails forbid the loop from
6
+ taking. This page is the short list of exactly those steps, in order.
7
+
8
+ The lowest-friction product to sell first is the **self-serve evidence license** (see
9
+ [`docs/EVIDENCE.md`](EVIDENCE.md)): a buyer pays, a signed license is delivered, and the license unlocks the
10
+ paid `vh evidence seal --sign` surface — verified **offline**, no server round-trip, no custody of funds.
11
+
12
+ ## The readiness proof (run this first)
13
+
14
+ ```
15
+ npm run go-live # node scripts/go-live-check.js
16
+ ```
17
+
18
+ This is an **offline, dependency-free** end-to-end proof (ephemeral `Wallet.createRandom()` keys, no network,
19
+ no deploy, no funds) that the three legs of the sale already work: **seal → independent-verify**, **issue →
20
+ verify → fail-closed gate**, and **fulfill → deliver → gate-accept**. It exits `0` and prints the verbatim
21
+ human steps last. If it is green, the software is ready; only the human steps below remain.
22
+
23
+ ## The human steps (needs-human — see `STRATEGY.md` › P-7)
24
+
25
+ 1. **Provision the vendor keypair.** Create a signing key **outside** this tool (a keystore/HSM/env var you
26
+ control). Its public address is the `--vendor` a buyer pins. The loop never holds it. **Publish** the
27
+ address so buyers can pin it.
28
+
29
+ 2. **Set the price and term.** Fill in the real price/term for each tier in your evidence plan catalog (the
30
+ bundled catalog is a **DRAFT** skeleton — the loop sets **no** price). Wire your billing provider
31
+ (e.g. Stripe Checkout) to those prices.
32
+
33
+ 3. **Wire self-serve fulfillment (no code to write).** Run the shipped reference webhook
34
+ **`vh fulfill-webhook`** (see [`docs/EVIDENCE.md` › _Reference self-serve fulfillment webhook_](EVIDENCE.md#reference-self-serve-fulfillment-webhook-vh-fulfill-webhook)),
35
+ point your provider's webhook at it with your **real** webhook secret (`--secret-env`), your **real**
36
+ vendor key (`--key-env`/`--key-file`), and a **`--binding`** file mapping each price to a plan — then every
37
+ paid event delivers a license automatically. This removes the human's last **code** step; deploying the
38
+ endpoint behind your own URL/TLS is a config/ops step, not a coding one.
39
+
40
+ 4. **Deploy.** Stand the endpoint up behind your own domain, TLS, and auth/ops posture. The loop binds
41
+ **loopback only** and **never deploys**.
42
+
43
+ 5. **Keep the public site fresh (`STRATEGY.md` › P-11).** verifyhash.com is the funnel's front door and
44
+ serves a pinned copy of the verifier artifacts, so it goes stale as the repo moves. Run
45
+ `node scripts/site-release.js --diff` to see, per file, what the live site is missing (a decision
46
+ signal — it exits `0` either way); the ~10-minute refresh is the REPLACE-mode runbook
47
+ [`docs/DEPLOY-PUBLIC-SITE.md`](DEPLOY-PUBLIC-SITE.md) §3c: **release → upload → `--mark-deployed` →
48
+ `--diff` clean**. Boundary (verbatim): the loop assembles and diffs INSIDE the repo only; uploading
49
+ to the live host is the human-owned P-11 step — never auto-executed.
50
+
51
+ ## The pilot fallback (TrustLedger, P-5)
52
+
53
+ If the self-serve evidence channel stalls, the **fallback** is the heavier TrustLedger design-partner
54
+ pilot (P-5: CPA/counsel review, a per-state policy table, a two-month broker tie-out — see
55
+ [`docs/DECIDE.md`](DECIDE.md) and `STRATEGY.md`; every one of those steps stays human-owned and
56
+ unchanged). Its zero-install pilot path is ONE emailed file —
57
+ [`trustledger/dist/trustledger-standalone.html`](../trustledger/dist/trustledger-standalone.html): the
58
+ partner double-clicks it, drags their real exports in, and the page makes **no network request** (see
59
+ [`docs/TRUSTLEDGER.md`](TRUSTLEDGER.md) › *Zero-install: the offline app*).
60
+
61
+ ## Revenue integrity (the hard line)
62
+
63
+ Income comes from **delivering value to paying customers** — a license is an **ACCESS credential for delivered
64
+ software value**, **NOT** a token/coin/NFT, not tradeable, and not a trusted timestamp. There is **no** token
65
+ sale, airdrop, staking/yield, or appreciating-asset scheme anywhere in this path. See the HARD GUARDRAILS and
66
+ P-7 in [`STRATEGY.md`](../STRATEGY.md).