@revealui/core 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +3 -2
  2. package/dist/client/admin/components/AdminDashboard.d.ts +8 -1
  3. package/dist/client/admin/components/AdminDashboard.d.ts.map +1 -1
  4. package/dist/client/admin/components/AdminDashboard.js +11 -10
  5. package/dist/client/admin/components/CollectionList.d.ts.map +1 -1
  6. package/dist/client/admin/components/CollectionList.js +4 -6
  7. package/dist/client/admin/components/DocumentForm.d.ts.map +1 -1
  8. package/dist/client/admin/components/DocumentForm.js +26 -26
  9. package/dist/client/admin/components/GlobalForm.d.ts.map +1 -1
  10. package/dist/client/admin/components/GlobalForm.js +10 -9
  11. package/dist/client/admin/page.d.ts.map +1 -1
  12. package/dist/client/admin/page.js +2 -1
  13. package/dist/client/richtext/components/ImageNodeComponent.d.ts.map +1 -1
  14. package/dist/client/richtext/components/ImageNodeComponent.js +3 -22
  15. package/dist/client/richtext/components/ImageUploadButton.d.ts.map +1 -1
  16. package/dist/client/richtext/components/ImageUploadButton.js +2 -4
  17. package/dist/client/richtext/plugins/FloatingToolbarPlugin.d.ts.map +1 -1
  18. package/dist/client/richtext/plugins/FloatingToolbarPlugin.js +3 -2
  19. package/dist/client/richtext/plugins/ToolbarPlugin.d.ts.map +1 -1
  20. package/dist/client/richtext/plugins/ToolbarPlugin.js +3 -2
  21. package/dist/client/ui/index.d.ts.map +1 -1
  22. package/dist/client/ui/index.js +10 -6
  23. package/dist/cogs-breaker.d.ts +27 -0
  24. package/dist/cogs-breaker.d.ts.map +1 -0
  25. package/dist/cogs-breaker.js +29 -0
  26. package/dist/error-handling/error-boundary.d.ts.map +1 -1
  27. package/dist/error-handling/error-boundary.js +2 -9
  28. package/dist/error-handling/fallback-components.d.ts.map +1 -1
  29. package/dist/error-handling/fallback-components.js +9 -86
  30. package/dist/instance/logger.d.ts +2 -1
  31. package/dist/instance/logger.d.ts.map +1 -1
  32. package/dist/license/mint-client.d.ts +15 -4
  33. package/dist/license/mint-client.d.ts.map +1 -1
  34. package/dist/license/mint-client.js +38 -9
  35. package/dist/license.d.ts +39 -14
  36. package/dist/license.d.ts.map +1 -1
  37. package/dist/license.js +111 -34
  38. package/dist/margin-governor.d.ts +82 -0
  39. package/dist/margin-governor.d.ts.map +1 -0
  40. package/dist/margin-governor.js +143 -0
  41. package/dist/revforge-license-boot.d.ts +0 -10
  42. package/dist/revforge-license-boot.d.ts.map +1 -1
  43. package/dist/revforge-license-boot.js +41 -20
  44. package/dist/types/runtime.d.ts +3 -6
  45. package/dist/types/runtime.d.ts.map +1 -1
  46. package/dist/utils/block-conversion.d.ts.map +1 -1
  47. package/dist/utils/block-conversion.js +2 -1
  48. package/package.json +64 -3
