UNPKG

@mastra/core

Version:
181 lines (180 loc) 6.54 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region 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 = 4320 * 60 * 1e3; DEFAULT_TTL_MS = 1440 * 60 * 1e3; 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.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 }; 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 * .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 }; } }; //#endregion exports.LicenseClient = LicenseClient; //# sourceMappingURL=index.cjs.map