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