@licenseseat/js
v0.5.0
Published
Official JavaScript SDK for LicenseSeat – simple, secure software licensing.
Maintainers
Readme
LicenseSeat - JavaScript SDK
The official JavaScript/TypeScript SDK for LicenseSeat – the simple, secure licensing platform for apps, games, and plugins.
Features
- License activation & deactivation – Activate licenses with a random installation ID or an explicit stable device ID
- Online & offline validation – Validate licenses with optional offline fallback
- Entitlement checking – Check feature access with
hasEntitlement()andcheckEntitlement() - Heartbeat – Automatic periodic heartbeats to report device activity
- Telemetry – Optional bounded device and environment data attached to POST requests
- Local caching – Bounded convenience state plus signed offline material; cached online responses never grant authority after reload
- Auto-retry with exponential backoff – Resilient network handling
- Event-driven architecture – Subscribe to SDK lifecycle events
- TypeScript support – Full type definitions included (auto-generated from JSDoc)
- Modern ESM package – Native ES modules, tree-shakeable
Installation
npm / yarn / pnpm
# npm
npm install @licenseseat/js
# yarn
yarn add @licenseseat/js
# pnpm
pnpm add @licenseseat/jsCDN (Browser)
<!-- ESM via esm.sh -->
<script type="module">
import LicenseSeat from 'https://esm.sh/@licenseseat/js';
const sdk = new LicenseSeat({
apiKey: 'your-api-key',
productSlug: 'your-product'
});
</script>
<!-- ESM via unpkg -->
<script type="module">
import LicenseSeat from 'https://unpkg.com/@licenseseat/js/dist/index.js';
</script>
<!-- ESM via jsDelivr -->
<script type="module">
import LicenseSeat from 'https://cdn.jsdelivr.net/npm/@licenseseat/js/dist/index.js';
</script>Cross-origin API access
Browser/CDN integrations call https://licenseseat.com from the application's
own origin. The hosted LicenseSeat API permits this for /api/v1/*, including
the preflighted Authorization and Content-Type headers used by the SDK.
There is no per-origin dashboard setting required. The SDK deliberately uses
bearer headers and does not send cookies or set Fetch's credentials option.
If a self-hosted or older deployment returns no
Access-Control-Allow-Origin header, that server must enable CORS for its
licensing API namespace; changing SDK configuration or using a different
publishable key cannot add a missing response header. Do not enable
credentialed wildcard CORS. Restrict the route, methods, and request headers
to the SDK contract instead.
Quick Start
JavaScript (ESM)
import LicenseSeat from '@licenseseat/js';
// Create SDK instance
const sdk = new LicenseSeat({
apiKey: 'your-api-key',
productSlug: 'your-product', // Required: Your product slug
debug: true
});
// Activate a license
await sdk.activate('YOUR-LICENSE-KEY');
// Check entitlements (simple boolean)
if (sdk.hasEntitlement('pro')) {
// Enable pro features
}
// Get current status
const status = sdk.getStatus();
console.log(status);
// { status: 'active', license: '...', entitlements: [...] }TypeScript
import LicenseSeat, {
type LicenseSeatConfig,
type ValidationResult,
type EntitlementCheckResult,
type LicenseStatus
} from '@licenseseat/js';
const config: LicenseSeatConfig = {
apiKey: 'your-api-key',
productSlug: 'your-product',
debug: true
};
const sdk = new LicenseSeat(config);
// Full type inference
const result: ValidationResult = await sdk.validateLicense('LICENSE-KEY');
const status: LicenseStatus = sdk.getStatus();
const hasPro: boolean = sdk.hasEntitlement('pro');TypeScript users get full type support automatically – the package includes generated .d.ts declaration files.
Security model
- HTTPS is required. Plain HTTP is accepted only for an explicitly enabled loopback development URL.
- License keys and device identifiers are sent in JSON request bodies, not URL paths. Bearer credentials are never sent to public health or signing-key endpoints.
- Browser and renderer-process code cannot keep an API key secret from the end user or from injected scripts. Use only least-privilege client credentials and enforce authorization, product tenancy, seat limits, and license state on the server.
localStorageis attacker-writable and is not a trust anchor. The SDK revalidates cached online state before granting entitlements. Persistent offline use requires Ed25519 public keys pinned through trusted application configuration.- Response bodies, JSON structure, identifiers, timestamps, retries, redirects, and request deadlines are bounded and validated fail-closed.
No client SDK can protect licensing decisions from an attacker who controls the application process. Treat local checks as enforcement for ordinary use and keep high-value authorization on a trusted server.
Configuration
const sdk = new LicenseSeat({
// Required
productSlug: 'your-product', // Your product slug from LicenseSeat dashboard
// Required for authenticated operations
apiKey: 'your-api-key',
// API Configuration
apiBaseUrl: 'https://licenseseat.com/api/v1', // Default
requestTimeoutMs: 15000, // Per-attempt request/body deadline
maxResponseBytes: 1048576, // Request/response JSON size ceiling
allowInsecureHttp: false, // HTTP only for explicit loopback development
// Storage
storagePrefix: 'licenseseat_', // localStorage key prefix
// Auto-Validation
autoValidateInterval: 3600000, // 1 hour (in ms)
autoInitialize: true, // Auto-validate cached license on init
// Heartbeat
heartbeatInterval: 300000, // 5 minutes (in ms), 0 to disable
// Telemetry
telemetryEnabled: true, // Set false to disable (e.g. GDPR)
browserHighEntropyTelemetryEnabled: false, // Opt in to Chromium UA-CH enrichment
appVersion: '1.2.0', // Your app version (sent in telemetry)
appBuild: '42', // Your app build number (sent in telemetry)
// Offline Support
offlineFallbackEnabled: false, // Enable offline validation fallback
maxOfflineDays: 0, // Max days offline (0 = disabled)
offlineLicenseRefreshInterval: 259200000, // 72 hours
maxClockSkewMs: 300000, // 5 minutes
signingPublicKeys: { // Trusted Ed25519 keys for persistent offline use
'key-id-001': 'base64url-encoded-32-byte-public-key'
},
// Network
maxRetries: 3, // Retry attempts for failed requests
retryDelay: 1000, // Initial retry delay (ms)
networkRecheckInterval: 30000, // Check connectivity every 30s when offline
// Debug
debug: false // Enable console logging
});Configuration Options
| Option | Type | Default | Description |
| ------------------------ | --------- | ---------------------------------- | --------------------------------------------------------- |
| productSlug | string | – | Required. Your product slug from the dashboard |
| apiKey | string | null | API key for authentication (required for most operations) |
| apiBaseUrl | string | 'https://licenseseat.com/api/v1' | HTTPS API base URL |
| requestTimeoutMs | number | 15000 | Per-attempt deadline covering headers and response body |
| maxResponseBytes | number | 1048576 | Maximum request or response JSON body size |
| allowInsecureHttp | boolean | false | Permit HTTP only for a loopback development URL |
| storagePrefix | string | 'licenseseat_' | Prefix for localStorage keys |
| autoValidateInterval | number | 3600000 | Auto-validation interval in ms (1 hour) |
| autoInitialize | boolean | true | Auto-initialize and validate cached license |
| heartbeatInterval | number | 300000 | Heartbeat interval in ms (5 minutes). Set 0 to disable |
| telemetryEnabled | boolean | true | Attach bounded platform telemetry to POST requests |
| browserHighEntropyTelemetryEnabled | boolean | false | Opt in to in-memory Chromium platform version, architecture, and model hints |
| appVersion | string | null | Your app version string (sent as app_version in telemetry) |
| appBuild | string | null | Your app build identifier (sent as app_build in telemetry) |
| offlineFallbackEnabled | boolean | false | Enable offline validation on network errors |
| maxOfflineDays | number | 0 | Maximum days license works offline (0 = disabled) |
| offlineLicenseRefreshInterval | number | 259200000 | Offline-token refresh interval in ms |
| maxClockSkewMs | number | 300000 | Permitted clock skew during offline verification |
| signingPublicKeys | object | {} | Trusted Ed25519 keys by key ID for offline use after reload |
| maxRetries | number | 3 | Max retry attempts for failed API calls |
| retryDelay | number | 1000 | Initial retry delay in ms (exponential backoff) |
| networkRecheckInterval | number | 30000 | Connectivity probe interval after transport failure |
| debug | boolean | false | Enable debug logging to console |
API Reference
Core Methods
sdk.activate(licenseKey, options?)
Activates a license key on this device.
const result = await sdk.activate('LICENSE-KEY', {
deviceId: 'custom-device-id', // Optional: auto-generated if not provided
deviceName: "John's MacBook Pro", // Optional: human-readable device name
metadata: { version: '1.0.0' } // Optional: custom metadata
});
console.log(result);
// {
// license_key: 'LICENSE-KEY',
// device_id: 'web-abc123',
// activated_at: '2024-01-15T10:30:00Z',
// activation: {
// object: 'activation',
// id: 123,
// device_id: 'web-abc123',
// license_key: 'LICENSE-KEY',
// activated_at: '2024-01-15T10:30:00Z',
// license: { ... }
// }
// }sdk.deactivate()
Deactivates the current license and clears cached data.
const result = await sdk.deactivate();
console.log(result);
// {
// object: 'deactivation',
// activation_id: 123,
// deactivated_at: '2024-01-15T12:00:00Z'
// }sdk.validateLicense(licenseKey, options?)
Validates a license with the server.
const result = await sdk.validateLicense('LICENSE-KEY', {
deviceId: 'device-id' // Optional: required for hardware_locked mode
});
console.log(result);
// {
// valid: true,
// license: {
// key: 'LICENSE-KEY',
// status: 'active',
// mode: 'hardware_locked',
// plan_key: 'pro',
// active_seats: 1,
// seat_limit: 3,
// active_entitlements: [
// { key: 'pro', expires_at: null, metadata: null },
// { key: 'beta', expires_at: '2024-12-31T23:59:59Z', metadata: null }
// ],
// product: { slug: 'your-product', name: 'Your Product' }
// },
// active_entitlements: [...]
// }Entitlement Methods
Note: Entitlements are optional. A license may have zero entitlements if the associated plan has no entitlements configured. The
active_entitlementsarray may be empty or the field may be undefined/null.
sdk.hasEntitlement(key)
Check if an entitlement is active. Returns a simple boolean. Returns false if no entitlements exist.
if (sdk.hasEntitlement('pro')) {
enableProFeatures();
}
if (sdk.hasEntitlement('beta')) {
showBetaUI();
}sdk.checkEntitlement(key)
Check entitlement with detailed information.
const result = sdk.checkEntitlement('pro');
if (result.active) {
console.log('Entitlement:', result.entitlement);
console.log('Expires:', result.entitlement.expires_at);
} else {
console.log('Reason:', result.reason);
// Possible reasons: 'no_license', 'not_found', 'expired'
}Status Methods
sdk.getStatus()
Get current license status.
const status = sdk.getStatus();
// Possible status values:
// - 'inactive': No license activated
// - 'pending': License pending validation
// - 'active': License valid (online)
// - 'invalid': License invalid
// - 'offline-valid': License valid (offline verification)
// - 'offline-invalid': License invalid (offline verification)
console.log(status);
// {
// status: 'active',
// license: 'LICENSE-KEY',
// device: 'web-abc123',
// activated_at: '2024-01-15T10:30:00Z',
// last_validated: '2024-01-15T11:30:00Z',
// entitlements: [...]
// }sdk.testAuth()
Verify the configured API credential and its baseline licenses:validate scope using the protected, side-effect-free /auth endpoint.
try {
const result = await sdk.testAuth();
console.log('Authenticated:', result.authenticated); // true only after protected auth succeeds
console.log('Healthy:', result.healthy); // endpoint was reachable and valid
console.log('API Version:', result.api_version); // e.g., '1.0.0'
} catch (error) {
console.error('Connection failed:', error);
}An invalid, expired, cross-tenant, or insufficiently scoped credential causes the method to reject. The public /health endpoint is deliberately not treated as proof of authentication.
sdk.heartbeat()
Send a heartbeat to report that the current device is still active. Heartbeats are sent automatically at the configured heartbeatInterval, but you can also send one manually.
try {
const result = await sdk.heartbeat();
console.log('Heartbeat received at:', result.received_at);
} catch (error) {
console.error('Heartbeat failed:', error);
}Returns undefined if no active license is cached. When auto-heartbeat is enabled (the default), the SDK sends heartbeats every 5 minutes while a license is active. Auto-heartbeat starts automatically after activate() or when the SDK initializes with a cached license.
To disable auto-heartbeat, set heartbeatInterval: 0 in the configuration.
sdk.reset()
Clear all cached data and reset SDK state.
sdk.reset();sdk.destroy()
Destroy the SDK instance and release all resources. Call this when you no longer need the SDK to prevent memory leaks. After calling destroy(), the SDK instance should not be used.
// When unmounting a component or closing an app
sdk.destroy();sdk.initialize()
Manually initialize the SDK (only needed if autoInitialize: false).
const sdk = new LicenseSeat({
apiKey: 'key',
productSlug: 'your-product',
autoInitialize: false // Don't auto-initialize
});
// Later, when ready:
sdk.initialize();Events
Subscribe to SDK lifecycle events for reactive UIs.
// Subscribe
const unsubscribe = sdk.on('activation:success', (data) => {
console.log('License activated:', data);
});
// Unsubscribe
unsubscribe();
// or
sdk.off('activation:success', handler);Available Events
| Event | Description | Data |
| ----------------------------------- | ----------------------------------- | ------------------------------- |
| Lifecycle | | |
| license:loaded | Cached license loaded on init | CachedLicense |
| sdk:reset | SDK was reset | – |
| sdk:destroyed | SDK was destroyed | – |
| sdk:error | General SDK error | { message, error? } |
| Activation | | |
| activation:start | Activation started | { licenseKey, deviceId } |
| activation:success | Activation succeeded | CachedLicense |
| activation:error | Activation failed | { licenseKey, error } |
| Deactivation | | |
| deactivation:start | Deactivation started | CachedLicense |
| deactivation:success | Deactivation succeeded | DeactivationResponse |
| deactivation:error | Deactivation failed | { error, license } |
| Validation | | |
| validation:start | Validation started | { licenseKey } |
| validation:success | Online validation succeeded | ValidationResult |
| validation:failed | Validation failed (invalid license) | ValidationResult |
| validation:error | Validation error (network, etc.) | { licenseKey, error } |
| validation:offline-success | Offline validation succeeded | ValidationResult |
| validation:offline-failed | Offline validation failed | ValidationResult |
| validation:auth-failed | Auth failed during validation | { licenseKey, error, cached } |
| Auto-Validation | | |
| autovalidation:cycle | Auto-validation scheduled | { nextRunAt: Date } |
| autovalidation:stopped | Auto-validation stopped | – |
| Heartbeat | | |
| heartbeat:success | Heartbeat acknowledged by server | HeartbeatResponse |
| heartbeat:cycle | Auto-heartbeat tick completed | { nextRunAt: Date } |
| Network | | |
| network:online | Network connectivity restored | – |
| network:offline | Network connectivity lost | { error } |
| Offline Token | | |
| offlineToken:fetching | Fetching offline token | { licenseKey } |
| offlineToken:fetched | Offline token fetched | { licenseKey, data } |
| offlineToken:fetchError | Offline token fetch failed | { licenseKey, error } |
| offlineToken:ready | Offline assets synced | { kid, exp_at } |
| offlineToken:verified | Offline signature verified | { payload } |
| offlineToken:verificationFailed | Offline signature invalid | { payload } |
Singleton Pattern
For applications that need a shared SDK instance:
import { configure, getSharedInstance, resetSharedInstance } from '@licenseseat/js';
// Configure once at app startup
configure({
apiKey: 'your-key',
productSlug: 'your-product'
});
// Use anywhere in your app
const sdk = getSharedInstance();
await sdk.activate('LICENSE-KEY');
// Reset if needed
resetSharedInstance();Offline Support
The SDK supports offline license validation using cryptographically signed offline tokens (Ed25519).
const sdk = new LicenseSeat({
apiKey: 'your-key',
productSlug: 'your-product',
offlineFallbackEnabled: true, // Enable offline fallback
maxOfflineDays: 7, // Allow at most 7 days offline
signingPublicKeys: {
'key-id-001': 'base64url-encoded-32-byte-public-key'
}
});
// After activation, offline assets are automatically synced
await sdk.activate('LICENSE-KEY');
// Later, even offline, validation will work using cached data
const result = await sdk.validateLicense('LICENSE-KEY');
if (result.offline) {
console.log('Validated offline');
}How Offline Validation Works
- On activation, the SDK fetches a signed offline token from the server
- The offline token contains:
- License data (key, plan, entitlements, expiration)
- Ed25519 signature
- Canonical JSON for verification
- When offline, the SDK verifies the signature locally
- The SDK enforces signed time bounds, local policy, product/license/device binding, and a defense-in-depth clock-rollback check
The public key is part of the trust decision. For offline authority that survives an application reload, embed or inject the expected key through trusted application configuration using signingPublicKeys. A public key recovered from localStorage is intentionally ignored. Fetching a key over authenticated HTTPS is suitable for the current process, but it does not turn attacker-writable storage into a durable trust anchor.
Clock-rollback detection is defense in depth, not tamper-proof storage. An attacker controlling the process, system clock, or application bundle can bypass local enforcement; keep valuable authorization on trusted infrastructure.
Offline Methods
sdk.syncOfflineAssets()
Fetches the offline token and signing key from the server. Uses the currently cached license. Call this after activation to prepare for offline usage.
// First activate (caches the license)
await sdk.activate('LICENSE-KEY');
// Then sync offline assets (uses cached license)
const assets = await sdk.syncOfflineAssets();
console.log('Offline token key ID:', assets.kid);
console.log('Expires at:', assets.exp_at);sdk.getOfflineToken()
Fetches a signed offline token for the currently cached license. Returns the token structure containing the license data and Ed25519 signature.
// Must have an active license cached first
const token = await sdk.getOfflineToken();
console.log(token);
// {
// object: 'offline_token',
// token: { license_key, product_slug, plan_key, ... },
// signature: { algorithm: 'Ed25519', key_id, value },
// canonical: '...'
// }sdk.getSigningKey(keyId)
Fetches the Ed25519 public key used for verifying offline token signatures.
const signingKey = await sdk.getSigningKey('key-id-001');
console.log(signingKey);
// {
// object: 'signing_key',
// key_id: 'key-id-001',
// public_key: 'base64-encoded-public-key',
// algorithm: 'Ed25519',
// created_at: '2024-01-01T00:00:00Z'
// }sdk.verifyOfflineToken(token, publicKeyB64)
Verifies an offline token's Ed25519 signature locally. Both parameters are required.
// Fetch the token and signing key first
const token = await sdk.getOfflineToken();
const signingKey = await sdk.getSigningKey(token.signature.key_id);
// Verify the signature
const isValid = await sdk.verifyOfflineToken(token, signingKey.public_key);
console.log('Signature valid:', isValid);Important:
verifyOfflineToken()proves that the token matches the public key you supply; it does not establish that the supplied key is trusted. Pin the expected key in application configuration for persistent offline decisions.
Offline Token Structure
{
object: 'offline_token',
token: {
schema_version: 1,
license_key: 'LICENSE-KEY',
product_slug: 'your-product',
plan_key: 'pro',
mode: 'hardware_locked',
fingerprint: 'install-random-id',
iat: 1704067200, // Issued at (Unix timestamp)
exp: 1706659200, // Expires at (Unix timestamp)
nbf: 1704067200, // Not before (Unix timestamp)
license_expires_at: null,
kid: 'key-id-001',
entitlements: [
{ key: 'pro', expires_at: null }
],
metadata: {}
},
signature: {
algorithm: 'Ed25519',
key_id: 'key-id-001',
value: 'base64url-encoded-signature'
},
canonical: '{"entitlements":[...],"exp":...}'
}Telemetry
The SDK can collect bounded device and environment data and include it with POST requests sent to the LicenseSeat API. This data helps you understand what platforms and environments your customers use.
Telemetry is enabled by default and can be disabled at any time.
Collected Fields
| Field | Type | Example | Description |
| -------------------- | -------- | ---------------------- | ------------------------------------------------ |
| sdk_name | string | "js" | Always "js" for this SDK |
| sdk_version | string | "0.5.0" | SDK version |
| os_name | string | "macOS" | Operating system name |
| os_version | string | "14.2.1" | Operating system version |
| platform | string | "browser" | Runtime platform (browser, node, electron, react-native, deno, bun) |
| device_model | string | null | Device model (Chromium userAgentData only) |
| device_type | string | "desktop" | Device type (desktop, phone, tablet, server) |
| locale | string | "en-US" | Full locale string |
| timezone | string | "America/New_York" | IANA timezone |
| language | string | "en" | 2-letter language code |
| architecture | string | "arm64" | CPU architecture |
| cpu_cores | number | 10 | Number of logical CPU cores |
| memory_gb | number | 16 | Approximate RAM in GB (supported browsers only) |
| screen_resolution | string | "1920x1080" | Screen resolution |
| display_scale | number | 2 | Device pixel ratio |
| browser_name | string | "Chrome" | Browser name (browser environments only) |
| browser_version | string | "123.0" | Browser version (browser environments only) |
| runtime_version | string | "20.11.0" | Runtime version (Node.js, Deno, Bun, Electron) |
| app_version | string | "1.2.0" | Your app version (from appVersion config) |
| app_build | string | "42" | Your app build (from appBuild config) |
Fields that cannot be detected in the current environment are omitted (not sent as null).
Browsers intentionally reduce several values in the ordinary user-agent
surface. To request Chromium's higher-entropy platformVersion,
architecture, and model values, opt in explicitly:
const sdk = new LicenseSeat({
apiKey: 'your-key',
productSlug: 'your-product',
browserHighEntropyTelemetryEnabled: true
});The request starts during initialize() but never delays activation,
validation, or another API call. Successfully returned values are bounded and
cached only in this SDK instance's memory; they are not written to
localStorage. Unsupported browsers, denied permissions, invalid values, and
failed requests simply use the ordinary reduced telemetry. Setting
telemetryEnabled: false prevents both the high-entropy request and telemetry
attachment.
Providing App Version
Pass your own app version and build number via configuration so they appear in telemetry:
const sdk = new LicenseSeat({
apiKey: 'your-key',
productSlug: 'your-product',
appVersion: '1.2.0',
appBuild: '42'
});Disabling Telemetry
To disable SDK telemetry collection entirely:
const sdk = new LicenseSeat({
apiKey: 'your-key',
productSlug: 'your-product',
telemetryEnabled: false
});When telemetry is disabled, no device or environment data is attached to API requests.
Privacy
The SDK does not intentionally collect names, email addresses, or application data in telemetry. Platform, locale, timezone, model, and display attributes can nevertheless be personal data or contribute to device fingerprinting when combined. The server will also ordinarily observe the request IP address independently of this telemetry object. Document the collection, apply appropriate retention/access controls, and assess the privacy rules that apply to your deployment.
Ordinary telemetry is enabled by default, but high-entropy browser hints are
disabled by default and require the separate opt-in above. Set
telemetryEnabled: false to disable SDK collection completely.
Heartbeat
The SDK sends periodic heartbeat signals to let the server know a device is still actively using the license. This enables usage analytics and helps detect inactive seats.
How It Works
- After a license is activated (or when the SDK initializes with a cached license), a heartbeat timer starts automatically.
- The default interval is 5 minutes (
300000ms), configurable viaheartbeatInterval. - Each heartbeat sends the current
device_idto the server. - Heartbeats also run alongside auto-validation cycles.
Manual Heartbeat
You can send a heartbeat at any time:
await sdk.heartbeat();Configuring the Interval
const sdk = new LicenseSeat({
apiKey: 'your-key',
productSlug: 'your-product',
heartbeatInterval: 600000 // 10 minutes
});Set heartbeatInterval: 0 to disable auto-heartbeat entirely. You can still call sdk.heartbeat() manually.
Heartbeat Events
| Event | Description | Data |
| -------------------- | ---------------------------------- | ----------------------- |
| heartbeat:success | Heartbeat acknowledged by server | HeartbeatResponse |
| heartbeat:cycle | Auto-heartbeat tick completed | { nextRunAt: Date } |
sdk.on('heartbeat:success', (data) => {
console.log('Heartbeat received at:', data.received_at);
});Heartbeat Lifecycle
- Starts automatically after
sdk.activate()succeeds, or on SDK init if a cached license exists. - Stops automatically when
sdk.deactivate(),sdk.reset(), orsdk.destroy()is called. - Heartbeat failures are logged (in debug mode) but do not throw or interrupt the SDK.
Error Handling
The SDK exports custom error classes for precise error handling:
import LicenseSeat, {
APIError,
LicenseError,
ConfigurationError,
CryptoError
} from '@licenseseat/js';
try {
await sdk.activate('INVALID-KEY');
} catch (error) {
if (error instanceof APIError) {
console.log('HTTP Status:', error.status);
console.log('Error Code:', error.data?.error?.code);
console.log('Error Message:', error.data?.error?.message);
} else if (error instanceof LicenseError) {
console.log('License error:', error.code);
} else if (error instanceof ConfigurationError) {
console.log('Config error:', error.message);
}
}Error Types
| Error | Description |
| -------------------- | ---------------------------------------------------- |
| APIError | HTTP request failures (includes status and data) |
| LicenseError | License operation failures (includes code) |
| ConfigurationError | SDK misconfiguration (e.g., missing productSlug) |
| CryptoError | Cryptographic operation failures |
API Error Format
API errors follow this structure:
{
error: {
code: 'license_not_found', // Machine-readable error code
message: 'License not found.', // Human-readable message
details: { ... } // Optional additional details
}
}Common error codes:
unauthorized- Invalid or missing API keylicense_not_found- License key doesn't existlicense_expired- License has expiredlicense_suspended- License is suspendedlicense_revoked- License has been revokedseat_limit_reached- No more seats availabledevice_already_activated- Device is already activatedactivation_not_found- Activation doesn't exist (for deactivation)
Browser Support
- Modern browsers: Chrome 80+, Firefox 75+, Safari 14+, Edge 80+
- Bundlers: Vite, Webpack, Rollup, esbuild, Parcel
- Node.js: 18+ (requires polyfills - see below)
Node.js Usage
The SDK is designed for browsers but works in Node.js with polyfills. Add these before importing the SDK:
// Required polyfills for Node.js
const storage = {};
globalThis.localStorage = {
getItem(key) { return Object.prototype.hasOwnProperty.call(storage, key) ? storage[key] : null; },
setItem(key, value) { storage[key] = String(value); },
removeItem(key) { delete storage[key]; },
clear() { for (const key in storage) delete storage[key]; },
};
// Override Object.keys to support localStorage iteration (used by cache.getAllKeys())
const originalKeys = Object.keys;
Object.keys = function(obj) {
if (obj === globalThis.localStorage) return originalKeys(storage);
return originalKeys(obj);
};
// Device fingerprinting polyfills (provides stable fallback values)
globalThis.document = { createElement: () => ({ getContext: () => null }), querySelector: () => null };
globalThis.window = { navigator: {}, screen: {} };
globalThis.navigator = { userAgent: 'Node.js', language: 'en', hardwareConcurrency: 4 };
// Now import the SDK
const { default: LicenseSeat } = await import('@licenseseat/js');Note: In Node.js, device fingerprinting will use fallback values since browser APIs aren't available. For consistent device identification across restarts, pass an explicit
deviceIdtoactivate().
Usage Guide
For JavaScript Users
Simply import and use:
import LicenseSeat from '@licenseseat/js';
const sdk = new LicenseSeat({
apiKey: 'your-key',
productSlug: 'your-product'
});For TypeScript Users
The package includes TypeScript declarations (.d.ts files) automatically. No additional @types/ package needed.
import LicenseSeat from '@licenseseat/js';
// Types are automatically available
const sdk = new LicenseSeat({
apiKey: 'your-key',
productSlug: 'your-product'
});
// Import specific types if needed
import type {
LicenseSeatConfig,
ValidationResult,
EntitlementCheckResult,
LicenseStatus,
Entitlement,
CachedLicense,
ActivationResponse,
DeactivationResponse,
OfflineToken
} from '@licenseseat/js';For CDN/Browser Users
Use ES modules via CDN:
<!DOCTYPE html>
<html>
<head>
<title>LicenseSeat Demo</title>
</head>
<body>
<script type="module">
import LicenseSeat from 'https://esm.sh/@licenseseat/js';
const sdk = new LicenseSeat({
apiKey: 'your-api-key',
productSlug: 'your-product',
debug: true
});
// Check for existing license
const status = sdk.getStatus();
if (status.status === 'active') {
console.log('Already licensed!');
}
// Activate (example with user input)
document.getElementById('activate-btn').onclick = async () => {
const key = document.getElementById('license-key').value;
try {
await sdk.activate(key);
alert('License activated!');
} catch (e) {
alert('Activation failed: ' + e.message);
}
};
</script>
<input id="license-key" placeholder="Enter license key" />
<button id="activate-btn">Activate</button>
</body>
</html>Development
Setup
git clone https://github.com/licenseseat/licenseseat-js.git
cd licenseseat-js
npm installScripts
| Command | Description |
| ----------------------- | ----------------------------------------- |
| npm run build | Build JS bundle + TypeScript declarations |
| npm run build:js | Build JavaScript bundle only |
| npm run build:types | Generate TypeScript declarations |
| npm run build:iife | Build global/IIFE bundle |
| npm run dev | Watch mode for development |
| npm test | Run tests |
| npm run test:watch | Run tests in watch mode |
| npm run test:coverage | Run tests with coverage report |
| npm run typecheck | Type-check without emitting |
Integration Tests
The SDK includes comprehensive integration tests that run against the live LicenseSeat API. These tests verify real-world functionality including activation, validation, deactivation, and offline cryptographic operations.
Running Integration Tests (Node.js)
# Set environment variables
export LICENSESEAT_API_KEY="ls_your_api_key_here"
export LICENSESEAT_PRODUCT_SLUG="your-product"
export LICENSESEAT_LICENSE_KEY="YOUR-LICENSE-KEY"
# Run the tests
node test-live.mjsOr with inline environment variables:
LICENSESEAT_API_KEY=ls_xxx LICENSESEAT_PRODUCT_SLUG=my-app LICENSESEAT_LICENSE_KEY=XXX-XXX node test-live.mjsRunning Integration Tests (Browser)
Open test-live.html in a browser. You'll be prompted to enter your credentials:
- API Key - Your LicenseSeat API key (starts with
ls_) - Product Slug - Your product identifier
- License Key - A valid license key for testing
Credentials are stored in localStorage for convenience during development.
What the Integration Tests Cover
| Category | Tests | |----------|-------| | Initialization | SDK setup, configuration defaults | | Activation | License activation, device ID generation | | Validation | Online validation, entitlement checking | | Deactivation | License deactivation, cache clearing | | Offline Crypto | Ed25519 signature verification, offline token fetching, tamper detection | | Error Handling | Invalid licenses, missing config | | Singleton | Shared instance pattern |
Project Structure
licenseseat-js/
├── src/
│ ├── index.js # Entry point, exports
│ ├── LicenseSeat.js # Main SDK class
│ ├── telemetry.js # Telemetry collection (device/environment data)
│ ├── cache.js # LicenseCache (localStorage)
│ ├── errors.js # Error classes
│ ├── types.js # JSDoc type definitions
│ └── utils.js # Utility functions
├── tests/ # Unit tests (mocked API)
│ ├── setup.js # Test setup
│ ├── mocks/ # MSW handlers
│ ├── LicenseSeat.test.js
│ └── utils.test.js
├── test-live.mjs # Integration tests (Node.js)
├── test-live.html # Integration tests (Browser)
├── dist/ # Build output
│ ├── index.js # ESM bundle
│ └── types/ # TypeScript declarations
├── package.json
├── tsconfig.json
└── vitest.config.jsPublishing
Publishing to npm
Update version in
package.json:npm version patch # or minor, majorBuild the package:
npm run buildVerify the build:
# Check what will be published npm pack --dry-run # Verify TypeScript types ls dist/types/Publish:
# Login if needed npm login # Publish (public package) npm publish --access public
What Gets Published
The files field in package.json controls what's included:
{
"files": ["dist/", "src/"]
}Users receive:
dist/index.js– ESM bundle (JavaScript)dist/types/*.d.ts– TypeScript declarationssrc/*.js– Source files (for debugging/reference)
Package Exports
{
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/types/index.d.ts"
}
}
}This ensures:
- JavaScript users get
dist/index.js - TypeScript users get type definitions from
dist/types/index.d.ts - Both ESM
importand bundlers work correctly
CDN Distribution
Once published to npm, the package is automatically available on CDNs:
| CDN | URL |
| ------------ | ------------------------------------------------------------ |
| esm.sh | https://esm.sh/@licenseseat/js |
| unpkg | https://unpkg.com/@licenseseat/js/dist/index.js |
| jsDelivr | https://cdn.jsdelivr.net/npm/@licenseseat/js/dist/index.js |
| Skypack | https://cdn.skypack.dev/@licenseseat/js |
Version pinning (recommended for production):
<script type="module">
import LicenseSeat from 'https://esm.sh/@licenseseat/[email protected]';
</script>Self-Hosting
To host the SDK yourself:
Build the package:
npm run buildCopy
dist/index.jsto your CDN/serverServe with correct MIME type (
application/javascript) and CORS headers
Building an IIFE Bundle (Legacy Browsers)
For a global LicenseSeat variable (non-module script tags):
npm run build:iifeThis creates dist/index.global.js:
<script src="/path/to/index.global.js"></script>
<script>
const sdk = new LicenseSeat({
apiKey: 'your-key',
productSlug: 'your-product'
});
</script>Versioning
This project follows Semantic Versioning:
- MAJOR (1.0.0 → 2.0.0): Breaking changes
- MINOR (1.0.0 → 1.1.0): New features (backward compatible)
- PATCH (1.0.0 → 1.0.1): Bug fixes
Migration from v0.2.x
Breaking Changes in v0.3.0
This version introduces the v1 API with significant changes:
| Change | Before (v0.2.x) | After (v0.3.0) |
| -------------------------------- | ------------------------------- | ----------------------------------------- |
| productSlug config | Not required | Required for all API operations |
| apiBaseUrl default | https://licenseseat.com/api | https://licenseseat.com/api/v1 |
| deviceIdentifier option | deviceIdentifier | deviceId |
| device_identifier field | device_identifier | device_id |
| Deactivation response | Returns full activation object | Returns { object, activation_id, deactivated_at } |
| getOfflineLicense() method | Available | Renamed to getOfflineToken() |
| getPublicKey() method | Available | Renamed to getSigningKey() |
| Offline license structure | Legacy format | New token/signature/canonical format |
| Error format | Various | { error: { code, message, details? } } |
Migration Steps
Add
productSlugto configuration:// Before const sdk = new LicenseSeat({ apiKey: 'key' }); // After const sdk = new LicenseSeat({ apiKey: 'key', productSlug: 'your-product' // Required! });Update activation options:
// Before await sdk.activate('KEY', { deviceIdentifier: 'id' }); // After await sdk.activate('KEY', { deviceId: 'id' });Update response field access:
// Before const result = await sdk.activate('KEY'); console.log(result.device_identifier); // After const result = await sdk.activate('KEY'); console.log(result.device_id);Update deactivation handling:
// Before const result = await sdk.deactivate(); console.log(result.license_key); // After const result = await sdk.deactivate(); console.log(result.activation_id); console.log(result.deactivated_at);Update offline method calls:
// Before await sdk.getOfflineLicense(key); await sdk.getPublicKey(keyId); // After (note: getOfflineToken uses cached license, no parameter needed) await sdk.getOfflineToken(); await sdk.getSigningKey(keyId);
License
MIT License – see LICENSE for details.
