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,495 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// cli/core/evidence-plans.js — THE EVIDENCE PLAN CATALOG (T-48.1).
|
|
5
|
+
//
|
|
6
|
+
// A versioned, strictly-validated planId -> { entitlements, termDays, displayName }
|
|
7
|
+
// mapping over the EVIDENCE product's CLOSED ENTITLEMENTS table (evidence_signed,
|
|
8
|
+
// evidence_unlimited) + a PURE deterministic fulfillEvidenceOrder(order, catalog).
|
|
9
|
+
// This MIRRORS trustledger/plans.js (validatePlanCatalog/getPlan) and
|
|
10
|
+
// trustledger/license.js's fulfillOrder EXACTLY, but is bound to the EVIDENCE CFG
|
|
11
|
+
// so the two products stay DISJOINT: a different catalog `kind`, a different closed
|
|
12
|
+
// entitlement set, and a different license `kind`. The two catalogs can never be
|
|
13
|
+
// mistaken for one another and a TrustLedger entitlement can never enter an
|
|
14
|
+
// evidence plan (nor vice-versa).
|
|
15
|
+
//
|
|
16
|
+
// WHY HERE (cli/core/, next to the evidence license framing).
|
|
17
|
+
// The closed entitlement table is the EVIDENCE module's `LICENSE_CFG.entitlements`
|
|
18
|
+
// (cli/evidence.js). This module imports that CFG and derives its allowed flag set
|
|
19
|
+
// from it via the SAME core `entitlementFlags(cfg)` helper the license gate uses —
|
|
20
|
+
// never a hard-coded copy — so the catalog and the gate that honors a license can
|
|
21
|
+
// never drift. The evidence CFG remains the SINGLE source of truth for the closed
|
|
22
|
+
// table.
|
|
23
|
+
//
|
|
24
|
+
// DESIGN PROPERTIES (identical posture to trustledger/plans.js).
|
|
25
|
+
// * PURE / I-O-FREE / DETERMINISTIC. NO filesystem, NO clock, NO network, NO
|
|
26
|
+
// ethers/key handling. validateEvidencePlanCatalog takes the parsed object as an
|
|
27
|
+
// argument (the caller reads the bundled JSON and passes it); getEvidencePlan is
|
|
28
|
+
// a pure lookup; fulfillEvidenceOrder is a pure mapping. The same inputs always
|
|
29
|
+
// produce byte-identical output, and a grep finds no fs / http / ethers-require /
|
|
30
|
+
// clock use in this file.
|
|
31
|
+
// * STRICT. A malformed catalog (wrong kind, unsupported schemaVersion,
|
|
32
|
+
// empty/missing plans, a duplicate planId, an unknown/forged entitlement flag, an
|
|
33
|
+
// empty entitlement bundle, a non-positive/non-integer termDays, a missing
|
|
34
|
+
// displayName) raises a NAMED error on the FIRST defect — never a silent pass,
|
|
35
|
+
// never a partial accept, never a last-wins mis-grant.
|
|
36
|
+
// * CLOSED ENTITLEMENTS, single source of truth. The set of entitlement flags a
|
|
37
|
+
// plan may grant is the EVIDENCE license CFG's CLOSED table, derived via
|
|
38
|
+
// coreLicense.entitlementFlags(LICENSE_CFG). A flag not in that table is a hard
|
|
39
|
+
// reject.
|
|
40
|
+
//
|
|
41
|
+
// HONEST POSTURE.
|
|
42
|
+
// A plan is an ACCESS DESCRIPTION for delivered software value: which evidence
|
|
43
|
+
// features the subscription unlocks and for how long. It is NOT a token, NOT
|
|
44
|
+
// tradeable, NOT an appreciating asset, and the catalog makes NO claim of
|
|
45
|
+
// regulatory compliance. The actual subscription agreement governs; this file only
|
|
46
|
+
// maps a purchased plan to the features it entitles.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const coreLicense = require("./license");
|
|
50
|
+
// The evidence product framing (the closed entitlement table + license kind) lives in
|
|
51
|
+
// cli/evidence.js. We import its LICENSE_CFG / error class so the catalog is bound to
|
|
52
|
+
// the SAME closed table the evidence license gate honors. (cli/evidence.js requires
|
|
53
|
+
// this module's siblings but NOT this file, so there is no require cycle.)
|
|
54
|
+
const evidence = require("../evidence");
|
|
55
|
+
|
|
56
|
+
// The evidence catalog has its OWN `kind`/`schemaVersion`, DISJOINT from the
|
|
57
|
+
// TrustLedger plan-catalog kind, the evidence seal/license payloads, etc. — so an
|
|
58
|
+
// evidence catalog can never be mistaken for one of them. Validation REJECTS any
|
|
59
|
+
// unsupported version rather than guessing.
|
|
60
|
+
const EVIDENCE_PLAN_CATALOG_KIND = "vh-evidence-plan-catalog";
|
|
61
|
+
const EVIDENCE_PLAN_CATALOG_SCHEMA_VERSION = 1;
|
|
62
|
+
const SUPPORTED_EVIDENCE_PLAN_CATALOG_SCHEMA_VERSIONS = Object.freeze([1]);
|
|
63
|
+
|
|
64
|
+
// The dedicated error type for catalog defects. Distinct, named class so callers/tests
|
|
65
|
+
// catch ONE evidence-plan-catalog error.
|
|
66
|
+
class EvidencePlanCatalogError extends Error {
|
|
67
|
+
constructor(message) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = "EvidencePlanCatalogError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The SINGLE source of valid entitlement flags: the EVIDENCE license CFG's CLOSED
|
|
74
|
+
// table, derived (never re-declared) via the SAME core helper the gate uses, so the
|
|
75
|
+
// catalog and the gate can never drift. Exposed as a GENUINELY IMMUTABLE frozen array
|
|
76
|
+
// (NOT a Set — a frozen Set's `.add()` is a no-op lock; a frozen array cannot be
|
|
77
|
+
// extended under "use strict") so the exported closed set cannot grow.
|
|
78
|
+
const ALLOWED_ENTITLEMENT_FLAGS = Object.freeze([
|
|
79
|
+
...coreLicense.entitlementFlags(evidence.LICENSE_CFG),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// A private, frozen null-prototype membership index for O(1) lookup, derived from the
|
|
83
|
+
// SAME frozen array. Not exported; frozen + null-proto so it can neither be widened nor
|
|
84
|
+
// polluted via __proto__.
|
|
85
|
+
const _ALLOWED_FLAG_INDEX = Object.freeze(
|
|
86
|
+
ALLOWED_ENTITLEMENT_FLAGS.reduce((m, f) => {
|
|
87
|
+
m[f] = true;
|
|
88
|
+
return m;
|
|
89
|
+
}, Object.create(null))
|
|
90
|
+
);
|
|
91
|
+
function _isAllowedEntitlement(flag) {
|
|
92
|
+
return Object.prototype.hasOwnProperty.call(_ALLOWED_FLAG_INDEX, flag);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// validateEvidencePlanCatalog(obj) -> validated, deeply-FROZEN catalog
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
//
|
|
99
|
+
// Strictly validates and returns a NEW deeply-frozen, canonical catalog object. Throws
|
|
100
|
+
// EvidencePlanCatalogError on the FIRST defect. Never mutates the input. PURE: the
|
|
101
|
+
// caller parses the JSON (or passes an object) and hands it in. The returned catalog
|
|
102
|
+
// carries a planId-sorted `plans` array + a frozen `plansById` lookup map.
|
|
103
|
+
function validateEvidencePlanCatalog(obj) {
|
|
104
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
105
|
+
throw new EvidencePlanCatalogError("evidence plan catalog must be a JSON object");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- kind: must be exactly the evidence catalog kind ---------------------
|
|
109
|
+
if (obj.kind !== EVIDENCE_PLAN_CATALOG_KIND) {
|
|
110
|
+
throw new EvidencePlanCatalogError(
|
|
111
|
+
`evidence plan catalog has wrong kind ${JSON.stringify(obj.kind)}; ` +
|
|
112
|
+
`expected ${JSON.stringify(EVIDENCE_PLAN_CATALOG_KIND)}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- schemaVersion: present and exactly a supported integer --------------
|
|
117
|
+
if (!Object.prototype.hasOwnProperty.call(obj, "schemaVersion")) {
|
|
118
|
+
throw new EvidencePlanCatalogError(
|
|
119
|
+
"evidence plan catalog is missing required field: schemaVersion"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (!SUPPORTED_EVIDENCE_PLAN_CATALOG_SCHEMA_VERSIONS.includes(obj.schemaVersion)) {
|
|
123
|
+
throw new EvidencePlanCatalogError(
|
|
124
|
+
`unsupported evidence plan catalog schemaVersion ${JSON.stringify(obj.schemaVersion)}; ` +
|
|
125
|
+
`this build understands: ${SUPPORTED_EVIDENCE_PLAN_CATALOG_SCHEMA_VERSIONS.join(", ")}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- plans: a non-empty array of plan entries ----------------------------
|
|
130
|
+
if (!Object.prototype.hasOwnProperty.call(obj, "plans")) {
|
|
131
|
+
throw new EvidencePlanCatalogError(
|
|
132
|
+
"evidence plan catalog is missing required field: plans"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(obj.plans)) {
|
|
136
|
+
throw new EvidencePlanCatalogError("evidence plan catalog plans must be an array");
|
|
137
|
+
}
|
|
138
|
+
if (obj.plans.length === 0) {
|
|
139
|
+
throw new EvidencePlanCatalogError(
|
|
140
|
+
"evidence plan catalog plans must be a non-empty array"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate each plan; collect into a planId -> frozen-plan map, rejecting a duplicate
|
|
145
|
+
// planId (an ambiguous catalog is a hard error, never a last-wins mis-grant).
|
|
146
|
+
const byId = new Map();
|
|
147
|
+
for (let i = 0; i < obj.plans.length; i++) {
|
|
148
|
+
const plan = _validatePlan(obj.plans[i], i);
|
|
149
|
+
if (byId.has(plan.planId)) {
|
|
150
|
+
throw new EvidencePlanCatalogError(
|
|
151
|
+
`evidence plan catalog has duplicate planId ${JSON.stringify(plan.planId)}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
byId.set(plan.planId, plan);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Build a canonical, deeply-frozen catalog. The plans array is emitted in
|
|
158
|
+
// planId-sorted order so enumeration is deterministic regardless of input order;
|
|
159
|
+
// `plansById` is a frozen lookup map for getEvidencePlan.
|
|
160
|
+
const sortedIds = [...byId.keys()].sort();
|
|
161
|
+
const plans = Object.freeze(sortedIds.map((id) => byId.get(id)));
|
|
162
|
+
const plansById = Object.freeze(
|
|
163
|
+
sortedIds.reduce((m, id) => {
|
|
164
|
+
m[id] = byId.get(id);
|
|
165
|
+
return m;
|
|
166
|
+
}, Object.create(null))
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
kind: EVIDENCE_PLAN_CATALOG_KIND,
|
|
171
|
+
schemaVersion: obj.schemaVersion,
|
|
172
|
+
plans,
|
|
173
|
+
plansById,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// _validatePlan(plan, index) -> a frozen, canonical plan
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
//
|
|
181
|
+
// Strictly validates ONE plan entry. `index` is woven into the error so a defect is
|
|
182
|
+
// locatable. Throws EvidencePlanCatalogError on the first problem.
|
|
183
|
+
function _validatePlan(plan, index) {
|
|
184
|
+
const at = `plan[${index}]`;
|
|
185
|
+
if (plan === null || typeof plan !== "object" || Array.isArray(plan)) {
|
|
186
|
+
throw new EvidencePlanCatalogError(`${at} must be an object`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---- planId: a non-empty string handle -----------------------------------
|
|
190
|
+
if (typeof plan.planId !== "string" || plan.planId.trim() === "") {
|
|
191
|
+
throw new EvidencePlanCatalogError(`${at}.planId must be a non-empty string`);
|
|
192
|
+
}
|
|
193
|
+
const planId = plan.planId;
|
|
194
|
+
|
|
195
|
+
// ---- displayName: a non-empty human label (required) ---------------------
|
|
196
|
+
if (typeof plan.displayName !== "string" || plan.displayName.trim() === "") {
|
|
197
|
+
throw new EvidencePlanCatalogError(
|
|
198
|
+
`evidence plan ${JSON.stringify(planId)} is missing a non-empty displayName`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---- entitlements: a non-empty closed set drawn ONLY from the EVIDENCE
|
|
203
|
+
// closed table; no unknown flag, no duplicate --------------------------
|
|
204
|
+
if (!Array.isArray(plan.entitlements)) {
|
|
205
|
+
throw new EvidencePlanCatalogError(
|
|
206
|
+
`evidence plan ${JSON.stringify(planId)} entitlements must be an array`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (plan.entitlements.length === 0) {
|
|
210
|
+
throw new EvidencePlanCatalogError(
|
|
211
|
+
`evidence plan ${JSON.stringify(planId)} entitlements must be a non-empty array`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
for (const flag of plan.entitlements) {
|
|
216
|
+
if (typeof flag !== "string") {
|
|
217
|
+
throw new EvidencePlanCatalogError(
|
|
218
|
+
`evidence plan ${JSON.stringify(planId)} has a non-string entitlement ${JSON.stringify(flag)}`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (!_isAllowedEntitlement(flag)) {
|
|
222
|
+
throw new EvidencePlanCatalogError(
|
|
223
|
+
`evidence plan ${JSON.stringify(planId)} has unknown entitlement ${JSON.stringify(flag)}; ` +
|
|
224
|
+
`closed set is: ${ALLOWED_ENTITLEMENT_FLAGS.join(", ")}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (seen.has(flag)) {
|
|
228
|
+
throw new EvidencePlanCatalogError(
|
|
229
|
+
`evidence plan ${JSON.stringify(planId)} has duplicate entitlement ${JSON.stringify(flag)}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
seen.add(flag);
|
|
233
|
+
}
|
|
234
|
+
// Emit entitlements in the CLOSED-table sort order regardless of input order, so a
|
|
235
|
+
// plan's serialization is byte-deterministic and order-independent.
|
|
236
|
+
const entitlements = Object.freeze([...seen].sort());
|
|
237
|
+
|
|
238
|
+
// ---- termDays: a positive INTEGER number of days -------------------------
|
|
239
|
+
// Integer discipline: a non-integer or non-positive term is a hard reject, never
|
|
240
|
+
// rounded or coerced.
|
|
241
|
+
if (!Object.prototype.hasOwnProperty.call(plan, "termDays")) {
|
|
242
|
+
throw new EvidencePlanCatalogError(
|
|
243
|
+
`evidence plan ${JSON.stringify(planId)} is missing required field: termDays`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (typeof plan.termDays !== "number" || !Number.isInteger(plan.termDays)) {
|
|
247
|
+
throw new EvidencePlanCatalogError(
|
|
248
|
+
`evidence plan ${JSON.stringify(planId)} termDays must be an integer; got ${JSON.stringify(plan.termDays)}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (plan.termDays <= 0) {
|
|
252
|
+
throw new EvidencePlanCatalogError(
|
|
253
|
+
`evidence plan ${JSON.stringify(planId)} termDays must be a positive integer; got ${JSON.stringify(plan.termDays)}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return Object.freeze({
|
|
258
|
+
planId,
|
|
259
|
+
displayName: plan.displayName,
|
|
260
|
+
entitlements,
|
|
261
|
+
termDays: plan.termDays,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// getEvidencePlan(catalog, planId) -> the frozen plan, or throws on an unknown id
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
//
|
|
269
|
+
// PURE lookup against a VALIDATED catalog. Returns the frozen plan for a known id;
|
|
270
|
+
// throws a NAMED EvidencePlanCatalogError NAMING the known plans for an unknown id
|
|
271
|
+
// (never returns undefined — an unknown plan is an error, not an empty entitlement).
|
|
272
|
+
function getEvidencePlan(catalog, planId) {
|
|
273
|
+
if (
|
|
274
|
+
catalog === null ||
|
|
275
|
+
typeof catalog !== "object" ||
|
|
276
|
+
catalog.plansById === null ||
|
|
277
|
+
typeof catalog.plansById !== "object"
|
|
278
|
+
) {
|
|
279
|
+
throw new EvidencePlanCatalogError(
|
|
280
|
+
"getEvidencePlan requires a validated evidence plan catalog"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (typeof planId !== "string" || planId.trim() === "") {
|
|
284
|
+
throw new EvidencePlanCatalogError("getEvidencePlan requires a non-empty planId");
|
|
285
|
+
}
|
|
286
|
+
if (!Object.prototype.hasOwnProperty.call(catalog.plansById, planId)) {
|
|
287
|
+
const known = Object.keys(catalog.plansById).sort().join(", ");
|
|
288
|
+
throw new EvidencePlanCatalogError(
|
|
289
|
+
`unknown evidence planId ${JSON.stringify(planId)}; known plans are: ${known}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return catalog.plansById[planId];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ===========================================================================
|
|
296
|
+
// THE EVIDENCE ORDER -> LICENSE-PARAMS MAPPING.
|
|
297
|
+
//
|
|
298
|
+
// fulfillEvidenceOrder turns a normalized ORDER — what a billing webhook knows after a
|
|
299
|
+
// payment succeeds: which `plan` was bought, for which `customer`, when it was
|
|
300
|
+
// `issuedAt`, and through when it is `paidThrough` — into the EXACT params
|
|
301
|
+
// buildLicensePayload/buildLicense (evidence.buildLicense) consume. It MIRRORS
|
|
302
|
+
// trustledger/license.js's fulfillOrder EXACTLY, but is bound to the EVIDENCE catalog +
|
|
303
|
+
// CFG.
|
|
304
|
+
//
|
|
305
|
+
// PURE + DETERMINISTIC. No filesystem, no clock, no network, no key. The SAME
|
|
306
|
+
// { plan, customer, paidThrough, issuedAt } + the SAME catalog yields a byte-identical
|
|
307
|
+
// params object EVERY time (so the signed evidence license bytes are reproducible). The
|
|
308
|
+
// caller resolves + validates the catalog (validateEvidencePlanCatalog) and passes it
|
|
309
|
+
// in; we never read it from disk.
|
|
310
|
+
//
|
|
311
|
+
// THE WINDOW.
|
|
312
|
+
// * issuedAt is REQUIRED and must be a canonical ISO instant (validateLicense's
|
|
313
|
+
// grammar — millis required, no rolled-over fields).
|
|
314
|
+
// * expiresAt comes from `paidThrough` when supplied (the billing system's own period
|
|
315
|
+
// end — the source of truth a renewal extends); otherwise it is DERIVED as issuedAt
|
|
316
|
+
// + plan.termDays days, so a plan with NO explicit period still mints a correct
|
|
317
|
+
// window from the catalog's term. Day arithmetic is on the UTC epoch
|
|
318
|
+
// (termDays * 86_400_000 ms) so it is DST-free and deterministic.
|
|
319
|
+
// * A paidThrough at or BEFORE issuedAt is a NAMED reject (an empty/negative window is
|
|
320
|
+
// never silently honored), exactly as validateLicense rejects expiresAt <= issuedAt.
|
|
321
|
+
//
|
|
322
|
+
// ENTITLEMENTS come ONLY from the resolved plan — never re-typed by the caller — so a
|
|
323
|
+
// typo can never under/over-entitle a paying customer. An unknown `plan` is a NAMED
|
|
324
|
+
// reject naming the known planIds. A malformed issuedAt/paidThrough is a NAMED reject.
|
|
325
|
+
// fulfillEvidenceOrder NEVER signs — it only produces the params; the caller supplies
|
|
326
|
+
// the key and signs via evidence.buildLicense.
|
|
327
|
+
// ===========================================================================
|
|
328
|
+
|
|
329
|
+
// Strict canonical-ISO check, reused so fulfillEvidenceOrder's date errors match the
|
|
330
|
+
// validateLicense grammar exactly (millis optional but seconds required, no
|
|
331
|
+
// rolled-over/impossible fields). Returns epoch-ms or throws the EVIDENCE error class
|
|
332
|
+
// naming the offending field. Mirrors trustledger/license.js _requireCanonicalInstant.
|
|
333
|
+
function _requireCanonicalInstant(field, value) {
|
|
334
|
+
const Err = evidence.EvidenceLicenseError;
|
|
335
|
+
if (typeof value !== "string" || !coreLicense.ISO_INSTANT_RE.test(value)) {
|
|
336
|
+
throw new Err(
|
|
337
|
+
`order ${field} must be an ISO-8601 UTC instant ("YYYY-MM-DDTHH:MM:SS(.mmm)Z"), got: ${String(value)}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const ms = Date.parse(value);
|
|
341
|
+
if (Number.isNaN(ms) || new Date(ms).toISOString() !== value) {
|
|
342
|
+
throw new Err(
|
|
343
|
+
`order ${field} must be a canonical ISO-8601 UTC instant ("YYYY-MM-DDTHH:MM:SS.mmmZ", millis required, ` +
|
|
344
|
+
`no rolled-over/impossible fields), got: ${String(value)}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return ms;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Resolve a planId against a VALIDATED catalog. We read the frozen plansById map the
|
|
351
|
+
// catalog carries; an unknown id is a NAMED reject naming the known plans. Errors are
|
|
352
|
+
// the EVIDENCE license error class so a fulfillment handler catches ONE error type
|
|
353
|
+
// across the resolve+build seam.
|
|
354
|
+
function _resolvePlan(catalog, planId) {
|
|
355
|
+
const Err = evidence.EvidenceLicenseError;
|
|
356
|
+
if (
|
|
357
|
+
catalog == null ||
|
|
358
|
+
typeof catalog !== "object" ||
|
|
359
|
+
catalog.plansById == null ||
|
|
360
|
+
typeof catalog.plansById !== "object"
|
|
361
|
+
) {
|
|
362
|
+
throw new Err(
|
|
363
|
+
"fulfillEvidenceOrder requires a validated evidence plan catalog (see validateEvidencePlanCatalog)"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (typeof planId !== "string" || planId.trim() === "") {
|
|
367
|
+
throw new Err("order `plan` must be a non-empty planId string");
|
|
368
|
+
}
|
|
369
|
+
if (!Object.prototype.hasOwnProperty.call(catalog.plansById, planId)) {
|
|
370
|
+
const known = Object.keys(catalog.plansById).sort().join(", ");
|
|
371
|
+
throw new Err(`unknown plan ${JSON.stringify(planId)}; known plans are: ${known}`);
|
|
372
|
+
}
|
|
373
|
+
return catalog.plansById[planId];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* fulfillEvidenceOrder(order, catalog) — PURE, DETERMINISTIC order -> license-params mapping.
|
|
378
|
+
*
|
|
379
|
+
* @param {object} order
|
|
380
|
+
* @param {string} order.plan a planId present in `catalog`
|
|
381
|
+
* @param {string} order.customer the customer name (non-empty)
|
|
382
|
+
* @param {string} order.issuedAt REQUIRED canonical ISO instant the license is issued at
|
|
383
|
+
* @param {string} [order.paidThrough] OPTIONAL canonical ISO instant the period is paid through;
|
|
384
|
+
* when omitted, expiresAt = issuedAt + plan.termDays days
|
|
385
|
+
* @param {string} [order.licenseId] OPTIONAL explicit id; defaulted deterministically when omitted
|
|
386
|
+
* @param {object} catalog a VALIDATED evidence plan catalog (validateEvidencePlanCatalog output)
|
|
387
|
+
* @returns {{ licenseId, customer, plan, entitlements, issuedAt, expiresAt }}
|
|
388
|
+
* the EXACT params buildLicensePayload/evidence.buildLicense consume.
|
|
389
|
+
*/
|
|
390
|
+
function fulfillEvidenceOrder(order, catalog) {
|
|
391
|
+
const Err = evidence.EvidenceLicenseError;
|
|
392
|
+
if (order == null || typeof order !== "object" || Array.isArray(order)) {
|
|
393
|
+
throw new Err(
|
|
394
|
+
"fulfillEvidenceOrder requires an order object { plan, customer, issuedAt, paidThrough?, licenseId? }"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const plan = _resolvePlan(catalog, order.plan);
|
|
398
|
+
|
|
399
|
+
if (typeof order.customer !== "string" || order.customer.length === 0) {
|
|
400
|
+
throw new Err("order `customer` must be a non-empty string");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// issuedAt is REQUIRED and held to the strict canonical grammar up front (a clear
|
|
404
|
+
// message rather than a buried buildLicensePayload throw).
|
|
405
|
+
const issuedMs = _requireCanonicalInstant("issuedAt", order.issuedAt);
|
|
406
|
+
|
|
407
|
+
// Derive expiresAt: an explicit paidThrough wins (the billing period's own end);
|
|
408
|
+
// otherwise issuedAt + termDays days on the UTC epoch (DST-free, deterministic).
|
|
409
|
+
let expiresAt;
|
|
410
|
+
if (order.paidThrough != null) {
|
|
411
|
+
const paidMs = _requireCanonicalInstant("paidThrough", order.paidThrough);
|
|
412
|
+
if (paidMs <= issuedMs) {
|
|
413
|
+
throw new Err(
|
|
414
|
+
`order paidThrough (${order.paidThrough}) must be strictly AFTER issuedAt (${order.issuedAt})`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
expiresAt = order.paidThrough;
|
|
418
|
+
} else {
|
|
419
|
+
// termDays * one UTC day in ms. termDays is a validated positive integer, so the
|
|
420
|
+
// result is a real future instant; toISOString re-canonicalizes it.
|
|
421
|
+
const DAY_MS = 86400000;
|
|
422
|
+
expiresAt = new Date(issuedMs + plan.termDays * DAY_MS).toISOString();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// licenseId: an explicit one wins; else a DETERMINISTIC default derived from the order
|
|
426
|
+
// (same order => same id => byte-identical params), mirroring fulfillOrder.
|
|
427
|
+
const licenseId =
|
|
428
|
+
order.licenseId != null && order.licenseId !== ""
|
|
429
|
+
? order.licenseId
|
|
430
|
+
: `LIC-${order.issuedAt}-${plan.planId}`;
|
|
431
|
+
|
|
432
|
+
// Entitlements come ONLY from the resolved plan, copied verbatim (a fresh array so the
|
|
433
|
+
// frozen catalog plan is never handed out by reference). buildLicensePayload
|
|
434
|
+
// re-canonicalizes order + validates the closed set.
|
|
435
|
+
return {
|
|
436
|
+
licenseId,
|
|
437
|
+
customer: order.customer,
|
|
438
|
+
plan: plan.planId,
|
|
439
|
+
entitlements: plan.entitlements.slice(),
|
|
440
|
+
issuedAt: order.issuedAt,
|
|
441
|
+
expiresAt,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ===========================================================================
|
|
446
|
+
// T-68.2 — the DRAFT `agent_signed` CAPABILITY (AgentTrace, EPIC-68). STRICTLY ADDITIVE.
|
|
447
|
+
//
|
|
448
|
+
// WHAT THIS IS. The AgentTrace paid surface — `vh agent seal --sign` — is gated by the
|
|
449
|
+
// SAME license mechanism the evidence product already sells through (cli/core/license.js
|
|
450
|
+
// + the `vh-evidence-license` kind, reused VERBATIM: same vendor key, same offline
|
|
451
|
+
// verify, same named-refusal shape, NO new needs-human step), keyed to the DRAFT
|
|
452
|
+
// capability flag below. The capability is declared HERE, next to the plan catalog, so
|
|
453
|
+
// this module stays the single place a plan/capability reader looks.
|
|
454
|
+
//
|
|
455
|
+
// WHAT THIS IS NOT (the additivity contract, pinned by tests).
|
|
456
|
+
// * The EVIDENCE product's closed table (evidence.LICENSE_CFG.entitlements) is
|
|
457
|
+
// UNTOUCHED: ALLOWED_ENTITLEMENT_FLAGS above stays EXACTLY {evidence_signed,
|
|
458
|
+
// evidence_unlimited}, the bundled DRAFT catalog is byte-unchanged, existing
|
|
459
|
+
// licenses/plans/tests keep their bytes, and `vh evidence go-live-preflight`
|
|
460
|
+
// behaves identically.
|
|
461
|
+
// * NO price is set anywhere — the capability is DRAFT; pricing stays the human's
|
|
462
|
+
// P-7 step (STRATEGY.md). A license is an ACCESS credential for delivered software
|
|
463
|
+
// value — NOT a token/coin/NFT, not tradeable, not an appreciating asset.
|
|
464
|
+
//
|
|
465
|
+
// The agent CLI (cli/agent.js) folds this table into ITS OWN license framing — a
|
|
466
|
+
// SUPERSET of the evidence entitlement table under the SAME license kind — so ONE
|
|
467
|
+
// vendor key + ONE license mechanism serves both products. An existing evidence
|
|
468
|
+
// license still validates under that framing; it simply does not CARRY `agent_signed`,
|
|
469
|
+
// so `vh agent seal --sign` refuses it (fail-closed), with the same named-refusal
|
|
470
|
+
// shape the evidence gate emits.
|
|
471
|
+
// ===========================================================================
|
|
472
|
+
|
|
473
|
+
const AGENT_SIGNED_CAPABILITY = "agent_signed";
|
|
474
|
+
|
|
475
|
+
// The DRAFT capability table for the AgentTrace paid surface: flag -> human meaning.
|
|
476
|
+
// Shaped exactly like an entitlements table so a license cfg can spread it verbatim.
|
|
477
|
+
const AGENT_CAPABILITIES = Object.freeze({
|
|
478
|
+
[AGENT_SIGNED_CAPABILITY]:
|
|
479
|
+
"Wrap a sealed agent-session packet's head in a signed attestation " +
|
|
480
|
+
"(`vh agent seal --sign`). DRAFT — no price set; pricing stays the human P-7 step.",
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
module.exports = {
|
|
484
|
+
EVIDENCE_PLAN_CATALOG_KIND,
|
|
485
|
+
EVIDENCE_PLAN_CATALOG_SCHEMA_VERSION,
|
|
486
|
+
SUPPORTED_EVIDENCE_PLAN_CATALOG_SCHEMA_VERSIONS,
|
|
487
|
+
ALLOWED_ENTITLEMENT_FLAGS,
|
|
488
|
+
EvidencePlanCatalogError,
|
|
489
|
+
validateEvidencePlanCatalog,
|
|
490
|
+
getEvidencePlan,
|
|
491
|
+
fulfillEvidenceOrder,
|
|
492
|
+
// T-68.2 — the DRAFT AgentTrace capability (strictly additive; see the block above).
|
|
493
|
+
AGENT_SIGNED_CAPABILITY,
|
|
494
|
+
AGENT_CAPABILITIES,
|
|
495
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_DRAFT": "DRAFT / SAMPLE evidence plan catalog. The human fills in the real PRICE and TERM for each tier before selling — the loop sets NO price. A plan is an ACCESS DESCRIPTION for delivered software value (which paid evidence features a subscription unlocks and for how long). It is NOT a token, NOT tradeable, NOT an appreciating asset, and this catalog makes NO claim of regulatory compliance. The actual subscription agreement governs.",
|
|
3
|
+
"kind": "vh-evidence-plan-catalog",
|
|
4
|
+
"schemaVersion": 1,
|
|
5
|
+
"plans": [
|
|
6
|
+
{
|
|
7
|
+
"planId": "evidence-signed-monthly",
|
|
8
|
+
"displayName": "Evidence Signed (monthly) — DRAFT",
|
|
9
|
+
"entitlements": ["evidence_signed"],
|
|
10
|
+
"termDays": 30
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"planId": "evidence-pro-annual",
|
|
14
|
+
"displayName": "Evidence Pro (annual) — DRAFT",
|
|
15
|
+
"entitlements": ["evidence_signed", "evidence_unlimited"],
|
|
16
|
+
"termDays": 365
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|