@licenseseat/js 0.2.0 → 0.2.1

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
@@ -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
@@ -151,7 +151,7 @@ const sdk = new LicenseSeat({
151
151
  | Option | Type | Default | Description |
152
152
  |--------|------|---------|-------------|
153
153
  | `apiKey` | `string` | `null` | API key for authentication (required for most operations) |
154
- | `apiBaseUrl` | `string` | `'https://api.licenseseat.com'` | API base URL |
154
+ | `apiBaseUrl` | `string` | `'https://licenseseat.com/api'` | API base URL |
155
155
  | `storagePrefix` | `string` | `'licenseseat_'` | Prefix for localStorage keys |
156
156
  | `autoValidateInterval` | `number` | `3600000` | Auto-validation interval in ms (1 hour) |
157
157
  | `autoInitialize` | `boolean` | `true` | Auto-initialize and validate cached license |
@@ -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`).
@@ -335,6 +344,7 @@ sdk.off('activation:success', handler);
335
344
  | **Lifecycle** | | |
336
345
  | `license:loaded` | Cached license loaded on init | `CachedLicense` |
337
346
  | `sdk:reset` | SDK was reset | – |
347
+ | `sdk:destroyed` | SDK was destroyed | – |
338
348
  | `sdk:error` | General SDK error | `{ message, error? }` |
339
349
  | **Activation** | | |
340
350
  | `activation:start` | Activation started | `{ licenseKey, deviceId }` |
@@ -723,7 +733,7 @@ This project follows [Semantic Versioning](https://semver.org/):
723
733
 
724
734
  | Change | Before | After | Migration |
725
735
  |--------|--------|-------|-----------|
726
- | `apiBaseUrl` default | `/api` | `https://api.licenseseat.com` | Set `apiBaseUrl` explicitly if using a relative URL |
736
+ | `apiBaseUrl` default | `/api` | `https://licenseseat.com/api` | Set `apiBaseUrl` explicitly if using a relative URL |
727
737
  | `offlineFallbackEnabled` default | `true` | `false` | Set `offlineFallbackEnabled: true` if you need offline fallback |
728
738
 
729
739
  ### New Features in v0.2.0
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
  }));
