UNPKG

@mastra/core

Version:
1 lines 13.9 kB
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/license/index.ts"],"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(`License validated${data.expiresAt ? `, expires ${data.expiresAt.slice(0, 10)}` : ''}`);\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"],"mappings":"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"}