diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a32d37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.log +.DS_Store +.env +.env.local +*.tgz +.ironlicensing/ diff --git a/README.md b/README.md index 047854e..91b556f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,324 @@ -# ironlicensing-js -IronLicensing SDK for JavaScript/TypeScript - Software licensing and activation +# IronLicensing SDK for JavaScript/TypeScript + +Software licensing and activation SDK for JavaScript/TypeScript applications. Validate licenses, manage activations, check features, and handle trials. + +[![npm](https://img.shields.io/npm/v/@ironservices/licensing.svg)](https://www.npmjs.com/package/@ironservices/licensing) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +## Installation + +```bash +npm install @ironservices/licensing +``` + +## Quick Start + +### Validate a License + +```typescript +import { LicenseClient } from '@ironservices/licensing'; + +const client = new LicenseClient({ + publicKey: 'pk_live_xxxxx', + productSlug: 'my-app', +}); + +// Validate a license key +const result = await client.validate('IRON-XXXX-XXXX-XXXX-XXXX'); + +if (result.valid) { + console.log('License is valid!'); + console.log('Expires:', result.license?.expiresAt); +} +``` + +### Activate a License + +```typescript +import { LicenseClient } from '@ironservices/licensing'; + +const client = new LicenseClient({ + publicKey: 'pk_live_xxxxx', + productSlug: 'my-app', +}); + +// Activate on this machine +const result = await client.activate('IRON-XXXX-XXXX-XXXX-XXXX'); + +if (result.valid) { + console.log('License activated!'); + console.log('Activations:', result.activations?.length); +} +``` + +### Check Features + +```typescript +import { LicenseClient, LicenseRequiredError } from '@ironservices/licensing'; + +const client = new LicenseClient({ + publicKey: 'pk_live_xxxxx', + productSlug: 'my-app', +}); + +await client.validate('IRON-XXXX-XXXX-XXXX-XXXX'); + +// Check if feature is enabled +if (client.hasFeature('premium')) { + // Enable premium features +} + +// Or require a feature (throws if not available) +try { + client.requireFeature('enterprise'); + // Enterprise code here +} catch (e) { + if (e instanceof LicenseRequiredError) { + console.log('Enterprise license required'); + } +} +``` + +### Using the Global Client + +```typescript +import * as licensing from '@ironservices/licensing'; + +// Initialize once +licensing.init({ + publicKey: 'pk_live_xxxxx', + productSlug: 'my-app', +}); + +// Use anywhere +await licensing.validate('IRON-XXXX-XXXX-XXXX-XXXX'); + +if (licensing.hasFeature('premium')) { + // Premium features +} + +console.log('Status:', licensing.getStatus()); +console.log('Licensed:', licensing.isLicensed()); +``` + +## Configuration + +```typescript +import { LicenseClient } from '@ironservices/licensing'; + +const client = new LicenseClient({ + publicKey: 'pk_live_xxxxx', // Required + productSlug: 'my-app', // Required + apiBaseUrl: 'https://api.ironlicensing.com', + debug: false, + enableOfflineCache: true, + cacheValidationMinutes: 60, + offlineGraceDays: 7, + storageKeyPrefix: 'ironlicensing', +}); +``` + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `publicKey` | string | required | Your public key (pk_live_xxx or pk_test_xxx) | +| `productSlug` | string | required | Your product slug | +| `apiBaseUrl` | string | https://api.ironlicensing.com | API base URL | +| `debug` | boolean | false | Enable debug logging | +| `enableOfflineCache` | boolean | true | Cache validation results | +| `cacheValidationMinutes` | number | 60 | Cache validity in minutes | +| `offlineGraceDays` | number | 7 | Offline grace period in days | +| `storageKeyPrefix` | string | 'ironlicensing' | Storage key prefix | + +## License Status + +```typescript +type LicenseStatus = + | 'valid' // License is valid + | 'expired' // License has expired + | 'suspended' // License is suspended + | 'revoked' // License is revoked + | 'invalid' // License key is invalid + | 'trial' // Trial license + | 'trial_expired' // Trial has expired + | 'not_activated' // No license activated + | 'unknown'; // Unknown status +``` + +## Features + +### Check Features + +```typescript +// Check if feature is enabled +if (client.hasFeature('premium')) { + enablePremiumFeatures(); +} + +// Get feature details +const feature = client.getFeature('max-users'); +if (feature?.enabled) { + console.log('Max users:', feature.metadata?.limit); +} + +// Get all features +const features = client.getFeatures(); +features.forEach(f => { + console.log(`${f.name}: ${f.enabled ? 'enabled' : 'disabled'}`); +}); +``` + +### Require Features + +```typescript +import { LicenseRequiredError } from '@ironservices/licensing'; + +try { + client.requireFeature('enterprise'); + // This code only runs if feature is available +} catch (e) { + if (e instanceof LicenseRequiredError) { + showUpgradeDialog(); + } +} +``` + +## Trial Management + +```typescript +// Start a trial +const result = await client.startTrial('user@example.com'); + +if (result.valid) { + console.log('Trial started!'); + console.log('Expires:', result.license?.expiresAt); +} + +// Check if trial +if (client.isTrial) { + showTrialBanner(client.expiresAt); +} +``` + +## Purchase Flow + +```typescript +// Get available tiers +const tiers = await client.getTiers(); + +tiers.forEach(tier => { + console.log(`${tier.name}: ${tier.price} ${tier.currency}/${tier.billingPeriod}`); +}); + +// Start checkout +const checkout = await client.startPurchase('tier-pro', 'user@example.com'); + +if (checkout.success) { + // Redirect to checkout URL + window.location.href = checkout.checkoutUrl!; +} +``` + +## Deactivation + +```typescript +// Deactivate on this machine +const success = await client.deactivate(); + +if (success) { + console.log('License deactivated'); +} +``` + +## License Change Events + +```typescript +// Listen for license changes +const unsubscribe = client.onLicenseChange((license) => { + if (license) { + console.log('License updated:', license.status); + } else { + console.log('License removed'); + } +}); + +// Later: unsubscribe +unsubscribe(); +``` + +## Offline Support + +The SDK automatically caches validation results and supports offline usage: + +```typescript +const client = new LicenseClient({ + publicKey: 'pk_live_xxxxx', + productSlug: 'my-app', + enableOfflineCache: true, + cacheValidationMinutes: 60, // Use cache for 60 minutes + offlineGraceDays: 7, // Allow 7 days offline +}); + +// This will use cache if available +const result = await client.validate(); + +if (result.cached) { + console.log('Using cached validation'); +} +``` + +## Custom Storage + +```typescript +import { LicenseClient, StorageAdapter, LicenseCache } from '@ironservices/licensing'; + +// Implement custom storage +class MyStorage implements StorageAdapter { + get(key: string): string | null { + return myDatabase.get(key); + } + + set(key: string, value: string): void { + myDatabase.set(key, value); + } + + remove(key: string): void { + myDatabase.delete(key); + } +} +``` + +## TypeScript Support + +Full TypeScript support with exported types: + +```typescript +import type { + LicenseOptions, + License, + LicenseResult, + LicenseStatus, + LicenseType, + Feature, + Activation, + CheckoutResult, + ProductTier, +} from '@ironservices/licensing'; +``` + +## Browser & Node.js + +The SDK works in both browser and Node.js environments: + +- **Browser**: Uses `localStorage` for caching +- **Node.js**: Uses in-memory storage by default + +## Links + +- [Documentation](https://www.ironlicensing.com/docs) +- [Dashboard](https://www.ironlicensing.com) + +## License + +MIT License - see [LICENSE](LICENSE) for details. diff --git a/package.json b/package.json new file mode 100644 index 0000000..c4cf818 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "@ironservices/licensing", + "version": "1.0.0", + "description": "Software licensing and activation SDK for JavaScript/TypeScript", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup src/index.ts --format cjs,esm --dts --clean", + "dev": "tsup src/index.ts --format cjs,esm --dts --watch", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "licensing", + "activation", + "license-key", + "software-licensing", + "feature-flags", + "trial", + "subscription" + ], + "author": "IronServices", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/IronServices/ironlicensing-js.git" + }, + "homepage": "https://www.ironlicensing.com", + "devDependencies": { + "tsup": "^8.0.0", + "typescript": "^5.3.0" + } +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..f1932d7 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,286 @@ +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'; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8f13fb8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,171 @@ +/** + * IronLicensing SDK for JavaScript/TypeScript + * + * Software licensing and activation SDK. + * + * @example + * ```typescript + * import { LicenseClient } from '@ironservices/licensing'; + * + * const client = new LicenseClient({ + * publicKey: 'pk_live_xxxxx', + * productSlug: 'my-app', + * }); + * + * // Validate license + * const result = await client.validate('IRON-XXXX-XXXX-XXXX-XXXX'); + * if (result.valid) { + * console.log('License is valid!'); + * } + * + * // Check features + * if (client.hasFeature('premium')) { + * // Enable premium features + * } + * ``` + */ + +export { LicenseClient, LicenseRequiredError } from './client'; + +export type { + LicenseOptions, + License, + LicenseResult, + LicenseStatus, + LicenseType, + Feature, + Activation, + CheckoutResult, + ProductTier, + LicenseChangeCallback, +} from './types'; + +export { + LicenseCache, + StorageAdapter, + MemoryStorage, + LocalStorageAdapter, + getDefaultStorage, +} from './storage'; + +// Global client for convenience +import { LicenseClient } from './client'; +import type { LicenseOptions, LicenseResult, CheckoutResult, ProductTier, Feature, License, LicenseStatus } from './types'; + +let globalClient: LicenseClient | null = null; + +/** + * Initializes the global license client. + */ +export function init(options: LicenseOptions): LicenseClient { + globalClient = new LicenseClient(options); + return globalClient; +} + +/** + * Gets the global license client. + */ +export function getClient(): LicenseClient { + if (!globalClient) { + throw new Error('IronLicensing not initialized. Call init() first.'); + } + return globalClient; +} + +/** + * Validates the current or provided license key using the global client. + */ +export async function validate(licenseKey?: string): Promise { + return getClient().validate(licenseKey); +} + +/** + * Activates a license using the global client. + */ +export async function activate(licenseKey: string, machineName?: string): Promise { + return getClient().activate(licenseKey, machineName); +} + +/** + * Deactivates the current license using the global client. + */ +export async function deactivate(): Promise { + return getClient().deactivate(); +} + +/** + * Starts a trial using the global client. + */ +export async function startTrial(email: string): Promise { + return getClient().startTrial(email); +} + +/** + * Checks if a feature is enabled using the global client. + */ +export function hasFeature(featureKey: string): boolean { + return getClient().hasFeature(featureKey); +} + +/** + * Requires a feature using the global client. + */ +export function requireFeature(featureKey: string): void { + getClient().requireFeature(featureKey); +} + +/** + * Gets a feature by key using the global client. + */ +export function getFeature(featureKey: string): Feature | undefined { + return getClient().getFeature(featureKey); +} + +/** + * Gets all features using the global client. + */ +export function getFeatures(): Feature[] { + return getClient().getFeatures(); +} + +/** + * Gets the current license using the global client. + */ +export function getLicense(): License | null { + return getClient().license; +} + +/** + * Gets the license status using the global client. + */ +export function getStatus(): LicenseStatus { + return getClient().status; +} + +/** + * Checks if the license is valid using the global client. + */ +export function isLicensed(): boolean { + return getClient().isLicensed; +} + +/** + * Checks if the license is a trial using the global client. + */ +export function isTrial(): boolean { + return getClient().isTrial; +} + +/** + * Gets product tiers using the global client. + */ +export async function getTiers(): Promise { + return getClient().getTiers(); +} + +/** + * Starts a purchase using the global client. + */ +export async function startPurchase(tierId: string, email: string): Promise { + return getClient().startPurchase(tierId, email); +} diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..ad32e49 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,172 @@ +import type { License, LicenseResult } from './types'; + +/** + * Storage interface for license caching. + */ +export interface StorageAdapter { + get(key: string): string | null; + set(key: string, value: string): void; + remove(key: string): void; +} + +/** + * In-memory storage adapter. + */ +export class MemoryStorage implements StorageAdapter { + private data: Map = new Map(); + + get(key: string): string | null { + return this.data.get(key) ?? null; + } + + set(key: string, value: string): void { + this.data.set(key, value); + } + + remove(key: string): void { + this.data.delete(key); + } +} + +/** + * LocalStorage adapter for browser. + */ +export class LocalStorageAdapter implements StorageAdapter { + get(key: string): string | null { + try { + return localStorage.getItem(key); + } catch { + return null; + } + } + + set(key: string, value: string): void { + try { + localStorage.setItem(key, value); + } catch { + // Ignore storage errors + } + } + + remove(key: string): void { + try { + localStorage.removeItem(key); + } catch { + // Ignore storage errors + } + } +} + +/** + * License cache manager. + */ +export class LicenseCache { + private storage: StorageAdapter; + private keyPrefix: string; + + constructor(storage: StorageAdapter, keyPrefix: string = 'ironlicensing') { + this.storage = storage; + this.keyPrefix = keyPrefix; + } + + private key(name: string): string { + return `${this.keyPrefix}:${name}`; + } + + /** + * Gets the cached license. + */ + getLicense(): License | null { + const data = this.storage.get(this.key('license')); + if (!data) return null; + + try { + const parsed = JSON.parse(data); + // Restore Date objects + if (parsed.expiresAt) parsed.expiresAt = new Date(parsed.expiresAt); + if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt); + if (parsed.lastValidatedAt) parsed.lastValidatedAt = new Date(parsed.lastValidatedAt); + return parsed; + } catch { + return null; + } + } + + /** + * Sets the cached license. + */ + setLicense(license: License): void { + this.storage.set(this.key('license'), JSON.stringify(license)); + } + + /** + * Gets the cached validation result. + */ + getValidationResult(): LicenseResult | null { + const data = this.storage.get(this.key('validation')); + if (!data) return null; + + try { + const parsed = JSON.parse(data); + if (parsed.license) { + if (parsed.license.expiresAt) parsed.license.expiresAt = new Date(parsed.license.expiresAt); + if (parsed.license.createdAt) parsed.license.createdAt = new Date(parsed.license.createdAt); + if (parsed.license.lastValidatedAt) parsed.license.lastValidatedAt = new Date(parsed.license.lastValidatedAt); + } + return parsed; + } catch { + return null; + } + } + + /** + * Sets the cached validation result. + */ + setValidationResult(result: LicenseResult, timestamp: Date): void { + this.storage.set(this.key('validation'), JSON.stringify(result)); + this.storage.set(this.key('validation_time'), timestamp.toISOString()); + } + + /** + * Gets the validation timestamp. + */ + getValidationTime(): Date | null { + const data = this.storage.get(this.key('validation_time')); + if (!data) return null; + return new Date(data); + } + + /** + * Gets the stored license key. + */ + getLicenseKey(): string | null { + return this.storage.get(this.key('license_key')); + } + + /** + * Sets the license key. + */ + setLicenseKey(key: string): void { + this.storage.set(this.key('license_key'), key); + } + + /** + * Clears all cached data. + */ + clear(): void { + this.storage.remove(this.key('license')); + this.storage.remove(this.key('validation')); + this.storage.remove(this.key('validation_time')); + this.storage.remove(this.key('license_key')); + } +} + +/** + * Gets the default storage adapter based on environment. + */ +export function getDefaultStorage(): StorageAdapter { + if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') { + return new LocalStorageAdapter(); + } + return new MemoryStorage(); +} diff --git a/src/transport.ts b/src/transport.ts new file mode 100644 index 0000000..5ec130d --- /dev/null +++ b/src/transport.ts @@ -0,0 +1,233 @@ +import type { License, LicenseResult, CheckoutResult, ProductTier, Activation } from './types'; + +/** + * HTTP transport for IronLicensing API. + */ +export class Transport { + private baseUrl: string; + private publicKey: string; + private productSlug: string; + private debug: boolean; + + constructor(baseUrl: string, publicKey: string, productSlug: string, debug: boolean) { + this.baseUrl = baseUrl; + this.publicKey = publicKey; + this.productSlug = productSlug; + this.debug = debug; + } + + private log(message: string): void { + if (this.debug) { + console.log(`[IronLicensing] ${message}`); + } + } + + private async request( + method: string, + path: string, + body?: unknown, + ): Promise { + const url = `${this.baseUrl}${path}`; + + this.log(`${method} ${path}`); + + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'X-Public-Key': this.publicKey, + 'X-Product-Slug': this.productSlug, + }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: `HTTP ${response.status}` })); + throw new Error(error.error || `HTTP ${response.status}`); + } + + return response.json(); + } + + private parseLicense(data: any): License { + return { + ...data, + expiresAt: data.expiresAt ? new Date(data.expiresAt) : undefined, + createdAt: new Date(data.createdAt), + lastValidatedAt: data.lastValidatedAt ? new Date(data.lastValidatedAt) : undefined, + }; + } + + private parseActivation(data: any): Activation { + return { + ...data, + activatedAt: new Date(data.activatedAt), + lastSeenAt: new Date(data.lastSeenAt), + }; + } + + /** + * Validates a license key. + */ + async validate(licenseKey: string): Promise { + try { + const result = await this.request('POST', '/api/v1/validate', { + licenseKey, + machineId: this.getMachineId(), + }); + + return { + valid: result.valid, + license: result.license ? this.parseLicense(result.license) : undefined, + activations: result.activations?.map((a: any) => this.parseActivation(a)), + cached: false, + }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : 'Validation failed', + }; + } + } + + /** + * Activates a license. + */ + async activate(licenseKey: string, machineName?: string): Promise { + try { + const result = await this.request('POST', '/api/v1/activate', { + licenseKey, + machineId: this.getMachineId(), + machineName: machineName || this.getMachineName(), + platform: this.getPlatform(), + }); + + return { + valid: result.valid, + license: result.license ? this.parseLicense(result.license) : undefined, + activations: result.activations?.map((a: any) => this.parseActivation(a)), + }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : 'Activation failed', + }; + } + } + + /** + * Deactivates a license. + */ + async deactivate(licenseKey: string): Promise { + try { + await this.request('POST', '/api/v1/deactivate', { + licenseKey, + machineId: this.getMachineId(), + }); + return true; + } catch { + return false; + } + } + + /** + * Starts a trial. + */ + async startTrial(email: string): Promise { + try { + const result = await this.request('POST', '/api/v1/trial', { + email, + machineId: this.getMachineId(), + }); + + return { + valid: result.valid, + license: result.license ? this.parseLicense(result.license) : undefined, + }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : 'Failed to start trial', + }; + } + } + + /** + * Gets product tiers. + */ + async getTiers(): Promise { + try { + const result = await this.request<{ tiers: ProductTier[] }>('GET', '/api/v1/tiers'); + return result.tiers || []; + } catch { + return []; + } + } + + /** + * Starts a checkout session. + */ + async startCheckout(tierId: string, email: string): Promise { + try { + const result = await this.request('POST', '/api/v1/checkout', { + tierId, + email, + }); + + return result; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Checkout failed', + }; + } + } + + /** + * Checks if the API is online. + */ + async isOnline(): Promise { + try { + const response = await fetch(`${this.baseUrl}/health`); + return response.ok; + } catch { + return false; + } + } + + private getMachineId(): string { + // Try to get a persistent machine ID + if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') { + let id = localStorage.getItem('ironlicensing:machine_id'); + if (!id) { + id = this.generateId(); + localStorage.setItem('ironlicensing:machine_id', id); + } + return id; + } + // For non-browser environments, generate a random ID + return this.generateId(); + } + + private getMachineName(): string { + if (typeof navigator !== 'undefined') { + return navigator.userAgent.substring(0, 100); + } + return 'Unknown'; + } + + private getPlatform(): string { + if (typeof navigator !== 'undefined') { + return navigator.platform || 'web'; + } + return 'unknown'; + } + + private generateId(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..4c831c0 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,123 @@ +/** + * License status. + */ +export type LicenseStatus = + | 'valid' + | 'expired' + | 'suspended' + | 'revoked' + | 'invalid' + | 'trial' + | 'trial_expired' + | 'not_activated' + | 'unknown'; + +/** + * License type. + */ +export type LicenseType = 'perpetual' | 'subscription' | 'trial'; + +/** + * Configuration options for the LicenseClient. + */ +export interface LicenseOptions { + /** Public key for validation (required). Format: pk_live_xxx or pk_test_xxx */ + publicKey: string; + /** Product slug (required) */ + productSlug: string; + /** API base URL */ + apiBaseUrl?: string; + /** Enable debug logging */ + debug?: boolean; + /** Enable offline validation caching */ + enableOfflineCache?: boolean; + /** Cache validation for this many minutes */ + cacheValidationMinutes?: number; + /** Offline grace period in days */ + offlineGraceDays?: number; + /** Storage key prefix */ + storageKeyPrefix?: string; +} + +/** + * Feature in a license. + */ +export interface Feature { + key: string; + name: string; + description?: string; + enabled: boolean; + metadata?: Record; +} + +/** + * License information. + */ +export interface License { + id: string; + key: string; + status: LicenseStatus; + type: LicenseType; + email?: string; + name?: string; + company?: string; + features: Feature[]; + maxActivations: number; + currentActivations: number; + expiresAt?: Date; + createdAt: Date; + lastValidatedAt?: Date; + metadata?: Record; +} + +/** + * Activation information. + */ +export interface Activation { + id: string; + machineId: string; + machineName?: string; + platform?: string; + activatedAt: Date; + lastSeenAt: Date; +} + +/** + * Result of license validation. + */ +export interface LicenseResult { + valid: boolean; + license?: License; + activations?: Activation[]; + error?: string; + cached?: boolean; +} + +/** + * Result of checkout. + */ +export interface CheckoutResult { + success: boolean; + checkoutUrl?: string; + sessionId?: string; + error?: string; +} + +/** + * Product tier for purchase. + */ +export interface ProductTier { + id: string; + slug: string; + name: string; + description?: string; + price: number; + currency: string; + billingPeriod?: 'monthly' | 'yearly' | 'lifetime'; + features: Feature[]; +} + +/** + * Callback for license changes. + */ +export type LicenseChangeCallback = (license: License | null) => void; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d049cda --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}