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