@mastra/core
Version:
209 lines (207 loc) • 7.25 kB
JavaScript
'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