@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 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 —
@@ -1 +1 @@
1
- {"version":3,"file":"license.d.ts","sourceRoot":"","sources":["../src/license.ts"],"names":[],"mappings":"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"}
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: graceConfig.subscriptionDays * 86_400,
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 grace) — try the next before giving up.
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 = 365 * 24 * 60 * 60, publicKey) {
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,CAuFrC"}
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"}
@@ -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): 365-day subscription, matching generateLicenseKey's default.
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
- : 365 * 24 * 60 * 60;
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.11.1",
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.4",
27
- "@revealui/contracts": "0.7.0",
26
+ "@revealui/cache": "0.2.5",
27
+ "@revealui/contracts": "0.8.0",
28
28
  "@revealui/resilience": "0.2.4",
29
- "@revealui/security": "0.4.3",
29
+ "@revealui/security": "0.5.0",
30
30
  "@revealui/utils": "0.3.6"
31
31
  },
32
32
  "devDependencies": {