@@ -1,12 +1,13 @@
1
1
  /**
2
- * License mint client (GAP-260 P4-3).
2
+ * License mint client (GAP-260 P4-3 + residual hosted gate).
3
3
  *
4
4
  * Single entry for online mint surfaces (Stripe webhooks, admin generate).
5
5
  * Routes to either:
6
6
  * - remote `apps/license-signer` POST /internal/mint when
7
- * REVEALUI_LICENSE_SIGN_VIA_SIGNER is truthy, or
7
+ * REVEALUI_LICENSE_SIGN_VIA_SIGNER is truthy, or when
8
+ * REVEALUI_DEPLOYMENT_MODE=hosted (explicit; never local private-key mint), or
8
9
  * - local {@link generateLicenseKey} with REVEALUI_LICENSE_PRIVATE_KEY
9
- * (default / fallback until P4-4 drops the private key from api).
10
+ * only when MODE is not hosted and SIGN_VIA is off (tests / dogfood).
10
11
  *
11
12
  * Offline stamper (revforge) keeps calling generateLicenseKey directly.
12
13
  *
@@ -14,6 +15,7 @@
14
15
  * REVEALUI_LICENSE_SIGN_VIA_SIGNER=1|true|yes|on
15
16
  * REVEALUI_LICENSE_SIGNER_URL base URL (e.g. http://127.0.0.1:8791)
16
17
  * REVEALUI_SIGNER_INVOKE_SECRET HMAC secret (no REVEALUI_SECRET fallback)
18
+ * REVEALUI_DEPLOYMENT_MODE=hosted forces remote-only (SIGN_VIA required)
17
19
  *
18
20
  * Env (local path):
19
21
  * REVEALUI_LICENSE_PRIVATE_KEY PKCS#8 Ed25519 PEM (required)
@@ -46,22 +48,43 @@ export function isSignViaSigner(env = process.env) {
46
48
  const raw = (env.REVEALUI_LICENSE_SIGN_VIA_SIGNER ?? '').trim().toLowerCase();
47
49
  return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
48
50
  }
51
+ /**
52
+ * Explicit hosted SaaS posture only. Does not use private-key inference
53
+ * (that belongs to detectDeploymentMode). Hosted online mint must never
54
+ * fall through to mintLocal.
55
+ */
56
+ export function isExplicitHostedMintMode(env = process.env) {
57
+ return (env.REVEALUI_DEPLOYMENT_MODE ?? '').trim().toLowerCase() === 'hosted';
58
+ }
59
+ function hasRemoteMintConfig(env) {
60
+ const url = env.REVEALUI_LICENSE_SIGNER_URL?.trim() ?? '';
61
+ const secret = env.REVEALUI_SIGNER_INVOKE_SECRET?.trim() ?? '';
62
+ return url.length > 0 && secret.length > 0;
63
+ }
49
64
  /**
50
65
  * Whether this process can mint a license key (local private key OR remote
51
66
  * signer fully configured). Used by call sites that used to gate only on
52
67
  * REVEALUI_LICENSE_PRIVATE_KEY presence.
68
+ *
69
+ * Explicit MODE=hosted never treats a local private key as sufficient.
53
70
  */
54
71
  export function canMintLicense(env = process.env) {
72
+ if (isExplicitHostedMintMode(env)) {
73
+ return isSignViaSigner(env) && hasRemoteMintConfig(env);
74
+ }
55
75
  if (isSignViaSigner(env)) {
56
- const url = env.REVEALUI_LICENSE_SIGNER_URL?.trim() ?? '';
57
- const secret = env.REVEALUI_SIGNER_INVOKE_SECRET?.trim() ?? '';
58
- return url.length > 0 && secret.length > 0;
76
+ return hasRemoteMintConfig(env);
59
77
  }
60
78
  return Boolean(env.REVEALUI_LICENSE_PRIVATE_KEY?.trim());
61
79
  }
62
80
  /** Human-readable reason mint is unavailable (for CRITICAL logs). */
