@revealui/core 0.11.1 → 0.12.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/dist/license.d.ts +103 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +138 -5
- package/dist/revforge-license.d.ts +9 -0
- package/dist/revforge-license.d.ts.map +1 -1
- package/dist/revforge-license.js +12 -2
- package/package.json +4 -4
package/dist/license.d.ts
CHANGED
|
@@ -155,6 +155,33 @@ export declare function computeKeyId(publicKeyPem: string): Promise<string>;
|
|
|
155
155
|
* options below. Signature is enforced via the public key.
|
|
156
156
|
*/
|
|
157
157
|
export declare function validateLicenseKey(licenseKey: string, publicKey: string | readonly string[], expectedCustomerId?: string): Promise<LicensePayload | null>;
|
|
158
|
+
/**
|
|
159
|
+
* How far past `exp` a license JWT is still accepted by the refresh endpoint
|
|
160
|
+
* (GAP-287 PR-1). Ratified by the owner 2026-07-18 at 30 days.
|
|
161
|
+
*
|
|
162
|
+
* Deliberately wider than the subscription validation grace
|
|
163
|
+
* (`LICENSE_GRACE_SUBSCRIPTION_DAYS`, default 3): the validation grace answers
|
|
164
|
+
* "may you still run", the refresh-accept window answers "may you still fetch
|
|
165
|
+
* your renewal". A customer returning from a month away should still be able to
|
|
166
|
+
* pull their current key rather than being bricked. Beyond this window the
|
|
167
|
+
* refresh path refuses and re-delivery goes through the authenticated admin
|
|
168
|
+
* account page.
|
|
169
|
+
*/
|
|
170
|
+
export declare const REFRESH_ACCEPT_DAYS = 30;
|
|
171
|
+
/**
|
|
172
|
+
* Refresh-path validator (GAP-287 PR-1). Verifies a presented license JWT for
|
|
173
|
+
* `POST /api/license/refresh`, accepting a token whose `exp` is past by at most
|
|
174
|
+
* `refreshAcceptDays` (default {@link REFRESH_ACCEPT_DAYS}).
|
|
175
|
+
*
|
|
176
|
+
* It reuses the SAME ordered multi-key verification (GAP-259 rotation
|
|
177
|
+
* composition) and payload schema as {@link validateLicenseKey}, so a token
|
|
178
|
+
* minted under the outgoing private key during a dual-key rotation window still
|
|
179
|
+
* verifies. It performs NO `customerId` binding: the refresh endpoint trusts
|
|
180
|
+
* the token's `customerId` claim and then independently requires an ACTIVE
|
|
181
|
+
* license row for it. This function only proves possession of a recently-valid
|
|
182
|
+
* signed key. It never mints and never grants entitlement on its own.
|
|
183
|
+
*/
|
|
184
|
+
export declare function validateLicenseKeyForRefresh(licenseKey: string, publicKey: string | readonly string[], refreshAcceptDays?: number): Promise<LicensePayload | null>;
|
|
158
185
|
/**
|
|
159
186
|
* Outcome of a signing-keypair self-test. See {@link selfVerifyLicenseKeypair}.
|
|
160
187
|
*/
|
|
@@ -259,6 +286,82 @@ export declare function getMaxUsers(): number;
|
|
|
259
286
|
* Track B metering: free=1K, pro=10K, max=50K, enterprise=unlimited.
|
|
260
287
|
*/
|
|
261
288
|
export declare function getMaxAgentTasks(): number;
|
|
289
|
+
/**
|
|
290
|
+
* Default subscription license JWT lifetime (seconds) — one year. Used as the
|
|
291
|
+
* fallback when a subscription's billing period is unavailable at mint time (an
|
|
292
|
+
* abnormal Stripe shape) so a period-bound token is never SHORTER than the
|
|
293
|
+
* pre-GAP-287 behavior, and as the default lifetime for {@link generateLicenseKey}
|
|
294
|
+
* when no explicit value is passed.
|
|
295
|
+
*/
|
|
296
|
+
export declare const DEFAULT_SUBSCRIPTION_TTL_SECONDS: number;
|
|
297
|
+
/**
|
|
298
|
+
* Slack added to a hosted subscription's `current_period_end` when deriving the
|
|
299
|
+
* license JWT `exp` (GAP-287 PR-2). Ratified by the owner 2026-07-18 at 7 days.
|
|
300
|
+
*
|
|
301
|
+
* The token's lifetime equals what billing has actually granted plus this slack.
|
|
302
|
+
* The slack must cover the Stripe dunning/retry window so a card that recovers
|
|
303
|
+
* on retry never presents a lapsed key. Renewals advance the exp each billing
|
|
304
|
+
* cycle by re-minting on `invoice.payment_succeeded`.
|
|
305
|
+
*/
|
|
306
|
+
export declare const RENEWAL_SLACK_DAYS = 7;
|
|
307
|
+
export declare const RENEWAL_SLACK_SECONDS: number;
|
|
308
|
+
/**
|
|
309
|
+
* Default lifetime, in days, for a license JWT minted by the admin
|
|
310
|
+
* `POST /generate` manual-mint endpoint when the caller does not supply an
|
|
311
|
+
* explicit `expiresInDays` (GAP-287 PR-3). Ratified by the owner 2026-07-18 at
|
|
312
|
+
* 90, down from the prior flat 365-day default: this is an operator tool, and
|
|
313
|
+
* a short default nudges the right renewal habit for manually-issued keys. An
|
|
314
|
+
* explicit `expiresInDays` on the request is always honored unchanged.
|
|
315
|
+
*/
|
|
316
|
+
export declare const DEFAULT_MANUAL_MINT_DAYS = 90;
|
|
317
|
+
/**
|
|
318
|
+
* Derives the relative `expiresInSeconds` argument for {@link generateLicenseKey}
|
|
319
|
+
* at the hosted subscription mint sites (GAP-287 PR-2): the token should expire
|
|
320
|
+
* at `periodEnd + RENEWAL_SLACK_SECONDS`. Because generateLicenseKey interprets a
|
|
321
|
+
* numeric lifetime relative to issue time, this returns that absolute bound minus
|
|
322
|
+
* now.
|
|
323
|
+
*
|
|
324
|
+
* Falls back to {@link DEFAULT_SUBSCRIPTION_TTL_SECONDS} when `periodEnd` is
|
|
325
|
+
* absent (a subscription with no billing period) so an unexpected Stripe shape
|
|
326
|
+
* never mints a shorter-than-before token. Never returns a non-positive lifetime:
|
|
327
|
+
* a period end already in the past floors at one second, leaving expiry to the
|
|
328
|
+
* validation grace + refresh window rather than minting an already-invalid token.
|
|
329
|
+
*/
|
|
330
|
+
export declare function subscriptionLicenseExpiresInSeconds(periodEnd: Date | null | undefined, nowMs?: number): number;
|
|
331
|
+
/**
|
|
332
|
+
* Absolute epoch-second bound `periodEnd + RENEWAL_SLACK_SECONDS` a hosted
|
|
333
|
+
* subscription key should reach (GAP-287 PR-2). The renewal cadence re-mints IFF
|
|
334
|
+
* the stored key's `exp` is below this bound, so the comparison uses this
|
|
335
|
+
* absolute form while the mint uses the relative
|
|
336
|
+
* {@link subscriptionLicenseExpiresInSeconds}.
|
|
337
|
+
*/
|
|
338
|
+
export declare function subscriptionExpBound(periodEnd: Date): number;
|
|
339
|
+
/**
|
|
340
|
+
* Idempotency tolerance for the renewal re-mint decision (fast-follow from the
|
|
341
|
+
* non-blocking finding on the #1978 guardrail-2 verdict). `subscriptionLicenseExpiresInSeconds`
|
|
342
|
+
* floors its relative TTL to whole seconds and the JWT signer floors `exp` to
|
|
343
|
+
* `iat + expiresInSeconds` at its own second granularity, so the `exp` actually
|
|
344
|
+
* persisted for a subscription key can land exactly 1s below
|
|
345
|
+
* {@link subscriptionExpBound}'s value even though the mint targeted that bound.
|
|
346
|
+
* Without this tolerance a duplicate/retried `invoice.payment_succeeded` lands on
|
|
347
|
+
* that 1s gap and re-enters the re-mint path instead of no-opping.
|
|
348
|
+
*/
|
|
349
|
+
export declare const RENEWAL_IDEMPOTENCY_TOLERANCE_SECONDS = 1;
|
|
350
|
+
/**
|
|
351
|
+
* True when a stored license `exp` already covers the renewal bound, within the
|
|
352
|
+
* {@link RENEWAL_IDEMPOTENCY_TOLERANCE_SECONDS} idempotency tolerance - i.e. the
|
|
353
|
+
* renewal re-mint should be SKIPPED. `null` (perpetual token, or an undecodable
|
|
354
|
+
* key) never covers, so the caller falls through to re-mint.
|
|
355
|
+
*/
|
|
356
|
+
export declare function coversRenewalBound(storedExp: number | null, newBound: number): boolean;
|
|
357
|
+
/**
|
|
358
|
+
* Reads the `exp` (epoch seconds) claim from a license JWT WITHOUT verifying its
|
|
359
|
+
* signature. Internal use only, for the GAP-287 PR-2 renewal cadence: the token
|
|
360
|
+
* comes from our OWN store and the caller needs only its expiry to decide whether
|
|
361
|
+
* to re-mint — never as an authentication or entitlement decision. Returns null
|
|
362
|
+
* for a perpetual token (no exp claim) or one that cannot be decoded.
|
|
363
|
+
*/
|
|
364
|
+
export declare function readLicenseExp(licenseKey: string): Promise<number | null>;
|
|
262
365
|
/**
|
|
263
366
|
* Generates a signed license key JWT.
|
|
264
367
|
* Server-only in practice (requires the private key) but edge-compatible —
|
package/dist/license.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"license.d.ts","sourceRoot":"","sources":["../src/license.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,8BAA8B;AAC9B,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,YAAY,CAAC;AAEhE;;;;;;;;;;GAUG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/F,gDAAgD;AAChD,MAAM,WAAW,kBAAkB;IACjC,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,6BAA6B;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,qBAAqB;IACrB,IAAI,EAAE,WAAW,CAAC;IAClB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;CACnB;AAmBD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAEjF;AAED,qCAAqC;AACrC,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;iBAuBxB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,sCAAsC;AACtC,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAAiB,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAW5E;AASD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAElF;AAkBD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAOxC;AAYD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACrC,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"license.d.ts","sourceRoot":"","sources":["../src/license.ts"],"names":[],"mappings":"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"}
|
package/dist/license.js
CHANGED
|
@@ -204,6 +204,47 @@ export async function computeKeyId(publicKeyPem) {
|
|
|
204
204
|
* options below. Signature is enforced via the public key.
|
|
205
205
|
*/
|
|
206
206
|
export async function validateLicenseKey(licenseKey, publicKey, expectedCustomerId) {
|
|
207
|
+
// Accept tokens expired within the subscription grace window so the payload
|
|
208
|
+
// is available for grace-period calculations in isLicensed().
|
|
209
|
+
return verifyAndParseLicenseJwt(licenseKey, publicKey, graceConfig.subscriptionDays * 86_400, expectedCustomerId);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* How far past `exp` a license JWT is still accepted by the refresh endpoint
|
|
213
|
+
* (GAP-287 PR-1). Ratified by the owner 2026-07-18 at 30 days.
|
|
214
|
+
*
|
|
215
|
+
* Deliberately wider than the subscription validation grace
|
|
216
|
+
* (`LICENSE_GRACE_SUBSCRIPTION_DAYS`, default 3): the validation grace answers
|
|
217
|
+
* "may you still run", the refresh-accept window answers "may you still fetch
|
|
218
|
+
* your renewal". A customer returning from a month away should still be able to
|
|
219
|
+
* pull their current key rather than being bricked. Beyond this window the
|
|
220
|
+
* refresh path refuses and re-delivery goes through the authenticated admin
|
|
221
|
+
* account page.
|
|
222
|
+
*/
|
|
223
|
+
export const REFRESH_ACCEPT_DAYS = 30;
|
|
224
|
+
/**
|
|
225
|
+
* Refresh-path validator (GAP-287 PR-1). Verifies a presented license JWT for
|
|
226
|
+
* `POST /api/license/refresh`, accepting a token whose `exp` is past by at most
|
|
227
|
+
* `refreshAcceptDays` (default {@link REFRESH_ACCEPT_DAYS}).
|
|
228
|
+
*
|
|
229
|
+
* It reuses the SAME ordered multi-key verification (GAP-259 rotation
|
|
230
|
+
* composition) and payload schema as {@link validateLicenseKey}, so a token
|
|
231
|
+
* minted under the outgoing private key during a dual-key rotation window still
|
|
232
|
+
* verifies. It performs NO `customerId` binding: the refresh endpoint trusts
|
|
233
|
+
* the token's `customerId` claim and then independently requires an ACTIVE
|
|
234
|
+
* license row for it. This function only proves possession of a recently-valid
|
|
235
|
+
* signed key. It never mints and never grants entitlement on its own.
|
|
236
|
+
*/
|
|
237
|
+
export async function validateLicenseKeyForRefresh(licenseKey, publicKey, refreshAcceptDays = REFRESH_ACCEPT_DAYS) {
|
|
238
|
+
return verifyAndParseLicenseJwt(licenseKey, publicKey, refreshAcceptDays * 86_400);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Shared verify + parse path for {@link validateLicenseKey} and
|
|
242
|
+
* {@link validateLicenseKeyForRefresh}. The ONLY behavioral difference between
|
|
243
|
+
* the two callers is `clockToleranceSeconds` — how far past `exp` a token is
|
|
244
|
+
* still accepted — so the rotation-aware multi-key loop and the payload schema
|
|
245
|
+
* live in exactly one place.
|
|
246
|
+
*/
|
|
247
|
+
async function verifyAndParseLicenseJwt(licenseKey, publicKey, clockToleranceSeconds, expectedCustomerId) {
|
|
207
248
|
const candidates = typeof publicKey === 'string' ? [publicKey] : [...publicKey];
|
|
208
249
|
if (candidates.length === 0)
|
|
209
250
|
return null;
|
|
@@ -218,11 +259,9 @@ export async function validateLicenseKey(licenseKey, publicKey, expectedCustomer
|
|
|
218
259
|
for (const candidate of ordered) {
|
|
219
260
|
try {
|
|
220
261
|
const key = await jose.importSPKI(candidate, 'EdDSA');
|
|
221
|
-
// Accept tokens expired within the subscription grace window so the
|
|
222
|
-
// payload is available for grace-period calculations in isLicensed().
|
|
223
262
|
const { payload } = await jose.jwtVerify(licenseKey, key, {
|
|
224
263
|
algorithms: ['EdDSA'],
|
|
225
|
-
clockTolerance:
|
|
264
|
+
clockTolerance: clockToleranceSeconds,
|
|
226
265
|
issuer: LICENSE_ISSUER,
|
|
227
266
|
audience: LICENSE_AUDIENCE,
|
|
228
267
|
});
|
|
@@ -232,7 +271,7 @@ export async function validateLicenseKey(licenseKey, publicKey, expectedCustomer
|
|
|
232
271
|
}
|
|
233
272
|
catch {
|
|
234
273
|
// This candidate did not verify (wrong key, malformed PEM, bad
|
|
235
|
-
// iss/aud, or expired beyond
|
|
274
|
+
// iss/aud, or expired beyond tolerance) — try the next before giving up.
|
|
236
275
|
}
|
|
237
276
|
}
|
|
238
277
|
if (!verified) {
|
|
@@ -617,6 +656,100 @@ export function getMaxAgentTasks() {
|
|
|
617
656
|
return 10_000;
|
|
618
657
|
return 1_000;
|
|
619
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Default subscription license JWT lifetime (seconds) — one year. Used as the
|
|
661
|
+
* fallback when a subscription's billing period is unavailable at mint time (an
|
|
662
|
+
* abnormal Stripe shape) so a period-bound token is never SHORTER than the
|
|
663
|
+
* pre-GAP-287 behavior, and as the default lifetime for {@link generateLicenseKey}
|
|
664
|
+
* when no explicit value is passed.
|
|
665
|
+
*/
|
|
666
|
+
export const DEFAULT_SUBSCRIPTION_TTL_SECONDS = 365 * 24 * 60 * 60;
|
|
667
|
+
/**
|
|
668
|
+
* Slack added to a hosted subscription's `current_period_end` when deriving the
|
|
669
|
+
* license JWT `exp` (GAP-287 PR-2). Ratified by the owner 2026-07-18 at 7 days.
|
|
670
|
+
*
|
|
671
|
+
* The token's lifetime equals what billing has actually granted plus this slack.
|
|
672
|
+
* The slack must cover the Stripe dunning/retry window so a card that recovers
|
|
673
|
+
* on retry never presents a lapsed key. Renewals advance the exp each billing
|
|
674
|
+
* cycle by re-minting on `invoice.payment_succeeded`.
|
|
675
|
+
*/
|
|
676
|
+
export const RENEWAL_SLACK_DAYS = 7;
|
|
677
|
+
export const RENEWAL_SLACK_SECONDS = RENEWAL_SLACK_DAYS * 24 * 60 * 60;
|
|
678
|
+
/**
|
|
679
|
+
* Default lifetime, in days, for a license JWT minted by the admin
|
|
680
|
+
* `POST /generate` manual-mint endpoint when the caller does not supply an
|
|
681
|
+
* explicit `expiresInDays` (GAP-287 PR-3). Ratified by the owner 2026-07-18 at
|
|
682
|
+
* 90, down from the prior flat 365-day default: this is an operator tool, and
|
|
683
|
+
* a short default nudges the right renewal habit for manually-issued keys. An
|
|
684
|
+
* explicit `expiresInDays` on the request is always honored unchanged.
|
|
685
|
+
*/
|
|
686
|
+
export const DEFAULT_MANUAL_MINT_DAYS = 90;
|
|
687
|
+
/**
|
|
688
|
+
* Derives the relative `expiresInSeconds` argument for {@link generateLicenseKey}
|
|
689
|
+
* at the hosted subscription mint sites (GAP-287 PR-2): the token should expire
|
|
690
|
+
* at `periodEnd + RENEWAL_SLACK_SECONDS`. Because generateLicenseKey interprets a
|
|
691
|
+
* numeric lifetime relative to issue time, this returns that absolute bound minus
|
|
692
|
+
* now.
|
|
693
|
+
*
|
|
694
|
+
* Falls back to {@link DEFAULT_SUBSCRIPTION_TTL_SECONDS} when `periodEnd` is
|
|
695
|
+
* absent (a subscription with no billing period) so an unexpected Stripe shape
|
|
696
|
+
* never mints a shorter-than-before token. Never returns a non-positive lifetime:
|
|
697
|
+
* a period end already in the past floors at one second, leaving expiry to the
|
|
698
|
+
* validation grace + refresh window rather than minting an already-invalid token.
|
|
699
|
+
*/
|
|
700
|
+
export function subscriptionLicenseExpiresInSeconds(periodEnd, nowMs = Date.now()) {
|
|
701
|
+
if (!periodEnd)
|
|
702
|
+
return DEFAULT_SUBSCRIPTION_TTL_SECONDS;
|
|
703
|
+
const boundMs = periodEnd.getTime() + RENEWAL_SLACK_SECONDS * 1000;
|
|
704
|
+
return Math.max(1, Math.floor((boundMs - nowMs) / 1000));
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Absolute epoch-second bound `periodEnd + RENEWAL_SLACK_SECONDS` a hosted
|
|
708
|
+
* subscription key should reach (GAP-287 PR-2). The renewal cadence re-mints IFF
|
|
709
|
+
* the stored key's `exp` is below this bound, so the comparison uses this
|
|
710
|
+
* absolute form while the mint uses the relative
|
|
711
|
+
* {@link subscriptionLicenseExpiresInSeconds}.
|
|
712
|
+
*/
|
|
713
|
+
export function subscriptionExpBound(periodEnd) {
|
|
714
|
+
return Math.floor(periodEnd.getTime() / 1000) + RENEWAL_SLACK_SECONDS;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Idempotency tolerance for the renewal re-mint decision (fast-follow from the
|
|
718
|
+
* non-blocking finding on the #1978 guardrail-2 verdict). `subscriptionLicenseExpiresInSeconds`
|
|
719
|
+
* floors its relative TTL to whole seconds and the JWT signer floors `exp` to
|
|
720
|
+
* `iat + expiresInSeconds` at its own second granularity, so the `exp` actually
|
|
721
|
+
* persisted for a subscription key can land exactly 1s below
|
|
722
|
+
* {@link subscriptionExpBound}'s value even though the mint targeted that bound.
|
|
723
|
+
* Without this tolerance a duplicate/retried `invoice.payment_succeeded` lands on
|
|
724
|
+
* that 1s gap and re-enters the re-mint path instead of no-opping.
|
|
725
|
+
*/
|
|
726
|
+
export const RENEWAL_IDEMPOTENCY_TOLERANCE_SECONDS = 1;
|
|
727
|
+
/**
|
|
728
|
+
* True when a stored license `exp` already covers the renewal bound, within the
|
|
729
|
+
* {@link RENEWAL_IDEMPOTENCY_TOLERANCE_SECONDS} idempotency tolerance - i.e. the
|
|
730
|
+
* renewal re-mint should be SKIPPED. `null` (perpetual token, or an undecodable
|
|
731
|
+
* key) never covers, so the caller falls through to re-mint.
|
|
732
|
+
*/
|
|
733
|
+
export function coversRenewalBound(storedExp, newBound) {
|
|
734
|
+
return storedExp !== null && storedExp >= newBound - RENEWAL_IDEMPOTENCY_TOLERANCE_SECONDS;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Reads the `exp` (epoch seconds) claim from a license JWT WITHOUT verifying its
|
|
738
|
+
* signature. Internal use only, for the GAP-287 PR-2 renewal cadence: the token
|
|
739
|
+
* comes from our OWN store and the caller needs only its expiry to decide whether
|
|
740
|
+
* to re-mint — never as an authentication or entitlement decision. Returns null
|
|
741
|
+
* for a perpetual token (no exp claim) or one that cannot be decoded.
|
|
742
|
+
*/
|
|
743
|
+
export async function readLicenseExp(licenseKey) {
|
|
744
|
+
try {
|
|
745
|
+
const jose = await getJose();
|
|
746
|
+
const payload = jose.decodeJwt(licenseKey);
|
|
747
|
+
return typeof payload.exp === 'number' ? payload.exp : null;
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
620
753
|
/**
|
|
621
754
|
* Generates a signed license key JWT.
|
|
622
755
|
* Server-only in practice (requires the private key) but edge-compatible —
|
|
@@ -630,7 +763,7 @@ export function getMaxAgentTasks() {
|
|
|
630
763
|
* claim is added to the JWT header for forward-compatible key rotation.
|
|
631
764
|
* @returns Signed JWT string
|
|
632
765
|
*/
|
|
633
|
-
export async function generateLicenseKey(payload, privateKey, expiresInSeconds =
|
|
766
|
+
export async function generateLicenseKey(payload, privateKey, expiresInSeconds = DEFAULT_SUBSCRIPTION_TTL_SECONDS, publicKey) {
|
|
634
767
|
const jose = await getJose();
|
|
635
768
|
const key = await jose.importPKCS8(privateKey, 'EdDSA');
|
|
636
769
|
const kid = publicKey ? await computeKeyId(publicKey) : undefined;
|
|
@@ -14,6 +14,15 @@ import { type LicensePayload } from './license.js';
|
|
|
14
14
|
export declare const SLUG_PATTERN: RegExp;
|
|
15
15
|
export declare const VALID_REVFORGE_TIERS: readonly ["pro", "max", "enterprise"];
|
|
16
16
|
export type RevForgeTier = (typeof VALID_REVFORGE_TIERS)[number];
|
|
17
|
+
/**
|
|
18
|
+
* Default lifetime, in days, for a RevForge kit license JWT when neither
|
|
19
|
+
* `expiresInDays` nor `perpetual` is supplied (GAP-287 PR-3). Ratified by the
|
|
20
|
+
* owner 2026-07-18: stays at 365 (no behavior change), matching the annual
|
|
21
|
+
* kit contract — the renewal path already exists via the GAP-316
|
|
22
|
+
* `stamp.sh --license-only` re-mint. This constant only names the value that
|
|
23
|
+
* was previously an inline literal.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_KIT_MINT_DAYS = 365;
|
|
17
26
|
export interface IssueRevForgeLicenseOptions {
|
|
18
27
|
/** Customer slug; becomes the JWT customerId. Must match SLUG_PATTERN. */
|
|
19
28
|
slug: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"revforge-license.d.ts","sourceRoot":"","sources":["../src/revforge-license.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAsB,KAAK,cAAc,EAAsB,MAAM,cAAc,CAAC;AAE3F,eAAO,MAAM,YAAY,QAAyB,CAAC;AAEnD,eAAO,MAAM,oBAAoB,uCAAwC,CAAC;AAC1E,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,MAAM,WAAW,2BAA2B;IAC1C,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,YAAY,CAAC;IACnB,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,oFAAoF;IACpF,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;CACtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,2BAA2B,EACjC,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC9C,OAAO,CAAC,0BAA0B,CAAC,
|
|
1
|
+
{"version":3,"file":"revforge-license.d.ts","sourceRoot":"","sources":["../src/revforge-license.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAsB,KAAK,cAAc,EAAsB,MAAM,cAAc,CAAC;AAE3F,eAAO,MAAM,YAAY,QAAyB,CAAC;AAEnD,eAAO,MAAM,oBAAoB,uCAAwC,CAAC;AAC1E,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,MAAM,WAAW,2BAA2B;IAC1C,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,YAAY,CAAC;IACnB,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,oFAAoF;IACpF,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;CACtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,2BAA2B,EACjC,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC9C,OAAO,CAAC,0BAA0B,CAAC,CAwFrC"}
|
package/dist/revforge-license.js
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
import { generateLicenseKey, validateLicenseKey } from './license.js';
|
|
14
14
|
export const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
15
15
|
export const VALID_REVFORGE_TIERS = ['pro', 'max', 'enterprise'];
|
|
16
|
+
/**
|
|
17
|
+
* Default lifetime, in days, for a RevForge kit license JWT when neither
|
|
18
|
+
* `expiresInDays` nor `perpetual` is supplied (GAP-287 PR-3). Ratified by the
|
|
19
|
+
* owner 2026-07-18: stays at 365 (no behavior change), matching the annual
|
|
20
|
+
* kit contract — the renewal path already exists via the GAP-316
|
|
21
|
+
* `stamp.sh --license-only` re-mint. This constant only names the value that
|
|
22
|
+
* was previously an inline literal.
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_KIT_MINT_DAYS = 365;
|
|
16
25
|
/**
|
|
17
26
|
* Issue a signed RevForge license JWT for a paying customer.
|
|
18
27
|
*
|
|
@@ -55,12 +64,13 @@ export async function issueRevForgeLicense(opts, keys) {
|
|
|
55
64
|
...(opts.domains && opts.domains.length > 0 ? { domains: opts.domains } : {}),
|
|
56
65
|
};
|
|
57
66
|
// Perpetual: pass null to omit exp claim. Otherwise translate days → seconds.
|
|
58
|
-
// Default (neither flag):
|
|
67
|
+
// Default (neither flag): DEFAULT_KIT_MINT_DAYS (GAP-287 PR-3 named constant;
|
|
68
|
+
// value unchanged at 365).
|
|
59
69
|
const expiresInSeconds = opts.perpetual
|
|
60
70
|
? null
|
|
61
71
|
: opts.expiresInDays !== undefined
|
|
62
72
|
? opts.expiresInDays * 24 * 60 * 60
|
|
63
|
-
:
|
|
73
|
+
: DEFAULT_KIT_MINT_DAYS * 24 * 60 * 60;
|
|
64
74
|
const issuedAtMs = Date.now();
|
|
65
75
|
const licenseKey = await generateLicenseKey(payload, keys.privateKey, expiresInSeconds, keys.publicKey);
|
|
66
76
|
// Pair-match guard: prove the supplied public key can verify the JWT we just
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revealui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "RevealUI's runtime engine — admin UI, REST API, auth, rich text, plugin system, and access control. The heart of the open-source platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"dependencies": {
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"pg": "^8.22.0",
|
|
24
24
|
"yjs": "^13.6.31",
|
|
25
25
|
"zod": "^4.4.3",
|
|
26
|
-
"@revealui/cache": "0.2.
|
|
27
|
-
"@revealui/contracts": "0.
|
|
26
|
+
"@revealui/cache": "0.2.5",
|
|
27
|
+
"@revealui/contracts": "0.8.0",
|
|
28
28
|
"@revealui/resilience": "0.2.4",
|
|
29
|
-
"@revealui/security": "0.
|
|
29
|
+
"@revealui/security": "0.5.0",
|
|
30
30
|
"@revealui/utils": "0.3.6"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|