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,563 @@
1
+ "use strict";
2
+
3
+ // TrustLedger — license.js (EPIC-? / T-29.1, T-30.1)
4
+ //
5
+ // THE PRODUCT LICENSE — a PURE, offline-verifiable, signed entitlement token, built on the project's
6
+ // EXISTING signed-attestation envelope (`cli/core/attestation.js`), reusing it VERBATIM.
7
+ //
8
+ // THIN ADAPTER (T-30.1). All of the license MACHINERY now lives in the PRODUCT-AGNOSTIC core
9
+ // `cli/core/license.js` (which itself reuses `cli/core/attestation.js` verbatim for ALL crypto — no new
10
+ // crypto, no new dependency). This module is the TrustLedger ADAPTER: it supplies the product-specific
11
+ // framing — the `kind`/`schemaVersion`, the CLOSED `ENTITLEMENTS` table, the standing trust notes, and the
12
+ // historical `LicenseError` type — as a single closed `cfg`, then re-exports the SAME public surface so
13
+ // its byte-for-byte mint/verify outputs and every reject reason are UNCHANGED. No TrustLedger caller
14
+ // changes; verifyLicense's localized reasons (bad_signature / wrong_issuer / expired / not_yet_valid /
15
+ // malformed / unknown-entitlement) are exactly as before.
16
+ //
17
+ // THE PROBLEM THIS SOLVES.
18
+ // TrustLedger's premium surfaces (multi-state policy packs, the reconciliation SEAL, unlimited
19
+ // reconcile runs) are how the product earns subscription/license revenue. We need a way for the
20
+ // VENDOR to issue a customer a `*.vhlicense.json` that the CLI can verify OFFLINE — no license
21
+ // server, no network call, no key on the customer's machine — and that strictly answers two
22
+ // questions: "did OUR vendor key sign this?" and "is it in-window and what does it entitle?".
23
+ // A license signed by anyone else, or expired, or carrying an unknown entitlement, must be a hard
24
+ // REJECT — never silently honored.
25
+ //
26
+ // PURE + I/O-FREE.
27
+ // Every function here is pure: no filesystem, no clock, no network, no key handling (the key lives
28
+ // only inside the caller's signer object). `verifyLicense` takes `now` as an explicit argument — it
29
+ // never reads the system clock — so the same container + same `now` + same `vendorAddress` always
30
+ // yield a byte-identical verdict.
31
+ //
32
+ // TRUST-BOUNDARIES — the license is an UNTRUSTED transport container.
33
+ // Consistent with docs/TRUST-BOUNDARIES.md, `verifyLicense` RE-DERIVES the signer from the supplied
34
+ // bytes and PINS it to the caller's `vendorAddress`. It NEVER trusts the file's own claims: a license
35
+ // that merely SAYS it was signed by the vendor, but recovers to a different key, is `wrong_issuer`,
36
+ // not trusted. Entitlements only mean anything once the verdict is `valid`.
37
+ //
38
+ // HONEST POSTURE — what a license DOES and DOES NOT prove.
39
+ // A valid verdict proves: the vendor key signed THESE exact entitlements for THIS customer/plan, and
40
+ // `now` falls within [issuedAt, expiresAt]. It is NOT a trusted timestamp (a self-asserted issuedAt/
41
+ // expiresAt rides the vendor's own honesty + key custody, P-3), and it is NOT a legal contract — the
42
+ // actual subscription agreement governs. The license gates FEATURES; it never replaces the SLA.
43
+
44
+ const coreLicense = require("../cli/core/license");
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Identity. The license has its OWN `kind`/`schemaVersion`, disjoint from the seal/dataset/parcel
48
+ // payloads so a license can never be confused for one of them. `validateLicense` REJECTS any
49
+ // unsupported version rather than guessing.
50
+ // ---------------------------------------------------------------------------
51
+
52
+ const LICENSE_KIND = "trustledger-license";
53
+ const LICENSE_SCHEMA_VERSION = 1;
54
+ const SUPPORTED_LICENSE_SCHEMA_VERSIONS = Object.freeze([1]);
55
+
56
+ // THE CLOSED ENTITLEMENT TABLE. Every entitlement flag a license can carry is enumerated HERE, in ONE
57
+ // exported place, with a human-readable meaning. `entitlements` is a closed set drawn ONLY from these
58
+ // keys: an unknown flag is a hard build error (never silently accepted), so a typo'd or forged
59
+ // entitlement can never grant a feature. To add a paid feature, add a key here — there is no other
60
+ // channel by which an entitlement enters the system.
61
+ const ENTITLEMENTS = Object.freeze({
62
+ // Unlock policy packs for more than one US state (multi-state trust-accounting rules).
63
+ multi_state_policy: "Multi-state trust-accounting policy packs (beyond a single state).",
64
+ // Unlock the tamper-evident reconciliation SEAL (EPIC-26) surface.
65
+ seal: "Tamper-evident reconciliation seal (build/verify *.vhseal).",
66
+ // Remove the per-period reconcile-run cap.
67
+ unlimited_reconcile: "Unlimited reconciliation runs (no per-period cap).",
68
+ });
69
+
70
+ // The frozen, SORTED list of valid entitlement flags — derived ONCE from the table so the two can
71
+ // never drift. Sorted so error messages + any iteration are deterministic.
72
+ const ENTITLEMENT_FLAGS = Object.freeze(Object.keys(ENTITLEMENTS).sort());
73
+
74
+ // The in-band trust caveat carried in EVERY license payload, stated in ONE place so it can never drift
75
+ // from the NatSpec above. It is the load-bearing honesty of the artifact.
76
+ const LICENSE_TRUST_NOTE =
77
+ "This TrustLedger license is a SIGNED entitlement token, verified OFFLINE by re-deriving the signer " +
78
+ "from these exact bytes and pinning it to the vendor key. A valid verdict proves the vendor signed " +
79
+ "THESE entitlements for THIS customer within [issuedAt, expiresAt]; it is an UNTRUSTED transport " +
80
+ "container (verifyLicense never trusts the file's own claims), it is NOT a trusted timestamp (the " +
81
+ "issuedAt/expiresAt are self-asserted and ride the vendor key custody, P-3), and it is NOT the legal " +
82
+ "subscription agreement (which governs). It gates product FEATURES; it never replaces the contract.";
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Errors — STRICT. A malformed/ambiguous license raises a NAMED error rather than being silently
86
+ // dropped, coerced, or partially accepted (mirrors seal.js / close.js). TrustLedger keeps its OWN
87
+ // LicenseError TYPE (handed to the core as cfg.ErrorClass) so existing callers that `catch (LicenseError)`
88
+ // and the byte-for-byte error messages are UNCHANGED.
89
+ // ---------------------------------------------------------------------------
90
+
91
+ class LicenseError extends Error {
92
+ constructor(message) {
93
+ super(message);
94
+ this.name = "LicenseError";
95
+ }
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // The signed-license container framing. The license is one more product on the shared signed-attestation
100
+ // envelope, exactly like the seal: an UNSIGNED license PAYLOAD wrapped in a detached signature. The
101
+ // signed container has its OWN kind/schema/note, disjoint from the embedded license payload's.
102
+ // ---------------------------------------------------------------------------
103
+
104
+ const SIGNED_LICENSE_KIND = "trustledger-license-signed";
105
+ const SIGNED_LICENSE_SCHEMA_VERSION = 1;
106
+ const SUPPORTED_SIGNED_LICENSE_SCHEMA_VERSIONS = Object.freeze([1]);
107
+
108
+ const SIGNED_LICENSE_TRUST_NOTE =
109
+ "This is a SIGNED TrustLedger license container: it WRAPS (never edits) the EXACT canonical license " +
110
+ "bytes in `attestation` and attaches a detached EIP-191 signature. verifyLicense RE-DERIVES the " +
111
+ "signer from those bytes and pins it to the vendor key — it never trusts the file's own claims. " +
112
+ "Every caveat of the embedded license applies. " +
113
+ LICENSE_TRUST_NOTE;
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // THE TRUSTLEDGER LICENSE CFG — the single closed object handed to cli/core/license.js. It carries the
117
+ // product framing (the unsigned license `kind`/`schema`/`note`/`entitlements`), the signed-container
118
+ // framing (`signedKind`/...), and the historical `ErrorClass` so the core throws TrustLedger's
119
+ // LicenseError verbatim. Every adapter function below routes through the core with THIS cfg, so the
120
+ // behaviour is byte-for-byte the pre-extraction behaviour.
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const CFG = Object.freeze({
124
+ // unsigned license payload framing
125
+ kind: LICENSE_KIND,
126
+ schemaVersion: LICENSE_SCHEMA_VERSION,
127
+ supportedSchemaVersions: SUPPORTED_LICENSE_SCHEMA_VERSIONS,
128
+ note: LICENSE_TRUST_NOTE,
129
+ entitlements: ENTITLEMENTS,
130
+ // signed-container framing
131
+ signedKind: SIGNED_LICENSE_KIND,
132
+ signedSchemaVersion: SIGNED_LICENSE_SCHEMA_VERSION,
133
+ supportedSignedSchemaVersions: SUPPORTED_SIGNED_LICENSE_SCHEMA_VERSIONS,
134
+ signedNote: SIGNED_LICENSE_TRUST_NOTE,
135
+ signedLabel: "signed trustledger license",
136
+ // historical error type (so callers + messages are unchanged)
137
+ ErrorClass: LicenseError,
138
+ });
139
+
140
+ // The SIGNED_LICENSE_CFG the previous module exported (the attestation-core framing). Re-derived here from
141
+ // the same pieces so any external reader sees the SAME object shape it did before the extraction.
142
+ const SIGNED_LICENSE_CFG = Object.freeze({
143
+ kind: SIGNED_LICENSE_KIND,
144
+ schemaVersion: SIGNED_LICENSE_SCHEMA_VERSION,
145
+ supportedSchemaVersions: SUPPORTED_SIGNED_LICENSE_SCHEMA_VERSIONS,
146
+ note: SIGNED_LICENSE_TRUST_NOTE,
147
+ label: "signed trustledger license",
148
+ validateUnsigned: (obj) => coreLicense.validateLicense(obj, CFG),
149
+ serializeUnsigned: (obj) => coreLicense.serializeLicense(obj, CFG),
150
+ });
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Public surface — each a THIN adapter binding the TrustLedger CFG to the product-agnostic core. The
154
+ // signatures match the pre-extraction module exactly, so NO TrustLedger caller changes.
155
+ // ---------------------------------------------------------------------------
156
+
157
+ /** STRICT structural validation of an UNSIGNED license PAYLOAD. Throws LicenseError on the first problem. */
158
+ function validateLicense(obj) {
159
+ return coreLicense.validateLicense(obj, CFG);
160
+ }
161
+
162
+ /** Canonical, byte-deterministic serialization of an UNSIGNED license payload (newline-terminated). */
163
+ function serializeLicense(payload) {
164
+ return coreLicense.serializeLicense(payload, CFG);
165
+ }
166
+
167
+ /** Assemble + strictly validate an UNSIGNED license payload from caller fields. PURE. */
168
+ function buildLicensePayload(params) {
169
+ return coreLicense.buildLicensePayload(params, CFG);
170
+ }
171
+
172
+ /** Mint a SIGNED license container from caller fields + an ethers signer object. */
173
+ async function buildLicense(params, signer) {
174
+ return coreLicense.buildLicense(params, signer, CFG);
175
+ }
176
+
177
+ /** Strictly validate a parsed SIGNED-license container. */
178
+ function validateSignedLicense(obj) {
179
+ return coreLicense.validateSignedLicense(obj, CFG);
180
+ }
181
+
182
+ /** Serialize a SIGNED-license container to its canonical bytes. */
183
+ function serializeSignedLicense(container) {
184
+ return coreLicense.serializeSignedLicense(container, CFG);
185
+ }
186
+
187
+ /** Parse + strictly validate a SIGNED-license container (JSON string or object). */
188
+ function readLicense(input) {
189
+ return coreLicense.readLicense(input, CFG);
190
+ }
191
+
192
+ /** The AUTHORITATIVE, PURE, OFFLINE verify — re-derive the signer, pin the vendor, check the window. */
193
+ function verifyLicense(container, opts) {
194
+ // Bind the TrustLedger CFG into the core's opts (the core requires opts.cfg). We never trust a
195
+ // caller-supplied cfg — TrustLedger's framing is fixed.
196
+ if (opts == null || typeof opts !== "object" || Array.isArray(opts)) {
197
+ throw new LicenseError("verifyLicense requires an options object { now, vendorAddress }");
198
+ }
199
+ return coreLicense.verifyLicense(container, { now: opts.now, vendorAddress: opts.vendorAddress, cfg: CFG });
200
+ }
201
+
202
+ /** PURE entitlement gate — true only for a present flag on a VALID verdict (product-agnostic). */
203
+ function hasEntitlement(verdict, flag) {
204
+ return coreLicense.hasEntitlement(verdict, flag);
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // THE ORDER -> LICENSE MAPPING (T-37.2).
209
+ //
210
+ // fulfillOrder turns a normalized ORDER — what a billing webhook knows after a
211
+ // payment succeeds: which `plan` was bought, for which `customer`, when it was
212
+ // `issuedAt`, and through when it is `paidThrough` — into the EXACT params
213
+ // `buildLicensePayload`/`buildLicense` consume. It is the single, deterministic
214
+ // seam a self-serve fulfillment handler calls: resolve the plan in the catalog,
215
+ // copy its entitlements VERBATIM, derive the window, and hand back the params.
216
+ //
217
+ // PURE + DETERMINISTIC. No filesystem, no clock, no network, no key. The SAME
218
+ // { plan, customer, paidThrough, issuedAt } + the SAME catalog yields a
219
+ // byte-identical params object EVERY time (so the signed license bytes are
220
+ // reproducible). The caller resolves + validates the catalog (validatePlanCatalog)
221
+ // and passes it in; we never read it from disk.
222
+ //
223
+ // THE WINDOW.
224
+ // * issuedAt is REQUIRED and must be a canonical ISO instant (validateLicense's
225
+ // grammar — millis required, no rolled-over fields).
226
+ // * expiresAt comes from `paidThrough` when supplied (the billing system's own
227
+ // period end — the source of truth a renewal extends); otherwise it is DERIVED
228
+ // as issuedAt + plan.termDays days, so a plan with NO explicit period still
229
+ // mints a correct window from the catalog's term. Day arithmetic is on the UTC
230
+ // epoch (termDays * 86_400_000 ms) so it is DST-free and deterministic.
231
+ // * A paidThrough at or BEFORE issuedAt is a NAMED reject (an empty/negative
232
+ // window is never silently honored), exactly as validateLicense rejects
233
+ // expiresAt <= issuedAt.
234
+ //
235
+ // ENTITLEMENTS come ONLY from the resolved plan — never re-typed by the caller —
236
+ // so a typo can never under/over-entitle a paying customer. An unknown `plan` is a
237
+ // NAMED reject naming the known planIds. A malformed issuedAt/paidThrough is a
238
+ // NAMED reject (it flows through validateLicense's strict grammar when the params
239
+ // are built into a payload, and we pre-check the obvious shape here for a clear
240
+ // message). fulfillOrder NEVER signs — it only produces the params; the caller
241
+ // (fulfill) supplies the key and signs via buildLicense.
242
+ // ---------------------------------------------------------------------------
243
+
244
+ // Strict canonical-ISO check, reused so fulfillOrder's date errors match the
245
+ // validateLicense grammar exactly (millis required, no rolled-over/impossible
246
+ // fields). Returns epoch-ms or throws a LicenseError naming the offending field.
247
+ function _requireCanonicalInstant(field, value) {
248
+ if (typeof value !== "string" || !coreLicense.ISO_INSTANT_RE.test(value)) {
249
+ throw new LicenseError(
250
+ `order ${field} must be an ISO-8601 UTC instant ("YYYY-MM-DDTHH:MM:SS(.mmm)Z"), got: ${String(value)}`
251
+ );
252
+ }
253
+ const ms = Date.parse(value);
254
+ if (Number.isNaN(ms) || new Date(ms).toISOString() !== value) {
255
+ throw new LicenseError(
256
+ `order ${field} must be a canonical ISO-8601 UTC instant ("YYYY-MM-DDTHH:MM:SS.mmmZ", millis required, ` +
257
+ `no rolled-over/impossible fields), got: ${String(value)}`
258
+ );
259
+ }
260
+ return ms;
261
+ }
262
+
263
+ // Resolve a planId against a VALIDATED catalog WITHOUT importing plans.js (which
264
+ // already depends on license.js — importing it back would be a require cycle). We
265
+ // read the frozen plansById map the catalog carries; an unknown id is a NAMED
266
+ // reject naming the known plans.
267
+ function _resolvePlan(catalog, planId) {
268
+ if (
269
+ catalog == null ||
270
+ typeof catalog !== "object" ||
271
+ catalog.plansById == null ||
272
+ typeof catalog.plansById !== "object"
273
+ ) {
274
+ throw new LicenseError("fulfillOrder requires a validated plan catalog (see plans.validatePlanCatalog)");
275
+ }
276
+ if (typeof planId !== "string" || planId.trim() === "") {
277
+ throw new LicenseError("order `plan` must be a non-empty planId string");
278
+ }
279
+ if (!Object.prototype.hasOwnProperty.call(catalog.plansById, planId)) {
280
+ const known = Object.keys(catalog.plansById).sort().join(", ");
281
+ throw new LicenseError(
282
+ `unknown plan ${JSON.stringify(planId)}; known plans are: ${known}`
283
+ );
284
+ }
285
+ return catalog.plansById[planId];
286
+ }
287
+
288
+ /**
289
+ * fulfillOrder(order, catalog) — PURE, DETERMINISTIC order -> license-params mapping.
290
+ *
291
+ * @param {object} order
292
+ * @param {string} order.plan a planId present in `catalog`
293
+ * @param {string} order.customer the customer name (non-empty)
294
+ * @param {string} order.issuedAt REQUIRED canonical ISO instant the license is issued at
295
+ * @param {string} [order.paidThrough] OPTIONAL canonical ISO instant the period is paid through;
296
+ * when omitted, expiresAt = issuedAt + plan.termDays days
297
+ * @param {string} [order.licenseId] OPTIONAL explicit id; defaulted deterministically when omitted
298
+ * @param {object} catalog a VALIDATED plan catalog (plans.validatePlanCatalog output)
299
+ * @returns {{ licenseId, customer, plan, entitlements, issuedAt, expiresAt }}
300
+ * the EXACT params buildLicensePayload/buildLicense consume.
301
+ */
302
+ function fulfillOrder(order, catalog) {
303
+ if (order == null || typeof order !== "object" || Array.isArray(order)) {
304
+ throw new LicenseError(
305
+ "fulfillOrder requires an order object { plan, customer, issuedAt, paidThrough?, licenseId? }"
306
+ );
307
+ }
308
+ const plan = _resolvePlan(catalog, order.plan);
309
+
310
+ if (typeof order.customer !== "string" || order.customer.length === 0) {
311
+ throw new LicenseError("order `customer` must be a non-empty string");
312
+ }
313
+
314
+ // issuedAt is REQUIRED and held to the strict canonical grammar up front (a clear
315
+ // message rather than a buried buildLicensePayload throw).
316
+ const issuedMs = _requireCanonicalInstant("issuedAt", order.issuedAt);
317
+
318
+ // Derive expiresAt: an explicit paidThrough wins (the billing period's own end);
319
+ // otherwise issuedAt + termDays days on the UTC epoch (DST-free, deterministic).
320
+ let expiresAt;
321
+ if (order.paidThrough != null) {
322
+ const paidMs = _requireCanonicalInstant("paidThrough", order.paidThrough);
323
+ if (paidMs <= issuedMs) {
324
+ throw new LicenseError(
325
+ `order paidThrough (${order.paidThrough}) must be strictly AFTER issuedAt (${order.issuedAt})`
326
+ );
327
+ }
328
+ expiresAt = order.paidThrough;
329
+ } else {
330
+ // termDays * one UTC day in ms. termDays is a validated positive integer, so the
331
+ // result is a real future instant; toISOString re-canonicalizes it.
332
+ const DAY_MS = 86400000;
333
+ expiresAt = new Date(issuedMs + plan.termDays * DAY_MS).toISOString();
334
+ }
335
+
336
+ // licenseId: an explicit one wins; else a DETERMINISTIC default derived from the
337
+ // order (same order => same id => byte-identical params), mirroring license issue.
338
+ const licenseId =
339
+ order.licenseId != null && order.licenseId !== ""
340
+ ? order.licenseId
341
+ : `LIC-${order.issuedAt}-${plan.planId}`;
342
+
343
+ // Entitlements come ONLY from the resolved plan, copied verbatim (a fresh array so
344
+ // the frozen catalog plan is never handed out by reference). buildLicensePayload
345
+ // re-canonicalizes order + validates the closed set.
346
+ return {
347
+ licenseId,
348
+ customer: order.customer,
349
+ plan: plan.planId,
350
+ entitlements: plan.entitlements.slice(),
351
+ issuedAt: order.issuedAt,
352
+ expiresAt,
353
+ };
354
+ }
355
+
356
+ // ===========================================================================
357
+ // THE EVENT -> ORDER NORMALIZER + IDEMPOTENCY KEY (T-38.2).
358
+ //
359
+ // fulfillOrder (above) consumes an ORDER already shaped to OUR vocabulary:
360
+ // `{ plan, customer, paidThrough, issuedAt }` with OUR planId and CANONICAL ISO
361
+ // instants. But a billing provider's webhook does NOT fire with that shape. A real
362
+ // Stripe `invoice.paid` / `checkout.session.completed` (or Paddle) event carries:
363
+ // * the PROVIDER's own price/product id (e.g. `price_...`) — NOT our planId;
364
+ // * a `customer` reference;
365
+ // * a period-end as a UNIX EPOCH in SECONDS (`current_period_end`) — NOT the
366
+ // canonical ISO `fulfillOrder` strictly requires;
367
+ // * and it is delivered AT-LEAST-ONCE, so the SAME event can arrive twice.
368
+ //
369
+ // normalizeEvent is the PURE seam that closes that gap: it maps a NORMALIZED EVENT
370
+ // ENVELOPE (a provider event already flattened to a single canonical shape by the
371
+ // integrator's thin per-provider extractor) onto the EXACT order fulfillOrder
372
+ // consumes. It:
373
+ // 1. reads `rawEvent.provider` + `rawEvent.priceId` and RESOLVES OUR planId via
374
+ // the supplied, catalog-validated price BINDING (plans.resolvePlanId) — an
375
+ // UNMAPPED (provider, priceId) is a NAMED reject, never a silent mis-grant of
376
+ // the wrong PLAN (the exact class T-38.1 closed one level up);
377
+ // 2. converts the period-end UNIX EPOCH SECONDS -> the canonical ISO `paidThrough`
378
+ // grammar fulfillOrder requires (a non-integer / negative / out-of-range epoch
379
+ // is a NAMED reject, never coerced/rounded);
380
+ // 3. derives `customer` (a missing/blank customer is a NAMED reject — a license
381
+ // with no holder is never silently minted);
382
+ // 4. sets `issuedAt` from `rawEvent.issuedAt` or an explicit `opts.issuedAt` —
383
+ // with NO hidden clock read, so the module stays PURE/testable (the caller, who
384
+ // DOES know the wall clock, supplies it; the loop never reads the system clock).
385
+ //
386
+ // PURE + DETERMINISTIC. No filesystem, no clock, no network, no key. The SAME
387
+ // rawEvent + the SAME binding (+ opts) yields a BYTE-IDENTICAL order EVERY time, so
388
+ // `fulfillOrder(normalizeEvent(ev, binding), catalog)` is reproducible end-to-end.
389
+ //
390
+ // IDEMPOTENCY. orderKey(order) returns the DETERMINISTIC `LIC-<issuedAt>-<plan>`
391
+ // seed — the SAME value fulfillOrder defaults the licenseId to. A handler that has
392
+ // already minted (and stored) the license under that key short-circuits a RETRIED
393
+ // delivery of the same event, so a retry re-mints the BYTE-IDENTICAL license, never
394
+ // a second/different one. (Authenticating the inbound webhook — verifying the
395
+ // provider's signing secret — is a HUMAN step; normalizeEvent only maps an
396
+ // ALREADY-AUTHENTICATED event's fields.)
397
+ //
398
+ // HONEST POSTURE. The normalized envelope is OPERATOR/integrator-supplied: this
399
+ // function does NOT call a provider API and does NOT trust an unauthenticated event
400
+ // on its own — it is the pure mapping the handler runs AFTER it authenticates.
401
+ // ===========================================================================
402
+
403
+ // The period-end epoch is in SECONDS (Stripe/Paddle convention). Guard the integer
404
+ // range so the *1000 ms math stays exact and inside JS's safe-integer window — a
405
+ // fractional, negative, or absurd epoch is a NAMED reject, never silently coerced.
406
+ const _MAX_EPOCH_SECONDS = 8640000000000; // == Date max (ms) / 1000; beyond this toISOString throws.
407
+
408
+ // Convert a UNIX epoch in SECONDS -> the canonical ISO instant fulfillOrder's
409
+ // grammar requires. STRICT: a non-number/non-integer/negative/out-of-range epoch
410
+ // throws a NAMED LicenseError naming the field. Returns the canonical ISO string.
411
+ function _epochSecondsToCanonicalISO(field, epochSeconds) {
412
+ if (
413
+ typeof epochSeconds !== "number" ||
414
+ !Number.isInteger(epochSeconds) ||
415
+ epochSeconds < 0 ||
416
+ epochSeconds > _MAX_EPOCH_SECONDS
417
+ ) {
418
+ throw new LicenseError(
419
+ `event ${field} must be a non-negative INTEGER UNIX epoch in SECONDS ` +
420
+ `(0..${_MAX_EPOCH_SECONDS}), got: ${String(epochSeconds)}`
421
+ );
422
+ }
423
+ // Exact: epochSeconds is a safe integer in-range, so *1000 is exact and
424
+ // toISOString re-canonicalizes to "YYYY-MM-DDTHH:MM:SS.mmmZ".
425
+ return new Date(epochSeconds * 1000).toISOString();
426
+ }
427
+
428
+ /**
429
+ * normalizeEvent(rawEvent, binding, opts?) — PURE, DETERMINISTIC map of a NORMALIZED
430
+ * provider event envelope onto the EXACT `{ plan, customer, paidThrough, issuedAt }`
431
+ * order fulfillOrder consumes.
432
+ *
433
+ * @param {object} rawEvent the normalized event envelope
434
+ * @param {string} rawEvent.provider the billing provider id (e.g. "stripe") — bound side of the key
435
+ * @param {string} [rawEvent.type] the provider event type (e.g. "invoice.paid"); carried through, advisory
436
+ * @param {string} rawEvent.priceId the PROVIDER's price/product id — resolved to OUR planId via `binding`
437
+ * @param {string} rawEvent.customer who the license is for (non-empty)
438
+ * @param {number} rawEvent.periodEnd the period end as a UNIX epoch in SECONDS -> canonical ISO `paidThrough`
439
+ * @param {string} [rawEvent.issuedAt] canonical ISO instant the license is issued at (or pass `opts.issuedAt`)
440
+ * @param {object} binding a VALIDATED price binding (plans.validatePriceBinding output)
441
+ * @param {object} [opts]
442
+ * @param {string} [opts.issuedAt] explicit canonical ISO issuedAt; WINS over rawEvent.issuedAt
443
+ * @returns {{ plan, customer, paidThrough, issuedAt }} the EXACT order fulfillOrder consumes.
444
+ */
445
+ function normalizeEvent(rawEvent, binding, opts) {
446
+ if (rawEvent == null || typeof rawEvent !== "object" || Array.isArray(rawEvent)) {
447
+ throw new LicenseError(
448
+ "normalizeEvent requires a normalized event envelope " +
449
+ "{ provider, priceId, customer, periodEnd, issuedAt? }"
450
+ );
451
+ }
452
+ if (opts != null && (typeof opts !== "object" || Array.isArray(opts))) {
453
+ throw new LicenseError("normalizeEvent opts, when given, must be an object { issuedAt? }");
454
+ }
455
+
456
+ // ---- provider + priceId -> OUR planId, via the catalog-validated binding ----
457
+ // We resolve THROUGH plans.resolvePlanId (the single authority): an unmapped
458
+ // (provider, priceId) is its NAMED reject. `plans` is required LAZILY inside the
459
+ // function (never at module top-level) because plans.js requires license.js — a
460
+ // top-level back-edge would be a cycle. By call time both modules are fully
461
+ // initialized, so the lazy require is safe and the dependency graph stays acyclic.
462
+ if (typeof rawEvent.provider !== "string" || rawEvent.provider.trim() === "") {
463
+ throw new LicenseError("event `provider` must be a non-empty string");
464
+ }
465
+ if (typeof rawEvent.priceId !== "string" || rawEvent.priceId.trim() === "") {
466
+ throw new LicenseError("event `priceId` must be a non-empty string");
467
+ }
468
+ // eslint-disable-next-line global-require
469
+ const plans = require("./plans");
470
+ let planId;
471
+ try {
472
+ planId = plans.resolvePlanId(binding, rawEvent.provider, rawEvent.priceId);
473
+ } catch (e) {
474
+ // Surface the binding's NAMED reason verbatim, but as a LicenseError so a
475
+ // fulfillment handler catches ONE error type across the normalize+fulfill seam.
476
+ throw new LicenseError(
477
+ `cannot normalize event: ${e && e.message ? e.message : String(e)}`
478
+ );
479
+ }
480
+
481
+ // ---- customer (a license with no holder is never silently minted) -----------
482
+ if (typeof rawEvent.customer !== "string" || rawEvent.customer.length === 0) {
483
+ throw new LicenseError("event `customer` must be a non-empty string");
484
+ }
485
+
486
+ // ---- period-end UNIX epoch SECONDS -> canonical ISO paidThrough -------------
487
+ if (!Object.prototype.hasOwnProperty.call(rawEvent, "periodEnd")) {
488
+ throw new LicenseError("event is missing required field: periodEnd (UNIX epoch seconds)");
489
+ }
490
+ const paidThrough = _epochSecondsToCanonicalISO("periodEnd", rawEvent.periodEnd);
491
+
492
+ // ---- issuedAt: explicit opts.issuedAt WINS, else rawEvent.issuedAt. NO clock.
493
+ // We require ONE of them be supplied so the module never has to read the system
494
+ // clock — it stays pure/testable. The chosen value is held to the canonical grammar so a
495
+ // malformed instant is a NAMED reject here (rather than a buried fulfillOrder throw).
496
+ const issuedAt =
497
+ opts != null && opts.issuedAt != null ? opts.issuedAt : rawEvent.issuedAt;
498
+ if (issuedAt == null) {
499
+ throw new LicenseError(
500
+ "event `issuedAt` is required (supply rawEvent.issuedAt or opts.issuedAt); " +
501
+ "normalizeEvent never reads the system clock"
502
+ );
503
+ }
504
+ _requireCanonicalInstant("issuedAt", issuedAt);
505
+
506
+ // The EXACT order shape fulfillOrder consumes — provider event type is advisory
507
+ // and intentionally NOT carried into the order (the order is provider-agnostic).
508
+ return { plan: planId, customer: rawEvent.customer, paidThrough, issuedAt };
509
+ }
510
+
511
+ /**
512
+ * orderKey(order) — the DETERMINISTIC `LIC-<issuedAt>-<plan>` idempotency seed.
513
+ *
514
+ * This is the SAME value fulfillOrder defaults the licenseId to, so an idempotent
515
+ * webhook handler dedupes on it: if a license already exists under this key, a
516
+ * RETRIED delivery of the same event resolves to the SAME order -> the SAME key ->
517
+ * the handler returns the already-minted, BYTE-IDENTICAL license rather than minting
518
+ * a second/different one. PURE — derives only from the order's own fields.
519
+ *
520
+ * @param {{ plan: string, issuedAt: string }} order an order (e.g. normalizeEvent output)
521
+ * @returns {string} `LIC-<issuedAt>-<plan>`
522
+ */
523
+ function orderKey(order) {
524
+ if (order == null || typeof order !== "object" || Array.isArray(order)) {
525
+ throw new LicenseError("orderKey requires an order object { plan, issuedAt }");
526
+ }
527
+ if (typeof order.plan !== "string" || order.plan.trim() === "") {
528
+ throw new LicenseError("order `plan` must be a non-empty planId string");
529
+ }
530
+ // issuedAt is held to the canonical grammar so the key is stable + unambiguous
531
+ // (the same instant always yields the same key).
532
+ _requireCanonicalInstant("issuedAt", order.issuedAt);
533
+ return `LIC-${order.issuedAt}-${order.plan}`;
534
+ }
535
+
536
+ module.exports = {
537
+ LICENSE_KIND,
538
+ LICENSE_SCHEMA_VERSION,
539
+ SUPPORTED_LICENSE_SCHEMA_VERSIONS,
540
+ LICENSE_TRUST_NOTE,
541
+ ENTITLEMENTS,
542
+ ENTITLEMENT_FLAGS,
543
+ LicenseError,
544
+ // unsigned payload
545
+ validateLicense,
546
+ serializeLicense,
547
+ buildLicensePayload,
548
+ // signed container (shared core)
549
+ SIGNED_LICENSE_CFG,
550
+ SIGNED_LICENSE_KIND,
551
+ SIGNED_LICENSE_TRUST_NOTE,
552
+ buildLicense,
553
+ validateSignedLicense,
554
+ serializeSignedLicense,
555
+ readLicense,
556
+ verifyLicense,
557
+ hasEntitlement,
558
+ // order -> license-params mapping (T-37.2)
559
+ fulfillOrder,
560
+ // event -> order normalizer + idempotency key (T-38.2)
561
+ normalizeEvent,
562
+ orderKey,
563
+ };