{"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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"}