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,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
+ }