@licenseseat/js 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # LicenseSeat JavaScript SDK
2
-
3
- Official JavaScript/TypeScript SDK for [LicenseSeat](https://licenseseat.com) – the simple, secure licensing platform for apps, games, and plugins.
1
+ # LicenseSeat - JavaScript SDK
4
2
 
5
3
  [![CI](https://github.com/licenseseat/licenseseat-js/actions/workflows/ci.yml/badge.svg)](https://github.com/licenseseat/licenseseat-js/actions/workflows/ci.yml)
6
4
  [![npm version](https://img.shields.io/npm/v/@licenseseat/js.svg)](https://www.npmjs.com/package/@licenseseat/js)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Official JavaScript/TypeScript SDK for [LicenseSeat](https://licenseseat.com) – the simple, secure licensing platform for apps, games, and plugins.
8
8
 
9
9
  ---
10
10
 
@@ -121,7 +121,7 @@ const sdk = new LicenseSeat({
121
121
  apiKey: 'your-api-key',
122
122
 
123
123
  // API Configuration
124
- apiBaseUrl: 'https://api.licenseseat.com', // Default
124
+ apiBaseUrl: 'https://licenseseat.com/api', // Default
125
125
 
126
126
  // Storage
127
127
  storagePrefix: 'licenseseat_', // localStorage key prefix
@@ -148,18 +148,18 @@ const sdk = new LicenseSeat({
148
148
 
149
149
  ### Configuration Options
150
150
 
151
- | Option | Type | Default | Description |
152
- |--------|------|---------|-------------|
153
- | `apiKey` | `string` | `null` | API key for authentication (required for most operations) |
154
- | `apiBaseUrl` | `string` | `'https://api.licenseseat.com'` | API base URL |
155
- | `storagePrefix` | `string` | `'licenseseat_'` | Prefix for localStorage keys |
156
- | `autoValidateInterval` | `number` | `3600000` | Auto-validation interval in ms (1 hour) |
157
- | `autoInitialize` | `boolean` | `true` | Auto-initialize and validate cached license |
158
- | `offlineFallbackEnabled` | `boolean` | `false` | Enable offline validation on network errors |
159
- | `maxOfflineDays` | `number` | `0` | Maximum days license works offline (0 = disabled) |
160
- | `maxRetries` | `number` | `3` | Max retry attempts for failed API calls |
161
- | `retryDelay` | `number` | `1000` | Initial retry delay in ms (exponential backoff) |
162
- | `debug` | `boolean` | `false` | Enable debug logging to console |
151
+ | Option | Type | Default | Description |
152
+ | ------------------------ | --------- | ------------------------------- | --------------------------------------------------------- |
153
+ | `apiKey` | `string` | `null` | API key for authentication (required for most operations) |
154
+ | `apiBaseUrl` | `string` | `'https://licenseseat.com/api'` | API base URL |
155
+ | `storagePrefix` | `string` | `'licenseseat_'` | Prefix for localStorage keys |
156
+ | `autoValidateInterval` | `number` | `3600000` | Auto-validation interval in ms (1 hour) |
157
+ | `autoInitialize` | `boolean` | `true` | Auto-initialize and validate cached license |
158
+ | `offlineFallbackEnabled` | `boolean` | `false` | Enable offline validation on network errors |
159
+ | `maxOfflineDays` | `number` | `0` | Maximum days license works offline (0 = disabled) |
160
+ | `maxRetries` | `number` | `3` | Max retry attempts for failed API calls |
161
+ | `retryDelay` | `number` | `1000` | Initial retry delay in ms (exponential backoff) |
162
+ | `debug` | `boolean` | `false` | Enable debug logging to console |
163
163
 
164
164
  ---
165
165
 
@@ -296,6 +296,15 @@ Clear all cached data and reset SDK state.
296
296
  sdk.reset();
297
297
  ```
298
298
 
299
+ #### `sdk.destroy()`
300
+
301
+ 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.
302
+
303
+ ```javascript
304
+ // When unmounting a component or closing an app
305
+ sdk.destroy();
306
+ ```
307
+
299
308
  #### `sdk.initialize()`
300
309
 
301
310
  Manually initialize the SDK (only needed if `autoInitialize: false`).
@@ -330,41 +339,42 @@ sdk.off('activation:success', handler);
330
339
 
331
340
  ### Available Events
332
341
 
333
- | Event | Description | Data |
334
- |-------|-------------|------|
335
- | **Lifecycle** | | |
336
- | `license:loaded` | Cached license loaded on init | `CachedLicense` |
337
- | `sdk:reset` | SDK was reset | – |
338
- | `sdk:error` | General SDK error | `{ message, error? }` |
339
- | **Activation** | | |
340
- | `activation:start` | Activation started | `{ licenseKey, deviceId }` |
341
- | `activation:success` | Activation succeeded | `CachedLicense` |
342
- | `activation:error` | Activation failed | `{ licenseKey, error }` |
343
- | **Deactivation** | | |
344
- | `deactivation:start` | Deactivation started | `CachedLicense` |
345
- | `deactivation:success` | Deactivation succeeded | `Object` |
346
- | `deactivation:error` | Deactivation failed | `{ error, license }` |
347
- | **Validation** | | |
348
- | `validation:start` | Validation started | `{ licenseKey }` |
349
- | `validation:success` | Online validation succeeded | `ValidationResult` |
350
- | `validation:failed` | Validation failed (invalid license) | `ValidationResult` |
351
- | `validation:error` | Validation error (network, etc.) | `{ licenseKey, error }` |
352
- | `validation:offline-success` | Offline validation succeeded | `ValidationResult` |
353
- | `validation:offline-failed` | Offline validation failed | `ValidationResult` |
354
- | `validation:auth-failed` | Auth failed during validation | `{ licenseKey, error, cached }` |
355
- | **Auto-Validation** | | |
356
- | `autovalidation:cycle` | Auto-validation scheduled | `{ nextRunAt: Date }` |
357
- | `autovalidation:stopped` | Auto-validation stopped | |
358
- | **Network** | | |
359
- | `network:online` | Network connectivity restored ||
360
- | `network:offline` | Network connectivity lost | `{ error }` |
361
- | **Offline License** | | |
362
- | `offlineLicense:fetching` | Fetching offline license | `{ licenseKey }` |
363
- | `offlineLicense:fetched` | Offline license fetched | `{ licenseKey, data }` |
364
- | `offlineLicense:fetchError` | Offline license fetch failed | `{ licenseKey, error }` |
365
- | `offlineLicense:ready` | Offline assets synced | `{ kid, exp_at }` |
366
- | `offlineLicense:verified` | Offline signature verified | `{ payload }` |
367
- | `offlineLicense:verificationFailed` | Offline signature invalid | `{ payload }` |
342
+ | Event | Description | Data |
343
+ | ----------------------------------- | ----------------------------------- | ------------------------------- |
344
+ | **Lifecycle** | | |
345
+ | `license:loaded` | Cached license loaded on init | `CachedLicense` |
346
+ | `sdk:reset` | SDK was reset | – |
347
+ | `sdk:destroyed` | SDK was destroyed | |
348
+ | `sdk:error` | General SDK error | `{ message, error? }` |
349
+ | **Activation** | | |
350
+ | `activation:start` | Activation started | `{ licenseKey, deviceId }` |
351
+ | `activation:success` | Activation succeeded | `CachedLicense` |
352
+ | `activation:error` | Activation failed | `{ licenseKey, error }` |
353
+ | **Deactivation** | | |
354
+ | `deactivation:start` | Deactivation started | `CachedLicense` |
355
+ | `deactivation:success` | Deactivation succeeded | `Object` |
356
+ | `deactivation:error` | Deactivation failed | `{ error, license }` |
357
+ | **Validation** | | |
358
+ | `validation:start` | Validation started | `{ licenseKey }` |
359
+ | `validation:success` | Online validation succeeded | `ValidationResult` |
360
+ | `validation:failed` | Validation failed (invalid license) | `ValidationResult` |
361
+ | `validation:error` | Validation error (network, etc.) | `{ licenseKey, error }` |
362
+ | `validation:offline-success` | Offline validation succeeded | `ValidationResult` |
363
+ | `validation:offline-failed` | Offline validation failed | `ValidationResult` |
364
+ | `validation:auth-failed` | Auth failed during validation | `{ licenseKey, error, cached }` |
365
+ | **Auto-Validation** | | |
366
+ | `autovalidation:cycle` | Auto-validation scheduled | `{ nextRunAt: Date }` |
367
+ | `autovalidation:stopped` | Auto-validation stopped | |
368
+ | **Network** | | |
369
+ | `network:online` | Network connectivity restored | |
370
+ | `network:offline` | Network connectivity lost | `{ error }` |
371
+ | **Offline License** | | |
372
+ | `offlineLicense:fetching` | Fetching offline license | `{ licenseKey }` |
373
+ | `offlineLicense:fetched` | Offline license fetched | `{ licenseKey, data }` |
374
+ | `offlineLicense:fetchError` | Offline license fetch failed | `{ licenseKey, error }` |
375
+ | `offlineLicense:ready` | Offline assets synced | `{ kid, exp_at }` |
376
+ | `offlineLicense:verified` | Offline signature verified | `{ payload }` |
377
+ | `offlineLicense:verificationFailed` | Offline signature invalid | `{ payload }` |
368
378
 
369
379
  ---
370
380
 
@@ -444,12 +454,12 @@ try {
444
454
 
445
455
  ### Error Types
446
456
 
447
- | Error | Description |
448
- |-------|-------------|
449
- | `APIError` | HTTP request failures (includes `status` and `data`) |
450
- | `LicenseError` | License operation failures (includes `code`) |
451
- | `ConfigurationError` | SDK misconfiguration |
452
- | `CryptoError` | Cryptographic operation failures |
457
+ | Error | Description |
458
+ | -------------------- | ---------------------------------------------------- |
459
+ | `APIError` | HTTP request failures (includes `status` and `data`) |
460
+ | `LicenseError` | License operation failures (includes `code`) |
461
+ | `ConfigurationError` | SDK misconfiguration |
462
+ | `CryptoError` | Cryptographic operation failures |
453
463
 
454
464
  ---
455
465
 
@@ -551,17 +561,17 @@ npm install
551
561
 
552
562
  ### Scripts
553
563
 
554
- | Command | Description |
555
- |---------|-------------|
556
- | `npm run build` | Build JS bundle + TypeScript declarations |
557
- | `npm run build:js` | Build JavaScript bundle only |
558
- | `npm run build:types` | Generate TypeScript declarations |
559
- | `npm run build:iife` | Build global/IIFE bundle |
560
- | `npm run dev` | Watch mode for development |
561
- | `npm test` | Run tests |
562
- | `npm run test:watch` | Run tests in watch mode |
563
- | `npm run test:coverage` | Run tests with coverage report |
564
- | `npm run typecheck` | Type-check without emitting |
564
+ | Command | Description |
565
+ | ----------------------- | ----------------------------------------- |
566
+ | `npm run build` | Build JS bundle + TypeScript declarations |
567
+ | `npm run build:js` | Build JavaScript bundle only |
568
+ | `npm run build:types` | Generate TypeScript declarations |
569
+ | `npm run build:iife` | Build global/IIFE bundle |
570
+ | `npm run dev` | Watch mode for development |
571
+ | `npm test` | Run tests |
572
+ | `npm run test:watch` | Run tests in watch mode |
573
+ | `npm run test:coverage` | Run tests with coverage report |
574
+ | `npm run typecheck` | Type-check without emitting |
565
575
 
566
576
  ### Project Structure
567
577
 
@@ -661,12 +671,12 @@ This ensures:
661
671
 
662
672
  Once published to npm, the package is automatically available on CDNs:
663
673
 
664
- | CDN | URL |
665
- |-----|-----|
666
- | **esm.sh** | `https://esm.sh/@licenseseat/js` |
667
- | **unpkg** | `https://unpkg.com/@licenseseat/js/dist/index.js` |
674
+ | CDN | URL |
675
+ | ------------ | ------------------------------------------------------------ |
676
+ | **esm.sh** | `https://esm.sh/@licenseseat/js` |
677
+ | **unpkg** | `https://unpkg.com/@licenseseat/js/dist/index.js` |
668
678
  | **jsDelivr** | `https://cdn.jsdelivr.net/npm/@licenseseat/js/dist/index.js` |
669
- | **Skypack** | `https://cdn.skypack.dev/@licenseseat/js` |
679
+ | **Skypack** | `https://cdn.skypack.dev/@licenseseat/js` |
670
680
 
671
681
  **Version pinning** (recommended for production):
672
682
  ```html
@@ -721,10 +731,10 @@ This project follows [Semantic Versioning](https://semver.org/):
721
731
 
722
732
  ### Breaking Changes in v0.2.0
723
733
 
724
- | Change | Before | After | Migration |
725
- |--------|--------|-------|-----------|
726
- | `apiBaseUrl` default | `/api` | `https://api.licenseseat.com` | Set `apiBaseUrl` explicitly if using a relative URL |
727
- | `offlineFallbackEnabled` default | `true` | `false` | Set `offlineFallbackEnabled: true` if you need offline fallback |
734
+ | Change | Before | After | Migration |
735
+ | -------------------------------- | ------ | ----------------------------- | --------------------------------------------------------------- |
736
+ | `apiBaseUrl` default | `/api` | `https://licenseseat.com/api` | Set `apiBaseUrl` explicitly if using a relative URL |
737
+ | `offlineFallbackEnabled` default | `true` | `false` | Set `offlineFallbackEnabled: true` if you need offline fallback |
728
738
 
729
739
  ### New Features in v0.2.0
730
740
 
package/dist/index.js CHANGED
@@ -221,8 +221,6 @@ function parseActiveEntitlements(payload = {}) {
221
221
  const raw = payload.active_ents || payload.active_entitlements || [];
222
222
  return raw.map((e) => ({
223
223
  key: e.key,
224
- name: e.name ?? null,
225
- description: e.description ?? null,
226
224
  expires_at: e.expires_at ?? null,
227
225
  metadata: e.metadata ?? null
228
226
  }));
@@ -262,7 +260,14 @@ function base64UrlDecode(base64UrlString) {
262
260
  while (base64.length % 4) {
263
261
  base64 += "=";
264
262
  }
265
- const raw = window.atob(base64);
263
+ let raw;
264
+ if (typeof atob === "function") {
265
+ raw = atob(base64);
266
+ } else if (typeof Buffer !== "undefined") {
267
+ raw = Buffer.from(base64, "base64").toString("binary");
268
+ } else {
269
+ throw new Error("No base64 decoder available (neither atob nor Buffer found)");
270
+ }
266
271
  const outputArray = new Uint8Array(raw.length);
267
272
  for (let i = 0; i < raw.length; ++i) {
268
273
  outputArray[i] = raw.charCodeAt(i);
@@ -291,6 +296,11 @@ function getCanvasFingerprint() {
291
296
  }
292
297
  }
293
298
  function generateDeviceId() {
299
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
300
+ const os = typeof process !== "undefined" ? process.platform : "unknown";
301
+ const arch = typeof process !== "undefined" ? process.arch : "unknown";
302
+ return `node-${hashCode(os + "|" + arch)}`;
303
+ }
294
304
  const nav = window.navigator;
295
305
  const screen = window.screen;
296
306
  const data = [
@@ -302,7 +312,7 @@ function generateDeviceId() {
302
312
  nav.hardwareConcurrency,
303
313
  getCanvasFingerprint()
304
314
  ].join("|");
305
- return `web-${hashCode(data)}-${Date.now().toString(36)}`;
315
+ return `web-${hashCode(data)}`;
306
316
  }
307
317
  function sleep(ms) {
308
318
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -314,7 +324,7 @@ function getCsrfToken() {
314
324
 
315
325
  // src/LicenseSeat.js
316
326
  var DEFAULT_CONFIG = {
317
- apiBaseUrl: "https://api.licenseseat.com",
327
+ apiBaseUrl: "https://licenseseat.com/api",
318
328
  storagePrefix: "licenseseat_",
319
329
  autoValidateInterval: 36e5,
320
330
  // 1 hour
@@ -352,6 +362,8 @@ var LicenseSeatSDK = class {
352
362
  this.connectivityTimer = null;
353
363
  this.offlineRefreshTimer = null;
354
364
  this.lastOfflineValidation = null;
365
+ this.syncingOfflineAssets = false;
366
+ this.destroyed = false;
355
367
  if (ed && ed.etc && sha512) {
356
368
  ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
357
369
  } else {
@@ -488,7 +500,7 @@ var LicenseSeatSDK = class {
488
500
  async validateLicense(licenseKey, options = {}) {
489
501
  try {
490
502
  this.emit("validation:start", { licenseKey });
491
- const response = await this.apiCall("/licenses/validate", {
503
+ const rawResponse = await this.apiCall("/licenses/validate", {
492
504
  method: "POST",
493
505
  body: {
494
506
  license_key: licenseKey,
@@ -496,6 +508,10 @@ var LicenseSeatSDK = class {
496
508
  product_slug: options.productSlug
497
509
  }
498
510
  });
511
+ const response = {
512
+ valid: rawResponse.valid,
513
+ ...rawResponse.license || {}
514
+ };
499
515
  const cachedLicense = this.cache.getLicense();
500
516
  if ((!response.active_entitlements || response.active_entitlements.length === 0) && cachedLicense?.validation?.active_entitlements?.length) {
501
517
  response.active_entitlements = cachedLicense.validation.active_entitlements;
@@ -744,12 +760,12 @@ var LicenseSeatSDK = class {
744
760
  * Test server authentication
745
761
  * Useful for verifying API key/session is valid.
746
762
  * @returns {Promise<Object>} Result from the server
747
- * @throws {Error} When API key is not configured
763
+ * @throws {ConfigurationError} When API key is not configured
748
764
  * @throws {APIError} When authentication fails
749
765
  */
750
766
  async testAuth() {
751
767
  if (!this.config.apiKey) {
752
- const err = new Error("API key is required for auth test");
768
+ const err = new ConfigurationError("API key is required for auth test");
753
769
  this.emit("auth_test:error", { error: err });
754
770
  throw err;
755
771
  }
@@ -769,10 +785,36 @@ var LicenseSeatSDK = class {
769
785
  */
770
786
  reset() {
771
787
  this.stopAutoValidation();
788
+ this.stopConnectivityPolling();
789
+ if (this.offlineRefreshTimer) {
790
+ clearInterval(this.offlineRefreshTimer);
791
+ this.offlineRefreshTimer = null;
792
+ }
772
793
  this.cache.clear();
773
794
  this.lastOfflineValidation = null;
795
+ this.currentAutoLicenseKey = null;
774
796
  this.emit("sdk:reset");
775
797
  }
798
+ /**
799
+ * Destroy the SDK instance and release all resources
800
+ * Call this when you no longer need the SDK to prevent memory leaks.
801
+ * After calling destroy(), the SDK instance should not be used.
802
+ * @returns {void}
803
+ */
804
+ destroy() {
805
+ this.destroyed = true;
806
+ this.stopAutoValidation();
807
+ this.stopConnectivityPolling();
808
+ if (this.offlineRefreshTimer) {
809
+ clearInterval(this.offlineRefreshTimer);
810
+ this.offlineRefreshTimer = null;
811
+ }
812
+ this.eventListeners = {};
813
+ this.cache.clear();
814
+ this.lastOfflineValidation = null;
815
+ this.currentAutoLicenseKey = null;
816
+ this.emit("sdk:destroyed");
817
+ }
776
818
  // ============================================================
777
819
  // Event Handling
778
820
  // ============================================================
@@ -907,10 +949,16 @@ var LicenseSeatSDK = class {
907
949
  // ============================================================
908
950
  /**
909
951
  * Fetch and cache offline license and public key
952
+ * Uses a lock to prevent concurrent calls from causing race conditions
910
953
  * @returns {Promise<void>}
911
954
  * @private
912
955
  */
913
956
  async syncOfflineAssets() {
957
+ if (this.syncingOfflineAssets || this.destroyed) {
958
+ this.log("Skipping syncOfflineAssets: already syncing or destroyed");
959
+ return;
960
+ }
961
+ this.syncingOfflineAssets = true;
914
962
  try {
915
963
  const offline = await this.getOfflineLicense();
916
964
  this.cache.setOfflineLicense(offline);
@@ -936,6 +984,8 @@ var LicenseSeatSDK = class {
936
984
  }
937
985
  } catch (err) {
938
986
  this.log("Failed to sync offline assets:", err);
987
+ } finally {
988
+ this.syncingOfflineAssets = false;
939
989
  }
940
990
  }
941
991
  /**
@@ -1016,6 +1066,7 @@ var LicenseSeatSDK = class {
1016
1066
  }
1017
1067
  /**
1018
1068
  * Quick offline verification using only local data (no network)
1069
+ * Performs signature verification plus basic validity checks (expiry, license key match)
1019
1070
  * @returns {Promise<import('./types.js').ValidationResult|null>}
1020
1071
  * @private
1021
1072
  */
@@ -1032,7 +1083,21 @@ var LicenseSeatSDK = class {
1032
1083
  if (!ok) {
1033
1084
  return { valid: false, offline: true, reason_code: "signature_invalid" };
1034
1085
  }
1035
- const active = parseActiveEntitlements(signed.payload || {});
1086
+ const payload = signed.payload || {};
1087
+ const cached = this.cache.getLicense();
1088
+ if (!cached || !constantTimeEqual(payload.lic_k || "", cached.license_key || "")) {
1089
+ return { valid: false, offline: true, reason_code: "license_mismatch" };
1090
+ }
1091
+ const now = Date.now();
1092
+ const expAt = payload.exp_at ? Date.parse(payload.exp_at) : null;
1093
+ if (expAt && expAt < now) {
1094
+ return { valid: false, offline: true, reason_code: "expired" };
1095
+ }
1096
+ const lastSeen = this.cache.getLastSeenTimestamp();
1097
+ if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
1098
+ return { valid: false, offline: true, reason_code: "clock_tamper" };
1099
+ }
1100
+ const active = parseActiveEntitlements(payload);
1036
1101
  return {
1037
1102
  valid: true,
1038
1103
  offline: true,
@@ -57,7 +57,7 @@ export class LicenseSeatSDK {
57
57
  private eventListeners;
58
58
  /**
59
59
  * Auto-validation timer ID
60
- * @type {number|null}
60
+ * @type {ReturnType<typeof setInterval>|null}
61
61
  * @private
62
62
  */
63
63
  private validationTimer;
@@ -81,13 +81,13 @@ export class LicenseSeatSDK {
81
81
  private currentAutoLicenseKey;
82
82
  /**
83
83
  * Connectivity polling timer ID
84
- * @type {number|null}
84
+ * @type {ReturnType<typeof setInterval>|null}
85
85
  * @private
86
86
  */
87
87
  private connectivityTimer;
88
88
  /**
89
89
  * Offline license refresh timer ID
90
- * @type {number|null}
90
+ * @type {ReturnType<typeof setInterval>|null}
91
91
  * @private
92
92
  */
93
93
  private offlineRefreshTimer;
@@ -97,6 +97,18 @@ export class LicenseSeatSDK {
97
97
  * @private
98
98
  */
99
99
  private lastOfflineValidation;
100
+ /**
101
+ * Flag to prevent concurrent syncOfflineAssets calls
102
+ * @type {boolean}
103
+ * @private
104
+ */
105
+ private syncingOfflineAssets;
106
+ /**
107
+ * Flag indicating if SDK has been destroyed
108
+ * @type {boolean}
109
+ * @private
110
+ */
111
+ private destroyed;
100
112
  /**
101
113
  * Initialize the SDK
102
114
  * Loads cached license and starts auto-validation if configured.
@@ -173,7 +185,7 @@ export class LicenseSeatSDK {
173
185
  * Test server authentication
174
186
  * Useful for verifying API key/session is valid.
175
187
  * @returns {Promise<Object>} Result from the server
176
- * @throws {Error} When API key is not configured
188
+ * @throws {ConfigurationError} When API key is not configured
177
189
  * @throws {APIError} When authentication fails
178
190
  */
179
191
  testAuth(): Promise<any>;
@@ -182,6 +194,13 @@ export class LicenseSeatSDK {
182
194
  * @returns {void}
183
195
  */
184
196
  reset(): void;
197
+ /**
198
+ * Destroy the SDK instance and release all resources
199
+ * Call this when you no longer need the SDK to prevent memory leaks.
200
+ * After calling destroy(), the SDK instance should not be used.
201
+ * @returns {void}
202
+ */
203
+ destroy(): void;
185
204
  /**
186
205
  * Subscribe to an event
187
206
  * @param {string} event - Event name
@@ -231,6 +250,7 @@ export class LicenseSeatSDK {
231
250
  private stopConnectivityPolling;
232
251
  /**
233
252
  * Fetch and cache offline license and public key
253
+ * Uses a lock to prevent concurrent calls from causing race conditions
234
254
  * @returns {Promise<void>}
235
255
  * @private
236
256
  */
@@ -249,6 +269,7 @@ export class LicenseSeatSDK {
249
269
  private verifyCachedOffline;
250
270
  /**
251
271
  * Quick offline verification using only local data (no network)
272
+ * Performs signature verification plus basic validity checks (expiry, license key match)
252
273
  * @returns {Promise<import('./types.js').ValidationResult|null>}
253
274
  * @private
254
275
  */
@@ -1 +1 @@
1
- {"version":3,"file":"LicenseSeat.d.ts","sourceRoot":"","sources":["../../src/LicenseSeat.js"],"names":[],"mappings":"AAomCA;;;;GAIG;AACH,2CAHW,OAAO,YAAY,EAAE,iBAAiB,GACpC,cAAc,CAO1B;AAED;;;;;GAKG;AACH,kCAJW,OAAO,YAAY,EAAE,iBAAiB,UACtC,OAAO,GACL,cAAc,CAW1B;AAED;;;GAGG;AACH,uCAFa,IAAI,CAOhB;AAplCD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IACE;;;OAGG;IACH,qBAFW,OAAO,YAAY,EAAE,iBAAiB,EAiFhD;IA9EC;;;OAGG;IACH,QAFU,OAAO,YAAY,EAAE,iBAAiB,CAK/C;IAED;;;;OAIG;IACH,uBAAwB;IAExB;;;;OAIG;IACH,wBAA2B;IAE3B;;;;OAIG;IACH,cAAwD;IAExD;;;;OAIG;IACH,eAAkB;IAElB;;;;OAIG;IACH,8BAAiC;IAEjC;;;;OAIG;IACH,0BAA6B;IAE7B;;;;OAIG;IACH,4BAA+B;IAE/B;;;;OAIG;IACH,8BAAiC;IAiBnC;;;;;OAKG;IACH,cAFa,IAAI,CAkDhB;IAED;;;;;;OAMG;IACH,qBALW,MAAM,YACN,OAAO,YAAY,EAAE,iBAAiB,GACpC,OAAO,CAAC,OAAO,YAAY,EAAE,aAAa,CAAC,CA4CvD;IAED;;;;;OAKG;IACH,cAJa,OAAO,KAAQ,CA+B3B;IAED;;;;;;OAMG;IACH,4BALW,MAAM,YACN,OAAO,YAAY,EAAE,iBAAiB,GACpC,OAAO,CAAC,OAAO,YAAY,EAAE,gBAAgB,CAAC,CAkF1D;IAED;;;;OAIG;IACH,iCAHW,MAAM,GACJ,OAAO,YAAY,EAAE,sBAAsB,CA6BvD;IAED;;;;;;OAMG;IACH,+BAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;OAKG;IACH,qBAJa,OAAO,CAAC,OAAO,YAAY,EAAE,oBAAoB,CAAC,CAmC9D;IAED;;;;;OAKG;IACH,oBAJW,MAAM,GACJ,OAAO,CAAC,MAAM,CAAC,CAwB3B;IAED;;;;;;;OAOG;IACH,wCANW,OAAO,YAAY,EAAE,oBAAoB,gBACzC,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAuD5B;IAED;;;OAGG;IACH,aAFa,OAAO,YAAY,EAAE,aAAa,CA6C9C;IAED;;;;;;OAMG;IACH,YAJa,OAAO,KAAQ,CAoB3B;IAED;;;OAGG;IACH,SAFa,IAAI,CAOhB;IAMD;;;;;OAKG;IACH,UAJW,MAAM,YACN,OAAO,YAAY,EAAE,aAAa,GAChC,OAAO,YAAY,EAAE,gBAAgB,CAQjD;IAED;;;;;OAKG;IACH,WAJW,MAAM,YACN,OAAO,YAAY,EAAE,aAAa,GAChC,IAAI,CAQhB;IAED;;;;;;OAMG;IACH,aAWC;IAMD;;;;;OAKG;IACH,4BAqBC;IAED;;;;OAIG;IACH,2BAMC;IAED;;;;OAIG;IACH,iCA4BC;IAED;;;;OAIG;IACH,gCAKC;IAMD;;;;OAIG;IACH,0BA+BC;IAED;;;;OAIG;IACH,+BAMC;IAED;;;;OAIG;IACH,4BA0EC;IAED;;;;OAIG;IACH,sCAsBC;IAMD;;;;;;;;;;OAUG;IACH,gBAiFC;IAED;;;;;OAKG;IACH,yBAsBC;IAMD;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;;;OAKG;IACH,YAIC;CACF"}
1
+ {"version":3,"file":"LicenseSeat.d.ts","sourceRoot":"","sources":["../../src/LicenseSeat.js"],"names":[],"mappings":"AAwrCA;;;;GAIG;AACH,2CAHW,OAAO,YAAY,EAAE,iBAAiB,GACpC,cAAc,CAO1B;AAED;;;;;GAKG;AACH,kCAJW,OAAO,YAAY,EAAE,iBAAiB,UACtC,OAAO,GACL,cAAc,CAW1B;AAED;;;GAGG;AACH,uCAFa,IAAI,CAOhB;AAxqCD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IACE;;;OAGG;IACH,qBAFW,OAAO,YAAY,EAAE,iBAAiB,EA+FhD;IA5FC;;;OAGG;IACH,QAFU,OAAO,YAAY,EAAE,iBAAiB,CAK/C;IAED;;;;OAIG;IACH,uBAAwB;IAExB;;;;OAIG;IACH,wBAA2B;IAE3B;;;;OAIG;IACH,cAAwD;IAExD;;;;OAIG;IACH,eAAkB;IAElB;;;;OAIG;IACH,8BAAiC;IAEjC;;;;OAIG;IACH,0BAA6B;IAE7B;;;;OAIG;IACH,4BAA+B;IAE/B;;;;OAIG;IACH,8BAAiC;IAEjC;;;;OAIG;IACH,6BAAiC;IAEjC;;;;OAIG;IACH,kBAAsB;IAiBxB;;;;;OAKG;IACH,cAFa,IAAI,CAkDhB;IAED;;;;;;OAMG;IACH,qBALW,MAAM,YACN,OAAO,YAAY,EAAE,iBAAiB,GACpC,OAAO,CAAC,OAAO,YAAY,EAAE,aAAa,CAAC,CA4CvD;IAED;;;;;OAKG;IACH,cAJa,OAAO,KAAQ,CA+B3B;IAED;;;;;;OAMG;IACH,4BALW,MAAM,YACN,OAAO,YAAY,EAAE,iBAAiB,GACpC,OAAO,CAAC,OAAO,YAAY,EAAE,gBAAgB,CAAC,CAyF1D;IAED;;;;OAIG;IACH,iCAHW,MAAM,GACJ,OAAO,YAAY,EAAE,sBAAsB,CA6BvD;IAED;;;;;;OAMG;IACH,+BAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;OAKG;IACH,qBAJa,OAAO,CAAC,OAAO,YAAY,EAAE,oBAAoB,CAAC,CAmC9D;IAED;;;;;OAKG;IACH,oBAJW,MAAM,GACJ,OAAO,CAAC,MAAM,CAAC,CAwB3B;IAED;;;;;;;OAOG;IACH,wCANW,OAAO,YAAY,EAAE,oBAAoB,gBACzC,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAuD5B;IAED;;;OAGG;IACH,aAFa,OAAO,YAAY,EAAE,aAAa,CA6C9C;IAED;;;;;;OAMG;IACH,YAJa,OAAO,KAAQ,CAoB3B;IAED;;;OAGG;IACH,SAFa,IAAI,CAahB;IAED;;;;;OAKG;IACH,WAFa,IAAI,CAehB;IAMD;;;;;OAKG;IACH,UAJW,MAAM,YACN,OAAO,YAAY,EAAE,aAAa,GAChC,OAAO,YAAY,EAAE,gBAAgB,CAQjD;IAED;;;;;OAKG;IACH,WAJW,MAAM,YACN,OAAO,YAAY,EAAE,aAAa,GAChC,IAAI,CAQhB;IAED;;;;;;OAMG;IACH,aAWC;IAMD;;;;;OAKG;IACH,4BAqBC;IAED;;;;OAIG;IACH,2BAMC;IAED;;;;OAIG;IACH,iCA4BC;IAED;;;;OAIG;IACH,gCAKC;IAMD;;;;;OAKG;IACH,0BAwCC;IAED;;;;OAIG;IACH,+BAMC;IAED;;;;OAIG;IACH,4BA0EC;IAED;;;;;OAKG;IACH,sCA+CC;IAMD;;;;;;;;;;OAUG;IACH,gBAiFC;IAED;;;;;OAKG;IACH,yBAsBC;IAMD;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;;;OAKG;IACH,YAIC;CACF"}
@@ -141,20 +141,13 @@ export type CachedLicense = {
141
141
  };
142
142
  /**
143
143
  * Entitlement object
144
+ * Note: API returns only key, expires_at, and metadata. Name/description are not provided.
144
145
  */
145
146
  export type Entitlement = {
146
147
  /**
147
148
  * - Unique entitlement key
148
149
  */
149
150
  key: string;
150
- /**
151
- * - Human-readable name
152
- */
153
- name: string | null;
154
- /**
155
- * - Description of the entitlement
156
- */
157
- description: string | null;
158
151
  /**
159
152
  * - ISO8601 expiration timestamp
160
153
  */
@@ -251,26 +244,50 @@ export type LicenseStatus = {
251
244
  * Offline license payload
252
245
  */
253
246
  export type OfflineLicensePayload = {
247
+ /**
248
+ * - Payload version (currently 1)
249
+ */
250
+ v?: number;
254
251
  /**
255
252
  * - License key
256
253
  */
257
254
  lic_k?: string;
258
255
  /**
259
- * - ISO8601 expiration timestamp
256
+ * - Product slug
257
+ */
258
+ prod_s?: string;
259
+ /**
260
+ * - License plan key
260
261
  */
261
- exp_at?: string;
262
+ plan_k?: string;
262
263
  /**
263
- * - Key ID for signature verification
264
+ * - ISO8601 expiration timestamp (null for perpetual)
265
+ */
266
+ exp_at?: string | null;
267
+ /**
268
+ * - Seat limit (null for unlimited)
269
+ */
270
+ sl?: number | null;
271
+ /**
272
+ * - Key ID for public key lookup
264
273
  */
265
274
  kid?: string;
266
275
  /**
267
276
  * - Active entitlements
268
277
  */
269
- active_ents?: Array<any>;
278
+ active_ents?: Array<{
279
+ key: string;
280
+ expires_at: string | null;
281
+ metadata: any | null;
282
+ }>;
270
283
  /**
271
284
  * - Active entitlements (alternative key)
272
285
  */
273
- active_entitlements?: Array<any>;
286
+ active_entitlements?: Array<{
287
+ key: string;
288
+ expires_at: string | null;
289
+ metadata: any | null;
290
+ }>;
274
291
  /**
275
292
  * - Additional metadata
276
293
  */
@@ -310,7 +327,11 @@ export type APIErrorData = {
310
327
  */
311
328
  error?: string;
312
329
  /**
313
- * - Error code
330
+ * - Machine-readable reason code (e.g., "license_not_found", "expired", "revoked")
331
+ */
332
+ reason_code?: string;
333
+ /**
334
+ * - Legacy error code (deprecated, use reason_code)
314
335
  */
315
336
  code?: string;
316
337
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.js"],"names":[],"mappings":";;;;;;;iBASc,MAAM;;;;aACN,MAAM;;;;oBACN,MAAM;;;;2BACN,MAAM;;;;6BACN,MAAM;;;;iBACN,MAAM;;;;iBACN,MAAM;;;;YACN,OAAO;;;;oCACP,MAAM;;;;6BACN,OAAO;;;;qBACP,MAAM;;;;qBACN,MAAM;;;;qBACN,OAAO;;;;;;;;;uBAMP,MAAM;;;;0BACN,MAAM;;;;;;;;;;;;;uBAON,MAAM;;;;kBACN,MAAM;;;;;;;;;QAMN,MAAM;;;;iBACN,MAAM;;;;uBACN,MAAM;;;;kBACN,MAAM;;;;;;;;;;;;;iBAON,MAAM;;;;uBACN,MAAM;;;;iBACN,kBAAkB;;;;kBAClB,MAAM;;;;oBACN,MAAM;;;;iBACN,gBAAgB;;;;;;;;;SAMhB,MAAM;;;;UACN,MAAM,GAAC,IAAI;;;;iBACX,MAAM,GAAC,IAAI;;;;gBACX,MAAM,GAAC,IAAI;;;;cACX,MAAO,IAAI;;;;;;;;;WAMX,OAAO;;;;cACP,OAAO;;;;aACP,MAAM;;;;kBACN,MAAM;;;;0BACN,WAAW,EAAE;;;;iBACb,OAAO;;;;;;;;;YAMP,OAAO;;;;aACP,MAAM;;;;iBACN,MAAM;;;;kBACN,WAAW;;;;;;;;;YAMX,MAAM;;;;cACN,MAAM;;;;cACN,MAAM;;;;aACN,MAAM;;;;mBACN,MAAM;;;;qBACN,MAAM;;;;mBACN,WAAW,EAAE;;;;;;;;;YAMb,MAAM;;;;aACN,MAAM;;;;UACN,MAAM;;;;kBACN,KAAK,KAAQ;;;;0BACb,KAAK,KAAQ;;;;;;;;;;;;;aAOb,qBAAqB;;;;oBACrB,MAAM;;;;UACN,MAAM;;;;;mCAMT,GAAC,KACC,IAAI;;;;qCAMJ,IAAI;;;;;;;;YAMH,MAAM;;;;WACN,MAAM"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.js"],"names":[],"mappings":";;;;;;;iBASc,MAAM;;;;aACN,MAAM;;;;oBACN,MAAM;;;;2BACN,MAAM;;;;6BACN,MAAM;;;;iBACN,MAAM;;;;iBACN,MAAM;;;;YACN,OAAO;;;;oCACP,MAAM;;;;6BACN,OAAO;;;;qBACP,MAAM;;;;qBACN,MAAM;;;;qBACN,OAAO;;;;;;;;;uBAMP,MAAM;;;;0BACN,MAAM;;;;;;;;;;;;;uBAON,MAAM;;;;kBACN,MAAM;;;;;;;;;QAMN,MAAM;;;;iBACN,MAAM;;;;uBACN,MAAM;;;;kBACN,MAAM;;;;;;;;;;;;;iBAON,MAAM;;;;uBACN,MAAM;;;;iBACN,kBAAkB;;;;kBAClB,MAAM;;;;oBACN,MAAM;;;;iBACN,gBAAgB;;;;;;;;;;SAOhB,MAAM;;;;gBACN,MAAM,GAAC,IAAI;;;;cACX,MAAO,IAAI;;;;;;;;;WAMX,OAAO;;;;cACP,OAAO;;;;aACP,MAAM;;;;kBACN,MAAM;;;;0BACN,WAAW,EAAE;;;;iBACb,OAAO;;;;;;;;;YAMP,OAAO;;;;aACP,MAAM;;;;iBACN,MAAM;;;;kBACN,WAAW;;;;;;;;;YAMX,MAAM;;;;cACN,MAAM;;;;cACN,MAAM;;;;aACN,MAAM;;;;mBACN,MAAM;;;;qBACN,MAAM;;;;mBACN,WAAW,EAAE;;;;;;;;;QAMb,MAAM;;;;YACN,MAAM;;;;aACN,MAAM;;;;aACN,MAAM;;;;aACN,MAAM,GAAC,IAAI;;;;SACX,MAAM,GAAC,IAAI;;;;UACX,MAAM;;;;kBACN,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,GAAC,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAO,IAAI,CAAA;KAAC,CAAC;;;;0BACpE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,GAAC,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAO,IAAI,CAAA;KAAC,CAAC;;;;;;;;;;;;;aAOpE,qBAAqB;;;;oBACrB,MAAM;;;;UACN,MAAM;;;;;mCAMT,GAAC,KACC,IAAI;;;;qCAMJ,IAAI;;;;;;;;YAMH,MAAM;;;;kBACN,MAAM;;;;WACN,MAAM"}
@@ -20,6 +20,7 @@ export function constantTimeEqual(a?: string, b?: string): boolean;
20
20
  export function canonicalJsonStringify(obj: any): string;
21
21
  /**
22
22
  * Decode a Base64URL string to a Uint8Array
23
+ * Works in both browser and Node.js environments.
23
24
  * @param {string} base64UrlString - The Base64URL encoded string
24
25
  * @returns {Uint8Array} Decoded bytes
25
26
  */
@@ -36,8 +37,9 @@ export function hashCode(str: string): string;
36
37
  */
37
38
  export function getCanvasFingerprint(): string;
38
39
  /**
39
- * Generate a unique device identifier based on browser characteristics
40
- * @returns {string} Unique device identifier
40
+ * Generate a stable device identifier based on browser characteristics.
41
+ * The ID is deterministic - same device will produce the same ID across calls.
42
+ * @returns {string} Stable device identifier
41
43
  */
42
44
  export function generateDeviceId(): string;
43
45
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.js"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,wDAFa,OAAO,YAAY,EAAE,WAAW,EAAE,CAW9C;AAED;;;;;GAKG;AACH,sCAJW,MAAM,MACN,MAAM,GACJ,OAAO,CASnB;AAED;;;;;GAKG;AACH,kDAFa,MAAM,CAuBlB;AAED;;;;GAIG;AACH,iDAHW,MAAM,GACJ,UAAU,CAatB;AAED;;;;GAIG;AACH,8BAHW,MAAM,GACJ,MAAM,CAUlB;AAED;;;GAGG;AACH,wCAFa,MAAM,CAalB;AAED;;;GAGG;AACH,oCAFa,MAAM,CAgBlB;AAED;;;;GAIG;AACH,0BAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAIzB;AAED;;;GAGG;AACH,gCAFa,MAAM,CAMlB"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.js"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,wDAFa,OAAO,YAAY,EAAE,WAAW,EAAE,CAS9C;AAED;;;;;GAKG;AACH,sCAJW,MAAM,MACN,MAAM,GACJ,OAAO,CASnB;AAED;;;;;GAKG;AACH,kDAFa,MAAM,CAuBlB;AAED;;;;;GAKG;AACH,iDAHW,MAAM,GACJ,UAAU,CA0BtB;AAED;;;;GAIG;AACH,8BAHW,MAAM,GACJ,MAAM,CAUlB;AAED;;;GAGG;AACH,wCAFa,MAAM,CAalB;AAED;;;;GAIG;AACH,oCAFa,MAAM,CAyBlB;AAED;;;;GAIG;AACH,0BAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAIzB;AAED;;;GAGG;AACH,gCAFa,MAAM,CAMlB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@licenseseat/js",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Official JavaScript SDK for LicenseSeat – simple, secure software licensing.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -62,6 +62,7 @@
62
62
  "node": ">=18.0.0"
63
63
  },
64
64
  "devDependencies": {
65
+ "@types/node": "^25.0.9",
65
66
  "@vitest/coverage-v8": "^1.6.0",
66
67
  "esbuild": "^0.20.2",
67
68
  "jsdom": "^24.1.3",
@@ -21,7 +21,7 @@ import * as ed from "@noble/ed25519";
21
21
  import { sha512 } from "@noble/hashes/sha512";
22
22
 
23
23
  import { LicenseCache } from "./cache.js";
24
- import { APIError, LicenseError, CryptoError } from "./errors.js";
24
+ import { APIError, ConfigurationError, LicenseError, CryptoError } from "./errors.js";
25
25
  import {
26
26
  parseActiveEntitlements,
27
27
  constantTimeEqual,
@@ -37,7 +37,7 @@ import {
37
37
  * @type {import('./types.js').LicenseSeatConfig}
38
38
  */
39
39
  const DEFAULT_CONFIG = {
40
- apiBaseUrl: "https://api.licenseseat.com",
40
+ apiBaseUrl: "https://licenseseat.com/api",
41
41
  storagePrefix: "licenseseat_",
42
42
  autoValidateInterval: 3600000, // 1 hour
43
43
  networkRecheckInterval: 30000, // 30 seconds
@@ -98,7 +98,7 @@ export class LicenseSeatSDK {
98
98
 
99
99
  /**
100
100
  * Auto-validation timer ID
101
- * @type {number|null}
101
+ * @type {ReturnType<typeof setInterval>|null}
102
102
  * @private
103
103
  */
104
104
  this.validationTimer = null;
@@ -126,14 +126,14 @@ export class LicenseSeatSDK {
126
126
 
127
127
  /**
128
128
  * Connectivity polling timer ID
129
- * @type {number|null}
129
+ * @type {ReturnType<typeof setInterval>|null}
130
130
  * @private
131
131
  */
132
132
  this.connectivityTimer = null;
133
133
 
134
134
  /**
135
135
  * Offline license refresh timer ID
136
- * @type {number|null}
136
+ * @type {ReturnType<typeof setInterval>|null}
137
137
  * @private
138
138
  */
139
139
  this.offlineRefreshTimer = null;
@@ -145,6 +145,20 @@ export class LicenseSeatSDK {
145
145
  */
146
146
  this.lastOfflineValidation = null;
147
147
 
148
+ /**
149
+ * Flag to prevent concurrent syncOfflineAssets calls
150
+ * @type {boolean}
151
+ * @private
152
+ */
153
+ this.syncingOfflineAssets = false;
154
+
155
+ /**
156
+ * Flag indicating if SDK has been destroyed
157
+ * @type {boolean}
158
+ * @private
159
+ */
160
+ this.destroyed = false;
161
+
148
162
  // Enable synchronous SHA512 for noble-ed25519
149
163
  if (ed && ed.etc && sha512) {
150
164
  ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
@@ -312,7 +326,7 @@ export class LicenseSeatSDK {
312
326
  try {
313
327
  this.emit("validation:start", { licenseKey });
314
328
 
315
- const response = await this.apiCall("/licenses/validate", {
329
+ const rawResponse = await this.apiCall("/licenses/validate", {
316
330
  method: "POST",
317
331
  body: {
318
332
  license_key: licenseKey,
@@ -321,6 +335,13 @@ export class LicenseSeatSDK {
321
335
  },
322
336
  });
323
337
 
338
+ // Normalize response: API returns { valid, license: { active_entitlements, ... } }
339
+ // SDK expects flat structure { valid, active_entitlements, ... }
340
+ const response = {
341
+ valid: rawResponse.valid,
342
+ ...(rawResponse.license || {}),
343
+ };
344
+
324
345
  // Preserve cached entitlements if server response omits them
325
346
  const cachedLicense = this.cache.getLicense();
326
347
  if (
@@ -616,12 +637,12 @@ export class LicenseSeatSDK {
616
637
  * Test server authentication
617
638
  * Useful for verifying API key/session is valid.
618
639
  * @returns {Promise<Object>} Result from the server
619
- * @throws {Error} When API key is not configured
640
+ * @throws {ConfigurationError} When API key is not configured
620
641
  * @throws {APIError} When authentication fails
621
642
  */
622
643
  async testAuth() {
623
644
  if (!this.config.apiKey) {
624
- const err = new Error("API key is required for auth test");
645
+ const err = new ConfigurationError("API key is required for auth test");
625
646
  this.emit("auth_test:error", { error: err });
626
647
  throw err;
627
648
  }
@@ -643,11 +664,38 @@ export class LicenseSeatSDK {
643
664
  */
644
665
  reset() {
645
666
  this.stopAutoValidation();
667
+ this.stopConnectivityPolling();
668
+ if (this.offlineRefreshTimer) {
669
+ clearInterval(this.offlineRefreshTimer);
670
+ this.offlineRefreshTimer = null;
671
+ }
646
672
  this.cache.clear();
647
673
  this.lastOfflineValidation = null;
674
+ this.currentAutoLicenseKey = null;
648
675
  this.emit("sdk:reset");
649
676
  }
650
677
 
678
+ /**
679
+ * Destroy the SDK instance and release all resources
680
+ * Call this when you no longer need the SDK to prevent memory leaks.
681
+ * After calling destroy(), the SDK instance should not be used.
682
+ * @returns {void}
683
+ */
684
+ destroy() {
685
+ this.destroyed = true;
686
+ this.stopAutoValidation();
687
+ this.stopConnectivityPolling();
688
+ if (this.offlineRefreshTimer) {
689
+ clearInterval(this.offlineRefreshTimer);
690
+ this.offlineRefreshTimer = null;
691
+ }
692
+ this.eventListeners = {};
693
+ this.cache.clear();
694
+ this.lastOfflineValidation = null;
695
+ this.currentAutoLicenseKey = null;
696
+ this.emit("sdk:destroyed");
697
+ }
698
+
651
699
  // ============================================================
652
700
  // Event Handling
653
701
  // ============================================================
@@ -799,10 +847,18 @@ export class LicenseSeatSDK {
799
847
 
800
848
  /**
801
849
  * Fetch and cache offline license and public key
850
+ * Uses a lock to prevent concurrent calls from causing race conditions
802
851
  * @returns {Promise<void>}
803
852
  * @private
804
853
  */
805
854
  async syncOfflineAssets() {
855
+ // Prevent concurrent syncs
856
+ if (this.syncingOfflineAssets || this.destroyed) {
857
+ this.log("Skipping syncOfflineAssets: already syncing or destroyed");
858
+ return;
859
+ }
860
+
861
+ this.syncingOfflineAssets = true;
806
862
  try {
807
863
  const offline = await this.getOfflineLicense();
808
864
  this.cache.setOfflineLicense(offline);
@@ -832,6 +888,8 @@ export class LicenseSeatSDK {
832
888
  }
833
889
  } catch (err) {
834
890
  this.log("Failed to sync offline assets:", err);
891
+ } finally {
892
+ this.syncingOfflineAssets = false;
835
893
  }
836
894
  }
837
895
 
@@ -931,6 +989,7 @@ export class LicenseSeatSDK {
931
989
 
932
990
  /**
933
991
  * Quick offline verification using only local data (no network)
992
+ * Performs signature verification plus basic validity checks (expiry, license key match)
934
993
  * @returns {Promise<import('./types.js').ValidationResult|null>}
935
994
  * @private
936
995
  */
@@ -947,7 +1006,32 @@ export class LicenseSeatSDK {
947
1006
  return { valid: false, offline: true, reason_code: "signature_invalid" };
948
1007
  }
949
1008
 
950
- const active = parseActiveEntitlements(signed.payload || {});
1009
+ /** @type {import('./types.js').OfflineLicensePayload} */
1010
+ const payload = signed.payload || {};
1011
+ const cached = this.cache.getLicense();
1012
+
1013
+ // License key match check
1014
+ if (
1015
+ !cached ||
1016
+ !constantTimeEqual(payload.lic_k || "", cached.license_key || "")
1017
+ ) {
1018
+ return { valid: false, offline: true, reason_code: "license_mismatch" };
1019
+ }
1020
+
1021
+ // Expiry check
1022
+ const now = Date.now();
1023
+ const expAt = payload.exp_at ? Date.parse(payload.exp_at) : null;
1024
+ if (expAt && expAt < now) {
1025
+ return { valid: false, offline: true, reason_code: "expired" };
1026
+ }
1027
+
1028
+ // Clock tamper detection
1029
+ const lastSeen = this.cache.getLastSeenTimestamp();
1030
+ if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
1031
+ return { valid: false, offline: true, reason_code: "clock_tamper" };
1032
+ }
1033
+
1034
+ const active = parseActiveEntitlements(payload);
951
1035
  return {
952
1036
  valid: true,
953
1037
  offline: true,
package/src/types.js CHANGED
@@ -7,7 +7,7 @@
7
7
  /**
8
8
  * SDK Configuration options
9
9
  * @typedef {Object} LicenseSeatConfig
10
- * @property {string} [apiBaseUrl="https://api.licenseseat.com"] - Base URL for the LicenseSeat API
10
+ * @property {string} [apiBaseUrl="https://licenseseat.com/api"] - Base URL for the LicenseSeat API
11
11
  * @property {string} [apiKey] - API key for authentication (required for most operations)
12
12
  * @property {string} [storagePrefix="licenseseat_"] - Prefix for localStorage keys
13
13
  * @property {number} [autoValidateInterval=3600000] - Interval in ms for automatic license validation (default: 1 hour)
@@ -60,10 +60,9 @@
60
60
 
61
61
  /**
62
62
  * Entitlement object
63
+ * Note: API returns only key, expires_at, and metadata. Name/description are not provided.
63
64
  * @typedef {Object} Entitlement
64
65
  * @property {string} key - Unique entitlement key
65
- * @property {string|null} name - Human-readable name
66
- * @property {string|null} description - Description of the entitlement
67
66
  * @property {string|null} expires_at - ISO8601 expiration timestamp
68
67
  * @property {Object|null} metadata - Additional metadata
69
68
  */
@@ -103,11 +102,15 @@
103
102
  /**
104
103
  * Offline license payload
105
104
  * @typedef {Object} OfflineLicensePayload
105
+ * @property {number} [v] - Payload version (currently 1)
106
106
  * @property {string} [lic_k] - License key
107
- * @property {string} [exp_at] - ISO8601 expiration timestamp
108
- * @property {string} [kid] - Key ID for signature verification
109
- * @property {Array<Object>} [active_ents] - Active entitlements
110
- * @property {Array<Object>} [active_entitlements] - Active entitlements (alternative key)
107
+ * @property {string} [prod_s] - Product slug
108
+ * @property {string} [plan_k] - License plan key
109
+ * @property {string|null} [exp_at] - ISO8601 expiration timestamp (null for perpetual)
110
+ * @property {number|null} [sl] - Seat limit (null for unlimited)
111
+ * @property {string} [kid] - Key ID for public key lookup
112
+ * @property {Array<{key: string, expires_at: string|null, metadata: Object|null}>} [active_ents] - Active entitlements
113
+ * @property {Array<{key: string, expires_at: string|null, metadata: Object|null}>} [active_entitlements] - Active entitlements (alternative key)
111
114
  * @property {Object} [metadata] - Additional metadata
112
115
  */
113
116
 
@@ -136,7 +139,8 @@
136
139
  * API Error data
137
140
  * @typedef {Object} APIErrorData
138
141
  * @property {string} [error] - Error message
139
- * @property {string} [code] - Error code
142
+ * @property {string} [reason_code] - Machine-readable reason code (e.g., "license_not_found", "expired", "revoked")
143
+ * @property {string} [code] - Legacy error code (deprecated, use reason_code)
140
144
  * @property {Object} [details] - Additional error details
141
145
  */
142
146
 
package/src/utils.js CHANGED
@@ -15,8 +15,6 @@ export function parseActiveEntitlements(payload = {}) {
15
15
  const raw = payload.active_ents || payload.active_entitlements || [];
16
16
  return raw.map((e) => ({
17
17
  key: e.key,
18
- name: e.name ?? null,
19
- description: e.description ?? null,
20
18
  expires_at: e.expires_at ?? null,
21
19
  metadata: e.metadata ?? null,
22
20
  }));
@@ -68,6 +66,7 @@ export function canonicalJsonStringify(obj) {
68
66
 
69
67
  /**
70
68
  * Decode a Base64URL string to a Uint8Array
69
+ * Works in both browser and Node.js environments.
71
70
  * @param {string} base64UrlString - The Base64URL encoded string
72
71
  * @returns {Uint8Array} Decoded bytes
73
72
  */
@@ -76,7 +75,20 @@ export function base64UrlDecode(base64UrlString) {
76
75
  while (base64.length % 4) {
77
76
  base64 += "=";
78
77
  }
79
- const raw = window.atob(base64);
78
+
79
+ // Cross-platform base64 decoding
80
+ /** @type {string} */
81
+ let raw;
82
+ if (typeof atob === "function") {
83
+ // Browser environment (or Node.js 16+ with global atob)
84
+ raw = atob(base64);
85
+ } else if (typeof Buffer !== "undefined") {
86
+ // Node.js environment
87
+ raw = Buffer.from(base64, "base64").toString("binary");
88
+ } else {
89
+ throw new Error("No base64 decoder available (neither atob nor Buffer found)");
90
+ }
91
+
80
92
  const outputArray = new Uint8Array(raw.length);
81
93
  for (let i = 0; i < raw.length; ++i) {
82
94
  outputArray[i] = raw.charCodeAt(i);
@@ -117,10 +129,19 @@ export function getCanvasFingerprint() {
117
129
  }
118
130
 
119
131
  /**
120
- * Generate a unique device identifier based on browser characteristics
121
- * @returns {string} Unique device identifier
132
+ * Generate a stable device identifier based on browser characteristics.
133
+ * The ID is deterministic - same device will produce the same ID across calls.
134
+ * @returns {string} Stable device identifier
122
135
  */
123
136
  export function generateDeviceId() {
137
+ // Check if we're in a browser environment
138
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
139
+ // Node.js or non-browser environment - use a fallback
140
+ const os = typeof process !== "undefined" ? process.platform : "unknown";
141
+ const arch = typeof process !== "undefined" ? process.arch : "unknown";
142
+ return `node-${hashCode(os + "|" + arch)}`;
143
+ }
144
+
124
145
  const nav = window.navigator;
125
146
  const screen = window.screen;
126
147
  const data = [
@@ -133,7 +154,8 @@ export function generateDeviceId() {
133
154
  getCanvasFingerprint(),
134
155
  ].join("|");
135
156
 
136
- return `web-${hashCode(data)}-${Date.now().toString(36)}`;
157
+ // Stable ID without timestamp - same device produces same ID
158
+ return `web-${hashCode(data)}`;
137
159
  }
138
160
 
139
161
  /**