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,481 @@
1
+ "use strict";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // cli/core/go-live-preflight.js — the OFFLINE, dependency-free GO-LIVE CONFIG PREFLIGHT
5
+ // for `vh evidence go-live-preflight` (T-61.3).
6
+ //
7
+ // WHY THIS EXISTS
8
+ // The evidence revenue chain is BUILT and green (webhook -> authenticate -> map price ->
9
+ // fulfill -> sign -> deliver -> the `--sign` gate accepts it). The one thing that stays
10
+ // HUMAN — and therefore the one thing a typo can silently break — is the operator's OWN
11
+ // configuration: their real price->plan BINDING, their plan CATALOG, and their vendor
12
+ // KEY. A single mistake there (a price bound to a plan that lacks the paid entitlement, a
13
+ // duplicate/typo'd price, a webhook secret wired to the wrong env var) produces the worst
14
+ // possible failure: the customer PAID, Stripe fired the webhook, but the delivered license
15
+ // does NOT unlock the product — and nobody notices until a refund request.
16
+ //
17
+ // This preflight turns that risk into an executable YES/NO. It drives the operator's REAL
18
+ // binding + catalog + key end-to-end, offline, with a throwaway workspace, and reports —
19
+ // per price — whether a paying customer would receive a license that PASSES the existing
20
+ // `vh evidence seal --sign` gate. A config error is a NAMED, non-zero failure that NAMES
21
+ // the offending price; a clean run is exit 0 ("every price delivers").
22
+ //
23
+ // WHAT IT PROVES (per price mapping in the binding)
24
+ // 1. RESOLVE ...... the price resolves to a catalog plan via the binding (never a silent
25
+ // default plan; an unmapped/duplicate/typo'd price is rejected up front
26
+ // by the SAME strict validator the live webhook uses, NAMING the price).
27
+ // 2. SECRET LEG ... (only with --secret-env, for Stripe prices) the operator's REAL webhook
28
+ // secret authenticates a correctly-signed synthetic event (fail-closed:
29
+ // a forged event is REJECTED) and the event parses to the same price.
30
+ // 3. FULFILL ...... the resolved order mints a signed license with the vendor KEY (the exact
31
+ // `fulfillEvidenceOrder` -> `buildLicense` path the live fulfiller uses).
32
+ // 4. GATE ......... the delivered license PASSES the existing paid gate — it is run through
33
+ // `vh evidence seal --sign` (which requires the `evidence_signed`
34
+ // entitlement). A plan that LACKS the paid entitlement is caught HERE
35
+ // (reported FAIL, never PASS) — the delivered license would not unlock
36
+ // the product the customer bought.
37
+ //
38
+ // THE PAID-ENTITLEMENT INVARIANT. Every purchasable evidence plan must deliver a license that
39
+ // unlocks the product's paid surface, `vh evidence seal --sign`, which requires
40
+ // `evidence_signed`. Every paid plan in the shipped DRAFT catalog includes it (the annual
41
+ // plan adds `evidence_unlimited` on top). A price mapped to a plan without `evidence_signed`
42
+ // means a paying customer gets a license that does not sign — exactly the silent failure this
43
+ // preflight exists to catch.
44
+ //
45
+ // POSTURE — GUARDRAILS BAKED IN. It holds NO real key beyond the one the operator provisions
46
+ // via --key-env/--key-file (read once through the SAME loadSigningWallet the paid gate uses,
47
+ // held in memory, NEVER written to disk or logged); the webhook secret comes ONLY from
48
+ // --secret-env and is used ONLY to HMAC-verify a synthetic event. It imports NONE of
49
+ // http/https/net/dns, opens NO network, deploys NOTHING, takes NO payment, and writes ONLY a
50
+ // throwaway workspace under the OS temp dir that it removes on exit (pass or fail). Exit
51
+ // contract matches the family: 0 all-deliver / 2 config error / 3 a price would not deliver.
52
+ //
53
+ // NEGATIVE SELF-TEST HOOK. `opts.injectFault` (an INTERNAL option the CLI never sets) injects
54
+ // a realistic fault into the secret leg so the preflight can demonstrate it is NOT a rubber
55
+ // stamp: with `injectFault:"signature"` the first Stripe price's synthetic event is signed
56
+ // with a corrupted signature, so the operator's real secret REJECTS it and the price is
57
+ // reported FAIL (fail-closed). Unset (the normal case), it exercises the real thing.
58
+ // ---------------------------------------------------------------------------
59
+
60
+ const fs = require("fs");
61
+ const os = require("os");
62
+ const path = require("path");
63
+ const crypto = require("crypto");
64
+
65
+ const coreAttestation = require("./attestation");
66
+ const evidencePlans = require("./evidence-plans");
67
+ const intake = require("./fulfill-intake");
68
+ const evidence = require("../evidence");
69
+
70
+ const EXIT = evidence.EXIT; // { OK:0, IO:1, USAGE:2, FAIL:3 } — one exit vocabulary for the family.
71
+
72
+ // The paid entitlement every purchasable plan must deliver (see THE PAID-ENTITLEMENT INVARIANT).
73
+ const REQUIRED_PAID_ENTITLEMENT = "evidence_signed";
74
+
75
+ // The one-line honest posture, stated ONCE so human + JSON output agree and can never drift.
76
+ const PREFLIGHT_TRUST_NOTE =
77
+ "A go-live preflight drives your REAL price->plan binding, plan catalog, and vendor key end-to-end " +
78
+ "OFFLINE (no network, no deploy, no funds) and reports whether every price delivers a license that " +
79
+ "PASSES the existing `vh evidence seal --sign` gate. It is NOT a legal/compliance sign-off and " +
80
+ "publishes NOTHING; going live (real key custody, real webhook secret, deploy) stays a HUMAN step. " +
81
+ "A license is an ACCESS credential for delivered software value — NOT a token/coin/NFT, not tradeable.";
82
+
83
+ // A fixed WRONG secret used ONLY to prove the operator's secret path is fail-closed (a forged event must
84
+ // be rejected). It is NOT a key and signs NOTHING of value — it is a throwaway HMAC label.
85
+ const _FAIL_CLOSED_PROBE_SECRET = "vh-preflight-fail-closed-probe-secret";
86
+
87
+ // ---------------------------------------------------------------------------------------------------
88
+ // Synthetic-event helpers (Stripe-shaped). Used ONLY to exercise the operator's real secret + parse
89
+ // path; nothing here is written to disk or sent anywhere.
90
+ // ---------------------------------------------------------------------------------------------------
91
+
92
+ // A minimal, real-shaped `checkout.session.completed` body carrying exactly the fields the intake parser
93
+ // reads: customer, the (expanded) subscription's single item price, and its billing-cycle end.
94
+ function _synthCheckoutEvent(priceId, customer, periodEndSec) {
95
+ return JSON.stringify({
96
+ type: "checkout.session.completed",
97
+ data: {
98
+ object: {
99
+ customer,
100
+ subscription: {
101
+ items: {
102
+ object: "list",
103
+ data: [{ price: { id: priceId }, current_period_end: periodEndSec }],
104
+ },
105
+ },
106
+ },
107
+ },
108
+ });
109
+ }
110
+
111
+ // A Stripe-compatible signature header `t=<unix>,v1=<hmac_sha256_hex>` over `${t}.${rawBody}`.
112
+ function _stripeSignatureHeader(rawBody, secret, tSec) {
113
+ const v1 = crypto.createHmac("sha256", secret).update(`${tSec}.${rawBody}`, "utf8").digest("hex");
114
+ return `t=${tSec},v1=${v1}`;
115
+ }
116
+
117
+ // Corrupt a v1 hex signature (flip its first hex digit) so verification MUST reject it — the injected
118
+ // fault for the negative self-test.
119
+ function _corruptSignatureHeader(header) {
120
+ return header.replace(/v1=([0-9a-f])/, (_m, c) => `v1=${c === "0" ? "1" : "0"}`);
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------------------------------
124
+ // Argument parsing. EXACTLY-ONE-of key sources is enforced downstream by loadSigningWallet; the parser
125
+ // only collects flags (mirrors the rest of the evidence CLI).
126
+ // ---------------------------------------------------------------------------------------------------
127
+
128
+ function parseGoLivePreflightArgs(argv) {
129
+ const opts = {
130
+ binding: undefined, // REQUIRED: the operator's price->plan binding JSON
131
+ catalog: undefined, // OPTIONAL: plan catalog (default = bundled DRAFT baseline)
132
+ secretEnv: undefined, // OPTIONAL: env var holding the webhook signing secret
133
+ keyEnv: undefined, // vendor key source (EXACTLY ONE of key-env/key-file)
134
+ keyFile: undefined,
135
+ json: false,
136
+ };
137
+ for (let i = 0; i < argv.length; i++) {
138
+ const a = argv[i];
139
+ const need = () => {
140
+ const v = argv[++i];
141
+ if (v === undefined || String(v).startsWith("--")) {
142
+ const e = new Error(`${a} requires a value`);
143
+ e.usage = true;
144
+ throw e;
145
+ }
146
+ return v;
147
+ };
148
+ switch (a) {
149
+ case "--binding": opts.binding = need(); break;
150
+ case "--catalog": opts.catalog = need(); break;
151
+ case "--secret-env": opts.secretEnv = need(); break;
152
+ case "--key-env": opts.keyEnv = need(); break;
153
+ case "--key-file": opts.keyFile = need(); break;
154
+ case "--json": opts.json = true; break;
155
+ default: {
156
+ const e = new Error(`unknown flag: ${a}`);
157
+ e.usage = true;
158
+ throw e;
159
+ }
160
+ }
161
+ }
162
+ return opts;
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------------------------------
166
+ // The per-price fulfillment+gate leg. Returns a result record { provider, priceId, plan, ok, steps[],
167
+ // reason? }. NEVER throws for an ordinary config/delivery failure — it records `ok:false` with a NAMED
168
+ // reason so the driver can surface EVERY offending price. `ws` is the throwaway workspace; `ctx` carries
169
+ // the loaded catalog/binding/wallet/secret + the injected clock + the fault hook.
170
+ // ---------------------------------------------------------------------------------------------------
171
+
172
+ async function _preflightPrice(mapping, index, ctx, ws) {
173
+ const { provider, priceId, planId } = mapping;
174
+ const label = `${provider}:${priceId}`;
175
+ const steps = [];
176
+ const fail = (reason) => ({ provider, priceId, plan: planId, ok: false, steps, reason });
177
+
178
+ // (1) RESOLVE — the price must resolve to the catalog plan via the binding (exercises the SAME resolver
179
+ // the live webhook uses; a mismatch is a bug, never a silent default).
180
+ let resolved;
181
+ try {
182
+ resolved = intake.resolveEvidencePlanId(ctx.binding, provider, priceId);
183
+ } catch (e) {
184
+ return fail(`price ${label} does not resolve to any plan: ${e.message}`);
185
+ }
186
+ if (resolved !== planId) {
187
+ return fail(`price ${label} resolved to plan '${resolved}' but the mapping declares '${planId}' (ambiguous binding)`);
188
+ }
189
+ steps.push(`resolved plan '${resolved}' via the price binding`);
190
+
191
+ // Build the fulfillment ORDER. With --secret-env on a Stripe price we drive the FULL real intake path
192
+ // (authenticate the operator's secret, fail-closed, then parse+normalize the event); otherwise we build
193
+ // the order directly from the resolved plan.
194
+ const periodEndSec = ctx.issuedSec + 30 * 86400; // a real 30-day window (paidThrough > issuedAt)
195
+ const customer = `go-live-preflight (price ${priceId})`;
196
+ let order;
197
+
198
+ if (ctx.secret != null && provider === intake.STRIPE_PROVIDER) {
199
+ const rawBody = _synthCheckoutEvent(priceId, customer, periodEndSec);
200
+ let header = _stripeSignatureHeader(rawBody, ctx.secret, ctx.issuedSec);
201
+ // Negative self-test: corrupt the FIRST Stripe price's signature so the real secret must reject it.
202
+ if (ctx.injectFault === "signature" && !ctx.faultUsed) {
203
+ ctx.faultUsed = true;
204
+ header = _corruptSignatureHeader(header);
205
+ }
206
+ // (2a) AUTHENTICATE with the operator's REAL secret — a rejected signature is a NAMED fail-closed FAIL.
207
+ const sig = intake.verifyProviderSignature(rawBody, header, ctx.secret, { nowSec: ctx.issuedSec });
208
+ if (!sig.ok) {
209
+ return fail(
210
+ `price ${label} FAILED the webhook secret path (${ctx.secretEnv}): the synthesized event's ` +
211
+ `signature was rejected (${sig.reason}) — a real paid event would be refused (fail-closed), ` +
212
+ `delivering NO license`
213
+ );
214
+ }
215
+ // (2b) FAIL-CLOSED PROOF — a forged signature (a wrong secret) MUST be rejected; if it authenticates,
216
+ // the secret path is broken.
217
+ const forged = intake.verifyProviderSignature(
218
+ rawBody,
219
+ _stripeSignatureHeader(rawBody, _FAIL_CLOSED_PROBE_SECRET, ctx.issuedSec),
220
+ ctx.secret,
221
+ { nowSec: ctx.issuedSec }
222
+ );
223
+ if (forged.ok) {
224
+ return fail(`price ${label} secret path is NOT fail-closed: a FORGED event authenticated against ${ctx.secretEnv}`);
225
+ }
226
+ // (2c) PARSE + NORMALIZE the authenticated event through the real intake seams.
227
+ let event;
228
+ try {
229
+ event = intake.parseEvidenceEvent(rawBody, { binding: ctx.binding });
230
+ } catch (e) {
231
+ return fail(`price ${label} authenticated but FAILED to parse: ${e.message}`);
232
+ }
233
+ if (event.priceId !== priceId) {
234
+ return fail(`price ${label} parsed to a different price '${event.priceId}'`);
235
+ }
236
+ try {
237
+ order = intake.normalizeEvidenceEvent(event, ctx.binding, { issuedAt: ctx.issuedAt });
238
+ } catch (e) {
239
+ return fail(`price ${label} could not normalize: ${e.message}`);
240
+ }
241
+ steps.push(`secret path (${ctx.secretEnv}) AUTHENTICATED a signed event and REJECTED a forged one (fail-closed)`);
242
+ } else {
243
+ order = { plan: planId, customer, issuedAt: ctx.issuedAt, paidThrough: new Date(periodEndSec * 1000).toISOString() };
244
+ if (ctx.secret != null) {
245
+ steps.push(`signature leg skipped (non-Stripe provider '${provider}'); the fulfillment path was still validated`);
246
+ }
247
+ }
248
+
249
+ // (3) FULFILL + MINT — the exact order->license-params->signed-license path the live fulfiller uses. The
250
+ // key lives ONLY inside ctx.wallet; the written license carries only PUBLIC bytes (signature + signer
251
+ // address).
252
+ let params;
253
+ let canonical;
254
+ try {
255
+ params = evidencePlans.fulfillEvidenceOrder(order, ctx.catalog);
256
+ const container = await evidence.buildLicense(params, ctx.wallet);
257
+ canonical = evidence.serializeSignedLicense(container);
258
+ } catch (e) {
259
+ return fail(`price ${label} could not fulfill/sign a license: ${e.message}`);
260
+ }
261
+ const licPath = path.join(ws, `license-${index}.vhevidence-license.json`);
262
+ try {
263
+ fs.writeFileSync(licPath, canonical);
264
+ } catch (e) {
265
+ return fail(`price ${label} could not write its license to the workspace: ${e.message}`);
266
+ }
267
+ steps.push(`minted a signed license (plan '${params.plan}', entitlements ${params.entitlements.join("+")})`);
268
+
269
+ // (4) GATE — run the delivered license through the EXISTING paid gate: `vh evidence seal --sign`
270
+ // requires `evidence_signed`. A plan that LACKS the paid entitlement is caught HERE (FAIL, never
271
+ // PASS). We drive the real command in-process, capturing its output so it never leaks to our stdout.
272
+ let capturedErr = "";
273
+ const sealOut = path.join(ws, `seal-${index}.vhevidence.json`);
274
+ let gateCode;
275
+ try {
276
+ gateCode = await evidence.runEvidenceSeal(
277
+ {
278
+ dir: ctx.dataDir,
279
+ sign: true,
280
+ license: licPath,
281
+ vendor: ctx.wallet.address,
282
+ keyEnv: ctx.keyEnv,
283
+ keyFile: ctx.keyFile,
284
+ out: sealOut,
285
+ },
286
+ { write: () => {}, writeErr: (s) => { capturedErr += s; }, now: ctx.today }
287
+ );
288
+ } catch (e) {
289
+ return fail(`price ${label} crashed the paid gate: ${e && e.message ? e.message : String(e)}`);
290
+ }
291
+ if (gateCode !== EXIT.OK) {
292
+ const detail = capturedErr.trim().split("\n").filter(Boolean).slice(-1)[0] || `gate exit ${gateCode}`;
293
+ return fail(
294
+ `price ${label} delivered a license the paid \`vh evidence seal --sign\` gate REJECTED for plan ` +
295
+ `'${planId}' (needs '${REQUIRED_PAID_ENTITLEMENT}'): ${detail}`
296
+ );
297
+ }
298
+ steps.push(`delivered license PASSED the paid \`vh evidence seal --sign\` gate ('${REQUIRED_PAID_ENTITLEMENT}')`);
299
+
300
+ return { provider, priceId, plan: planId, ok: true, steps };
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------------------------------
304
+ // runGoLivePreflight(opts, io) — validate the operator's config, then drive every price end-to-end in a
305
+ // throwaway workspace. Resolves to a NUMBER exit code: 0 all-deliver / 2 config error / 3 a price would
306
+ // not deliver. `io` is injectable (write/writeErr sinks + a `now` Date + a `nowISO`) so the command is
307
+ // deterministic under test. The workspace is ALWAYS removed (pass or fail).
308
+ // ---------------------------------------------------------------------------------------------------
309
+
310
+ async function runGoLivePreflight(opts, io = {}) {
311
+ const write = io.write || ((s) => process.stdout.write(s));
312
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
313
+ const today = io.now instanceof Date ? io.now : new Date();
314
+ const issuedAt = today.toISOString();
315
+ const issuedSec = Math.floor(today.getTime() / 1000);
316
+
317
+ // ---- required flags (a clear, key-free message per missing one) ----------
318
+ if (opts.binding == null) {
319
+ writeErr("error: `vh evidence go-live-preflight` requires --binding <file> (your price->plan binding)\n");
320
+ return EXIT.USAGE;
321
+ }
322
+
323
+ // ---- the plan catalog (bundled DRAFT by default) -------------------------
324
+ const catalogPath = opts.catalog != null ? path.resolve(opts.catalog) : evidence.BUNDLED_EVIDENCE_CATALOG;
325
+ let catalog;
326
+ try {
327
+ catalog = evidencePlans.validateEvidencePlanCatalog(JSON.parse(fs.readFileSync(catalogPath, "utf8")));
328
+ } catch (e) {
329
+ writeErr(`error: cannot load evidence plan catalog ${catalogPath}: ${e.message}\n`);
330
+ return EXIT.USAGE;
331
+ }
332
+
333
+ // ---- the price binding (validated against the catalog: an UNMAPPED / duplicate / typo'd price is a
334
+ // NAMED reject here, NEVER a silent default plan) ----------------------
335
+ const bindingPath = path.resolve(opts.binding);
336
+ let binding;
337
+ try {
338
+ binding = intake.validateEvidencePriceBinding(JSON.parse(fs.readFileSync(bindingPath, "utf8")), catalog);
339
+ } catch (e) {
340
+ writeErr(`error: cannot load --binding ${opts.binding}: ${e.message}\n`);
341
+ return EXIT.USAGE;
342
+ }
343
+
344
+ // ---- the webhook secret (OPTIONAL; from --secret-env only — name the VAR, never the value) ----
345
+ let secret = null;
346
+ if (opts.secretEnv != null) {
347
+ secret = process.env[opts.secretEnv];
348
+ if (secret === undefined || secret === "") {
349
+ writeErr(`error: environment variable ${opts.secretEnv} is not set (or empty); it must hold the webhook signing secret\n`);
350
+ return EXIT.USAGE;
351
+ }
352
+ }
353
+
354
+ // ---- the VENDOR key (EXACTLY ONE of --key-env/--key-file; read-used-held-in-memory, never persisted) ----
355
+ let wallet;
356
+ try {
357
+ ({ wallet } = coreAttestation.loadSigningWallet({ keyEnv: opts.keyEnv, keyFile: opts.keyFile }));
358
+ } catch (e) {
359
+ writeErr(`error: ${e.message}\n`);
360
+ return EXIT.USAGE;
361
+ }
362
+
363
+ // ---- the throwaway workspace + a tiny data folder the gate seals ----------
364
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), "vh-golive-preflight-"));
365
+ const dataDir = path.join(ws, "data");
366
+ const results = [];
367
+ let code = EXIT.OK;
368
+ try {
369
+ fs.mkdirSync(dataDir, { recursive: true });
370
+ fs.writeFileSync(path.join(dataDir, "sample.txt"), "go-live preflight sample payload\n");
371
+
372
+ const ctx = {
373
+ catalog,
374
+ binding,
375
+ wallet,
376
+ keyEnv: opts.keyEnv,
377
+ keyFile: opts.keyFile,
378
+ secret,
379
+ secretEnv: opts.secretEnv,
380
+ today,
381
+ issuedAt,
382
+ issuedSec,
383
+ dataDir,
384
+ injectFault: opts.injectFault || null,
385
+ faultUsed: false,
386
+ };
387
+
388
+ // Drive every price mapping in the (validated, deterministically-sorted) binding.
389
+ for (let i = 0; i < binding.mappings.length; i++) {
390
+ // eslint-disable-next-line no-await-in-loop
391
+ const r = await _preflightPrice(binding.mappings[i], i, ctx, ws);
392
+ results.push(r);
393
+ if (!r.ok) code = EXIT.FAIL;
394
+ }
395
+ } catch (e) {
396
+ // An unexpected workspace/IO error (never leaks the key/secret).
397
+ writeErr(`error: go-live preflight could not run: ${e && e.message ? e.message : String(e)}\n`);
398
+ code = EXIT.IO;
399
+ } finally {
400
+ fs.rmSync(ws, { recursive: true, force: true });
401
+ }
402
+
403
+ const passed = results.filter((r) => r.ok).length;
404
+ const failed = results.length - passed;
405
+
406
+ // ---- emit the verdict ----------------------------------------------------
407
+ if (opts.json) {
408
+ write(
409
+ JSON.stringify(
410
+ {
411
+ ok: code === EXIT.OK,
412
+ note: PREFLIGHT_TRUST_NOTE,
413
+ catalog: catalogPath,
414
+ binding: bindingPath,
415
+ secretExercised: secret != null,
416
+ requiredEntitlement: REQUIRED_PAID_ENTITLEMENT,
417
+ priceCount: results.length,
418
+ passed,
419
+ failed,
420
+ results: results.map((r) => ({
421
+ provider: r.provider,
422
+ priceId: r.priceId,
423
+ plan: r.plan,
424
+ ok: r.ok,
425
+ ...(r.ok ? { steps: r.steps } : { reason: r.reason }),
426
+ })),
427
+ },
428
+ null,
429
+ 2
430
+ ) + "\n"
431
+ );
432
+ } else {
433
+ write(PREFLIGHT_TRUST_NOTE + "\n\n");
434
+ write("verifyhash — GO-LIVE CONFIG PREFLIGHT (offline; no network; no deploy)\n");
435
+ write(` catalog: ${catalogPath}\n`);
436
+ write(` binding: ${bindingPath} (${results.length} price mapping${results.length === 1 ? "" : "s"})\n`);
437
+ write(
438
+ ` secret: ${secret != null ? `exercising the real webhook secret path (--secret-env ${opts.secretEnv})` : "not exercised (pass --secret-env to test it)"}\n\n`
439
+ );
440
+ for (const r of results) {
441
+ write(`PRICE ${r.provider}:${r.priceId} -> plan ${r.plan} ... ${r.ok ? "PASS" : "FAIL"}\n`);
442
+ if (r.ok) {
443
+ for (const s of r.steps) write(` - ${s}\n`);
444
+ } else {
445
+ write(` - ${r.reason}\n`);
446
+ }
447
+ }
448
+ write("\n");
449
+ if (code === EXIT.OK) {
450
+ write(`ALL ${results.length} price${results.length === 1 ? "" : "s"} deliver a license that PASSES the paid gate — the binding is go-live-ready.\n`);
451
+ } else if (code === EXIT.FAIL) {
452
+ write(`PREFLIGHT FAILED: ${failed} of ${results.length} price(s) would NOT deliver a working license — fix the NAMED price(s) before going live.\n`);
453
+ }
454
+ }
455
+ return code;
456
+ }
457
+
458
+ // ---------------------------------------------------------------------------------------------------
459
+ // cmdGoLivePreflight(argv, io) — parse argv, then run. Resolves to a NUMBER exit code (2 on a bad flag).
460
+ // ---------------------------------------------------------------------------------------------------
461
+
462
+ function cmdGoLivePreflight(argv, io = {}) {
463
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
464
+ let opts;
465
+ try {
466
+ opts = parseGoLivePreflightArgs(argv);
467
+ } catch (e) {
468
+ writeErr(`error: ${e.message}\n`);
469
+ return Promise.resolve(EXIT.USAGE);
470
+ }
471
+ return runGoLivePreflight(opts, io);
472
+ }
473
+
474
+ module.exports = {
475
+ EXIT,
476
+ REQUIRED_PAID_ENTITLEMENT,
477
+ PREFLIGHT_TRUST_NOTE,
478
+ parseGoLivePreflightArgs,
479
+ runGoLivePreflight,
480
+ cmdGoLivePreflight,
481
+ };