{"id":40005678,"url":"https://github.com/licenseseat/licenseseat-js","last_synced_at":"2026-02-25T05:43:08.181Z","repository":{"id":300053698,"uuid":"1005086028","full_name":"licenseseat/licenseseat-js","owner":"licenseseat","description":"Official JavaScript + TypeScript SDK for LicenseSeat - Easily add license keys to your game, plugin, or app","archived":false,"fork":false,"pushed_at":"2026-02-09T04:52:11.000Z","size":347,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-02-09T10:53:14.661Z","etag":null,"topics":["browser-extension","browser-game","electron","electron-app","javascript","js","license","license-checking","license-generator","license-management","license-server","licensing","npm","npm-package","pnpm","ts","typescript","yarn"],"latest_commit_sha":null,"homepage":"https://licenseseat.com","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/licenseseat.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-06-19T16:36:55.000Z","updated_at":"2026-02-09T04:52:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"7dc90270-f0f2-4432-85df-ad7784af6c87","html_url":"https://github.com/licenseseat/licenseseat-js","commit_stats":null,"previous_names":["licenseseat/licenseseat-js"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/licenseseat/licenseseat-js","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licenseseat%2Flicenseseat-js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licenseseat%2Flicenseseat-js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licenseseat%2Flicenseseat-js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licenseseat%2Flicenseseat-js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/licenseseat","download_url":"https://codeload.github.com/licenseseat/licenseseat-js/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licenseseat%2Flicenseseat-js/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29810416,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T03:30:18.102Z","status":"ssl_error","status_checked_at":"2026-02-25T03:30:17.799Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["browser-extension","browser-game","electron","electron-app","javascript","js","license","license-checking","license-generator","license-management","license-server","licensing","npm","npm-package","pnpm","ts","typescript","yarn"],"created_at":"2026-01-19T02:05:19.649Z","updated_at":"2026-02-25T05:43:08.173Z","avatar_url":"https://github.com/licenseseat.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LicenseSeat - JavaScript SDK\n\n[![CI](https://github.com/licenseseat/licenseseat-js/actions/workflows/ci.yml/badge.svg)](https://github.com/licenseseat/licenseseat-js/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/@licenseseat/js.svg)](https://www.npmjs.com/package/@licenseseat/js)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n\nThe official JavaScript/TypeScript SDK for [LicenseSeat](https://licenseseat.com) – the simple, secure licensing platform for apps, games, and plugins.\n\n---\n\n## Features\n\n- **License activation \u0026 deactivation** – Activate licenses with automatic device fingerprinting\n- **Online \u0026 offline validation** – Validate licenses with optional offline fallback\n- **Entitlement checking** – Check feature access with `hasEntitlement()` and `checkEntitlement()`\n- **Heartbeat** – Automatic periodic heartbeats to report device activity\n- **Telemetry** – Auto-collected device and environment data sent with each API request\n- **Local caching** – Secure localStorage-based caching with clock tamper detection\n- **Auto-retry with exponential backoff** – Resilient network handling\n- **Event-driven architecture** – Subscribe to SDK lifecycle events\n- **TypeScript support** – Full type definitions included (auto-generated from JSDoc)\n- **Modern ESM package** – Native ES modules, tree-shakeable\n\n---\n\n## Installation\n\n### npm / yarn / pnpm\n\n```bash\n# npm\nnpm install @licenseseat/js\n\n# yarn\nyarn add @licenseseat/js\n\n# pnpm\npnpm add @licenseseat/js\n```\n\n### CDN (Browser)\n\n```html\n\u003c!-- ESM via esm.sh --\u003e\n\u003cscript type=\"module\"\u003e\n  import LicenseSeat from 'https://esm.sh/@licenseseat/js';\n\n  const sdk = new LicenseSeat({\n    apiKey: 'your-api-key',\n    productSlug: 'your-product'\n  });\n\u003c/script\u003e\n\n\u003c!-- ESM via unpkg --\u003e\n\u003cscript type=\"module\"\u003e\n  import LicenseSeat from 'https://unpkg.com/@licenseseat/js/dist/index.js';\n\u003c/script\u003e\n\n\u003c!-- ESM via jsDelivr --\u003e\n\u003cscript type=\"module\"\u003e\n  import LicenseSeat from 'https://cdn.jsdelivr.net/npm/@licenseseat/js/dist/index.js';\n\u003c/script\u003e\n```\n\n---\n\n## Quick Start\n\n### JavaScript (ESM)\n\n```javascript\nimport LicenseSeat from '@licenseseat/js';\n\n// Create SDK instance\nconst sdk = new LicenseSeat({\n  apiKey: 'your-api-key',\n  productSlug: 'your-product',  // Required: Your product slug\n  debug: true\n});\n\n// Activate a license\nawait sdk.activate('YOUR-LICENSE-KEY');\n\n// Check entitlements (simple boolean)\nif (sdk.hasEntitlement('pro')) {\n  // Enable pro features\n}\n\n// Get current status\nconst status = sdk.getStatus();\nconsole.log(status);\n// { status: 'active', license: '...', entitlements: [...] }\n```\n\n### TypeScript\n\n```typescript\nimport LicenseSeat, {\n  type LicenseSeatConfig,\n  type ValidationResult,\n  type EntitlementCheckResult,\n  type LicenseStatus\n} from '@licenseseat/js';\n\nconst config: LicenseSeatConfig = {\n  apiKey: 'your-api-key',\n  productSlug: 'your-product',\n  debug: true\n};\n\nconst sdk = new LicenseSeat(config);\n\n// Full type inference\nconst result: ValidationResult = await sdk.validateLicense('LICENSE-KEY');\nconst status: LicenseStatus = sdk.getStatus();\nconst hasPro: boolean = sdk.hasEntitlement('pro');\n```\n\nTypeScript users get full type support automatically – the package includes generated `.d.ts` declaration files.\n\n---\n\n## Configuration\n\n```javascript\nconst sdk = new LicenseSeat({\n  // Required\n  productSlug: 'your-product',            // Your product slug from LicenseSeat dashboard\n\n  // Required for authenticated operations\n  apiKey: 'your-api-key',\n\n  // API Configuration\n  apiBaseUrl: 'https://licenseseat.com/api/v1',  // Default\n\n  // Storage\n  storagePrefix: 'licenseseat_',              // localStorage key prefix\n\n  // Auto-Validation\n  autoValidateInterval: 3600000,              // 1 hour (in ms)\n  autoInitialize: true,                       // Auto-validate cached license on init\n\n  // Heartbeat\n  heartbeatInterval: 300000,                  // 5 minutes (in ms), 0 to disable\n\n  // Telemetry\n  telemetryEnabled: true,                     // Set false to disable (e.g. GDPR)\n  appVersion: '1.2.0',                        // Your app version (sent in telemetry)\n  appBuild: '42',                             // Your app build number (sent in telemetry)\n\n  // Offline Support\n  offlineFallbackEnabled: false,              // Enable offline validation fallback\n  maxOfflineDays: 0,                          // Max days offline (0 = disabled)\n  offlineLicenseRefreshInterval: 259200000,   // 72 hours\n  maxClockSkewMs: 300000,                     // 5 minutes\n\n  // Network\n  maxRetries: 3,                              // Retry attempts for failed requests\n  retryDelay: 1000,                           // Initial retry delay (ms)\n  networkRecheckInterval: 30000,              // Check connectivity every 30s when offline\n\n  // Debug\n  debug: false                                // Enable console logging\n});\n```\n\n### Configuration Options\n\n| Option                   | Type      | Default                            | Description                                               |\n| ------------------------ | --------- | ---------------------------------- | --------------------------------------------------------- |\n| `productSlug`            | `string`  | –                                  | **Required.** Your product slug from the dashboard        |\n| `apiKey`                 | `string`  | `null`                             | API key for authentication (required for most operations) |\n| `apiBaseUrl`             | `string`  | `'https://licenseseat.com/api/v1'` | API base URL                                              |\n| `storagePrefix`          | `string`  | `'licenseseat_'`                   | Prefix for localStorage keys                              |\n| `autoValidateInterval`   | `number`  | `3600000`                          | Auto-validation interval in ms (1 hour)                   |\n| `autoInitialize`         | `boolean` | `true`                             | Auto-initialize and validate cached license               |\n| `heartbeatInterval`      | `number`  | `300000`                           | Heartbeat interval in ms (5 minutes). Set `0` to disable  |\n| `telemetryEnabled`       | `boolean` | `true`                             | Enable telemetry collection. Set `false` for GDPR compliance |\n| `appVersion`             | `string`  | `null`                             | Your app version string (sent as `app_version` in telemetry) |\n| `appBuild`               | `string`  | `null`                             | Your app build identifier (sent as `app_build` in telemetry) |\n| `offlineFallbackEnabled` | `boolean` | `false`                            | Enable offline validation on network errors               |\n| `maxOfflineDays`         | `number`  | `0`                                | Maximum days license works offline (0 = disabled)         |\n| `maxRetries`             | `number`  | `3`                                | Max retry attempts for failed API calls                   |\n| `retryDelay`             | `number`  | `1000`                             | Initial retry delay in ms (exponential backoff)           |\n| `debug`                  | `boolean` | `false`                            | Enable debug logging to console                           |\n\n---\n\n## API Reference\n\n### Core Methods\n\n#### `sdk.activate(licenseKey, options?)`\n\nActivates a license key on this device.\n\n```javascript\nconst result = await sdk.activate('LICENSE-KEY', {\n  deviceId: 'custom-device-id',       // Optional: auto-generated if not provided\n  deviceName: \"John's MacBook Pro\",   // Optional: human-readable device name\n  metadata: { version: '1.0.0' }      // Optional: custom metadata\n});\n\nconsole.log(result);\n// {\n//   license_key: 'LICENSE-KEY',\n//   device_id: 'web-abc123',\n//   activated_at: '2024-01-15T10:30:00Z',\n//   activation: {\n//     object: 'activation',\n//     id: 123,\n//     device_id: 'web-abc123',\n//     license_key: 'LICENSE-KEY',\n//     activated_at: '2024-01-15T10:30:00Z',\n//     license: { ... }\n//   }\n// }\n```\n\n#### `sdk.deactivate()`\n\nDeactivates the current license and clears cached data.\n\n```javascript\nconst result = await sdk.deactivate();\nconsole.log(result);\n// {\n//   object: 'deactivation',\n//   activation_id: 123,\n//   deactivated_at: '2024-01-15T12:00:00Z'\n// }\n```\n\n#### `sdk.validateLicense(licenseKey, options?)`\n\nValidates a license with the server.\n\n```javascript\nconst result = await sdk.validateLicense('LICENSE-KEY', {\n  deviceId: 'device-id'  // Optional: required for hardware_locked mode\n});\n\nconsole.log(result);\n// {\n//   valid: true,\n//   license: {\n//     key: 'LICENSE-KEY',\n//     status: 'active',\n//     mode: 'hardware_locked',\n//     plan_key: 'pro',\n//     active_seats: 1,\n//     seat_limit: 3,\n//     active_entitlements: [\n//       { key: 'pro', expires_at: null, metadata: null },\n//       { key: 'beta', expires_at: '2024-12-31T23:59:59Z', metadata: null }\n//     ],\n//     product: { slug: 'your-product', name: 'Your Product' }\n//   },\n//   active_entitlements: [...]\n// }\n```\n\n### Entitlement Methods\n\n\u003e **Note:** Entitlements are optional. A license may have zero entitlements if the associated plan has no entitlements configured. The `active_entitlements` array may be empty or the field may be undefined/null.\n\n#### `sdk.hasEntitlement(key)`\n\nCheck if an entitlement is active. Returns a simple boolean. Returns `false` if no entitlements exist.\n\n```javascript\nif (sdk.hasEntitlement('pro')) {\n  enableProFeatures();\n}\n\nif (sdk.hasEntitlement('beta')) {\n  showBetaUI();\n}\n```\n\n#### `sdk.checkEntitlement(key)`\n\nCheck entitlement with detailed information.\n\n```javascript\nconst result = sdk.checkEntitlement('pro');\n\nif (result.active) {\n  console.log('Entitlement:', result.entitlement);\n  console.log('Expires:', result.entitlement.expires_at);\n} else {\n  console.log('Reason:', result.reason);\n  // Possible reasons: 'no_license', 'not_found', 'expired'\n}\n```\n\n### Status Methods\n\n#### `sdk.getStatus()`\n\nGet current license status.\n\n```javascript\nconst status = sdk.getStatus();\n\n// Possible status values:\n// - 'inactive': No license activated\n// - 'pending': License pending validation\n// - 'active': License valid (online)\n// - 'invalid': License invalid\n// - 'offline-valid': License valid (offline verification)\n// - 'offline-invalid': License invalid (offline verification)\n\nconsole.log(status);\n// {\n//   status: 'active',\n//   license: 'LICENSE-KEY',\n//   device: 'web-abc123',\n//   activated_at: '2024-01-15T10:30:00Z',\n//   last_validated: '2024-01-15T11:30:00Z',\n//   entitlements: [...]\n// }\n```\n\n#### `sdk.testAuth()`\n\nTest API connectivity by calling the `/health` endpoint. Returns health status and API version.\n\n```javascript\ntry {\n  const result = await sdk.testAuth();\n  console.log('Authenticated:', result.authenticated);  // Always true if request succeeds\n  console.log('Healthy:', result.healthy);              // API health status\n  console.log('API Version:', result.api_version);      // e.g., '1.0.0'\n} catch (error) {\n  console.error('Connection failed:', error);\n}\n```\n\n\u003e **Note:** This method tests API connectivity, not API key validity. A successful response means the API is reachable. Authentication errors will surface when calling protected endpoints like `activate()` or `validateLicense()`.\n\n#### `sdk.heartbeat()`\n\nSend 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.\n\n```javascript\ntry {\n  const result = await sdk.heartbeat();\n  console.log('Heartbeat received at:', result.received_at);\n} catch (error) {\n  console.error('Heartbeat failed:', error);\n}\n```\n\nReturns `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.\n\nTo disable auto-heartbeat, set `heartbeatInterval: 0` in the configuration.\n\n#### `sdk.reset()`\n\nClear all cached data and reset SDK state.\n\n```javascript\nsdk.reset();\n```\n\n#### `sdk.destroy()`\n\nDestroy 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.\n\n```javascript\n// When unmounting a component or closing an app\nsdk.destroy();\n```\n\n#### `sdk.initialize()`\n\nManually initialize the SDK (only needed if `autoInitialize: false`).\n\n```javascript\nconst sdk = new LicenseSeat({\n  apiKey: 'key',\n  productSlug: 'your-product',\n  autoInitialize: false  // Don't auto-initialize\n});\n\n// Later, when ready:\nsdk.initialize();\n```\n\n---\n\n## Events\n\nSubscribe to SDK lifecycle events for reactive UIs.\n\n```javascript\n// Subscribe\nconst unsubscribe = sdk.on('activation:success', (data) =\u003e {\n  console.log('License activated:', data);\n});\n\n// Unsubscribe\nunsubscribe();\n// or\nsdk.off('activation:success', handler);\n```\n\n### Available Events\n\n| Event                               | Description                         | Data                            |\n| ----------------------------------- | ----------------------------------- | ------------------------------- |\n| **Lifecycle**                       |                                     |                                 |\n| `license:loaded`                    | Cached license loaded on init       | `CachedLicense`                 |\n| `sdk:reset`                         | SDK was reset                       | –                               |\n| `sdk:destroyed`                     | SDK was destroyed                   | –                               |\n| `sdk:error`                         | General SDK error                   | `{ message, error? }`           |\n| **Activation**                      |                                     |                                 |\n| `activation:start`                  | Activation started                  | `{ licenseKey, deviceId }`      |\n| `activation:success`                | Activation succeeded                | `CachedLicense`                 |\n| `activation:error`                  | Activation failed                   | `{ licenseKey, error }`         |\n| **Deactivation**                    |                                     |                                 |\n| `deactivation:start`                | Deactivation started                | `CachedLicense`                 |\n| `deactivation:success`              | Deactivation succeeded              | `DeactivationResponse`          |\n| `deactivation:error`                | Deactivation failed                 | `{ error, license }`            |\n| **Validation**                      |                                     |                                 |\n| `validation:start`                  | Validation started                  | `{ licenseKey }`                |\n| `validation:success`                | Online validation succeeded         | `ValidationResult`              |\n| `validation:failed`                 | Validation failed (invalid license) | `ValidationResult`              |\n| `validation:error`                  | Validation error (network, etc.)    | `{ licenseKey, error }`         |\n| `validation:offline-success`        | Offline validation succeeded        | `ValidationResult`              |\n| `validation:offline-failed`         | Offline validation failed           | `ValidationResult`              |\n| `validation:auth-failed`            | Auth failed during validation       | `{ licenseKey, error, cached }` |\n| **Auto-Validation**                 |                                     |                                 |\n| `autovalidation:cycle`              | Auto-validation scheduled           | `{ nextRunAt: Date }`           |\n| `autovalidation:stopped`            | Auto-validation stopped             | –                               |\n| **Heartbeat**                       |                                     |                                 |\n| `heartbeat:success`                 | Heartbeat acknowledged by server    | `HeartbeatResponse`             |\n| `heartbeat:cycle`                   | Auto-heartbeat tick completed       | `{ nextRunAt: Date }`           |\n| **Network**                         |                                     |                                 |\n| `network:online`                    | Network connectivity restored       | –                               |\n| `network:offline`                   | Network connectivity lost           | `{ error }`                     |\n| **Offline Token**                   |                                     |                                 |\n| `offlineToken:fetching`             | Fetching offline token              | `{ licenseKey }`                |\n| `offlineToken:fetched`              | Offline token fetched               | `{ licenseKey, data }`          |\n| `offlineToken:fetchError`           | Offline token fetch failed          | `{ licenseKey, error }`         |\n| `offlineToken:ready`                | Offline assets synced               | `{ kid, exp_at }`               |\n| `offlineToken:verified`             | Offline signature verified          | `{ payload }`                   |\n| `offlineToken:verificationFailed`   | Offline signature invalid           | `{ payload }`                   |\n\n---\n\n## Singleton Pattern\n\nFor applications that need a shared SDK instance:\n\n```javascript\nimport { configure, getSharedInstance, resetSharedInstance } from '@licenseseat/js';\n\n// Configure once at app startup\nconfigure({\n  apiKey: 'your-key',\n  productSlug: 'your-product'\n});\n\n// Use anywhere in your app\nconst sdk = getSharedInstance();\nawait sdk.activate('LICENSE-KEY');\n\n// Reset if needed\nresetSharedInstance();\n```\n\n---\n\n## Offline Support\n\nThe SDK supports offline license validation using cryptographically signed offline tokens (Ed25519).\n\n```javascript\nconst sdk = new LicenseSeat({\n  apiKey: 'your-key',\n  productSlug: 'your-product',\n  offlineFallbackEnabled: true,  // Enable offline fallback\n  maxOfflineDays: 7              // Allow 7 days offline\n});\n\n// After activation, offline assets are automatically synced\nawait sdk.activate('LICENSE-KEY');\n\n// Later, even offline, validation will work using cached data\nconst result = await sdk.validateLicense('LICENSE-KEY');\nif (result.offline) {\n  console.log('Validated offline');\n}\n```\n\n### How Offline Validation Works\n\n1. On activation, the SDK fetches a signed offline token from the server\n2. The offline token contains:\n   - License data (key, plan, entitlements, expiration)\n   - Ed25519 signature\n   - Canonical JSON for verification\n3. When offline, the SDK verifies the signature locally\n4. Clock tamper detection prevents users from bypassing expiration\n\n### Offline Methods\n\n#### `sdk.syncOfflineAssets()`\n\nFetches the offline token and signing key from the server. Uses the currently cached license. Call this after activation to prepare for offline usage.\n\n```javascript\n// First activate (caches the license)\nawait sdk.activate('LICENSE-KEY');\n\n// Then sync offline assets (uses cached license)\nconst assets = await sdk.syncOfflineAssets();\nconsole.log('Offline token key ID:', assets.kid);\nconsole.log('Expires at:', assets.exp_at);\n```\n\n#### `sdk.getOfflineToken()`\n\nFetches a signed offline token for the currently cached license. Returns the token structure containing the license data and Ed25519 signature.\n\n```javascript\n// Must have an active license cached first\nconst token = await sdk.getOfflineToken();\nconsole.log(token);\n// {\n//   object: 'offline_token',\n//   token: { license_key, product_slug, plan_key, ... },\n//   signature: { algorithm: 'Ed25519', key_id, value },\n//   canonical: '...'\n// }\n```\n\n#### `sdk.getSigningKey(keyId)`\n\nFetches the Ed25519 public key used for verifying offline token signatures.\n\n```javascript\nconst signingKey = await sdk.getSigningKey('key-id-001');\nconsole.log(signingKey);\n// {\n//   object: 'signing_key',\n//   kid: 'key-id-001',\n//   public_key: 'base64-encoded-public-key',\n//   algorithm: 'Ed25519',\n//   created_at: '2024-01-01T00:00:00Z'\n// }\n```\n\n#### `sdk.verifyOfflineToken(token, publicKeyB64)`\n\nVerifies an offline token's Ed25519 signature locally. **Both parameters are required.**\n\n```javascript\n// Fetch the token and signing key first\nconst token = await sdk.getOfflineToken();\nconst signingKey = await sdk.getSigningKey(token.signature.key_id);\n\n// Verify the signature\nconst isValid = await sdk.verifyOfflineToken(token, signingKey.public_key);\nconsole.log('Signature valid:', isValid);\n```\n\n\u003e **Important:** The `verifyOfflineToken()` method requires you to pass both the token and the public key. Fetch the signing key using `getSigningKey()` with the `key_id` from the token's signature.\n\n### Offline Token Structure\n\n```javascript\n{\n  object: 'offline_token',\n  token: {\n    schema_version: 1,\n    license_key: 'LICENSE-KEY',\n    product_slug: 'your-product',\n    plan_key: 'pro',\n    mode: 'hardware_locked',\n    device_id: 'web-abc123',\n    iat: 1704067200,        // Issued at (Unix timestamp)\n    exp: 1706659200,        // Expires at (Unix timestamp)\n    nbf: 1704067200,        // Not before (Unix timestamp)\n    license_expires_at: null,\n    kid: 'key-id-001',\n    entitlements: [\n      { key: 'pro', expires_at: null }\n    ],\n    metadata: {}\n  },\n  signature: {\n    algorithm: 'Ed25519',\n    key_id: 'key-id-001',\n    value: 'base64url-encoded-signature'\n  },\n  canonical: '{\"entitlements\":[...],\"exp\":...}'\n}\n```\n\n---\n\n## Telemetry\n\nThe SDK automatically collects non-PII (non-personally-identifiable) device and environment data and includes it with every POST request sent to the LicenseSeat API. This data helps you understand what platforms and environments your customers use.\n\nTelemetry is **enabled by default** and can be disabled at any time.\n\n### Collected Fields\n\n| Field                | Type     | Example                | Description                                      |\n| -------------------- | -------- | ---------------------- | ------------------------------------------------ |\n| `sdk_name`           | `string` | `\"js\"`                 | Always `\"js\"` for this SDK                       |\n| `sdk_version`        | `string` | `\"0.4.0\"`              | SDK version                                      |\n| `os_name`            | `string` | `\"macOS\"`              | Operating system name                            |\n| `os_version`         | `string` | `\"14.2.1\"`             | Operating system version                         |\n| `platform`           | `string` | `\"browser\"`            | Runtime platform (`browser`, `node`, `electron`, `react-native`, `deno`, `bun`) |\n| `device_model`       | `string` | `null`                 | Device model (Chromium userAgentData only)        |\n| `device_type`        | `string` | `\"desktop\"`            | Device type (`desktop`, `phone`, `tablet`, `server`) |\n| `locale`             | `string` | `\"en-US\"`              | Full locale string                               |\n| `timezone`           | `string` | `\"America/New_York\"`   | IANA timezone                                    |\n| `language`           | `string` | `\"en\"`                 | 2-letter language code                           |\n| `architecture`       | `string` | `\"arm64\"`              | CPU architecture                                 |\n| `cpu_cores`          | `number` | `10`                   | Number of logical CPU cores                      |\n| `memory_gb`          | `number` | `16`                   | Approximate RAM in GB (Chrome/Node.js only)      |\n| `screen_resolution`  | `string` | `\"1920x1080\"`          | Screen resolution                                |\n| `display_scale`      | `number` | `2`                    | Device pixel ratio                               |\n| `browser_name`       | `string` | `\"Chrome\"`             | Browser name (browser environments only)         |\n| `browser_version`    | `string` | `\"123.0\"`              | Browser version (browser environments only)      |\n| `runtime_version`    | `string` | `\"20.11.0\"`            | Runtime version (Node.js, Deno, Bun, Electron)   |\n| `app_version`        | `string` | `\"1.2.0\"`              | Your app version (from `appVersion` config)      |\n| `app_build`          | `string` | `\"42\"`                 | Your app build (from `appBuild` config)          |\n\nFields that cannot be detected in the current environment are omitted (not sent as `null`).\n\n### Providing App Version\n\nPass your own app version and build number via configuration so they appear in telemetry:\n\n```javascript\nconst sdk = new LicenseSeat({\n  apiKey: 'your-key',\n  productSlug: 'your-product',\n  appVersion: '1.2.0',\n  appBuild: '42'\n});\n```\n\n### Disabling Telemetry\n\nTo disable telemetry collection entirely (for example, to comply with GDPR or other privacy regulations):\n\n```javascript\nconst sdk = new LicenseSeat({\n  apiKey: 'your-key',\n  productSlug: 'your-product',\n  telemetryEnabled: false\n});\n```\n\nWhen telemetry is disabled, no device or environment data is attached to API requests.\n\n### Privacy\n\nTelemetry collects only non-personally-identifiable information. No IP addresses, user names, email addresses, or other PII are collected by the SDK. The data is used solely to help you understand the platforms and environments where your software is used.\n\nTelemetry is opt-out: set `telemetryEnabled: false` to disable it completely.\n\n---\n\n## Heartbeat\n\nThe 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.\n\n### How It Works\n\n- After a license is activated (or when the SDK initializes with a cached license), a heartbeat timer starts automatically.\n- The default interval is **5 minutes** (`300000` ms), configurable via `heartbeatInterval`.\n- Each heartbeat sends the current `device_id` to the server.\n- Heartbeats also run alongside auto-validation cycles.\n\n### Manual Heartbeat\n\nYou can send a heartbeat at any time:\n\n```javascript\nawait sdk.heartbeat();\n```\n\n### Configuring the Interval\n\n```javascript\nconst sdk = new LicenseSeat({\n  apiKey: 'your-key',\n  productSlug: 'your-product',\n  heartbeatInterval: 600000  // 10 minutes\n});\n```\n\nSet `heartbeatInterval: 0` to disable auto-heartbeat entirely. You can still call `sdk.heartbeat()` manually.\n\n### Heartbeat Events\n\n| Event               | Description                        | Data                    |\n| -------------------- | ---------------------------------- | ----------------------- |\n| `heartbeat:success`  | Heartbeat acknowledged by server   | `HeartbeatResponse`     |\n| `heartbeat:cycle`    | Auto-heartbeat tick completed      | `{ nextRunAt: Date }`   |\n\n```javascript\nsdk.on('heartbeat:success', (data) =\u003e {\n  console.log('Heartbeat received at:', data.received_at);\n});\n```\n\n### Heartbeat Lifecycle\n\n- **Starts** automatically after `sdk.activate()` succeeds, or on SDK init if a cached license exists.\n- **Stops** automatically when `sdk.deactivate()`, `sdk.reset()`, or `sdk.destroy()` is called.\n- Heartbeat failures are logged (in debug mode) but do not throw or interrupt the SDK.\n\n---\n\n## Error Handling\n\nThe SDK exports custom error classes for precise error handling:\n\n```javascript\nimport LicenseSeat, {\n  APIError,\n  LicenseError,\n  ConfigurationError,\n  CryptoError\n} from '@licenseseat/js';\n\ntry {\n  await sdk.activate('INVALID-KEY');\n} catch (error) {\n  if (error instanceof APIError) {\n    console.log('HTTP Status:', error.status);\n    console.log('Error Code:', error.data?.error?.code);\n    console.log('Error Message:', error.data?.error?.message);\n  } else if (error instanceof LicenseError) {\n    console.log('License error:', error.code);\n  } else if (error instanceof ConfigurationError) {\n    console.log('Config error:', error.message);\n  }\n}\n```\n\n### Error Types\n\n| Error                | Description                                          |\n| -------------------- | ---------------------------------------------------- |\n| `APIError`           | HTTP request failures (includes `status` and `data`) |\n| `LicenseError`       | License operation failures (includes `code`)         |\n| `ConfigurationError` | SDK misconfiguration (e.g., missing `productSlug`)   |\n| `CryptoError`        | Cryptographic operation failures                     |\n\n### API Error Format\n\nAPI errors follow this structure:\n\n```javascript\n{\n  error: {\n    code: 'license_not_found',       // Machine-readable error code\n    message: 'License not found.',   // Human-readable message\n    details: { ... }                 // Optional additional details\n  }\n}\n```\n\nCommon error codes:\n- `unauthorized` - Invalid or missing API key\n- `license_not_found` - License key doesn't exist\n- `license_expired` - License has expired\n- `license_suspended` - License is suspended\n- `license_revoked` - License has been revoked\n- `seat_limit_reached` - No more seats available\n- `device_already_activated` - Device is already activated\n- `activation_not_found` - Activation doesn't exist (for deactivation)\n\n---\n\n## Browser Support\n\n- **Modern browsers**: Chrome 80+, Firefox 75+, Safari 14+, Edge 80+\n- **Bundlers**: Vite, Webpack, Rollup, esbuild, Parcel\n- **Node.js**: 18+ (requires polyfills - see below)\n\n### Node.js Usage\n\nThe SDK is designed for browsers but works in Node.js with polyfills. Add these before importing the SDK:\n\n```javascript\n// Required polyfills for Node.js\nconst storage = {};\nglobalThis.localStorage = {\n  getItem(key) { return Object.prototype.hasOwnProperty.call(storage, key) ? storage[key] : null; },\n  setItem(key, value) { storage[key] = String(value); },\n  removeItem(key) { delete storage[key]; },\n  clear() { for (const key in storage) delete storage[key]; },\n};\n\n// Override Object.keys to support localStorage iteration (used by cache.getAllKeys())\nconst originalKeys = Object.keys;\nObject.keys = function(obj) {\n  if (obj === globalThis.localStorage) return originalKeys(storage);\n  return originalKeys(obj);\n};\n\n// Device fingerprinting polyfills (provides stable fallback values)\nglobalThis.document = { createElement: () =\u003e ({ getContext: () =\u003e null }), querySelector: () =\u003e null };\nglobalThis.window = { navigator: {}, screen: {} };\nglobalThis.navigator = { userAgent: 'Node.js', language: 'en', hardwareConcurrency: 4 };\n\n// Now import the SDK\nconst { default: LicenseSeat } = await import('@licenseseat/js');\n```\n\n\u003e **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 `deviceId` to `activate()`.\n\n---\n\n## Usage Guide\n\n### For JavaScript Users\n\nSimply import and use:\n\n```javascript\nimport LicenseSeat from '@licenseseat/js';\n\nconst sdk = new LicenseSeat({\n  apiKey: 'your-key',\n  productSlug: 'your-product'\n});\n```\n\n### For TypeScript Users\n\nThe package includes TypeScript declarations (`.d.ts` files) automatically. No additional `@types/` package needed.\n\n```typescript\nimport LicenseSeat from '@licenseseat/js';\n\n// Types are automatically available\nconst sdk = new LicenseSeat({\n  apiKey: 'your-key',\n  productSlug: 'your-product'\n});\n\n// Import specific types if needed\nimport type {\n  LicenseSeatConfig,\n  ValidationResult,\n  EntitlementCheckResult,\n  LicenseStatus,\n  Entitlement,\n  CachedLicense,\n  ActivationResponse,\n  DeactivationResponse,\n  OfflineToken\n} from '@licenseseat/js';\n```\n\n### For CDN/Browser Users\n\nUse ES modules via CDN:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003ctitle\u003eLicenseSeat Demo\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cscript type=\"module\"\u003e\n    import LicenseSeat from 'https://esm.sh/@licenseseat/js';\n\n    const sdk = new LicenseSeat({\n      apiKey: 'your-api-key',\n      productSlug: 'your-product',\n      debug: true\n    });\n\n    // Check for existing license\n    const status = sdk.getStatus();\n    if (status.status === 'active') {\n      console.log('Already licensed!');\n    }\n\n    // Activate (example with user input)\n    document.getElementById('activate-btn').onclick = async () =\u003e {\n      const key = document.getElementById('license-key').value;\n      try {\n        await sdk.activate(key);\n        alert('License activated!');\n      } catch (e) {\n        alert('Activation failed: ' + e.message);\n      }\n    };\n  \u003c/script\u003e\n\n  \u003cinput id=\"license-key\" placeholder=\"Enter license key\" /\u003e\n  \u003cbutton id=\"activate-btn\"\u003eActivate\u003c/button\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n---\n\n## Development\n\n### Setup\n\n```bash\ngit clone https://github.com/licenseseat/licenseseat-js.git\ncd licenseseat-js\nnpm install\n```\n\n### Scripts\n\n| Command                 | Description                               |\n| ----------------------- | ----------------------------------------- |\n| `npm run build`         | Build JS bundle + TypeScript declarations |\n| `npm run build:js`      | Build JavaScript bundle only              |\n| `npm run build:types`   | Generate TypeScript declarations          |\n| `npm run build:iife`    | Build global/IIFE bundle                  |\n| `npm run dev`           | Watch mode for development                |\n| `npm test`              | Run tests                                 |\n| `npm run test:watch`    | Run tests in watch mode                   |\n| `npm run test:coverage` | Run tests with coverage report            |\n| `npm run typecheck`     | Type-check without emitting               |\n\n### Integration Tests\n\nThe 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.\n\n#### Running Integration Tests (Node.js)\n\n```bash\n# Set environment variables\nexport LICENSESEAT_API_KEY=\"ls_your_api_key_here\"\nexport LICENSESEAT_PRODUCT_SLUG=\"your-product\"\nexport LICENSESEAT_LICENSE_KEY=\"YOUR-LICENSE-KEY\"\n\n# Run the tests\nnode test-live.mjs\n```\n\nOr with inline environment variables:\n\n```bash\nLICENSESEAT_API_KEY=ls_xxx LICENSESEAT_PRODUCT_SLUG=my-app LICENSESEAT_LICENSE_KEY=XXX-XXX node test-live.mjs\n```\n\n#### Running Integration Tests (Browser)\n\nOpen `test-live.html` in a browser. You'll be prompted to enter your credentials:\n\n1. **API Key** - Your LicenseSeat API key (starts with `ls_`)\n2. **Product Slug** - Your product identifier\n3. **License Key** - A valid license key for testing\n\nCredentials are stored in `localStorage` for convenience during development.\n\n#### What the Integration Tests Cover\n\n| Category | Tests |\n|----------|-------|\n| **Initialization** | SDK setup, configuration defaults |\n| **Activation** | License activation, device ID generation |\n| **Validation** | Online validation, entitlement checking |\n| **Deactivation** | License deactivation, cache clearing |\n| **Offline Crypto** | Ed25519 signature verification, offline token fetching, tamper detection |\n| **Error Handling** | Invalid licenses, missing config |\n| **Singleton** | Shared instance pattern |\n\n### Project Structure\n\n```\nlicenseseat-js/\n├── src/\n│   ├── index.js          # Entry point, exports\n│   ├── LicenseSeat.js    # Main SDK class\n│   ├── telemetry.js      # Telemetry collection (device/environment data)\n│   ├── cache.js          # LicenseCache (localStorage)\n│   ├── errors.js         # Error classes\n│   ├── types.js          # JSDoc type definitions\n│   └── utils.js          # Utility functions\n├── tests/                # Unit tests (mocked API)\n│   ├── setup.js          # Test setup\n│   ├── mocks/            # MSW handlers\n│   ├── LicenseSeat.test.js\n│   └── utils.test.js\n├── test-live.mjs         # Integration tests (Node.js)\n├── test-live.html        # Integration tests (Browser)\n├── dist/                 # Build output\n│   ├── index.js          # ESM bundle\n│   └── types/            # TypeScript declarations\n├── package.json\n├── tsconfig.json\n└── vitest.config.js\n```\n\n---\n\n## Publishing\n\n### Publishing to npm\n\n1. **Update version** in `package.json`:\n   ```bash\n   npm version patch  # or minor, major\n   ```\n\n2. **Build the package**:\n   ```bash\n   npm run build\n   ```\n\n3. **Verify the build**:\n   ```bash\n   # Check what will be published\n   npm pack --dry-run\n\n   # Verify TypeScript types\n   ls dist/types/\n   ```\n\n4. **Publish**:\n   ```bash\n   # Login if needed\n   npm login\n\n   # Publish (public package)\n   npm publish --access public\n   ```\n\n### What Gets Published\n\nThe `files` field in `package.json` controls what's included:\n\n```json\n{\n  \"files\": [\"dist/\", \"src/\"]\n}\n```\n\nUsers receive:\n- `dist/index.js` – ESM bundle (JavaScript)\n- `dist/types/*.d.ts` – TypeScript declarations\n- `src/*.js` – Source files (for debugging/reference)\n\n### Package Exports\n\n```json\n{\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.js\",\n  \"types\": \"dist/types/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.js\",\n      \"types\": \"./dist/types/index.d.ts\"\n    }\n  }\n}\n```\n\nThis ensures:\n- JavaScript users get `dist/index.js`\n- TypeScript users get type definitions from `dist/types/index.d.ts`\n- Both ESM `import` and bundlers work correctly\n\n### CDN Distribution\n\nOnce published to npm, the package is automatically available on CDNs:\n\n| CDN          | URL                                                          |\n| ------------ | ------------------------------------------------------------ |\n| **esm.sh**   | `https://esm.sh/@licenseseat/js`                             |\n| **unpkg**    | `https://unpkg.com/@licenseseat/js/dist/index.js`            |\n| **jsDelivr** | `https://cdn.jsdelivr.net/npm/@licenseseat/js/dist/index.js` |\n| **Skypack**  | `https://cdn.skypack.dev/@licenseseat/js`                    |\n\n**Version pinning** (recommended for production):\n```html\n\u003cscript type=\"module\"\u003e\n  import LicenseSeat from 'https://esm.sh/@licenseseat/js@0.4.0';\n\u003c/script\u003e\n```\n\n### Self-Hosting\n\nTo host the SDK yourself:\n\n1. Build the package:\n   ```bash\n   npm run build\n   ```\n\n2. Copy `dist/index.js` to your CDN/server\n\n3. Serve with correct MIME type (`application/javascript`) and CORS headers\n\n### Building an IIFE Bundle (Legacy Browsers)\n\nFor a global `LicenseSeat` variable (non-module script tags):\n\n```bash\nnpm run build:iife\n```\n\nThis creates `dist/index.global.js`:\n\n```html\n\u003cscript src=\"/path/to/index.global.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  const sdk = new LicenseSeat({\n    apiKey: 'your-key',\n    productSlug: 'your-product'\n  });\n\u003c/script\u003e\n```\n\n---\n\n## Versioning\n\nThis project follows [Semantic Versioning](https://semver.org/):\n\n- **MAJOR** (1.0.0 → 2.0.0): Breaking changes\n- **MINOR** (1.0.0 → 1.1.0): New features (backward compatible)\n- **PATCH** (1.0.0 → 1.0.1): Bug fixes\n\n---\n\n## Migration from v0.2.x\n\n### Breaking Changes in v0.3.0\n\nThis version introduces the v1 API with significant changes:\n\n| Change                           | Before (v0.2.x)                 | After (v0.3.0)                            |\n| -------------------------------- | ------------------------------- | ----------------------------------------- |\n| `productSlug` config             | Not required                    | **Required** for all API operations       |\n| `apiBaseUrl` default             | `https://licenseseat.com/api`   | `https://licenseseat.com/api/v1`          |\n| `deviceIdentifier` option        | `deviceIdentifier`              | `deviceId`                                |\n| `device_identifier` field        | `device_identifier`             | `device_id`                               |\n| Deactivation response            | Returns full activation object  | Returns `{ object, activation_id, deactivated_at }` |\n| `getOfflineLicense()` method     | Available                       | Renamed to `getOfflineToken()`            |\n| `getPublicKey()` method          | Available                       | Renamed to `getSigningKey()`              |\n| Offline license structure        | Legacy format                   | New token/signature/canonical format      |\n| Error format                     | Various                         | `{ error: { code, message, details? } }`  |\n\n### Migration Steps\n\n1. **Add `productSlug` to configuration:**\n   ```javascript\n   // Before\n   const sdk = new LicenseSeat({ apiKey: 'key' });\n\n   // After\n   const sdk = new LicenseSeat({\n     apiKey: 'key',\n     productSlug: 'your-product'  // Required!\n   });\n   ```\n\n2. **Update activation options:**\n   ```javascript\n   // Before\n   await sdk.activate('KEY', { deviceIdentifier: 'id' });\n\n   // After\n   await sdk.activate('KEY', { deviceId: 'id' });\n   ```\n\n3. **Update response field access:**\n   ```javascript\n   // Before\n   const result = await sdk.activate('KEY');\n   console.log(result.device_identifier);\n\n   // After\n   const result = await sdk.activate('KEY');\n   console.log(result.device_id);\n   ```\n\n4. **Update deactivation handling:**\n   ```javascript\n   // Before\n   const result = await sdk.deactivate();\n   console.log(result.license_key);\n\n   // After\n   const result = await sdk.deactivate();\n   console.log(result.activation_id);\n   console.log(result.deactivated_at);\n   ```\n\n5. **Update offline method calls:**\n   ```javascript\n   // Before\n   await sdk.getOfflineLicense(key);\n   await sdk.getPublicKey(keyId);\n\n   // After (note: getOfflineToken uses cached license, no parameter needed)\n   await sdk.getOfflineToken();\n   await sdk.getSigningKey(keyId);\n   ```\n\n---\n\n## License\n\nMIT License – see [LICENSE](LICENSE) for details.\n\n---\n\n## Links\n\n- [LicenseSeat Website](https://licenseseat.com)\n- [Documentation](https://licenseseat.com/docs)\n- [API Reference](https://licenseseat.com/docs/api)\n- [GitHub Repository](https://github.com/licenseseat/licenseseat-js)\n- [npm Package](https://www.npmjs.com/package/@licenseseat/js)\n- [Report Issues](https://github.com/licenseseat/licenseseat-js/issues)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flicenseseat%2Flicenseseat-js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flicenseseat%2Flicenseseat-js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flicenseseat%2Flicenseseat-js/lists"}