ironlicensing-js/src/client.ts

287 lines
6.9 KiB
TypeScript

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<LicenseOptions> = {
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<LicenseOptions>;
private transport: Transport;
private cache: LicenseCache;
private currentLicense: License | null = null;
private onLicenseChangeCallbacks: LicenseChangeCallback[] = [];
constructor(options: LicenseOptions) {
this.options = { ...DEFAULT_OPTIONS, ...options } as Required<LicenseOptions>;
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<LicenseResult> {
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<LicenseResult> {
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<boolean> {
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<LicenseResult> {
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<ProductTier[]> {
return this.transport.getTiers();
}
/**
* Starts a purchase checkout.
*/
async startPurchase(tierId: string, email: string): Promise<CheckoutResult> {
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';
}
}