import type { LicenseOptions, License, LicenseResult, LicenseStatus, CheckoutResult, ProductTier, Feature, LicenseChangeCallback, } from './types'; import { Transport } from './transport'; import { LicenseCache, getDefaultStorage } from './storage'; /** * Default configuration options. */ const DEFAULT_OPTIONS: Partial = { apiBaseUrl: 'https://api.ironlicensing.com', debug: false, enableOfflineCache: true, cacheValidationMinutes: 60, offlineGraceDays: 7, storageKeyPrefix: 'ironlicensing', }; /** * IronLicensing client for license validation and activation. */ export class LicenseClient { private options: Required; private transport: Transport; private cache: LicenseCache; private currentLicense: License | null = null; private onLicenseChangeCallbacks: LicenseChangeCallback[] = []; constructor(options: LicenseOptions) { this.options = { ...DEFAULT_OPTIONS, ...options } as Required; if (!this.options.publicKey) { throw new Error('Public key is required'); } if (!this.options.productSlug) { throw new Error('Product slug is required'); } this.transport = new Transport( this.options.apiBaseUrl, this.options.publicKey, this.options.productSlug, this.options.debug, ); this.cache = new LicenseCache(getDefaultStorage(), this.options.storageKeyPrefix); // Load cached license this.currentLicense = this.cache.getLicense(); if (this.options.debug) { console.log('[IronLicensing] Client initialized'); } } /** * Validates the current or provided license key. */ async validate(licenseKey?: string): Promise { const key = licenseKey || this.cache.getLicenseKey(); if (!key) { return { valid: false, error: 'No license key provided' }; } // Check cache first if (this.options.enableOfflineCache) { const cached = this.getCachedValidation(); if (cached) { return { ...cached, cached: true }; } } // Validate online const result = await this.transport.validate(key); if (result.valid && result.license) { this.setLicense(result.license, key); this.cache.setValidationResult(result, new Date()); } return result; } /** * Activates a license. */ async activate(licenseKey: string, machineName?: string): Promise { const result = await this.transport.activate(licenseKey, machineName); if (result.valid && result.license) { this.setLicense(result.license, licenseKey); this.cache.setValidationResult(result, new Date()); } return result; } /** * Deactivates the current license. */ async deactivate(): Promise { const key = this.cache.getLicenseKey(); if (!key) return false; const success = await this.transport.deactivate(key); if (success) { this.setLicense(null, null); this.cache.clear(); } return success; } /** * Starts a trial. */ async startTrial(email: string): Promise { const result = await this.transport.startTrial(email); if (result.valid && result.license) { this.setLicense(result.license, result.license.key); this.cache.setValidationResult(result, new Date()); } return result; } /** * Checks if a feature is enabled. */ hasFeature(featureKey: string): boolean { if (!this.currentLicense) return false; const feature = this.currentLicense.features.find(f => f.key === featureKey); return feature?.enabled ?? false; } /** * Requires a feature, throwing if not available. */ requireFeature(featureKey: string): void { if (!this.hasFeature(featureKey)) { throw new LicenseRequiredError(`Feature '${featureKey}' requires a valid license`); } } /** * Gets a feature by key. */ getFeature(featureKey: string): Feature | undefined { return this.currentLicense?.features.find(f => f.key === featureKey); } /** * Gets all features. */ getFeatures(): Feature[] { return this.currentLicense?.features || []; } /** * Gets the current license. */ get license(): License | null { return this.currentLicense; } /** * Gets the current license status. */ get status(): LicenseStatus { return this.currentLicense?.status ?? 'not_activated'; } /** * Checks if the license is valid. */ get isLicensed(): boolean { return this.currentLicense?.status === 'valid' || this.currentLicense?.status === 'trial'; } /** * Checks if the license is a trial. */ get isTrial(): boolean { return this.currentLicense?.status === 'trial' || this.currentLicense?.type === 'trial'; } /** * Gets the license expiration date. */ get expiresAt(): Date | undefined { return this.currentLicense?.expiresAt; } /** * Gets product tiers for purchase. */ async getTiers(): Promise { return this.transport.getTiers(); } /** * Starts a purchase checkout. */ async startPurchase(tierId: string, email: string): Promise { return this.transport.startCheckout(tierId, email); } /** * Registers a callback for license changes. */ onLicenseChange(callback: LicenseChangeCallback): () => void { this.onLicenseChangeCallbacks.push(callback); return () => { const index = this.onLicenseChangeCallbacks.indexOf(callback); if (index > -1) { this.onLicenseChangeCallbacks.splice(index, 1); } }; } private setLicense(license: License | null, key: string | null): void { this.currentLicense = license; if (license) { this.cache.setLicense(license); } if (key) { this.cache.setLicenseKey(key); } // Notify listeners for (const callback of this.onLicenseChangeCallbacks) { try { callback(license); } catch (e) { console.error('[IronLicensing] Error in license change callback:', e); } } } private getCachedValidation(): LicenseResult | null { const cached = this.cache.getValidationResult(); const time = this.cache.getValidationTime(); if (!cached || !time) return null; // Check if cache is still valid const cacheAge = Date.now() - time.getTime(); const maxAge = this.options.cacheValidationMinutes * 60 * 1000; if (cacheAge <= maxAge) { return cached; } // Check offline grace period const graceAge = this.options.offlineGraceDays * 24 * 60 * 60 * 1000; if (cacheAge <= graceAge && cached.valid) { if (this.options.debug) { console.log('[IronLicensing] Using offline grace period'); } return cached; } return null; } } /** * Error thrown when a license or feature is required but not available. */ export class LicenseRequiredError extends Error { constructor(message: string) { super(message); this.name = 'LicenseRequiredError'; } }