@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 +88 -78
- package/dist/index.js +74 -9
- package/dist/types/LicenseSeat.d.ts +25 -4
- package/dist/types/LicenseSeat.d.ts.map +1 -1
- package/dist/types/types.d.ts +35 -14
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +4 -2
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/LicenseSeat.js +93 -9
- package/src/types.js +12 -8
- package/src/utils.js +28 -6
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
|
[](https://github.com/licenseseat/licenseseat-js/actions/workflows/ci.yml)
|
|
6
4
|
[](https://www.npmjs.com/package/@licenseseat/js)
|
|
7
|
-
[](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://
|
|
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
|
|
152
|
-
|
|
153
|
-
| `apiKey`
|
|
154
|
-
| `apiBaseUrl`
|
|
155
|
-
| `storagePrefix`
|
|
156
|
-
| `autoValidateInterval`
|
|
157
|
-
| `autoInitialize`
|
|
158
|
-
| `offlineFallbackEnabled` | `boolean` | `false`
|
|
159
|
-
| `maxOfflineDays`
|
|
160
|
-
| `maxRetries`
|
|
161
|
-
| `retryDelay`
|
|
162
|
-
| `debug`
|
|
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
|
|
334
|
-
|
|
335
|
-
| **Lifecycle**
|
|
336
|
-
| `license:loaded`
|
|
337
|
-
| `sdk:reset`
|
|
338
|
-
| `sdk:
|
|
339
|
-
|
|
|
340
|
-
|
|
|
341
|
-
| `activation:
|
|
342
|
-
| `activation:
|
|
343
|
-
|
|
|
344
|
-
|
|
|
345
|
-
| `deactivation:
|
|
346
|
-
| `deactivation:
|
|
347
|
-
|
|
|
348
|
-
|
|
|
349
|
-
| `validation:
|
|
350
|
-
| `validation:
|
|
351
|
-
| `validation:
|
|
352
|
-
| `validation:
|
|
353
|
-
| `validation:offline-
|
|
354
|
-
| `validation:
|
|
355
|
-
|
|
|
356
|
-
|
|
|
357
|
-
| `autovalidation:
|
|
358
|
-
|
|
|
359
|
-
|
|
|
360
|
-
| `network:
|
|
361
|
-
|
|
|
362
|
-
|
|
|
363
|
-
| `offlineLicense:
|
|
364
|
-
| `offlineLicense:
|
|
365
|
-
| `offlineLicense:
|
|
366
|
-
| `offlineLicense:
|
|
367
|
-
| `offlineLicense:
|
|
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
|
|
448
|
-
|
|
449
|
-
| `APIError`
|
|
450
|
-
| `LicenseError`
|
|
451
|
-
| `ConfigurationError` | SDK misconfiguration
|
|
452
|
-
| `CryptoError`
|
|
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
|
|
555
|
-
|
|
556
|
-
| `npm run build`
|
|
557
|
-
| `npm run build:js`
|
|
558
|
-
| `npm run build:types`
|
|
559
|
-
| `npm run build:iife`
|
|
560
|
-
| `npm run dev`
|
|
561
|
-
| `npm test`
|
|
562
|
-
| `npm run test:watch`
|
|
563
|
-
| `npm run test:coverage` | Run tests with coverage report
|
|
564
|
-
| `npm run typecheck`
|
|
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
|
|
665
|
-
|
|
666
|
-
| **esm.sh**
|
|
667
|
-
| **unpkg**
|
|
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**
|
|
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
|
|
725
|
-
|
|
726
|
-
| `apiBaseUrl` default
|
|
727
|
-
| `offlineFallbackEnabled` default | `true` | `false`
|
|
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
|
-
|
|
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)}
|
|
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://
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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":"
|
|
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"}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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
|
-
* -
|
|
256
|
+
* - Product slug
|
|
257
|
+
*/
|
|
258
|
+
prod_s?: string;
|
|
259
|
+
/**
|
|
260
|
+
* - License plan key
|
|
260
261
|
*/
|
|
261
|
-
|
|
262
|
+
plan_k?: string;
|
|
262
263
|
/**
|
|
263
|
-
* -
|
|
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<
|
|
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<
|
|
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
|
-
* -
|
|
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
|
|
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"}
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -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
|
|
40
|
-
*
|
|
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,
|
|
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.
|
|
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",
|
package/src/LicenseSeat.js
CHANGED
|
@@ -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://
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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://
|
|
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} [
|
|
108
|
-
* @property {string} [
|
|
109
|
-
* @property {
|
|
110
|
-
* @property {
|
|
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} [
|
|
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
|
-
|
|
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
|
|
121
|
-
*
|
|
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
|
-
|
|
157
|
+
// Stable ID without timestamp - same device produces same ID
|
|
158
|
+
return `web-${hashCode(data)}`;
|
|
137
159
|
}
|
|
138
160
|
|
|
139
161
|
/**
|