@licenseseat/js 0.2.2 → 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 +227 -66
- package/dist/index.js +181 -138
- package/dist/types/LicenseSeat.d.ts +22 -12
- 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 +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/LicenseSeat.js +188 -138
- package/src/cache.js +16 -18
- package/src/types.js +126 -34
- package/src/utils.js +3 -2
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,
|
|
@@ -324,7 +322,9 @@ function getCsrfToken() {
|
|
|
324
322
|
|
|
325
323
|
// src/LicenseSeat.js
|
|
326
324
|
var DEFAULT_CONFIG = {
|
|
327
|
-
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")
|
|
328
328
|
storagePrefix: "licenseseat_",
|
|
329
329
|
autoValidateInterval: 36e5,
|
|
330
330
|
// 1 hour
|
|
@@ -423,27 +423,33 @@ var LicenseSeatSDK = class {
|
|
|
423
423
|
* @param {string} licenseKey - The license key to activate
|
|
424
424
|
* @param {import('./types.js').ActivationOptions} [options={}] - Activation options
|
|
425
425
|
* @returns {Promise<import('./types.js').CachedLicense>} Activation result with cached license data
|
|
426
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
426
427
|
* @throws {APIError} When the API request fails
|
|
427
428
|
*/
|
|
428
429
|
async activate(licenseKey, options = {}) {
|
|
429
|
-
|
|
430
|
+
if (!this.config.productSlug) {
|
|
431
|
+
throw new ConfigurationError("productSlug is required for activation");
|
|
432
|
+
}
|
|
433
|
+
const deviceId = options.deviceId || generateDeviceId();
|
|
430
434
|
const payload = {
|
|
431
|
-
|
|
432
|
-
device_identifier: deviceId,
|
|
435
|
+
device_id: deviceId,
|
|
433
436
|
metadata: options.metadata || {}
|
|
434
437
|
};
|
|
435
|
-
if (options.
|
|
436
|
-
payload.
|
|
438
|
+
if (options.deviceName) {
|
|
439
|
+
payload.device_name = options.deviceName;
|
|
437
440
|
}
|
|
438
441
|
try {
|
|
439
442
|
this.emit("activation:start", { licenseKey, deviceId });
|
|
440
|
-
const response = await this.apiCall(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
443
|
+
const response = await this.apiCall(
|
|
444
|
+
`/products/${this.config.productSlug}/licenses/${encodeURIComponent(licenseKey)}/activate`,
|
|
445
|
+
{
|
|
446
|
+
method: "POST",
|
|
447
|
+
body: payload
|
|
448
|
+
}
|
|
449
|
+
);
|
|
444
450
|
const licenseData = {
|
|
445
451
|
license_key: licenseKey,
|
|
446
|
-
|
|
452
|
+
device_id: deviceId,
|
|
447
453
|
activation: response,
|
|
448
454
|
activated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
449
455
|
last_validated: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -463,25 +469,31 @@ var LicenseSeatSDK = class {
|
|
|
463
469
|
/**
|
|
464
470
|
* Deactivate the current license
|
|
465
471
|
* @returns {Promise<Object>} Deactivation result from the API
|
|
472
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
466
473
|
* @throws {LicenseError} When no active license is found
|
|
467
474
|
* @throws {APIError} When the API request fails
|
|
468
475
|
*/
|
|
469
476
|
async deactivate() {
|
|
477
|
+
if (!this.config.productSlug) {
|
|
478
|
+
throw new ConfigurationError("productSlug is required for deactivation");
|
|
479
|
+
}
|
|
470
480
|
const cachedLicense = this.cache.getLicense();
|
|
471
481
|
if (!cachedLicense) {
|
|
472
482
|
throw new LicenseError("No active license found", "no_license");
|
|
473
483
|
}
|
|
474
484
|
try {
|
|
475
485
|
this.emit("deactivation:start", cachedLicense);
|
|
476
|
-
const response = await this.apiCall(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
}
|
|
481
493
|
}
|
|
482
|
-
|
|
494
|
+
);
|
|
483
495
|
this.cache.clearLicense();
|
|
484
|
-
this.cache.
|
|
496
|
+
this.cache.clearOfflineToken();
|
|
485
497
|
this.stopAutoValidation();
|
|
486
498
|
this.emit("deactivation:success", response);
|
|
487
499
|
return response;
|
|
@@ -495,22 +507,33 @@ var LicenseSeatSDK = class {
|
|
|
495
507
|
* @param {string} licenseKey - License key to validate
|
|
496
508
|
* @param {import('./types.js').ValidationOptions} [options={}] - Validation options
|
|
497
509
|
* @returns {Promise<import('./types.js').ValidationResult>} Validation result
|
|
510
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
498
511
|
* @throws {APIError} When the API request fails and offline fallback is not available
|
|
499
512
|
*/
|
|
500
513
|
async validateLicense(licenseKey, options = {}) {
|
|
514
|
+
if (!this.config.productSlug) {
|
|
515
|
+
throw new ConfigurationError("productSlug is required for validation");
|
|
516
|
+
}
|
|
501
517
|
try {
|
|
502
518
|
this.emit("validation:start", { licenseKey });
|
|
503
|
-
const rawResponse = await this.apiCall(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
}
|
|
509
526
|
}
|
|
510
|
-
|
|
527
|
+
);
|
|
511
528
|
const response = {
|
|
512
529
|
valid: rawResponse.valid,
|
|
513
|
-
|
|
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 || []
|
|
514
537
|
};
|
|
515
538
|
const cachedLicense = this.cache.getLicense();
|
|
516
539
|
if ((!response.active_entitlements || response.active_entitlements.length === 0) && cachedLicense?.validation?.active_entitlements?.length) {
|
|
@@ -550,7 +573,13 @@ var LicenseSeatSDK = class {
|
|
|
550
573
|
if (error instanceof APIError && error.data) {
|
|
551
574
|
const cachedLicense = this.cache.getLicense();
|
|
552
575
|
if (cachedLicense && cachedLicense.license_key === licenseKey) {
|
|
553
|
-
|
|
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
|
+
});
|
|
554
583
|
}
|
|
555
584
|
if (![0, 408, 429].includes(error.status)) {
|
|
556
585
|
this.stopAutoValidation();
|
|
@@ -599,33 +628,50 @@ var LicenseSeatSDK = class {
|
|
|
599
628
|
return this.checkEntitlement(entitlementKey).active;
|
|
600
629
|
}
|
|
601
630
|
/**
|
|
602
|
-
* Get offline
|
|
603
|
-
* @
|
|
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
|
|
604
637
|
* @throws {LicenseError} When no active license is found
|
|
605
638
|
* @throws {APIError} When the API request fails
|
|
606
639
|
*/
|
|
607
|
-
async
|
|
640
|
+
async getOfflineToken(options = {}) {
|
|
641
|
+
if (!this.config.productSlug) {
|
|
642
|
+
throw new ConfigurationError("productSlug is required for offline token");
|
|
643
|
+
}
|
|
608
644
|
const license = this.cache.getLicense();
|
|
609
645
|
if (!license || !license.license_key) {
|
|
610
|
-
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.";
|
|
611
647
|
this.emit("sdk:error", { message: errorMsg });
|
|
612
648
|
throw new LicenseError(errorMsg, "no_license");
|
|
613
649
|
}
|
|
614
650
|
try {
|
|
615
|
-
this.emit("
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
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", {
|
|
619
665
|
licenseKey: license.license_key,
|
|
620
666
|
data: response
|
|
621
667
|
});
|
|
622
668
|
return response;
|
|
623
669
|
} catch (error) {
|
|
624
670
|
this.log(
|
|
625
|
-
`Failed to get offline
|
|
671
|
+
`Failed to get offline token for ${license.license_key}:`,
|
|
626
672
|
error
|
|
627
673
|
);
|
|
628
|
-
this.emit("
|
|
674
|
+
this.emit("offlineToken:fetchError", {
|
|
629
675
|
licenseKey: license.license_key,
|
|
630
676
|
error
|
|
631
677
|
});
|
|
@@ -633,45 +679,45 @@ var LicenseSeatSDK = class {
|
|
|
633
679
|
}
|
|
634
680
|
}
|
|
635
681
|
/**
|
|
636
|
-
* Fetch a
|
|
637
|
-
* @param {string} keyId - The Key ID (kid) for which to fetch the
|
|
638
|
-
* @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
|
|
639
685
|
* @throws {Error} When keyId is not provided or the key is not found
|
|
640
686
|
*/
|
|
641
|
-
async
|
|
687
|
+
async getSigningKey(keyId) {
|
|
642
688
|
if (!keyId) {
|
|
643
|
-
throw new Error("Key ID is required to fetch a
|
|
689
|
+
throw new Error("Key ID is required to fetch a signing key.");
|
|
644
690
|
}
|
|
645
691
|
try {
|
|
646
|
-
this.log(`Fetching
|
|
647
|
-
const response = await this.apiCall(`/
|
|
692
|
+
this.log(`Fetching signing key for kid: ${keyId}`);
|
|
693
|
+
const response = await this.apiCall(`/signing-keys/${encodeURIComponent(keyId)}`, {
|
|
648
694
|
method: "GET"
|
|
649
695
|
});
|
|
650
|
-
if (response && response.
|
|
651
|
-
this.log(`Successfully fetched
|
|
652
|
-
return response
|
|
696
|
+
if (response && response.public_key) {
|
|
697
|
+
this.log(`Successfully fetched signing key for kid: ${keyId}`);
|
|
698
|
+
return response;
|
|
653
699
|
} else {
|
|
654
700
|
throw new Error(
|
|
655
|
-
`
|
|
701
|
+
`Signing key not found or invalid response for kid: ${keyId}`
|
|
656
702
|
);
|
|
657
703
|
}
|
|
658
704
|
} catch (error) {
|
|
659
|
-
this.log(`Failed to fetch
|
|
705
|
+
this.log(`Failed to fetch signing key for kid ${keyId}:`, error);
|
|
660
706
|
throw error;
|
|
661
707
|
}
|
|
662
708
|
}
|
|
663
709
|
/**
|
|
664
|
-
* Verify a signed offline
|
|
665
|
-
* @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
|
|
666
712
|
* @param {string} publicKeyB64 - Base64-encoded public Ed25519 key
|
|
667
713
|
* @returns {Promise<boolean>} True if verification is successful
|
|
668
714
|
* @throws {CryptoError} When crypto library is not available
|
|
669
715
|
* @throws {Error} When inputs are invalid
|
|
670
716
|
*/
|
|
671
|
-
async
|
|
672
|
-
this.log("Attempting to verify offline
|
|
673
|
-
if (!
|
|
674
|
-
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 }");
|
|
675
721
|
}
|
|
676
722
|
if (!publicKeyB64) {
|
|
677
723
|
throw new Error("Public key (Base64 encoded) is required.");
|
|
@@ -684,27 +730,20 @@ var LicenseSeatSDK = class {
|
|
|
684
730
|
throw err;
|
|
685
731
|
}
|
|
686
732
|
try {
|
|
687
|
-
const
|
|
688
|
-
const
|
|
733
|
+
const messageBytes = new TextEncoder().encode(offlineTokenData.canonical);
|
|
734
|
+
const signatureBytes = base64UrlDecode(offlineTokenData.signature.value);
|
|
689
735
|
const publicKeyBytes = base64UrlDecode(publicKeyB64);
|
|
690
|
-
const signatureBytes = base64UrlDecode(signedLicenseData.signature_b64u);
|
|
691
736
|
const isValid = ed.verify(signatureBytes, messageBytes, publicKeyBytes);
|
|
692
737
|
if (isValid) {
|
|
693
|
-
this.log(
|
|
694
|
-
|
|
695
|
-
);
|
|
696
|
-
this.emit("offlineLicense:verified", {
|
|
697
|
-
payload: signedLicenseData.payload
|
|
698
|
-
});
|
|
738
|
+
this.log("Offline token signature VERIFIED successfully client-side.");
|
|
739
|
+
this.emit("offlineToken:verified", { token: offlineTokenData.token });
|
|
699
740
|
} else {
|
|
700
|
-
this.log("Offline
|
|
701
|
-
this.emit("
|
|
702
|
-
payload: signedLicenseData.payload
|
|
703
|
-
});
|
|
741
|
+
this.log("Offline token signature INVALID client-side.");
|
|
742
|
+
this.emit("offlineToken:verificationFailed", { token: offlineTokenData.token });
|
|
704
743
|
}
|
|
705
744
|
return isValid;
|
|
706
745
|
} catch (error) {
|
|
707
|
-
this.log("Client-side offline
|
|
746
|
+
this.log("Client-side offline token verification error:", error);
|
|
708
747
|
this.emit("sdk:error", {
|
|
709
748
|
message: "Client-side verification failed.",
|
|
710
749
|
error
|
|
@@ -729,19 +768,19 @@ var LicenseSeatSDK = class {
|
|
|
729
768
|
if (validation.offline) {
|
|
730
769
|
return {
|
|
731
770
|
status: "offline-invalid",
|
|
732
|
-
message: validation.
|
|
771
|
+
message: validation.code || "License invalid (offline)"
|
|
733
772
|
};
|
|
734
773
|
}
|
|
735
774
|
return {
|
|
736
775
|
status: "invalid",
|
|
737
|
-
message: validation.
|
|
776
|
+
message: validation.message || validation.code || "License invalid"
|
|
738
777
|
};
|
|
739
778
|
}
|
|
740
779
|
if (validation.offline) {
|
|
741
780
|
return {
|
|
742
781
|
status: "offline-valid",
|
|
743
782
|
license: license.license_key,
|
|
744
|
-
device: license.
|
|
783
|
+
device: license.device_id,
|
|
745
784
|
activated_at: license.activated_at,
|
|
746
785
|
last_validated: license.last_validated,
|
|
747
786
|
entitlements: validation.active_entitlements || []
|
|
@@ -750,7 +789,7 @@ var LicenseSeatSDK = class {
|
|
|
750
789
|
return {
|
|
751
790
|
status: "active",
|
|
752
791
|
license: license.license_key,
|
|
753
|
-
device: license.
|
|
792
|
+
device: license.device_id,
|
|
754
793
|
activated_at: license.activated_at,
|
|
755
794
|
last_validated: license.last_validated,
|
|
756
795
|
entitlements: validation.active_entitlements || []
|
|
@@ -910,9 +949,9 @@ var LicenseSeatSDK = class {
|
|
|
910
949
|
startConnectivityPolling() {
|
|
911
950
|
if (this.connectivityTimer)
|
|
912
951
|
return;
|
|
913
|
-
const
|
|
952
|
+
const healthCheck = async () => {
|
|
914
953
|
try {
|
|
915
|
-
await fetch(`${this.config.apiBaseUrl}/
|
|
954
|
+
await fetch(`${this.config.apiBaseUrl}/health`, {
|
|
916
955
|
method: "GET",
|
|
917
956
|
credentials: "omit"
|
|
918
957
|
});
|
|
@@ -929,7 +968,7 @@ var LicenseSeatSDK = class {
|
|
|
929
968
|
}
|
|
930
969
|
};
|
|
931
970
|
this.connectivityTimer = setInterval(
|
|
932
|
-
|
|
971
|
+
healthCheck,
|
|
933
972
|
this.config.networkRecheckInterval
|
|
934
973
|
);
|
|
935
974
|
}
|
|
@@ -948,7 +987,7 @@ var LicenseSeatSDK = class {
|
|
|
948
987
|
// Offline License Management
|
|
949
988
|
// ============================================================
|
|
950
989
|
/**
|
|
951
|
-
* Fetch and cache offline
|
|
990
|
+
* Fetch and cache offline token and signing key
|
|
952
991
|
* Uses a lock to prevent concurrent calls from causing race conditions
|
|
953
992
|
* @returns {Promise<void>}
|
|
954
993
|
* @private
|
|
@@ -960,19 +999,19 @@ var LicenseSeatSDK = class {
|
|
|
960
999
|
}
|
|
961
1000
|
this.syncingOfflineAssets = true;
|
|
962
1001
|
try {
|
|
963
|
-
const offline = await this.
|
|
964
|
-
this.cache.
|
|
965
|
-
const kid = offline.
|
|
1002
|
+
const offline = await this.getOfflineToken();
|
|
1003
|
+
this.cache.setOfflineToken(offline);
|
|
1004
|
+
const kid = offline.signature?.key_id || offline.token?.kid;
|
|
966
1005
|
if (kid) {
|
|
967
1006
|
const existingKey = this.cache.getPublicKey(kid);
|
|
968
1007
|
if (!existingKey) {
|
|
969
|
-
const
|
|
970
|
-
this.cache.setPublicKey(kid,
|
|
1008
|
+
const signingKey = await this.getSigningKey(kid);
|
|
1009
|
+
this.cache.setPublicKey(kid, signingKey.public_key);
|
|
971
1010
|
}
|
|
972
1011
|
}
|
|
973
|
-
this.emit("
|
|
974
|
-
kid
|
|
975
|
-
|
|
1012
|
+
this.emit("offlineToken:ready", {
|
|
1013
|
+
kid,
|
|
1014
|
+
exp: offline.token?.exp
|
|
976
1015
|
});
|
|
977
1016
|
const res = await this.quickVerifyCachedOfflineLocal();
|
|
978
1017
|
if (res) {
|
|
@@ -1002,39 +1041,40 @@ var LicenseSeatSDK = class {
|
|
|
1002
1041
|
);
|
|
1003
1042
|
}
|
|
1004
1043
|
/**
|
|
1005
|
-
* Verify cached offline
|
|
1044
|
+
* Verify cached offline token
|
|
1006
1045
|
* @returns {Promise<import('./types.js').ValidationResult>}
|
|
1007
1046
|
* @private
|
|
1008
1047
|
*/
|
|
1009
1048
|
async verifyCachedOffline() {
|
|
1010
|
-
const signed = this.cache.
|
|
1049
|
+
const signed = this.cache.getOfflineToken();
|
|
1011
1050
|
if (!signed) {
|
|
1012
|
-
return { valid: false, offline: true,
|
|
1051
|
+
return { valid: false, offline: true, code: "no_offline_token" };
|
|
1013
1052
|
}
|
|
1014
|
-
const kid = signed.
|
|
1053
|
+
const kid = signed.signature?.key_id || signed.token?.kid;
|
|
1015
1054
|
let pub = kid ? this.cache.getPublicKey(kid) : null;
|
|
1016
1055
|
if (!pub) {
|
|
1017
1056
|
try {
|
|
1018
|
-
|
|
1057
|
+
const signingKey = await this.getSigningKey(kid);
|
|
1058
|
+
pub = signingKey.public_key;
|
|
1019
1059
|
this.cache.setPublicKey(kid, pub);
|
|
1020
1060
|
} catch (e) {
|
|
1021
|
-
return { valid: false, offline: true,
|
|
1061
|
+
return { valid: false, offline: true, code: "no_public_key" };
|
|
1022
1062
|
}
|
|
1023
1063
|
}
|
|
1024
1064
|
try {
|
|
1025
|
-
const ok = await this.
|
|
1065
|
+
const ok = await this.verifyOfflineToken(signed, pub);
|
|
1026
1066
|
if (!ok) {
|
|
1027
|
-
return { valid: false, offline: true,
|
|
1067
|
+
return { valid: false, offline: true, code: "signature_invalid" };
|
|
1028
1068
|
}
|
|
1029
|
-
const
|
|
1069
|
+
const token = signed.token;
|
|
1030
1070
|
const cached = this.cache.getLicense();
|
|
1031
|
-
if (!cached || !constantTimeEqual(
|
|
1032
|
-
return { valid: false, offline: true,
|
|
1071
|
+
if (!cached || !constantTimeEqual(token.license_key || "", cached.license_key || "")) {
|
|
1072
|
+
return { valid: false, offline: true, code: "license_mismatch" };
|
|
1033
1073
|
}
|
|
1034
1074
|
const now = Date.now();
|
|
1035
|
-
const expAt =
|
|
1075
|
+
const expAt = token.exp ? token.exp * 1e3 : null;
|
|
1036
1076
|
if (expAt && expAt < now) {
|
|
1037
|
-
return { valid: false, offline: true,
|
|
1077
|
+
return { valid: false, offline: true, code: "expired" };
|
|
1038
1078
|
}
|
|
1039
1079
|
if (!expAt && this.config.maxOfflineDays > 0) {
|
|
1040
1080
|
const pivot = cached.last_validated || cached.activated_at;
|
|
@@ -1044,24 +1084,24 @@ var LicenseSeatSDK = class {
|
|
|
1044
1084
|
return {
|
|
1045
1085
|
valid: false,
|
|
1046
1086
|
offline: true,
|
|
1047
|
-
|
|
1087
|
+
code: "grace_period_expired"
|
|
1048
1088
|
};
|
|
1049
1089
|
}
|
|
1050
1090
|
}
|
|
1051
1091
|
}
|
|
1052
1092
|
const lastSeen = this.cache.getLastSeenTimestamp();
|
|
1053
1093
|
if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
|
|
1054
|
-
return { valid: false, offline: true,
|
|
1094
|
+
return { valid: false, offline: true, code: "clock_tamper" };
|
|
1055
1095
|
}
|
|
1056
1096
|
this.cache.setLastSeenTimestamp(now);
|
|
1057
|
-
const active = parseActiveEntitlements(
|
|
1097
|
+
const active = parseActiveEntitlements(token);
|
|
1058
1098
|
return {
|
|
1059
1099
|
valid: true,
|
|
1060
1100
|
offline: true,
|
|
1061
1101
|
...active.length ? { active_entitlements: active } : {}
|
|
1062
1102
|
};
|
|
1063
1103
|
} catch (e) {
|
|
1064
|
-
return { valid: false, offline: true,
|
|
1104
|
+
return { valid: false, offline: true, code: "verification_error" };
|
|
1065
1105
|
}
|
|
1066
1106
|
}
|
|
1067
1107
|
/**
|
|
@@ -1071,40 +1111,40 @@ var LicenseSeatSDK = class {
|
|
|
1071
1111
|
* @private
|
|
1072
1112
|
*/
|
|
1073
1113
|
async quickVerifyCachedOfflineLocal() {
|
|
1074
|
-
const signed = this.cache.
|
|
1114
|
+
const signed = this.cache.getOfflineToken();
|
|
1075
1115
|
if (!signed)
|
|
1076
1116
|
return null;
|
|
1077
|
-
const kid = signed.
|
|
1117
|
+
const kid = signed.signature?.key_id || signed.token?.kid;
|
|
1078
1118
|
const pub = kid ? this.cache.getPublicKey(kid) : null;
|
|
1079
1119
|
if (!pub)
|
|
1080
1120
|
return null;
|
|
1081
1121
|
try {
|
|
1082
|
-
const ok = await this.
|
|
1122
|
+
const ok = await this.verifyOfflineToken(signed, pub);
|
|
1083
1123
|
if (!ok) {
|
|
1084
|
-
return { valid: false, offline: true,
|
|
1124
|
+
return { valid: false, offline: true, code: "signature_invalid" };
|
|
1085
1125
|
}
|
|
1086
|
-
const
|
|
1126
|
+
const token = signed.token;
|
|
1087
1127
|
const cached = this.cache.getLicense();
|
|
1088
|
-
if (!cached || !constantTimeEqual(
|
|
1089
|
-
return { valid: false, offline: true,
|
|
1128
|
+
if (!cached || !constantTimeEqual(token.license_key || "", cached.license_key || "")) {
|
|
1129
|
+
return { valid: false, offline: true, code: "license_mismatch" };
|
|
1090
1130
|
}
|
|
1091
1131
|
const now = Date.now();
|
|
1092
|
-
const expAt =
|
|
1132
|
+
const expAt = token.exp ? token.exp * 1e3 : null;
|
|
1093
1133
|
if (expAt && expAt < now) {
|
|
1094
|
-
return { valid: false, offline: true,
|
|
1134
|
+
return { valid: false, offline: true, code: "expired" };
|
|
1095
1135
|
}
|
|
1096
1136
|
const lastSeen = this.cache.getLastSeenTimestamp();
|
|
1097
1137
|
if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
|
|
1098
|
-
return { valid: false, offline: true,
|
|
1138
|
+
return { valid: false, offline: true, code: "clock_tamper" };
|
|
1099
1139
|
}
|
|
1100
|
-
const active = parseActiveEntitlements(
|
|
1140
|
+
const active = parseActiveEntitlements(token);
|
|
1101
1141
|
return {
|
|
1102
1142
|
valid: true,
|
|
1103
1143
|
offline: true,
|
|
1104
1144
|
...active.length ? { active_entitlements: active } : {}
|
|
1105
1145
|
};
|
|
1106
1146
|
} catch (_) {
|
|
1107
|
-
return { valid: false, offline: true,
|
|
1147
|
+
return { valid: false, offline: true, code: "verification_error" };
|
|
1108
1148
|
}
|
|
1109
1149
|
}
|
|
1110
1150
|
// ============================================================
|
|
@@ -1146,11 +1186,14 @@ var LicenseSeatSDK = class {
|
|
|
1146
1186
|
});
|
|
1147
1187
|
const data = await response.json();
|
|
1148
1188
|
if (!response.ok) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
)
|
|
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);
|
|
1154
1197
|
}
|
|
1155
1198
|
if (!this.online) {
|
|
1156
1199
|
this.online = true;
|