287 lines
6.9 KiB
TypeScript
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';
|
|
}
|
|
}
|