Implement IronLicensing JavaScript SDK
- LicenseClient for validation and activation - Feature checking with hasFeature/requireFeature - Trial management - Purchase/checkout flow - Offline caching with grace period - License change events - Browser/Node.js compatible storage - Full TypeScript support - Comprehensive README
This commit is contained in:
parent
38ed535f32
commit
ed66914a92
|
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.tgz
|
||||||
|
.ironlicensing/
|
||||||
326
README.md
326
README.md
|
|
@ -1,2 +1,324 @@
|
||||||
# ironlicensing-js
|
# IronLicensing SDK for JavaScript/TypeScript
|
||||||
IronLicensing SDK for JavaScript/TypeScript - Software licensing and activation
|
|
||||||
|
Software licensing and activation SDK for JavaScript/TypeScript applications. Validate licenses, manage activations, check features, and handle trials.
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/@ironservices/licensing)
|
||||||
|
[](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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<LicenseResult> {
|
||||||
|
return getClient().validate(licenseKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates a license using the global client.
|
||||||
|
*/
|
||||||
|
export async function activate(licenseKey: string, machineName?: string): Promise<LicenseResult> {
|
||||||
|
return getClient().activate(licenseKey, machineName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivates the current license using the global client.
|
||||||
|
*/
|
||||||
|
export async function deactivate(): Promise<boolean> {
|
||||||
|
return getClient().deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a trial using the global client.
|
||||||
|
*/
|
||||||
|
export async function startTrial(email: string): Promise<LicenseResult> {
|
||||||
|
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<ProductTier[]> {
|
||||||
|
return getClient().getTiers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a purchase using the global client.
|
||||||
|
*/
|
||||||
|
export async function startPurchase(tierId: string, email: string): Promise<CheckoutResult> {
|
||||||
|
return getClient().startPurchase(tierId, email);
|
||||||
|
}
|
||||||
|
|
@ -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<string, string> = 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();
|
||||||
|
}
|
||||||
|
|
@ -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<T>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: unknown,
|
||||||
|
): Promise<T> {
|
||||||
|
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<LicenseResult> {
|
||||||
|
try {
|
||||||
|
const result = await this.request<any>('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<LicenseResult> {
|
||||||
|
try {
|
||||||
|
const result = await this.request<any>('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<boolean> {
|
||||||
|
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<LicenseResult> {
|
||||||
|
try {
|
||||||
|
const result = await this.request<any>('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<ProductTier[]> {
|
||||||
|
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<CheckoutResult> {
|
||||||
|
try {
|
||||||
|
const result = await this.request<CheckoutResult>('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<boolean> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue