UNPKG

@mastra/core

Version:
1 lines 17.3 kB
{"version":3,"sources":["../src/license/index.ts"],"names":[],"mappings":"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","file":"chunk-6LWJF7BY.cjs","sourcesContent":["import type { IMastraLogger } from '../logger';\n\nexport interface LicenseValidationSuccess {\n valid: true;\n /** Feature entitlements granted by the license (e.g. 'rbac', 'sso', 'fga') */\n entitlements: string[];\n /** Plan tier the license was issued for (e.g. 'teams', 'enterprise') */\n planTier: string;\n expiresAt: string | null;\n leaseTtlSeconds: number;\n}\n\nexport interface LicenseValidationError {\n valid: false;\n code: 'INVALID_KEY' | 'LICENSE_EXPIRED' | 'LICENSE_REVOKED' | 'RATE_LIMITED';\n reason: string;\n}\n\nexport type LicenseValidationResponse = LicenseValidationSuccess | LicenseValidationError;\n\nexport type LicenseMode = 'enterprise' | 'open-source';\n\nexport type LicenseStatus = 'pending' | 'valid' | 'invalid';\n\nexport interface LicenseSnapshot {\n mode: LicenseMode;\n status: LicenseStatus;\n entitlements: string[] | null;\n planTier: string | null;\n expiresAt: string | null;\n}\n\nexport class LicenseClient {\n private static instance: LicenseClient | undefined;\n private logger?: IMastraLogger;\n\n private licenseKey?: string;\n private licenseUrl?: string;\n\n private mode: LicenseMode = 'open-source';\n private status: LicenseStatus = 'pending';\n\n private cachedResult: LicenseValidationSuccess | null = null;\n private cacheExpiry: number = 0;\n private gracePeriodEnd: number = 0;\n\n private revalidationTimeout: NodeJS.Timeout | null = null;\n private readonly GRACE_PERIOD_MS = 72 * 60 * 60 * 1000; // 72 hours\n private readonly DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n private constructor(logger?: IMastraLogger) {\n this.logger = logger;\n // MASTRA_LICENSE_KEY is the primary env var; MASTRA_EE_LICENSE is a\n // supported legacy alias kept for backward compatibility.\n this.licenseKey = process.env.MASTRA_LICENSE_KEY || process.env.MASTRA_EE_LICENSE;\n this.licenseUrl = process.env.MASTRA_LICENSE_URL || 'https://license.mastra.ai';\n\n if (this.licenseKey) {\n this.mode = 'enterprise';\n } else {\n this.mode = 'open-source';\n }\n }\n\n public static getInstance(logger?: IMastraLogger): LicenseClient {\n if (!LicenseClient.instance) {\n LicenseClient.instance = new LicenseClient(logger);\n } else if (logger) {\n LicenseClient.instance.logger = logger;\n }\n return LicenseClient.instance;\n }\n\n /**\n * Reset the singleton so the next getInstance() re-reads env vars.\n * Intended for tests.\n */\n public static resetInstance(): void {\n if (LicenseClient.instance?.revalidationTimeout) {\n clearTimeout(LicenseClient.instance.revalidationTimeout);\n }\n LicenseClient.instance = undefined;\n }\n\n private readonly REQUEST_TIMEOUT_MS = 10_000;\n\n private async fetchWithRetry(url: string, options: RequestInit, retries: number = 3): Promise<Response> {\n for (let i = 0; i < retries; i++) {\n // Bound each attempt so a stalled socket can't hang the in-flight\n // validation promise that all concurrent callers share.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.REQUEST_TIMEOUT_MS);\n timer.unref?.();\n try {\n const signal = options.signal\n ? AbortSignal.any([options.signal as AbortSignal, controller.signal])\n : controller.signal;\n const response = await fetch(url, { ...options, signal });\n if (response.status === 429 || response.status >= 500) {\n if (i === retries - 1) return response;\n } else {\n return response;\n }\n } catch (error) {\n if (i === retries - 1) throw error;\n } finally {\n clearTimeout(timer);\n }\n const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n throw new Error('Unreachable');\n }\n\n private validationPromise: Promise<boolean> | null = null;\n\n public async validate(): Promise<boolean> {\n if (this.mode === 'open-source') {\n return true;\n }\n\n // Check if cache is valid\n if (this.cachedResult && Date.now() < this.cacheExpiry) {\n return true;\n }\n\n return this.revalidate();\n }\n\n /**\n * Contact the server regardless of cache freshness, coalescing concurrent\n * callers (e.g. the Mastra constructor and the auth/ee helpers both kicking\n * off validation at startup) into a single in-flight request so the server\n * is contacted — and the outcome logged — only once. Used directly by the\n * background revalidation timer, which must bypass the cache check.\n */\n private revalidate(): Promise<boolean> {\n if (!this.validationPromise) {\n this.validationPromise = this.performValidation().finally(() => {\n this.validationPromise = null;\n });\n }\n return this.validationPromise;\n }\n\n private async performValidation(): Promise<boolean> {\n const now = Date.now();\n\n // Attempt to validate against server\n try {\n if (!this.licenseUrl?.startsWith('https://') && !this.licenseUrl?.includes('localhost')) {\n this.logger?.warn('License URL is not HTTPS. Proceeding, but this is insecure.');\n }\n\n const response = await this.fetchWithRetry(`${this.licenseUrl}/validate`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n },\n body: JSON.stringify({ licenseKey: this.licenseKey }),\n });\n\n // A 429 or 5xx that survived the retries is a transient server\n // condition, not a verdict on the license — treat it like an\n // unreachable server so the lease/grace semantics below apply\n // instead of invalidating the key.\n if (response.status === 429 || response.status >= 500) {\n throw new Error(`License server responded with ${response.status}`);\n }\n\n const data = (await response.json()) as LicenseValidationResponse;\n\n if (data.valid) {\n this.status = 'valid';\n this.logger?.info(\n `License validated: ${data.planTier} tier${data.expiresAt ? `, expires ${data.expiresAt.slice(0, 10)}` : ''}`,\n );\n this.cachedResult = data;\n\n const ttlSeconds = data.leaseTtlSeconds || this.DEFAULT_TTL_MS / 1000;\n this.cacheExpiry = now + ttlSeconds * 1000;\n this.gracePeriodEnd = now + this.GRACE_PERIOD_MS;\n\n this.scheduleRevalidation(ttlSeconds);\n return true;\n } else if (data.code === 'RATE_LIMITED') {\n // Defensive: a throttle marker in the body without a 429 status is\n // still transient, not a license verdict.\n throw new Error(`License server rate limited: ${data.reason}`);\n } else {\n this.status = 'invalid';\n this.logger?.error(`License validation failed: ${data.code} - ${data.reason}`);\n this.clearCache();\n return false;\n }\n } catch {\n // Network error or server unreachable\n if (this.cachedResult && now < this.gracePeriodEnd) {\n this.logger?.warn('License server unreachable. Using cached license (within grace period).');\n this.status = 'valid';\n this.scheduleRevalidation(this.DEFAULT_TTL_MS / 1000); // Retry later\n return true;\n } else if (this.cachedResult) {\n this.logger?.error('License server unreachable and grace period expired. Disabling enterprise features.');\n this.status = 'invalid';\n this.clearCache();\n return false;\n } else {\n // First call failed\n this.logger?.warn('License server unreachable on startup. Failing open (allowing features) and will retry.');\n\n // Mock a success to fail open, but set a short TTL to force quick retry\n this.status = 'valid';\n this.cachedResult = {\n valid: true,\n entitlements: [],\n planTier: 'unknown',\n expiresAt: null,\n leaseTtlSeconds: 300, // 5 minutes\n };\n this.cacheExpiry = now + 300 * 1000;\n this.gracePeriodEnd = now + this.GRACE_PERIOD_MS;\n this.scheduleRevalidation(300);\n return true;\n }\n }\n }\n\n private scheduleRevalidation(ttlSeconds: number) {\n if (this.revalidationTimeout) {\n clearTimeout(this.revalidationTimeout);\n }\n\n // Revalidate at 75% of TTL\n const revalidateMs = ttlSeconds * 1000 * 0.75;\n this.revalidationTimeout = setTimeout(() => {\n this.logger?.info('Performing background license revalidation...');\n // revalidate(), not validate(): at 75% of TTL the cache is still fresh,\n // so validate()'s early return would skip the refresh entirely.\n this.revalidate().catch(err => {\n this.logger?.error('Background license revalidation failed', err);\n });\n }, revalidateMs);\n\n // Ensure the timeout doesn't keep the Node process alive\n this.revalidationTimeout.unref();\n }\n\n private clearCache() {\n this.cachedResult = null;\n this.cacheExpiry = 0;\n this.gracePeriodEnd = 0;\n this.status = 'invalid';\n if (this.revalidationTimeout) {\n clearTimeout(this.revalidationTimeout);\n this.revalidationTimeout = null;\n }\n }\n\n public hasFeature(featureName: string): boolean {\n if (this.mode === 'open-source') return true;\n if (this.status === 'pending') return true;\n if (this.status === 'invalid') return false;\n if (!this.cachedResult) return false;\n\n // While failing open (server unreachable on startup) the entitlements\n // list is empty but the unknown planTier marks the result as tentative.\n if (this.cachedResult.planTier === 'unknown') return true;\n\n return this.cachedResult.entitlements.includes(featureName);\n }\n\n public getEntitlements(): string[] | null {\n if (this.mode === 'open-source') return null;\n return this.cachedResult?.entitlements || null;\n }\n\n public getSnapshot(): LicenseSnapshot {\n return {\n mode: this.mode,\n status: this.status,\n entitlements: this.cachedResult?.entitlements ?? null,\n planTier: this.cachedResult?.planTier ?? null,\n expiresAt: this.cachedResult?.expiresAt ?? null,\n };\n }\n}\n"]}