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/docs/EVIDENCE.md
ADDED
|
@@ -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>
|
package/docs/GO-LIVE.md
ADDED
|
@@ -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).
|