@licenseseat/js 0.2.1 → 0.3.0
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 +279 -118
- package/dist/index.js +197 -142
- package/dist/types/LicenseSeat.d.ts +26 -16
- package/dist/types/LicenseSeat.d.ts.map +1 -1
- package/dist/types/cache.d.ts +10 -10
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/types.d.ts +291 -63
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +5 -3
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/LicenseSeat.js +194 -144
- package/src/cache.js +16 -18
- package/src/types.js +126 -34
- package/src/utils.js +31 -6
package/dist/index.js
CHANGED
|
@@ -51,12 +51,12 @@ var LicenseCache = class {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* Get the device
|
|
55
|
-
* @returns {string|null} Device
|
|
54
|
+
* Get the device ID from the cached license
|
|
55
|
+
* @returns {string|null} Device ID or null if not found
|
|
56
56
|
*/
|
|
57
57
|
getDeviceId() {
|
|
58
58
|
const license = this.getLicense();
|
|
59
|
-
return license ? license.
|
|
59
|
+
return license ? license.device_id : null;
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* Clear the cached license data
|
|
@@ -66,39 +66,39 @@ var LicenseCache = class {
|
|
|
66
66
|
localStorage.removeItem(this.prefix + "license");
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Get the cached offline
|
|
70
|
-
* @returns {import('./types.js').
|
|
69
|
+
* Get the cached offline token
|
|
70
|
+
* @returns {import('./types.js').OfflineToken|null} Offline token or null if not found
|
|
71
71
|
*/
|
|
72
|
-
|
|
72
|
+
getOfflineToken() {
|
|
73
73
|
try {
|
|
74
|
-
const data = localStorage.getItem(this.prefix + "
|
|
74
|
+
const data = localStorage.getItem(this.prefix + "offline_token");
|
|
75
75
|
return data ? JSON.parse(data) : null;
|
|
76
76
|
} catch (e) {
|
|
77
|
-
console.error("Failed to read offline
|
|
77
|
+
console.error("Failed to read offline token cache:", e);
|
|
78
78
|
return null;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Store offline
|
|
83
|
-
* @param {import('./types.js').
|
|
82
|
+
* Store offline token data in cache
|
|
83
|
+
* @param {import('./types.js').OfflineToken} data - Offline token to cache
|
|
84
84
|
* @returns {void}
|
|
85
85
|
*/
|
|
86
|
-
|
|
86
|
+
setOfflineToken(data) {
|
|
87
87
|
try {
|
|
88
88
|
localStorage.setItem(
|
|
89
|
-
this.prefix + "
|
|
89
|
+
this.prefix + "offline_token",
|
|
90
90
|
JSON.stringify(data)
|
|
91
91
|
);
|
|
92
92
|
} catch (e) {
|
|
93
|
-
console.error("Failed to cache offline
|
|
93
|
+
console.error("Failed to cache offline token:", e);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
|
-
* Clear the cached offline
|
|
97
|
+
* Clear the cached offline token
|
|
98
98
|
* @returns {void}
|
|
99
99
|
*/
|
|
100
|
-
|
|
101
|
-
localStorage.removeItem(this.prefix + "
|
|
100
|
+
clearOfflineToken() {
|
|
101
|
+
localStorage.removeItem(this.prefix + "offline_token");
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
104
|
* Get a cached public key by key ID
|
|
@@ -143,8 +143,6 @@ var LicenseCache = class {
|
|
|
143
143
|
localStorage.removeItem(key);
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
|
-
this.clearOfflineLicense();
|
|
147
|
-
localStorage.removeItem(this.prefix + "last_seen_ts");
|
|
148
146
|
}
|
|
149
147
|
/**
|
|
150
148
|
* Get the last seen timestamp (for clock tamper detection)
|
|
@@ -218,7 +216,7 @@ var CryptoError = class extends Error {
|
|
|
218
216
|
// src/utils.js
|
|
219
217
|
import CJSON from "canonical-json";
|
|
220
218
|
function parseActiveEntitlements(payload = {}) {
|
|
221
|
-
const raw = payload.
|
|
219
|
+
const raw = payload.entitlements || payload.active_entitlements || [];
|
|
222
220
|
return raw.map((e) => ({
|
|
223
221
|
key: e.key,
|
|
224
222
|
expires_at: e.expires_at ?? null,
|
|
@@ -260,7 +258,14 @@ function base64UrlDecode(base64UrlString) {
|
|
|
260
258
|
while (base64.length % 4) {
|
|
261
259
|
base64 += "=";
|
|
262
260
|
}
|
|
263
|
-
|
|
261
|
+
let raw;
|
|
262
|
+
if (typeof atob === "function") {
|
|
263
|
+
raw = atob(base64);
|
|
264
|
+
} else if (typeof Buffer !== "undefined") {
|
|
265
|
+
raw = Buffer.from(base64, "base64").toString("binary");
|
|
266
|
+
} else {
|
|
267
|
+
throw new Error("No base64 decoder available (neither atob nor Buffer found)");
|
|
268
|
+
}
|
|
264
269
|
const outputArray = new Uint8Array(raw.length);
|
|
265
270
|
for (let i = 0; i < raw.length; ++i) {
|
|
266
271
|
outputArray[i] = raw.charCodeAt(i);
|
|
@@ -289,6 +294,11 @@ function getCanvasFingerprint() {
|
|
|
289
294
|
}
|
|
290
295
|
}
|
|
291
296
|
function generateDeviceId() {
|
|
297
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
298
|
+
const os = typeof process !== "undefined" ? process.platform : "unknown";
|
|
299
|
+
const arch = typeof process !== "undefined" ? process.arch : "unknown";
|
|
300
|
+
return `node-${hashCode(os + "|" + arch)}`;
|
|
301
|
+
}
|
|
292
302
|
const nav = window.navigator;
|
|
293
303
|
const screen = window.screen;
|
|
294
304
|
const data = [
|
|
@@ -300,7 +310,7 @@ function generateDeviceId() {
|
|
|
300
310
|
nav.hardwareConcurrency,
|
|
301
311
|
getCanvasFingerprint()
|
|
302
312
|
].join("|");
|
|
303
|
-
return `web-${hashCode(data)}
|
|
313
|
+
return `web-${hashCode(data)}`;
|
|
304
314
|
}
|
|
305
315
|
function sleep(ms) {
|
|
306
316
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -312,7 +322,9 @@ function getCsrfToken() {
|
|
|
312
322
|
|
|
313
323
|
// src/LicenseSeat.js
|
|
314
324
|
var DEFAULT_CONFIG = {
|
|
315
|
-
apiBaseUrl: "https://licenseseat.com/api",
|
|
325
|
+
apiBaseUrl: "https://licenseseat.com/api/v1",
|
|
326
|
+
productSlug: null,
|
|
327
|
+
// Required: Product slug for API calls (e.g., "my-app")
|
|
316
328
|
storagePrefix: "licenseseat_",
|
|
317
329
|
autoValidateInterval: 36e5,
|
|
318
330
|
// 1 hour
|
|
@@ -411,27 +423,33 @@ var LicenseSeatSDK = class {
|
|
|
411
423
|
* @param {string} licenseKey - The license key to activate
|
|
412
424
|
* @param {import('./types.js').ActivationOptions} [options={}] - Activation options
|
|
413
425
|
* @returns {Promise<import('./types.js').CachedLicense>} Activation result with cached license data
|
|
426
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
414
427
|
* @throws {APIError} When the API request fails
|
|
415
428
|
*/
|
|
416
429
|
async activate(licenseKey, options = {}) {
|
|
417
|
-
|
|
430
|
+
if (!this.config.productSlug) {
|
|
431
|
+
throw new ConfigurationError("productSlug is required for activation");
|
|
432
|
+
}
|
|
433
|
+
const deviceId = options.deviceId || generateDeviceId();
|
|
418
434
|
const payload = {
|
|
419
|
-
|
|
420
|
-
device_identifier: deviceId,
|
|
435
|
+
device_id: deviceId,
|
|
421
436
|
metadata: options.metadata || {}
|
|
422
437
|
};
|
|
423
|
-
if (options.
|
|
424
|
-
payload.
|
|
438
|
+
if (options.deviceName) {
|
|
439
|
+
payload.device_name = options.deviceName;
|
|
425
440
|
}
|
|
426
441
|
try {
|
|
427
442
|
this.emit("activation:start", { licenseKey, deviceId });
|
|
428
|
-
const response = await this.apiCall(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
443
|
+
const response = await this.apiCall(
|
|
444
|
+
`/products/${this.config.productSlug}/licenses/${encodeURIComponent(licenseKey)}/activate`,
|
|
445
|
+
{
|
|
446
|
+
method: "POST",
|
|
447
|
+
body: payload
|
|
448
|
+
}
|
|
449
|
+
);
|
|
432
450
|
const licenseData = {
|
|
433
451
|
license_key: licenseKey,
|
|
434
|
-
|
|
452
|
+
device_id: deviceId,
|
|
435
453
|
activation: response,
|
|
436
454
|
activated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
437
455
|
last_validated: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -451,25 +469,31 @@ var LicenseSeatSDK = class {
|
|
|
451
469
|
/**
|
|
452
470
|
* Deactivate the current license
|
|
453
471
|
* @returns {Promise<Object>} Deactivation result from the API
|
|
472
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
454
473
|
* @throws {LicenseError} When no active license is found
|
|
455
474
|
* @throws {APIError} When the API request fails
|
|
456
475
|
*/
|
|
457
476
|
async deactivate() {
|
|
477
|
+
if (!this.config.productSlug) {
|
|
478
|
+
throw new ConfigurationError("productSlug is required for deactivation");
|
|
479
|
+
}
|
|
458
480
|
const cachedLicense = this.cache.getLicense();
|
|
459
481
|
if (!cachedLicense) {
|
|
460
482
|
throw new LicenseError("No active license found", "no_license");
|
|
461
483
|
}
|
|
462
484
|
try {
|
|
463
485
|
this.emit("deactivation:start", cachedLicense);
|
|
464
|
-
const response = await this.apiCall(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
486
|
+
const response = await this.apiCall(
|
|
487
|
+
`/products/${this.config.productSlug}/licenses/${encodeURIComponent(cachedLicense.license_key)}/deactivate`,
|
|
488
|
+
{
|
|
489
|
+
method: "POST",
|
|
490
|
+
body: {
|
|
491
|
+
device_id: cachedLicense.device_id
|
|
492
|
+
}
|
|
469
493
|
}
|
|
470
|
-
|
|
494
|
+
);
|
|
471
495
|
this.cache.clearLicense();
|
|
472
|
-
this.cache.
|
|
496
|
+
this.cache.clearOfflineToken();
|
|
473
497
|
this.stopAutoValidation();
|
|
474
498
|
this.emit("deactivation:success", response);
|
|
475
499
|
return response;
|
|
@@ -483,22 +507,33 @@ var LicenseSeatSDK = class {
|
|
|
483
507
|
* @param {string} licenseKey - License key to validate
|
|
484
508
|
* @param {import('./types.js').ValidationOptions} [options={}] - Validation options
|
|
485
509
|
* @returns {Promise<import('./types.js').ValidationResult>} Validation result
|
|
510
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
486
511
|
* @throws {APIError} When the API request fails and offline fallback is not available
|
|
487
512
|
*/
|
|
488
513
|
async validateLicense(licenseKey, options = {}) {
|
|
514
|
+
if (!this.config.productSlug) {
|
|
515
|
+
throw new ConfigurationError("productSlug is required for validation");
|
|
516
|
+
}
|
|
489
517
|
try {
|
|
490
518
|
this.emit("validation:start", { licenseKey });
|
|
491
|
-
const rawResponse = await this.apiCall(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
519
|
+
const rawResponse = await this.apiCall(
|
|
520
|
+
`/products/${this.config.productSlug}/licenses/${encodeURIComponent(licenseKey)}/validate`,
|
|
521
|
+
{
|
|
522
|
+
method: "POST",
|
|
523
|
+
body: {
|
|
524
|
+
device_id: options.deviceId || this.cache.getDeviceId()
|
|
525
|
+
}
|
|
497
526
|
}
|
|
498
|
-
|
|
527
|
+
);
|
|
499
528
|
const response = {
|
|
500
529
|
valid: rawResponse.valid,
|
|
501
|
-
|
|
530
|
+
code: rawResponse.code,
|
|
531
|
+
message: rawResponse.message,
|
|
532
|
+
warnings: rawResponse.warnings,
|
|
533
|
+
license: rawResponse.license,
|
|
534
|
+
activation: rawResponse.activation,
|
|
535
|
+
// Extract entitlements from license for easy access
|
|
536
|
+
active_entitlements: rawResponse.license?.active_entitlements || []
|
|
502
537
|
};
|
|
503
538
|
const cachedLicense = this.cache.getLicense();
|
|
504
539
|
if ((!response.active_entitlements || response.active_entitlements.length === 0) && cachedLicense?.validation?.active_entitlements?.length) {
|
|
@@ -538,7 +573,13 @@ var LicenseSeatSDK = class {
|
|
|
538
573
|
if (error instanceof APIError && error.data) {
|
|
539
574
|
const cachedLicense = this.cache.getLicense();
|
|
540
575
|
if (cachedLicense && cachedLicense.license_key === licenseKey) {
|
|
541
|
-
|
|
576
|
+
const errorCode = error.data.error?.code || error.data.code;
|
|
577
|
+
const errorMessage = error.data.error?.message || error.data.message;
|
|
578
|
+
this.cache.updateValidation({
|
|
579
|
+
valid: false,
|
|
580
|
+
code: errorCode,
|
|
581
|
+
message: errorMessage
|
|
582
|
+
});
|
|
542
583
|
}
|
|
543
584
|
if (![0, 408, 429].includes(error.status)) {
|
|
544
585
|
this.stopAutoValidation();
|
|
@@ -587,33 +628,50 @@ var LicenseSeatSDK = class {
|
|
|
587
628
|
return this.checkEntitlement(entitlementKey).active;
|
|
588
629
|
}
|
|
589
630
|
/**
|
|
590
|
-
* Get offline
|
|
591
|
-
* @
|
|
631
|
+
* Get offline token data from the server
|
|
632
|
+
* @param {Object} [options={}] - Options for offline token generation
|
|
633
|
+
* @param {string} [options.deviceId] - Device ID to bind the token to (required for hardware_locked mode)
|
|
634
|
+
* @param {number} [options.ttlDays] - Token lifetime in days (default: 30, max: 90)
|
|
635
|
+
* @returns {Promise<import('./types.js').OfflineToken>} Offline token data
|
|
636
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
592
637
|
* @throws {LicenseError} When no active license is found
|
|
593
638
|
* @throws {APIError} When the API request fails
|
|
594
639
|
*/
|
|
595
|
-
async
|
|
640
|
+
async getOfflineToken(options = {}) {
|
|
641
|
+
if (!this.config.productSlug) {
|
|
642
|
+
throw new ConfigurationError("productSlug is required for offline token");
|
|
643
|
+
}
|
|
596
644
|
const license = this.cache.getLicense();
|
|
597
645
|
if (!license || !license.license_key) {
|
|
598
|
-
const errorMsg = "No active license key found in cache to fetch offline
|
|
646
|
+
const errorMsg = "No active license key found in cache to fetch offline token.";
|
|
599
647
|
this.emit("sdk:error", { message: errorMsg });
|
|
600
648
|
throw new LicenseError(errorMsg, "no_license");
|
|
601
649
|
}
|
|
602
650
|
try {
|
|
603
|
-
this.emit("
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
651
|
+
this.emit("offlineToken:fetching", { licenseKey: license.license_key });
|
|
652
|
+
const body = {};
|
|
653
|
+
if (options.deviceId) {
|
|
654
|
+
body.device_id = options.deviceId;
|
|
655
|
+
}
|
|
656
|
+
if (options.ttlDays) {
|
|
657
|
+
body.ttl_days = options.ttlDays;
|
|
658
|
+
}
|
|
659
|
+
const path = `/products/${this.config.productSlug}/licenses/${encodeURIComponent(license.license_key)}/offline-token`;
|
|
660
|
+
const response = await this.apiCall(path, {
|
|
661
|
+
method: "POST",
|
|
662
|
+
body: Object.keys(body).length > 0 ? body : void 0
|
|
663
|
+
});
|
|
664
|
+
this.emit("offlineToken:fetched", {
|
|
607
665
|
licenseKey: license.license_key,
|
|
608
666
|
data: response
|
|
609
667
|
});
|
|
610
668
|
return response;
|
|
611
669
|
} catch (error) {
|
|
612
670
|
this.log(
|
|
613
|
-
`Failed to get offline
|
|
671
|
+
`Failed to get offline token for ${license.license_key}:`,
|
|
614
672
|
error
|
|
615
673
|
);
|
|
616
|
-
this.emit("
|
|
674
|
+
this.emit("offlineToken:fetchError", {
|
|
617
675
|
licenseKey: license.license_key,
|
|
618
676
|
error
|
|
619
677
|
});
|
|
@@ -621,45 +679,45 @@ var LicenseSeatSDK = class {
|
|
|
621
679
|
}
|
|
622
680
|
}
|
|
623
681
|
/**
|
|
624
|
-
* Fetch a
|
|
625
|
-
* @param {string} keyId - The Key ID (kid) for which to fetch the
|
|
626
|
-
* @returns {Promise<
|
|
682
|
+
* Fetch a signing key from the server by key ID
|
|
683
|
+
* @param {string} keyId - The Key ID (kid) for which to fetch the signing key
|
|
684
|
+
* @returns {Promise<import('./types.js').SigningKey>} Signing key data
|
|
627
685
|
* @throws {Error} When keyId is not provided or the key is not found
|
|
628
686
|
*/
|
|
629
|
-
async
|
|
687
|
+
async getSigningKey(keyId) {
|
|
630
688
|
if (!keyId) {
|
|
631
|
-
throw new Error("Key ID is required to fetch a
|
|
689
|
+
throw new Error("Key ID is required to fetch a signing key.");
|
|
632
690
|
}
|
|
633
691
|
try {
|
|
634
|
-
this.log(`Fetching
|
|
635
|
-
const response = await this.apiCall(`/
|
|
692
|
+
this.log(`Fetching signing key for kid: ${keyId}`);
|
|
693
|
+
const response = await this.apiCall(`/signing-keys/${encodeURIComponent(keyId)}`, {
|
|
636
694
|
method: "GET"
|
|
637
695
|
});
|
|
638
|
-
if (response && response.
|
|
639
|
-
this.log(`Successfully fetched
|
|
640
|
-
return response
|
|
696
|
+
if (response && response.public_key) {
|
|
697
|
+
this.log(`Successfully fetched signing key for kid: ${keyId}`);
|
|
698
|
+
return response;
|
|
641
699
|
} else {
|
|
642
700
|
throw new Error(
|
|
643
|
-
`
|
|
701
|
+
`Signing key not found or invalid response for kid: ${keyId}`
|
|
644
702
|
);
|
|
645
703
|
}
|
|
646
704
|
} catch (error) {
|
|
647
|
-
this.log(`Failed to fetch
|
|
705
|
+
this.log(`Failed to fetch signing key for kid ${keyId}:`, error);
|
|
648
706
|
throw error;
|
|
649
707
|
}
|
|
650
708
|
}
|
|
651
709
|
/**
|
|
652
|
-
* Verify a signed offline
|
|
653
|
-
* @param {import('./types.js').
|
|
710
|
+
* Verify a signed offline token client-side using Ed25519
|
|
711
|
+
* @param {import('./types.js').OfflineToken} offlineTokenData - The offline token data
|
|
654
712
|
* @param {string} publicKeyB64 - Base64-encoded public Ed25519 key
|
|
655
713
|
* @returns {Promise<boolean>} True if verification is successful
|
|
656
714
|
* @throws {CryptoError} When crypto library is not available
|
|
657
715
|
* @throws {Error} When inputs are invalid
|
|
658
716
|
*/
|
|
659
|
-
async
|
|
660
|
-
this.log("Attempting to verify offline
|
|
661
|
-
if (!
|
|
662
|
-
throw new Error("Invalid
|
|
717
|
+
async verifyOfflineToken(offlineTokenData, publicKeyB64) {
|
|
718
|
+
this.log("Attempting to verify offline token client-side.");
|
|
719
|
+
if (!offlineTokenData || !offlineTokenData.canonical || !offlineTokenData.signature) {
|
|
720
|
+
throw new Error("Invalid offline token object provided. Expected format: { token, signature, canonical }");
|
|
663
721
|
}
|
|
664
722
|
if (!publicKeyB64) {
|
|
665
723
|
throw new Error("Public key (Base64 encoded) is required.");
|
|
@@ -672,27 +730,20 @@ var LicenseSeatSDK = class {
|
|
|
672
730
|
throw err;
|
|
673
731
|
}
|
|
674
732
|
try {
|
|
675
|
-
const
|
|
676
|
-
const
|
|
733
|
+
const messageBytes = new TextEncoder().encode(offlineTokenData.canonical);
|
|
734
|
+
const signatureBytes = base64UrlDecode(offlineTokenData.signature.value);
|
|
677
735
|
const publicKeyBytes = base64UrlDecode(publicKeyB64);
|
|
678
|
-
const signatureBytes = base64UrlDecode(signedLicenseData.signature_b64u);
|
|
679
736
|
const isValid = ed.verify(signatureBytes, messageBytes, publicKeyBytes);
|
|
680
737
|
if (isValid) {
|
|
681
|
-
this.log(
|
|
682
|
-
|
|
683
|
-
);
|
|
684
|
-
this.emit("offlineLicense:verified", {
|
|
685
|
-
payload: signedLicenseData.payload
|
|
686
|
-
});
|
|
738
|
+
this.log("Offline token signature VERIFIED successfully client-side.");
|
|
739
|
+
this.emit("offlineToken:verified", { token: offlineTokenData.token });
|
|
687
740
|
} else {
|
|
688
|
-
this.log("Offline
|
|
689
|
-
this.emit("
|
|
690
|
-
payload: signedLicenseData.payload
|
|
691
|
-
});
|
|
741
|
+
this.log("Offline token signature INVALID client-side.");
|
|
742
|
+
this.emit("offlineToken:verificationFailed", { token: offlineTokenData.token });
|
|
692
743
|
}
|
|
693
744
|
return isValid;
|
|
694
745
|
} catch (error) {
|
|
695
|
-
this.log("Client-side offline
|
|
746
|
+
this.log("Client-side offline token verification error:", error);
|
|
696
747
|
this.emit("sdk:error", {
|
|
697
748
|
message: "Client-side verification failed.",
|
|
698
749
|
error
|
|
@@ -717,19 +768,19 @@ var LicenseSeatSDK = class {
|
|
|
717
768
|
if (validation.offline) {
|
|
718
769
|
return {
|
|
719
770
|
status: "offline-invalid",
|
|
720
|
-
message: validation.
|
|
771
|
+
message: validation.code || "License invalid (offline)"
|
|
721
772
|
};
|
|
722
773
|
}
|
|
723
774
|
return {
|
|
724
775
|
status: "invalid",
|
|
725
|
-
message: validation.
|
|
776
|
+
message: validation.message || validation.code || "License invalid"
|
|
726
777
|
};
|
|
727
778
|
}
|
|
728
779
|
if (validation.offline) {
|
|
729
780
|
return {
|
|
730
781
|
status: "offline-valid",
|
|
731
782
|
license: license.license_key,
|
|
732
|
-
device: license.
|
|
783
|
+
device: license.device_id,
|
|
733
784
|
activated_at: license.activated_at,
|
|
734
785
|
last_validated: license.last_validated,
|
|
735
786
|
entitlements: validation.active_entitlements || []
|
|
@@ -738,7 +789,7 @@ var LicenseSeatSDK = class {
|
|
|
738
789
|
return {
|
|
739
790
|
status: "active",
|
|
740
791
|
license: license.license_key,
|
|
741
|
-
device: license.
|
|
792
|
+
device: license.device_id,
|
|
742
793
|
activated_at: license.activated_at,
|
|
743
794
|
last_validated: license.last_validated,
|
|
744
795
|
entitlements: validation.active_entitlements || []
|
|
@@ -748,12 +799,12 @@ var LicenseSeatSDK = class {
|
|
|
748
799
|
* Test server authentication
|
|
749
800
|
* Useful for verifying API key/session is valid.
|
|
750
801
|
* @returns {Promise<Object>} Result from the server
|
|
751
|
-
* @throws {
|
|
802
|
+
* @throws {ConfigurationError} When API key is not configured
|
|
752
803
|
* @throws {APIError} When authentication fails
|
|
753
804
|
*/
|
|
754
805
|
async testAuth() {
|
|
755
806
|
if (!this.config.apiKey) {
|
|
756
|
-
const err = new
|
|
807
|
+
const err = new ConfigurationError("API key is required for auth test");
|
|
757
808
|
this.emit("auth_test:error", { error: err });
|
|
758
809
|
throw err;
|
|
759
810
|
}
|
|
@@ -898,9 +949,9 @@ var LicenseSeatSDK = class {
|
|
|
898
949
|
startConnectivityPolling() {
|
|
899
950
|
if (this.connectivityTimer)
|
|
900
951
|
return;
|
|
901
|
-
const
|
|
952
|
+
const healthCheck = async () => {
|
|
902
953
|
try {
|
|
903
|
-
await fetch(`${this.config.apiBaseUrl}/
|
|
954
|
+
await fetch(`${this.config.apiBaseUrl}/health`, {
|
|
904
955
|
method: "GET",
|
|
905
956
|
credentials: "omit"
|
|
906
957
|
});
|
|
@@ -917,7 +968,7 @@ var LicenseSeatSDK = class {
|
|
|
917
968
|
}
|
|
918
969
|
};
|
|
919
970
|
this.connectivityTimer = setInterval(
|
|
920
|
-
|
|
971
|
+
healthCheck,
|
|
921
972
|
this.config.networkRecheckInterval
|
|
922
973
|
);
|
|
923
974
|
}
|
|
@@ -936,7 +987,7 @@ var LicenseSeatSDK = class {
|
|
|
936
987
|
// Offline License Management
|
|
937
988
|
// ============================================================
|
|
938
989
|
/**
|
|
939
|
-
* Fetch and cache offline
|
|
990
|
+
* Fetch and cache offline token and signing key
|
|
940
991
|
* Uses a lock to prevent concurrent calls from causing race conditions
|
|
941
992
|
* @returns {Promise<void>}
|
|
942
993
|
* @private
|
|
@@ -948,19 +999,19 @@ var LicenseSeatSDK = class {
|
|
|
948
999
|
}
|
|
949
1000
|
this.syncingOfflineAssets = true;
|
|
950
1001
|
try {
|
|
951
|
-
const offline = await this.
|
|
952
|
-
this.cache.
|
|
953
|
-
const kid = offline.
|
|
1002
|
+
const offline = await this.getOfflineToken();
|
|
1003
|
+
this.cache.setOfflineToken(offline);
|
|
1004
|
+
const kid = offline.signature?.key_id || offline.token?.kid;
|
|
954
1005
|
if (kid) {
|
|
955
1006
|
const existingKey = this.cache.getPublicKey(kid);
|
|
956
1007
|
if (!existingKey) {
|
|
957
|
-
const
|
|
958
|
-
this.cache.setPublicKey(kid,
|
|
1008
|
+
const signingKey = await this.getSigningKey(kid);
|
|
1009
|
+
this.cache.setPublicKey(kid, signingKey.public_key);
|
|
959
1010
|
}
|
|
960
1011
|
}
|
|
961
|
-
this.emit("
|
|
962
|
-
kid
|
|
963
|
-
|
|
1012
|
+
this.emit("offlineToken:ready", {
|
|
1013
|
+
kid,
|
|
1014
|
+
exp: offline.token?.exp
|
|
964
1015
|
});
|
|
965
1016
|
const res = await this.quickVerifyCachedOfflineLocal();
|
|
966
1017
|
if (res) {
|
|
@@ -990,39 +1041,40 @@ var LicenseSeatSDK = class {
|
|
|
990
1041
|
);
|
|
991
1042
|
}
|
|
992
1043
|
/**
|
|
993
|
-
* Verify cached offline
|
|
1044
|
+
* Verify cached offline token
|
|
994
1045
|
* @returns {Promise<import('./types.js').ValidationResult>}
|
|
995
1046
|
* @private
|
|
996
1047
|
*/
|
|
997
1048
|
async verifyCachedOffline() {
|
|
998
|
-
const signed = this.cache.
|
|
1049
|
+
const signed = this.cache.getOfflineToken();
|
|
999
1050
|
if (!signed) {
|
|
1000
|
-
return { valid: false, offline: true,
|
|
1051
|
+
return { valid: false, offline: true, code: "no_offline_token" };
|
|
1001
1052
|
}
|
|
1002
|
-
const kid = signed.
|
|
1053
|
+
const kid = signed.signature?.key_id || signed.token?.kid;
|
|
1003
1054
|
let pub = kid ? this.cache.getPublicKey(kid) : null;
|
|
1004
1055
|
if (!pub) {
|
|
1005
1056
|
try {
|
|
1006
|
-
|
|
1057
|
+
const signingKey = await this.getSigningKey(kid);
|
|
1058
|
+
pub = signingKey.public_key;
|
|
1007
1059
|
this.cache.setPublicKey(kid, pub);
|
|
1008
1060
|
} catch (e) {
|
|
1009
|
-
return { valid: false, offline: true,
|
|
1061
|
+
return { valid: false, offline: true, code: "no_public_key" };
|
|
1010
1062
|
}
|
|
1011
1063
|
}
|
|
1012
1064
|
try {
|
|
1013
|
-
const ok = await this.
|
|
1065
|
+
const ok = await this.verifyOfflineToken(signed, pub);
|
|
1014
1066
|
if (!ok) {
|
|
1015
|
-
return { valid: false, offline: true,
|
|
1067
|
+
return { valid: false, offline: true, code: "signature_invalid" };
|
|
1016
1068
|
}
|
|
1017
|
-
const
|
|
1069
|
+
const token = signed.token;
|
|
1018
1070
|
const cached = this.cache.getLicense();
|
|
1019
|
-
if (!cached || !constantTimeEqual(
|
|
1020
|
-
return { valid: false, offline: true,
|
|
1071
|
+
if (!cached || !constantTimeEqual(token.license_key || "", cached.license_key || "")) {
|
|
1072
|
+
return { valid: false, offline: true, code: "license_mismatch" };
|
|
1021
1073
|
}
|
|
1022
1074
|
const now = Date.now();
|
|
1023
|
-
const expAt =
|
|
1075
|
+
const expAt = token.exp ? token.exp * 1e3 : null;
|
|
1024
1076
|
if (expAt && expAt < now) {
|
|
1025
|
-
return { valid: false, offline: true,
|
|
1077
|
+
return { valid: false, offline: true, code: "expired" };
|
|
1026
1078
|
}
|
|
1027
1079
|
if (!expAt && this.config.maxOfflineDays > 0) {
|
|
1028
1080
|
const pivot = cached.last_validated || cached.activated_at;
|
|
@@ -1032,24 +1084,24 @@ var LicenseSeatSDK = class {
|
|
|
1032
1084
|
return {
|
|
1033
1085
|
valid: false,
|
|
1034
1086
|
offline: true,
|
|
1035
|
-
|
|
1087
|
+
code: "grace_period_expired"
|
|
1036
1088
|
};
|
|
1037
1089
|
}
|
|
1038
1090
|
}
|
|
1039
1091
|
}
|
|
1040
1092
|
const lastSeen = this.cache.getLastSeenTimestamp();
|
|
1041
1093
|
if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
|
|
1042
|
-
return { valid: false, offline: true,
|
|
1094
|
+
return { valid: false, offline: true, code: "clock_tamper" };
|
|
1043
1095
|
}
|
|
1044
1096
|
this.cache.setLastSeenTimestamp(now);
|
|
1045
|
-
const active = parseActiveEntitlements(
|
|
1097
|
+
const active = parseActiveEntitlements(token);
|
|
1046
1098
|
return {
|
|
1047
1099
|
valid: true,
|
|
1048
1100
|
offline: true,
|
|
1049
1101
|
...active.length ? { active_entitlements: active } : {}
|
|
1050
1102
|
};
|
|
1051
1103
|
} catch (e) {
|
|
1052
|
-
return { valid: false, offline: true,
|
|
1104
|
+
return { valid: false, offline: true, code: "verification_error" };
|
|
1053
1105
|
}
|
|
1054
1106
|
}
|
|
1055
1107
|
/**
|
|
@@ -1059,40 +1111,40 @@ var LicenseSeatSDK = class {
|
|
|
1059
1111
|
* @private
|
|
1060
1112
|
*/
|
|
1061
1113
|
async quickVerifyCachedOfflineLocal() {
|
|
1062
|
-
const signed = this.cache.
|
|
1114
|
+
const signed = this.cache.getOfflineToken();
|
|
1063
1115
|
if (!signed)
|
|
1064
1116
|
return null;
|
|
1065
|
-
const kid = signed.
|
|
1117
|
+
const kid = signed.signature?.key_id || signed.token?.kid;
|
|
1066
1118
|
const pub = kid ? this.cache.getPublicKey(kid) : null;
|
|
1067
1119
|
if (!pub)
|
|
1068
1120
|
return null;
|
|
1069
1121
|
try {
|
|
1070
|
-
const ok = await this.
|
|
1122
|
+
const ok = await this.verifyOfflineToken(signed, pub);
|
|
1071
1123
|
if (!ok) {
|
|
1072
|
-
return { valid: false, offline: true,
|
|
1124
|
+
return { valid: false, offline: true, code: "signature_invalid" };
|
|
1073
1125
|
}
|
|
1074
|
-
const
|
|
1126
|
+
const token = signed.token;
|
|
1075
1127
|
const cached = this.cache.getLicense();
|
|
1076
|
-
if (!cached || !constantTimeEqual(
|
|
1077
|
-
return { valid: false, offline: true,
|
|
1128
|
+
if (!cached || !constantTimeEqual(token.license_key || "", cached.license_key || "")) {
|
|
1129
|
+
return { valid: false, offline: true, code: "license_mismatch" };
|
|
1078
1130
|
}
|
|
1079
1131
|
const now = Date.now();
|
|
1080
|
-
const expAt =
|
|
1132
|
+
const expAt = token.exp ? token.exp * 1e3 : null;
|
|
1081
1133
|
if (expAt && expAt < now) {
|
|
1082
|
-
return { valid: false, offline: true,
|
|
1134
|
+
return { valid: false, offline: true, code: "expired" };
|
|
1083
1135
|
}
|
|
1084
1136
|
const lastSeen = this.cache.getLastSeenTimestamp();
|
|
1085
1137
|
if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
|
|
1086
|
-
return { valid: false, offline: true,
|
|
1138
|
+
return { valid: false, offline: true, code: "clock_tamper" };
|
|
1087
1139
|
}
|
|
1088
|
-
const active = parseActiveEntitlements(
|
|
1140
|
+
const active = parseActiveEntitlements(token);
|
|
1089
1141
|
return {
|
|
1090
1142
|
valid: true,
|
|
1091
1143
|
offline: true,
|
|
1092
1144
|
...active.length ? { active_entitlements: active } : {}
|
|
1093
1145
|
};
|
|
1094
1146
|
} catch (_) {
|
|
1095
|
-
return { valid: false, offline: true,
|
|
1147
|
+
return { valid: false, offline: true, code: "verification_error" };
|
|
1096
1148
|
}
|
|
1097
1149
|
}
|
|
1098
1150
|
// ============================================================
|
|
@@ -1134,11 +1186,14 @@ var LicenseSeatSDK = class {
|
|
|
1134
1186
|
});
|
|
1135
1187
|
const data = await response.json();
|
|
1136
1188
|
if (!response.ok) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
)
|
|
1189
|
+
const errorObj = data.error;
|
|
1190
|
+
let errorMessage = "Request failed";
|
|
1191
|
+
if (typeof errorObj === "object" && errorObj !== null) {
|
|
1192
|
+
errorMessage = errorObj.message || "Request failed";
|
|
1193
|
+
} else if (typeof errorObj === "string") {
|
|
1194
|
+
errorMessage = errorObj;
|
|
1195
|
+
}
|
|
1196
|
+
throw new APIError(errorMessage, response.status, data);
|
|
1142
1197
|
}
|
|
1143
1198
|
if (!this.online) {
|
|
1144
1199
|
this.online = true;
|