63
81
  export function mintConfigMissingMessage(env = process.env) {
64
- if (isSignViaSigner(env)) {
82
+ if (isExplicitHostedMintMode(env) && !isSignViaSigner(env)) {
83
+ return ('REVEALUI_DEPLOYMENT_MODE=hosted requires REVEALUI_LICENSE_SIGN_VIA_SIGNER ' +
84
+ 'with REVEALUI_LICENSE_SIGNER_URL and REVEALUI_SIGNER_INVOKE_SECRET ' +
85
+ '(local private-key mint is disabled on hosted)');
86
+ }
87
+ if (isSignViaSigner(env) || isExplicitHostedMintMode(env)) {
65
88
  return ('REVEALUI_LICENSE_SIGN_VIA_SIGNER is set but REVEALUI_LICENSE_SIGNER_URL ' +
66
89
  'and/or REVEALUI_SIGNER_INVOKE_SECRET are missing');
67
90
  }
@@ -180,13 +203,19 @@ async function mintLocal(input, env) {
180
203
  return generateLicenseKey(payload, privateKey, expiresInSeconds, publicKey);
181
204
  }
182
205
  /**
183
- * Mint a signed license JWT — remote signer when flagged, else local private key.
206
+ * Mint a signed license JWT — remote signer when SIGN_VIA is on or MODE is
207
+ * explicitly hosted; else local private key (tests / dogfood only).
184
208
  */
185
209
  export async function mintLicenseKey(input, options = {}) {
186
210
  const env = options.env ?? process.env;
187
211
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
188
212
  const normalized = withPerpetualSiteCaps(input);
189
- if (isSignViaSigner(env)) {
213
+ // Hosted online mint never uses the local private-key path, even when the
214
+ // key is still present in env during migration.
215
+ if (isExplicitHostedMintMode(env) && !isSignViaSigner(env)) {
216
+ throw new LicenseMintConfigError(mintConfigMissingMessage(env));
217
+ }
218
+ if (isSignViaSigner(env) || isExplicitHostedMintMode(env)) {
190
219
  return mintViaSigner(normalized, env, fetchImpl);
191
220
  }
192
221
  return mintLocal(normalized, env);
package/dist/license.d.ts CHANGED
@@ -48,15 +48,27 @@ export interface GracePeriodConfig {
48
48
  /** Days of cached-license grace when infra is unreachable (default: 7) */
49
49
  infraDays: number;
50
50
  }
51
+ /**
52
+ * Hard cap on LICENSE_GRACE_* days. Env values above this become jwtVerify
53
+ * clockTolerance and must not keep an expired subscription JWT valid forever.
54
+ * Same spirit as {@link MAX_LICENSE_CACHE_TTL_MS}.
55
+ */
56
+ export declare const MAX_LICENSE_GRACE_DAYS = 30;
57
+ /**
58
+ * Parse a LICENSE_GRACE_* env value. Unset / non-numeric / negative → fallback.
59
+ * Values above {@link MAX_LICENSE_GRACE_DAYS} are clamped and warned.
60
+ */
61
+ export declare function parseLicenseGraceDaysEnv(envValue: string | undefined, fallback: number): number;
51
62
  /**
52
63
  * Configure grace period durations. Useful for testing.
64
+ * Values are clamped to {@link MAX_LICENSE_GRACE_DAYS}.
53
65
  */
54
66
  export declare function configureGracePeriods(overrides: Partial<GracePeriodConfig>): void;
55
67
  /** Decoded license payload schema */
56
68
  declare const licensePayloadSchema: z.ZodObject<{
57
69
  tier: z.ZodEnum<{
58
- max: "max";
59
70
  pro: "pro";
71
+ max: "max";
60
72
  enterprise: "enterprise";
61
73
  }>;
62
74
  customerId: z.ZodString;
@@ -179,15 +191,16 @@ export declare const REFRESH_ACCEPT_DAYS = 30;
179
191
  * `POST /api/license/refresh`, accepting a token whose `exp` is past by at most
180
192
  * `refreshAcceptDays` (default {@link REFRESH_ACCEPT_DAYS}).
181
193
  *
194
+ * `expectedCustomerId` is required (fail closed). A token for customer A must
195
+ * not verify when the caller bound customer B. An empty / missing bind returns
196
+ * null so an unbound refresh cannot exfil another customer's current key.
197
+ *
182
198
  * It reuses the SAME ordered multi-key verification (GAP-259 rotation
183
- * composition) and payload schema as {@link validateLicenseKey}, so a token
184
- * minted under the outgoing private key during a dual-key rotation window still
185
- * verifies. It performs NO `customerId` binding: the refresh endpoint trusts
186
- * the token's `customerId` claim and then independently requires an ACTIVE
187
- * license row for it. This function only proves possession of a recently-valid
188
- * signed key. It never mints and never grants entitlement on its own.
199
+ * composition) and payload schema as {@link validateLicenseKey}. The 30-day
200
+ * window stays so a recently-expired key can still fetch its own renewal after
201
+ * the bind succeeds. It never mints and never grants entitlement on its own.
189
202
  */
190
- export declare function validateLicenseKeyForRefresh(licenseKey: string, publicKey: string | readonly string[], refreshAcceptDays?: number): Promise<LicensePayload | null>;
203
+ export declare function validateLicenseKeyForRefresh(licenseKey: string, publicKey: string | readonly string[], expectedCustomerId: string, refreshAcceptDays?: number): Promise<LicensePayload | null>;
191
204
  /**
192
205
  * Outcome of a signing-keypair self-test. See {@link selfVerifyLicenseKeypair}.
193
206
  */
@@ -229,13 +242,20 @@ export declare function selfVerifyLicenseKeypair(privateKey: string, publicKeys:
229
242
  * @returns The resolved license tier
230
243
  */
231
244
  export declare function initializeLicense(): Promise<LicenseTier>;
245
+ /**
246
+ * Await an in-flight or due revalidation. Tests and async request gates use
247
+ * this so TTL expiry still proves licensed after the key is re-checked.
248
+ */
249
+ export declare function refreshLicenseIfStale(): Promise<LicenseTier>;
232
250
  /**
233
251
  * Returns the current license tier.
234
- * If the license hasn't been initialized or the cache has expired, returns 'free'.
252
+ * Uninitialized state is free. A stale cache keeps the last validated tier
253
+ * and triggers revalidation of the configured key.
235
254
  */
236
255
  export declare function getCurrentTier(): LicenseTier;
237
256
  /**
238
- * Returns the full license payload, or null if no valid license or cache expired.
257
+ * Returns the full license payload, or null if no valid license is cached.
258
+ * Stale TTL does not clear a still-valid payload.
239
259
  */
240
260
  export declare function getLicensePayload(): LicensePayload | null;
241
261
  /**
@@ -249,15 +269,19 @@ export declare function getLicensePayload(): LicensePayload | null;
249
269
  *
250
270
  * Matching rules (no authored regex, per the fleet no-regex rule):
251
271
  * - `host` is lower-cased and stripped of any `:port` suffix
252
- * - `localhost` / `127.0.0.1` are always allowed, so a trial kit boots and
253
- * serves on its default `http://localhost` regardless of the licensed domain
272
+ * - `localhost` / `127.0.0.1` match only when `allowLoopback` is true (trial /
273
+ * unpublished). A published API with a `domains` claim must not accept a
274
+ * spoofed Host: localhost bypass.
254
275
  * - otherwise `host` must equal a licensed domain OR be a subdomain of one
255
276
  * (`app.example.com` matches `example.com`)
256
277
  *
257
278
  * @param host raw Host header value or URL hostname (a `:port` suffix is tolerated)
258
279
  * @param domains the license payload's `domains` claim
280
+ * @param options.allowLoopback opt-in loopback bypass (default false)
259
281
  */
260
- export declare function hostMatchesLicensedDomains(host: string, domains: readonly string[]): boolean;
282
+ export declare function hostMatchesLicensedDomains(host: string, domains: readonly string[], options?: {
283
+ allowLoopback?: boolean;
284
+ }): boolean;
261
285
  /**
262
286
  * Checks whether the current license is at least the given tier.
263
287
  * Also validates that the license has not expired (checks JWT exp claim).
@@ -289,7 +313,8 @@ export declare function getMaxSites(): number;
289
313
  export declare function getMaxUsers(): number;
290
314
  /**
291
315
  * Returns the maximum agent tasks per billing cycle for the current license.
292
- * Track B metering: free=1K, pro=10K, max=50K, enterprise=unlimited.
316
+ * Track B metering: free=0 (Local AI, no public agent quota), pro=10K,
317
+ * max=50K, enterprise=unlimited.
293
318
  */
294
319
  export declare function getMaxAgentTasks(): number;
295
320
  /**
@@ -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;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,MAAM,GAAG,SAAS,CAMpB;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,CAShC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;;;;;;;;;GAYG;AACH,wBAAsB,4BAA4B,CAChD,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACrC,iBAAiB,GAAE,MAA4B,GAC9C,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAEhC;AA+GD;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACzC;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,GACtB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CA4D9B;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CA8C9D;AAaD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,WAAW,CAG5C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,IAAI,CAGzD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQ5F;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,YAAY,EAAE,WAAW,GAAG,OAAO,CA2B7D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,GAAE,WAAmB,GAAG,kBAAkB,CAkEtF;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,QAAQ,CAAC,iBAAiB,CAAC,CAE5D;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAMzC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,QAAqB,CAAC;AAEnE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,qBAAqB,QAAoC,CAAC;AAEvE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,EAClC,KAAK,GAAE,MAAmB,GACzB,MAAM,CAIR;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,IAAI,GAAG,MAAM,CAE5D;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,qCAAqC,IAAI,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEtF;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQ/E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQ/E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACvE,UAAU,EAAE,MAAM,EAClB,gBAAgB,GAAE,MAAM,GAAG,IAAuC,EAClE,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
1
+ {"version":3,"file":"license.d.ts","sourceRoot":"","sources":["../src/license.ts"],"names":[],"mappings":"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"}
package/dist/license.js CHANGED
@@ -22,26 +22,50 @@ async function getJose() {
22
22
  /** JWT issuer and audience for license tokens — prevents cross-environment replay */
23
23
  const LICENSE_ISSUER = process.env.REVEALUI_LICENSE_ISSUER ?? 'https://revealui.com';
24
24
  const LICENSE_AUDIENCE = process.env.REVEALUI_LICENSE_AUDIENCE ?? 'revealui-license';
25
- const DEFAULT_GRACE = {
26
- subscriptionDays: parseEnvInt('LICENSE_GRACE_SUBSCRIPTION_DAYS', 3),
27
- perpetualDays: parseEnvInt('LICENSE_GRACE_PERPETUAL_DAYS', 30),
28
- infraDays: parseEnvInt('LICENSE_GRACE_INFRA_DAYS', 7),
29
- };
30
- function parseEnvInt(key, fallback) {
31
- const val = process.env[key];
32
- if (val) {
33
- const parsed = Number.parseInt(val, 10);
34
- if (Number.isFinite(parsed) && parsed >= 0)
35
- return parsed;
25
+ /**
26
+ * Hard cap on LICENSE_GRACE_* days. Env values above this become jwtVerify
27
+ * clockTolerance and must not keep an expired subscription JWT valid forever.
28
+ * Same spirit as {@link MAX_LICENSE_CACHE_TTL_MS}.
29
+ */
30
+ export const MAX_LICENSE_GRACE_DAYS = 30;
31
+ /**
32
+ * Parse a LICENSE_GRACE_* env value. Unset / non-numeric / negative → fallback.
33
+ * Values above {@link MAX_LICENSE_GRACE_DAYS} are clamped and warned.
34
+ */
35
+ export function parseLicenseGraceDaysEnv(envValue, fallback) {
36
+ if (!envValue)
37
+ return fallback;
38
+ const parsed = Number.parseInt(envValue, 10);
39
+ if (!Number.isFinite(parsed) || parsed < 0)
40
+ return fallback;
41
+ if (parsed > MAX_LICENSE_GRACE_DAYS) {
42
+ logger.warn(`LICENSE_GRACE_* days=${parsed} exceeds the ${MAX_LICENSE_GRACE_DAYS}-day cap; using ${MAX_LICENSE_GRACE_DAYS}. Larger values extend jwtVerify clockTolerance so expired subscription JWTs stay valid and are not permitted.`);
43
+ return MAX_LICENSE_GRACE_DAYS;
36
44
  }
37
- return fallback;
45
+ return parsed;
38
46
  }
47
+ function clampGraceDays(days) {
48
+ if (!Number.isFinite(days) || days < 0)
49
+ return 0;
50
+ return Math.min(days, MAX_LICENSE_GRACE_DAYS);
51
+ }
52
+ const DEFAULT_GRACE = {
53
+ subscriptionDays: parseLicenseGraceDaysEnv(process.env.LICENSE_GRACE_SUBSCRIPTION_DAYS, 3),
54
+ perpetualDays: parseLicenseGraceDaysEnv(process.env.LICENSE_GRACE_PERPETUAL_DAYS, 30),
55
+ infraDays: parseLicenseGraceDaysEnv(process.env.LICENSE_GRACE_INFRA_DAYS, 7),
56
+ };
39
57
  let graceConfig = { ...DEFAULT_GRACE };
40
58
  /**
41
59
  * Configure grace period durations. Useful for testing.
60
+ * Values are clamped to {@link MAX_LICENSE_GRACE_DAYS}.
42
61
  */
43
62
  export function configureGracePeriods(overrides) {
44
- graceConfig = { ...DEFAULT_GRACE, ...overrides };
63
+ const merged = { ...DEFAULT_GRACE, ...overrides };
64
+ graceConfig = {
65
+ subscriptionDays: clampGraceDays(merged.subscriptionDays),
66
+ perpetualDays: clampGraceDays(merged.perpetualDays),
67
+ infraDays: clampGraceDays(merged.infraDays),
68
+ };
45
69
  }
46
70
  /** Decoded license payload schema */
47
71
  const licensePayloadSchema = z.object({
@@ -106,6 +130,8 @@ const DEFAULT_CACHE_CONFIG = {
106
130
  };
107
131
  let cacheConfig = { ...DEFAULT_CACHE_CONFIG };
108
132
  let cachedAt = 0;
133
+ let refreshInFlight = null;
134
+ let refreshEpoch = 0;
109
135
  /**
110
136
  * Configure the license cache TTL.
111
137
  * Useful for tests (short TTL) or deployments needing faster revocation detection.
@@ -240,16 +266,20 @@ export const REFRESH_ACCEPT_DAYS = 30;
240
266
  * `POST /api/license/refresh`, accepting a token whose `exp` is past by at most
241
267
  * `refreshAcceptDays` (default {@link REFRESH_ACCEPT_DAYS}).
242
268
  *
269
+ * `expectedCustomerId` is required (fail closed). A token for customer A must
270
+ * not verify when the caller bound customer B. An empty / missing bind returns
271
+ * null so an unbound refresh cannot exfil another customer's current key.
272
+ *
243
273
  * It reuses the SAME ordered multi-key verification (GAP-259 rotation
244
- * composition) and payload schema as {@link validateLicenseKey}, so a token
245
- * minted under the outgoing private key during a dual-key rotation window still
246
- * verifies. It performs NO `customerId` binding: the refresh endpoint trusts
247
- * the token's `customerId` claim and then independently requires an ACTIVE
248
- * license row for it. This function only proves possession of a recently-valid
249
- * signed key. It never mints and never grants entitlement on its own.
274
+ * composition) and payload schema as {@link validateLicenseKey}. The 30-day
275
+ * window stays so a recently-expired key can still fetch its own renewal after
276
+ * the bind succeeds. It never mints and never grants entitlement on its own.
250
277
  */
251
- export async function validateLicenseKeyForRefresh(licenseKey, publicKey, refreshAcceptDays = REFRESH_ACCEPT_DAYS) {
252
- return verifyAndParseLicenseJwt(licenseKey, publicKey, refreshAcceptDays * 86_400);
278
+ export async function validateLicenseKeyForRefresh(licenseKey, publicKey, expectedCustomerId, refreshAcceptDays = REFRESH_ACCEPT_DAYS) {
279
+ const bound = expectedCustomerId.trim();
280
+ if (bound.length === 0)
281
+ return null;
282
+ return verifyAndParseLicenseJwt(licenseKey, publicKey, refreshAcceptDays * 86_400, bound);
253
283
  }
254
284
  /**
255
285
  * Shared verify + parse path for {@link validateLicenseKey} and
@@ -468,26 +498,66 @@ export async function initializeLicense() {
468
498
  }
469
499
  return payload.tier;
470
500
  }
501
+ function isCacheStale() {
502
+ return cachedAt > 0 && Date.now() - cachedAt > cacheConfig.ttlMs;
503
+ }
504
+ /**
505
+ * Re-validate the configured key. Single-flight so concurrent readers share one
506
+ * verify. On failure, keep the last validated state (do not flap to free).
507
+ */
508
+ function scheduleLicenseRefresh() {
509
+ if (refreshInFlight)
510
+ return refreshInFlight;
511
+ const epoch = refreshEpoch;
512
+ refreshInFlight = initializeLicense()
513
+ .then((tier) => {
514
+ if (epoch !== refreshEpoch)
515
+ return cachedState.tier;
516
+ return tier;
517
+ })
518
+ .catch((err) => {
519
+ logger.warn('License revalidation failed; keeping last validated state', {
520
+ error: err instanceof Error ? err.message : String(err),
521
+ });
522
+ return cachedState.tier;
523
+ })
524
+ .finally(() => {
525
+ refreshInFlight = null;
526
+ });
527
+ return refreshInFlight;
528
+ }
471
529
  /**
472
- * Invalidates the cached license state if it has exceeded the configured TTL.
473
- * After invalidation, the license defaults to 'free' until re-initialized.
530
+ * When the cache TTL elapses, re-validate the current key instead of dropping
531
+ * to free. Request-path readers keep the last validated tier until refresh
532
+ * completes so a live Pro process cannot flap to free while a valid key is set.
474
533
  */
475
534
  function evictStaleCache() {
476
- if (cachedAt > 0 && Date.now() - cachedAt > cacheConfig.ttlMs) {
477
- cachedState = { tier: 'free', payload: null, validatedAt: null, keyPresentButInvalid: false };
478
- cachedAt = 0;
535
+ if (isCacheStale()) {
536
+ void scheduleLicenseRefresh();
479
537
  }
480
538
  }
539
+ /**
540
+ * Await an in-flight or due revalidation. Tests and async request gates use
541
+ * this so TTL expiry still proves licensed after the key is re-checked.
542
+ */
543
+ export async function refreshLicenseIfStale() {
544
+ if (isCacheStale() || refreshInFlight) {
545
+ return scheduleLicenseRefresh();
546
+ }
547
+ return cachedState.tier;
548
+ }
481
549
  /**
482
550
  * Returns the current license tier.
483
- * If the license hasn't been initialized or the cache has expired, returns 'free'.
551
+ * Uninitialized state is free. A stale cache keeps the last validated tier
552
+ * and triggers revalidation of the configured key.
484
553
  */
485
554
  export function getCurrentTier() {
486
555
  evictStaleCache();
487
556
  return cachedState.tier;
488
557
  }
489
558
  /**
490
- * Returns the full license payload, or null if no valid license or cache expired.
559
+ * Returns the full license payload, or null if no valid license is cached.
560
+ * Stale TTL does not clear a still-valid payload.
491
561
  */
492
562
  export function getLicensePayload() {
493
563
  evictStaleCache();
@@ -504,20 +574,24 @@ export function getLicensePayload() {
504
574
  *
505
575
  * Matching rules (no authored regex, per the fleet no-regex rule):
506
576
  * - `host` is lower-cased and stripped of any `:port` suffix
507
- * - `localhost` / `127.0.0.1` are always allowed, so a trial kit boots and
508
- * serves on its default `http://localhost` regardless of the licensed domain
577
+ * - `localhost` / `127.0.0.1` match only when `allowLoopback` is true (trial /
578
+ * unpublished). A published API with a `domains` claim must not accept a
579
+ * spoofed Host: localhost bypass.
509
580
  * - otherwise `host` must equal a licensed domain OR be a subdomain of one
510
581
  * (`app.example.com` matches `example.com`)
511
582
  *
512
583
  * @param host raw Host header value or URL hostname (a `:port` suffix is tolerated)
513
584
  * @param domains the license payload's `domains` claim
585
+ * @param options.allowLoopback opt-in loopback bypass (default false)
514
586
  */
515
- export function hostMatchesLicensedDomains(host, domains) {
587
+ export function hostMatchesLicensedDomains(host, domains, options = {}) {
516
588
  const normalized = (host.trim().toLowerCase().split(':')[0] ?? '').trim();
517
589
  if (!normalized)
518
590
  return false;
519
- if (normalized === 'localhost' || normalized === '127.0.0.1')
591
+ if (options.allowLoopback === true &&
592
+ (normalized === 'localhost' || normalized === '127.0.0.1')) {
520
593
  return true;
594
+ }
521
595
  return domains.some((domain) => {
522
596
  const d = domain.trim().toLowerCase();
523
597
  return d.length > 0 && (normalized === d || normalized.endsWith(`.${d}`));
@@ -658,7 +732,8 @@ export function getMaxUsers() {
658
732
  }
659
733
  /**
660
734
  * Returns the maximum agent tasks per billing cycle for the current license.
661
- * Track B metering: free=1K, pro=10K, max=50K, enterprise=unlimited.
735
+ * Track B metering: free=0 (Local AI, no public agent quota), pro=10K,
736
+ * max=50K, enterprise=unlimited.
662
737
  */
663
738
  export function getMaxAgentTasks() {
664
739
  evictStaleCache();
@@ -668,7 +743,7 @@ export function getMaxAgentTasks() {
668
743
  return 50_000;
669
744
  if (cachedState.tier === 'pro')
670
745
  return 10_000;
671
- return 1_000;
746
+ return 0;
672
747
  }
673
748
  /**
674
749
  * Default subscription license JWT lifetime (seconds) — one year. Used as the
@@ -825,6 +900,8 @@ export async function generateLicenseKey(payload, privateKey, expiresInSeconds =
825
900
  * Reset license state. Primarily for testing.
826
901
  */
827
902
  export function resetLicenseState() {
903
+ refreshEpoch += 1;
904
+ refreshInFlight = null;
828
905
  cachedState = { tier: 'free', payload: null, validatedAt: null, keyPresentButInvalid: false };
829
906
  cachedAt = 0;
830
907
  }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * GAP-256 PR-3 — pure margin admission decide (no DB, no I/O).
3
+ *
4
+ * Call sites: apps/server admitFreeIntake (reads snapshot, then this).
5
+ * HARD: never call after users insert for free_signup; pure module has no COUNT.
6
+ *
7
+ * @see docs/specs/2026-08-09-gap-256-margin-admission-layers-2-3.md
8
+ */
9
+ export type AdmissionChannel = 'free_signup' | 'paid_signup' | 'paid_checkout' | 'invite_accept' | 'admin_provision' | 'waitlist_claim_free' | 'waitlist_claim_paid';
10
+ export type PayingIntentSignal = {
11
+ kind: 'none';
12
+ } | {
13
+ kind: 'checkout';
14
+ tier: 'pro' | 'max' | 'enterprise';
15
+ } | {
16
+ kind: 'existing_paid';
17
+ accountId: string;
18
+ tier: string;
19
+ status: 'active';
20
+ };
21
+ export type CohortLimits = {
22
+ maxSites: number;
23
+ maxUsers: number;
24
+ maxAgentTasks: number;
25
+ };
26
+ export type SnapshotMode = 'open' | 'lean' | 'waitlist';
27
+ /** Minimal snapshot view for pure decide (from margin_snapshots row). */
28
+ export interface MarginSnapshotView {
29
+ id: string;
30
+ mode: SnapshotMode;
31
+ computedAt: Date;
32
+ netCents?: number;
33
+ freeCostRatio?: string | null;
34
+ projected7dCents?: number | null;
35
+ }
36
+ export interface GovernorFlags {
37
+ /** Master; off → admit always with open limits */
38
+ enabled: boolean;
39
+ /**
40
+ * When true (default for first enable): compute mode for logs but never
41
+ * waitlist / never lean-enforce on admit (always admit).
42
+ */
43
+ shadow: boolean;
44
+ /** Hours after which a snapshot is ignored (fail-open to open). Default 36. */
45
+ staleHours: number;
46
+ }
47
+ export type PureAdmitResult = {
48
+ decision: 'admit';
49
+ mode: SnapshotMode | 'bypass' | 'disabled' | 'shadow';
50
+ cohortLimits: CohortLimits;
51
+ snapshotId: string | null;
52
+ reason: string;
53
+ shadow: boolean;
54
+ } | {
55
+ decision: 'waitlist';
56
+ mode: 'waitlist';
57
+ reason: string;
58
+ snapshotId: string | null;
59
+ shadow: false;
60
+ httpStatus: 202;
61
+ code: 'WAITLISTED';
62
+ };
63
+ /** Open free cohort = standard hosted free limits (caller supplies base). */
64
+ export declare function freeCohortLimitsForMode(mode: 'open' | 'lean', openLimits: CohortLimits, leanMaxAgentTasks?: number): CohortLimits;
65
+ /** Paid-signup pending only — never free cohort (K20). */
66
+ export declare function paidPendingLimits(): CohortLimits;
67
+ export declare function governorFlagsFromEnv(env?: Record<string, string | undefined>): GovernorFlags;
68
+ /**
69
+ * Pure admit decision for free intake.
70
+ * Does not touch users table, COUNT, or waitlist I/O.
71
+ */
72
+ export declare function decideFreeIntake(input: {
73
+ channel: AdmissionChannel;
74
+ deploymentMode: 'hosted' | 'forge' | 'unknown';
75
+ payingIntent: PayingIntentSignal;
76
+ snapshot: MarginSnapshotView | null;
77
+ flags: GovernorFlags;
78
+ openLimits: CohortLimits;
79
+ leanMaxAgentTasks?: number;
80
+ now?: Date;
81
+ }): PureAdmitResult;
82
+ //# sourceMappingURL=margin-governor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"margin-governor.d.ts","sourceRoot":"","sources":["../src/margin-governor.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,aAAa,GACb,eAAe,GACf,eAAe,GACf,iBAAiB,GACjB,qBAAqB,GACrB,qBAAqB,CAAC;AAE1B,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,YAAY,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEjF,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAExD,yEAAyE;AACzE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,YAAY,CAAC;IACnB,UAAU,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GACvB;IACE,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;IACtD,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB,GACD;IACE,QAAQ,EAAE,UAAU,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,KAAK,CAAC;IACd,UAAU,EAAE,GAAG,CAAC;IAChB,IAAI,EAAE,YAAY,CAAC;CACpB,CAAC;AAEN,6EAA6E;AAC7E,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,UAAU,EAAE,YAAY,EACxB,iBAAiB,SAAM,GACtB,YAAY,CASd;AAED,0DAA0D;AAC1D,wBAAgB,iBAAiB,IAAI,YAAY,CAEhD;AAED,wBAAgB,oBAAoB,CAClC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,aAAa,CAOf;AAkCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,cAAc,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAC/C,YAAY,EAAE,kBAAkB,CAAC;IACjC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,KAAK,EAAE,aAAa,CAAC;IACrB,UAAU,EAAE,YAAY,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ,GAAG,eAAe,CAyFlB"}