UNPKG

@mastra/core

Version:
209 lines (207 loc) 7.25 kB
'use strict'; // src/license/index.ts var LicenseClient = class _LicenseClient { static instance; logger; licenseKey; licenseUrl; mode = "open-source"; status = "pending"; cachedResult = null; cacheExpiry = 0; gracePeriodEnd = 0; revalidationTimeout = null; GRACE_PERIOD_MS = 72 * 60 * 60 * 1e3; // 72 hours DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3; // 24 hours constructor(logger) { this.logger = logger; this.licenseKey = process.env.MASTRA_LICENSE_KEY || process.env.MASTRA_EE_LICENSE; this.licenseUrl = process.env.MASTRA_LICENSE_URL || "https://license.mastra.ai"; if (this.licenseKey) { this.mode = "enterprise"; } else { this.mode = "open-source"; } } static getInstance(logger) { if (!_LicenseClient.instance) { _LicenseClient.instance = new _LicenseClient(logger); } else if (logger) { _LicenseClient.instance.logger = logger; } return _LicenseClient.instance; } /** * Reset the singleton so the next getInstance() re-reads env vars. * Intended for tests. */ static resetInstance() { if (_LicenseClient.instance?.revalidationTimeout) { clearTimeout(_LicenseClient.instance.revalidationTimeout); } _LicenseClient.instance = void 0; } REQUEST_TIMEOUT_MS = 1e4; async fetchWithRetry(url, options, retries = 3) { for (let i = 0; i < retries; i++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.REQUEST_TIMEOUT_MS); timer.unref?.(); try { const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal; const response = await fetch(url, { ...options, signal }); if (response.status === 429 || response.status >= 500) { if (i === retries - 1) return response; } else { return response; } } catch (error) { if (i === retries - 1) throw error; } finally { clearTimeout(timer); } const delay = Math.pow(2, i) * 1e3; await new Promise((resolve) => setTimeout(resolve, delay)); } throw new Error("Unreachable"); } validationPromise = null; async validate() { if (this.mode === "open-source") { return true; } if (this.cachedResult && Date.now() < this.cacheExpiry) { return true; } return this.revalidate(); } /** * Contact the server regardless of cache freshness, coalescing concurrent * callers (e.g. the Mastra constructor and the auth/ee helpers both kicking * off validation at startup) into a single in-flight request so the server * is contacted — and the outcome logged — only once. Used directly by the * background revalidation timer, which must bypass the cache check. */ revalidate() { if (!this.validationPromise) { this.validationPromise = this.performValidation().finally(() => { this.validationPromise = null; }); } return this.validationPromise; } async performValidation() { const now = Date.now(); try { if (!this.licenseUrl?.startsWith("https://") && !this.licenseUrl?.includes("localhost")) { this.logger?.warn("License URL is not HTTPS. Proceeding, but this is insecure."); } const response = await this.fetchWithRetry(`${this.licenseUrl}/validate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ licenseKey: this.licenseKey }) }); if (response.status === 429 || response.status >= 500) { throw new Error(`License server responded with ${response.status}`); } const data = await response.json(); if (data.valid) { this.status = "valid"; this.logger?.info( `License validated: ${data.planTier} tier${data.expiresAt ? `, expires ${data.expiresAt.slice(0, 10)}` : ""}` ); this.cachedResult = data; const ttlSeconds = data.leaseTtlSeconds || this.DEFAULT_TTL_MS / 1e3; this.cacheExpiry = now + ttlSeconds * 1e3; this.gracePeriodEnd = now + this.GRACE_PERIOD_MS; this.scheduleRevalidation(ttlSeconds); return true; } else if (data.code === "RATE_LIMITED") { throw new Error(`License server rate limited: ${data.reason}`); } else { this.status = "invalid"; this.logger?.error(`License validation failed: ${data.code} - ${data.reason}`); this.clearCache(); return false; } } catch { if (this.cachedResult && now < this.gracePeriodEnd) { this.logger?.warn("License server unreachable. Using cached license (within grace period)."); this.status = "valid"; this.scheduleRevalidation(this.DEFAULT_TTL_MS / 1e3); return true; } else if (this.cachedResult) { this.logger?.error("License server unreachable and grace period expired. Disabling enterprise features."); this.status = "invalid"; this.clearCache(); return false; } else { this.logger?.warn("License server unreachable on startup. Failing open (allowing features) and will retry."); this.status = "valid"; this.cachedResult = { valid: true, entitlements: [], planTier: "unknown", expiresAt: null, leaseTtlSeconds: 300 // 5 minutes }; this.cacheExpiry = now + 300 * 1e3; this.gracePeriodEnd = now + this.GRACE_PERIOD_MS; this.scheduleRevalidation(300); return true; } } } scheduleRevalidation(ttlSeconds) { if (this.revalidationTimeout) { clearTimeout(this.revalidationTimeout); } const revalidateMs = ttlSeconds * 1e3 * 0.75; this.revalidationTimeout = setTimeout(() => { this.logger?.info("Performing background license revalidation..."); this.revalidate().catch((err) => { this.logger?.error("Background license revalidation failed", err); }); }, revalidateMs); this.revalidationTimeout.unref(); } clearCache() { this.cachedResult = null; this.cacheExpiry = 0; this.gracePeriodEnd = 0; this.status = "invalid"; if (this.revalidationTimeout) { clearTimeout(this.revalidationTimeout); this.revalidationTimeout = null; } } hasFeature(featureName) { if (this.mode === "open-source") return true; if (this.status === "pending") return true; if (this.status === "invalid") return false; if (!this.cachedResult) return false; if (this.cachedResult.planTier === "unknown") return true; return this.cachedResult.entitlements.includes(featureName); } getEntitlements() { if (this.mode === "open-source") return null; return this.cachedResult?.entitlements || null; } getSnapshot() { return { mode: this.mode, status: this.status, entitlements: this.cachedResult?.entitlements ?? null, planTier: this.cachedResult?.planTier ?? null, expiresAt: this.cachedResult?.expiresAt ?? null }; } }; exports.LicenseClient = LicenseClient; //# sourceMappingURL=chunk-6LWJF7BY.cjs.map //# sourceMappingURL=chunk-6LWJF7BY.cjs.map