@@ -314,7 +312,7 @@ function getCsrfToken() {
314
312
 
315
313
  // src/LicenseSeat.js
316
314
  var DEFAULT_CONFIG = {
317
- apiBaseUrl: "https://api.licenseseat.com",
315
+ apiBaseUrl: "https://licenseseat.com/api",
318
316
  storagePrefix: "licenseseat_",
319
317
  autoValidateInterval: 36e5,
320
318
  // 1 hour
@@ -352,6 +350,8 @@ var LicenseSeatSDK = class {
352
350
  this.connectivityTimer = null;
353
351
  this.offlineRefreshTimer = null;
354
352
  this.lastOfflineValidation = null;
353
+ this.syncingOfflineAssets = false;
354
+ this.destroyed = false;
355
355
  if (ed && ed.etc && sha512) {
356
356
  ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
357
357
  } else {
@@ -488,7 +488,7 @@ var LicenseSeatSDK = class {
488
488
  async validateLicense(licenseKey, options = {}) {
489
489
  try {
490
490
  this.emit("validation:start", { licenseKey });
491
- const response = await this.apiCall("/licenses/validate", {
491
+ const rawResponse = await this.apiCall("/licenses/validate", {
492
492
  method: "POST",
493
493
  body: {
494
494
  license_key: licenseKey,
@@ -496,6 +496,10 @@ var LicenseSeatSDK = class {
496
496
  product_slug: options.productSlug
497
497
  }
498
498
  });
499
+ const response = {
500
+ valid: rawResponse.valid,
501
+ ...rawResponse.license || {}
502
+ };
499
503
  const cachedLicense = this.cache.getLicense();
500
504
  if ((!response.active_entitlements || response.active_entitlements.length === 0) && cachedLicense?.validation?.active_entitlements?.length) {
501
505
  response.active_entitlements = cachedLicense.validation.active_entitlements;
@@ -769,10 +773,36 @@ var LicenseSeatSDK = class {
769
773
  */
770
774
  reset() {
771
775
  this.stopAutoValidation();
776
+ this.stopConnectivityPolling();
777
+ if (this.offlineRefreshTimer) {
778
+ clearInterval(this.offlineRefreshTimer);
779
+ this.offlineRefreshTimer = null;
780
+ }
772
781
  this.cache.clear();
773
782
  this.lastOfflineValidation = null;
783
+ this.currentAutoLicenseKey = null;
774
784
  this.emit("sdk:reset");
775
785
  }
786
+ /**
787
+ * Destroy the SDK instance and release all resources
788
+ * Call this when you no longer need the SDK to prevent memory leaks.
789
+ * After calling destroy(), the SDK instance should not be used.
790
+ * @returns {void}
791
+ */
792
+ destroy() {
793
+ this.destroyed = true;
794
+ this.stopAutoValidation();
795
+ this.stopConnectivityPolling();
796
+ if (this.offlineRefreshTimer) {
797
+ clearInterval(this.offlineRefreshTimer);
798
+ this.offlineRefreshTimer = null;
799
+ }
800
+ this.eventListeners = {};
801
+ this.cache.clear();
802
+ this.lastOfflineValidation = null;
803
+ this.currentAutoLicenseKey = null;
804
+ this.emit("sdk:destroyed");
805
+ }
776
806
  // ============================================================
777
807
  // Event Handling
778
808
  // ============================================================
@@ -907,10 +937,16 @@ var LicenseSeatSDK = class {
907
937
  // ============================================================
908
938
  /**
909
939
  * Fetch and cache offline license and public key
940
+ * Uses a lock to prevent concurrent calls from causing race conditions
910
941
  * @returns {Promise<void>}
911
942
  * @private
912
943
  */
913
944
  async syncOfflineAssets() {
945
+ if (this.syncingOfflineAssets || this.destroyed) {
946
+ this.log("Skipping syncOfflineAssets: already syncing or destroyed");
947
+ return;
948
+ }
949
+ this.syncingOfflineAssets = true;
914
950
  try {
915
951
  const offline = await this.getOfflineLicense();
916
952
  this.cache.setOfflineLicense(offline);
@@ -936,6 +972,8 @@ var LicenseSeatSDK = class {
936
972
  }
937
973
  } catch (err) {
938
974
  this.log("Failed to sync offline assets:", err);
975
+ } finally {
976
+ this.syncingOfflineAssets = false;
939
977
  }
940
978
  }
941
979
  /**
@@ -1016,6 +1054,7 @@ var LicenseSeatSDK = class {
1016
1054
  }
1017
1055
  /**
1018
1056
  * Quick offline verification using only local data (no network)
1057
+ * Performs signature verification plus basic validity checks (expiry, license key match)
1019
1058
  * @returns {Promise<import('./types.js').ValidationResult|null>}
1020
1059
  * @private
1021
1060
  */
@@ -1032,7 +1071,21 @@ var LicenseSeatSDK = class {
1032
1071
  if (!ok) {
1033
1072
  return { valid: false, offline: true, reason_code: "signature_invalid" };
1034
1073
  }
1035
- const active = parseActiveEntitlements(signed.payload || {});
1074
+ const payload = signed.payload || {};
1075
+ const cached = this.cache.getLicense();
1076
+ if (!cached || !constantTimeEqual(payload.lic_k || "", cached.license_key || "")) {
1077
+ return { valid: false, offline: true, reason_code: "license_mismatch" };
1078
+ }
1079
+ const now = Date.now();
1080
+ const expAt = payload.exp_at ? Date.parse(payload.exp_at) : null;
1081
+ if (expAt && expAt < now) {
1082
+ return { valid: false, offline: true, reason_code: "expired" };
1083
+ }
1084
+ const lastSeen = this.cache.getLastSeenTimestamp();
1085
+ if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
1086
+ return { valid: false, offline: true, reason_code: "clock_tamper" };
1087
+ }
1088
+ const active = parseActiveEntitlements(payload);
1036
1089
  return {
1037
1090
  valid: true,
1038
1091
  offline: true,
@@ -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.
@@ -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"}
@@ -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;;;;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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@licenseseat/js",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Official JavaScript SDK for LicenseSeat – simple, secure software licensing.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -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
@@ -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 (
@@ -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
  }));