JavaScript SDK
Official JavaScript/TypeScript SDK for LicenseSeat. Full TypeScript support with auto-generated type definitions.
Current stable release: v0.5.0, published as @licenseseat/js 0.5.0 on npm. Node.js 18 or newer is required.
Installation
Package Managers
# npm
npm install @licenseseat/[email protected]
# yarn
yarn add @licenseseat/[email protected]
# pnpm
pnpm add @licenseseat/[email protected]
CDN (Browser)
<!-- ESM via esm.sh (recommended) -->
<script type="module">
import LicenseSeat from 'https://esm.sh/@licenseseat/[email protected]';
</script>
<!-- ESM via unpkg -->
<script type="module">
import LicenseSeat from 'https://unpkg.com/@licenseseat/[email protected]/dist/index.js';
</script>
<!-- ESM via jsDelivr -->
<script type="module">
import LicenseSeat from 'https://cdn.jsdelivr.net/npm/@licenseseat/[email protected]/dist/index.js';
</script>
The CDN examples pin the reviewed release. Keep an exact version in production instead of allowing a CDN URL to move to a future SDK automatically.
Security Model
- HTTPS is required. Plain HTTP is accepted only for an explicitly enabled loopback development URL.
- License keys and installation identifiers are sent in JSON request bodies, not URL paths. Bearer credentials are omitted from public health and signing-key requests.
- Browser and renderer-process code cannot keep an API key secret from the end user or injected scripts. Use only least-privilege publishable client credentials; enforce product tenancy, seat limits, license state, and valuable authorization on a trusted server.
localStorageis attacker-writable and is not a trust anchor. Cached online state is revalidated before it grants entitlements. Persistent offline operation requires Ed25519 public keys pinned through trusted application configuration.- Request and response bodies, JSON structure, identifiers, timestamps, retries, redirects, and deadlines are bounded and validated fail-closed.
No client SDK can protect licensing decisions from an attacker who controls the application process. Keep high-value authorization on trusted infrastructure.
Quick Start
JavaScript
import LicenseSeat from '@licenseseat/js';
// Create SDK instance
const sdk = new LicenseSeat({
apiKey: 'pk_live_xxxxxxxx',
productSlug: 'your-product', // Required
debug: true
});
// Activate a license
await sdk.activate('YOUR-LICENSE-KEY');
// Check entitlements (simple boolean)
if (sdk.hasEntitlement('pro')) {
enableProFeatures();
}
// Get current status
const status = sdk.getStatus();
console.log('License status:', status.status);
TypeScript
import LicenseSeat, {
type LicenseSeatConfig,
type ValidationResult,
type EntitlementCheckResult,
type LicenseStatus
} from '@licenseseat/js';
const config: LicenseSeatConfig = {
apiKey: 'pk_live_xxxxxxxx',
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.
Configuration
const sdk = new LicenseSeat({
// Required
productSlug: 'your-product', // Your product slug from dashboard
// Required for authenticated operations
apiKey: 'pk_live_xxxxxxxx',
// 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
// Initialization
autoInitialize: true, // Auto-validate cached license on init
// Auto-Validation
autoValidateInterval: 3600000, // 1 hour (in ms)
// Heartbeat
heartbeatInterval: 300000, // 5 minutes (in ms)
// Telemetry and app info
telemetryEnabled: true, // Attach optional bounded telemetry
browserHighEntropyTelemetryEnabled: false, // Opt in to Chromium UA-CH enrichment
appVersion: null, // Your app version (e.g., '2.1.0')
appBuild: null, // Your app build number (e.g., '42')
// 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 keys for offline use after reload
'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 |
Your publishable API key |
apiBaseUrl |
string |
https://licenseseat.com/api/v1 |
HTTPS API base URL |
requestTimeoutMs |
number |
15000 |
Per-attempt deadline covering headers and the response body |
maxResponseBytes |
number |
1048576 |
Maximum request or response JSON body size |
allowInsecureHttp |
boolean |
false |
Permit HTTP only for an explicit loopback development URL |
storagePrefix |
string |
licenseseat_ |
Prefix for localStorage keys |
autoInitialize |
boolean |
true |
Auto-initialize on construction |
autoValidateInterval |
number |
3600000 |
Auto-validation interval (ms) |
heartbeatInterval |
number |
300000 |
Standalone heartbeat interval (ms, 0 = disabled) |
telemetryEnabled |
boolean |
true |
Attach bounded platform telemetry to supported POST requests |
browserHighEntropyTelemetryEnabled |
boolean |
false |
Opt in to in-memory Chromium platform-version, architecture, and model hints |
appVersion |
string |
null |
Your app version for telemetry |
appBuild |
string |
null |
Your app build number for telemetry |
offlineFallbackEnabled |
boolean |
false |
Enable offline validation |
maxOfflineDays |
number |
0 |
Max offline days (0 = disabled) |
offlineLicenseRefreshInterval |
number |
259200000 |
Legacy offline-token refresh (72h) |
maxClockSkewMs |
number |
300000 |
Max clock skew (5 min) |
signingPublicKeys |
object |
{} |
Trusted Ed25519 public keys by key ID for offline use after reload |
maxRetries |
number |
3 |
Max API retry attempts |
retryDelay |
number |
1000 |
Base retry delay (exponential backoff) |
networkRecheckInterval |
number |
30000 |
Network check interval when offline |
debug |
boolean |
false |
Enable debug logging |
Core Methods
Activation
// Omit the second argument for default device identification.
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('Activation ID:', result.activation?.id);
// {
// 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: { ... }
// }
// }
Deactivation
const result = await sdk.deactivate();
console.log('Deactivated activation:', result.activation_id);
// {
// object: 'deactivation',
// activation_id: 123,
// deactivated_at: '2024-01-15T12:00:00Z'
// }
Validation
const result = await sdk.validateLicense('LICENSE-KEY', {
deviceId: 'device-id' // Optional: required for hardware_locked mode
});
console.log('License valid:', result.valid);
// {
// 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: [...]
// }
Entitlements
Entitlements are optional. A license may have zero entitlements if the associated plan has none configured.
Simple Check (Boolean)
if (sdk.hasEntitlement('pro')) {
enableProFeatures();
}
if (sdk.hasEntitlement('beta')) {
showBetaUI();
}
Detailed Check
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'
}
EntitlementCheckResult Type
| Property | Type | Description |
|---|---|---|
active |
boolean |
Whether the entitlement is active |
reason |
string? |
Why inactive: no_license, not_found, expired |
expires_at |
string? |
ISO8601 expiration date |
entitlement |
Entitlement? |
Full entitlement object if active |
Status
Get Current Status
const status = sdk.getStatus();
switch (status.status) {
case 'inactive':
showActivationScreen();
break;
case 'pending':
showLoadingIndicator();
break;
case 'active':
enableFeatures(status.entitlements);
break;
case 'offline-valid':
enableFeatures(status.entitlements);
showOfflineBanner();
break;
case 'invalid':
showErrorScreen(status.message);
break;
case 'offline-invalid':
showRenewalScreen();
break;
}
Status Values
| Status | Description |
|---|---|
inactive |
No license activated |
pending |
License pending validation |
active |
License valid (online) |
offline-valid |
License valid (offline verification) |
invalid |
License invalid |
offline-invalid |
License invalid (offline) |
LicenseStatus Type
| Property | Type | Description |
|---|---|---|
status |
string |
Status value (see above) |
message |
string? |
Status message |
license |
string? |
License key (if active) |
device |
string? |
Device fingerprint (if active) |
activated_at |
string? |
ISO8601 activation timestamp |
last_validated |
string? |
ISO8601 last validation timestamp |
entitlements |
Entitlement[]? |
Active entitlements |
Events
Subscribe to SDK lifecycle events for reactive UIs.
// Subscribe
const unsubscribe = sdk.on('activation:success', () => {
console.log('License activated');
});
// Unsubscribe
unsubscribe();
// or
sdk.off('activation:success', handler);
Available Events
| Event | Data | Description |
|---|---|---|
| Lifecycle | ||
license:loaded |
CachedLicense |
Cached license loaded on init |
sdk:reset |
— | SDK was reset |
sdk:destroyed |
— | SDK was destroyed |
sdk:error |
{ message, error? } |
General SDK error |
| Activation | ||
activation:start |
{ licenseKey, deviceId } |
Activation started |
activation:success |
CachedLicense |
Activation succeeded |
activation:error |
{ licenseKey, error } |
Activation failed |
| Deactivation | ||
deactivation:start |
CachedLicense |
Deactivation started |
deactivation:success |
DeactivationResponse |
Deactivation succeeded |
deactivation:error |
{ error, license } |
Deactivation failed |
| Validation | ||
validation:start |
{ licenseKey } |
Validation started |
validation:success |
ValidationResult |
Online validation succeeded |
validation:failed |
ValidationResult |
Validation failed (invalid) |
validation:error |
{ licenseKey, error } |
Validation error (network) |
validation:offline-success |
ValidationResult |
Offline validation succeeded |
validation:offline-failed |
ValidationResult |
Offline validation failed |
validation:auth-failed |
{ licenseKey, error, cached } |
Auth failed during validation |
| Heartbeat | ||
heartbeat:success |
{ received_at } |
Heartbeat acknowledged by server |
heartbeat:error |
{ error } |
Heartbeat request failed |
| Auto-Validation | ||
autovalidation:cycle |
{ nextRunAt: Date } |
Auto-validation scheduled |
autovalidation:stopped |
— | Auto-validation stopped |
| Network | ||
network:online |
— | Network connectivity restored |
network:offline |
{ error } |
Network connectivity lost |
| Offline Token | ||
offlineToken:fetching |
{ licenseKey } |
Fetching legacy offline token |
offlineToken:fetched |
{ licenseKey, data } |
Legacy offline token fetched |
offlineToken:fetchError |
{ licenseKey, error } |
Fetch failed |
offlineToken:ready |
{ kid, exp_at } |
Offline assets synced |
offlineToken:verified |
{ payload } |
Signature verified |
offlineToken:verificationFailed |
{ payload } |
Signature invalid |
Singleton Pattern
For applications that need a shared SDK instance across modules:
import { configure, getSharedInstance, resetSharedInstance } from '@licenseseat/js';
// Configure once at app startup
configure({
apiKey: 'pk_live_xxxxxxxx',
productSlug: 'your-product'
});
// Use anywhere in your app
const sdk = getSharedInstance();
await sdk.activate('LICENSE-KEY');
// Reset if needed (clears all state)
resetSharedInstance();
Lazy Initialization
By default, the SDK initializes immediately and validates any cached license. To disable this:
const sdk = new LicenseSeat({
apiKey: 'pk_live_xxxxxxxx',
productSlug: 'your-product',
autoInitialize: false // Don't auto-initialize
});
// Later, when ready:
sdk.initialize();
This is useful when you need to:
- Delay network requests until user interaction
- Set up event listeners before initialization
- Control exactly when validation occurs
Offline Support
The SDK supports offline license validation using Ed25519 cryptographic signatures.
Note: The current JavaScript SDK offline flow still uses signed offline tokens. Machine files are the newer preferred offline artifact at the API level, but the JavaScript SDK has not migrated to them yet. Treat offline tokens as the current JS implementation detail, not the long-term product direction.
Enable Offline Fallback
const sdk = new LicenseSeat({
apiKey: 'pk_live_xxxxxxxx',
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'
}
});
How It Works
- Online: License validated against server
- Activation: Legacy offline token + public key established for the current process
- Offline: Cached token verified cryptographically (Ed25519)
- Clock Tamper Detection: Prevents users from rolling back system clock
Automatic fallback is limited to transport failures and HTTP 408. Other HTTP responses, malformed responses, identity mismatches, and local cryptographic or protocol failures remain authoritative and cannot be replaced by cached authorization.
For offline authority that survives an application reload, embed or inject the
expected public key with signingPublicKeys. A key recovered from
localStorage is deliberately ignored because attacker-writable storage cannot
establish trust. A key fetched over authenticated HTTPS can verify artifacts in
the current process, but it is not a durable trust anchor.
Manual Offline Methods
// Sync offline assets (downloads the legacy token + signing key, caches them)
await sdk.syncOfflineAssets();
// Verify cached offline token (use when offline)
const result = await sdk.verifyCachedOffline();
// { valid: true, offline: true, license: {...}, activation: {...} }
// Get offline token from server
const token = await sdk.getOfflineToken();
// Get signing key
const signingKey = await sdk.getSigningKey('key-id-001');
// Verify a specific token manually
const isValid = await sdk.verifyOfflineToken(token, signingKey.public_key);
Offline Token Structure
{
object: 'offline_token',
token: {
schema_version: 1,
license_key: 'LICENSE-KEY',
product_slug: 'your-product',
plan_key: 'pro',
mode: 'hardware_locked',
device_id: 'web-abc123',
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":...}'
}
Offline Validation Result
When offline, validateLicense() returns with offline: true:
const result = await sdk.validateLicense('LICENSE-KEY');
if (result.offline) {
console.log('Validated offline');
}
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);
} else if (error instanceof LicenseError) {
console.log('License error:', error.code);
} else if (error instanceof CryptoError) {
console.log('Crypto error:', error.message);
}
}
Error Types
| Error | Properties | Description |
|---|---|---|
APIError |
status, data |
HTTP request failures |
LicenseError |
code |
License operation failures |
ConfigurationError |
— | SDK misconfiguration |
CryptoError |
— | Cryptographic operation failures |
API Error Format
{
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)
Utility Methods
Test Authentication
try {
const result = await sdk.testAuth();
console.log('Authenticated:', result.authenticated); // Always true if succeeds
console.log('Healthy:', result.healthy); // API health status
console.log('API Version:', result.api_version); // e.g., '1.0.0'
} catch (error) {
console.error('Auth failed:', error?.status ?? 'client_error');
}
Note:
testAuth()calls the protected, side-effect-free/authendpoint. Success proves that the configured credential authenticated with the requiredlicenses:validatescope; the public health endpoint is not used as proof of credential validity.
Reset SDK
Clears all cached data and stops timers:
sdk.reset();
Destroy SDK
Fully destroys the instance and releases all resources:
sdk.destroy();
// Do not use sdk after this
TypeScript Types
All types are exported from the package:
import type {
LicenseSeatConfig,
ActivationOptions,
ValidationOptions,
ValidationResult,
EntitlementCheckResult,
LicenseStatus,
Entitlement,
CachedLicense,
ActivationResponse,
DeactivationResponse,
OfflineToken
} from '@licenseseat/js';
React Integration
import { useState, useEffect, createContext, useContext } from 'react';
import LicenseSeat from '@licenseseat/js';
// Context
const LicenseContext = createContext(null);
// Provider
export function LicenseProvider({ children, config }) {
const [sdk] = useState(() => new LicenseSeat(config));
const [status, setStatus] = useState(sdk.getStatus());
useEffect(() => {
const events = [
'activation:success',
'deactivation:success',
'validation:success',
'validation:failed',
'validation:offline-success',
'validation:offline-failed'
];
const unsubscribers = events.map(event =>
sdk.on(event, () => setStatus(sdk.getStatus()))
);
return () => {
unsubscribers.forEach(unsub => unsub());
sdk.destroy(); // Clean up on unmount
};
}, [sdk]);
return (
<LicenseContext.Provider value={{ sdk, status }}>
{children}
</LicenseContext.Provider>
);
}
// Hook
export function useLicense() {
return useContext(LicenseContext);
}
// Usage
function App() {
return (
<LicenseProvider config={{ apiKey: 'pk_live_xxxxxxxx', productSlug: 'your-product' }}>
<MainApp />
</LicenseProvider>
);
}
function MainApp() {
const { sdk, status } = useLicense();
if (status.status === 'active') {
return <Dashboard hasPro={sdk.hasEntitlement('pro')} />;
}
return <ActivationForm sdk={sdk} />;
}
Browser Usage (No Build Tools)
<!DOCTYPE html>
<html>
<head>
<title>LicenseSeat Demo</title>
</head>
<body>
<input id="license-key" placeholder="Enter license key" />
<button id="activate-btn">Activate</button>
<div id="status"></div>
<script type="module">
import LicenseSeat from 'https://esm.sh/@licenseseat/[email protected]';
const sdk = new LicenseSeat({
apiKey: 'pk_live_xxxxxxxx',
productSlug: 'your-product',
debug: true
});
// Display current status
function updateUI() {
const status = sdk.getStatus();
document.getElementById('status').textContent =
`Status: ${status.status}`;
}
// Listen for changes
sdk.on('activation:success', updateUI);
sdk.on('validation:success', updateUI);
// Handle activation
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);
}
};
updateUI();
</script>
</body>
</html>
Node.js Usage
The SDK is designed for browsers but works in Node.js with polyfills:
// 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]; },
};
const originalKeys = Object.keys;
Object.keys = function(obj) {
if (obj === globalThis.localStorage) return originalKeys(storage);
return originalKeys(obj);
};
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 uses fallback values. For consistent device identification, pass an explicit
deviceIdtoactivate().
Telemetry
When telemetryEnabled is true (the default), the SDK includes the following
optional telemetry on supported POST requests:
| Field | Browser | Node.js | Electron |
|---|---|---|---|
sdk_name |
js |
js |
js |
sdk_version |
Yes | Yes | Yes |
os_name |
Yes | Yes | Yes |
os_version |
Yes | Yes | Yes |
platform |
browser |
node |
electron |
device_model |
Via userAgentData | -- | -- |
device_type |
desktop/phone/tablet |
server |
desktop |
architecture |
Via userAgentData | process.arch |
process.arch |
cpu_cores |
navigator.hardwareConcurrency |
os.cpus().length |
Yes |
memory_gb |
navigator.deviceMemory |
os.totalmem() |
Yes |
locale |
navigator.language |
process.env.LANG |
Yes |
language |
2-letter code from locale | 2-letter code from locale | Yes |
timezone |
Intl.DateTimeFormat |
Intl.DateTimeFormat |
Yes |
screen_resolution |
screen.widthxscreen.height |
-- | Yes |
display_scale |
window.devicePixelRatio |
-- | Yes |
browser_name |
Chrome, Safari, Firefox, Edge | -- | -- |
browser_version |
Detected from UA/brands | -- | -- |
runtime_version |
-- | process.versions.node |
process.versions.electron |
app_version |
From config | From config | From config |
app_build |
From config | From config | From config |
Browsers expose reduced values by default. Set
browserHighEntropyTelemetryEnabled: true to request bounded Chromium
platformVersion, architecture, and model hints. The request never delays
a licensing call, successful values remain in this SDK instance's memory, and
disabling telemetryEnabled prevents both the high-entropy request and
telemetry attachment.
Set telemetryEnabled: false to omit the optional telemetry object. The license
key, product context, device/installation identifier, successful request
activity, and server-visible source IP remain part of the licensing service.
Browser/runtime attributes and persistent fingerprints can be linked device
data; disabling telemetry alone does not establish GDPR or marketplace-policy
compliance.
See Telemetry for the full field reference.
Platform Support
| Platform | Version | Notes |
|---|---|---|
| Chrome | 80+ | Full support |
| Firefox | 75+ | Full support |
| Safari | 14+ | Full support |
| Edge | 80+ | Full support |
| Node.js | 18+ | Requires polyfills |
| Electron | Latest | Full support |
Next Steps
- Swift SDK - For Apple platforms
- Offline Licensing - Air-gapped validation
- API Reference - Direct API access