@licenseseat/js 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +381 -72
- package/dist/index.global.js +2339 -0
- package/dist/index.js +196 -146
- package/dist/types/LicenseSeat.d.ts +33 -18
- 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/index.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 +203 -146
- package/src/cache.js +16 -18
- package/src/index.js +2 -1
- 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,18 +789,19 @@ 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 || []
|
|
757
796
|
};
|
|
758
797
|
}
|
|
759
798
|
/**
|
|
760
|
-
* Test
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
* @
|
|
764
|
-
* @throws {
|
|
799
|
+
* Test API connectivity
|
|
800
|
+
* Makes a request to the health endpoint to verify connectivity.
|
|
801
|
+
* Note: To fully verify API key validity, attempt an actual operation like activate() or validateLicense().
|
|
802
|
+
* @returns {Promise<{authenticated: boolean, healthy: boolean, api_version: string}>}
|
|
803
|
+
* @throws {ConfigurationError} If API key is not configured
|
|
804
|
+
* @throws {APIError} If the health check fails
|
|
765
805
|
*/
|
|
766
806
|
async testAuth() {
|
|
767
807
|
if (!this.config.apiKey) {
|
|
@@ -771,9 +811,15 @@ var LicenseSeatSDK = class {
|
|
|
771
811
|
}
|
|
772
812
|
try {
|
|
773
813
|
this.emit("auth_test:start");
|
|
774
|
-
const response = await this.apiCall("/
|
|
775
|
-
|
|
776
|
-
|
|
814
|
+
const response = await this.apiCall("/health", { method: "GET" });
|
|
815
|
+
const result = {
|
|
816
|
+
authenticated: true,
|
|
817
|
+
// API key was included in request
|
|
818
|
+
healthy: response.status === "healthy",
|
|
819
|
+
api_version: response.api_version
|
|
820
|
+
};
|
|
821
|
+
this.emit("auth_test:success", result);
|
|
822
|
+
return result;
|
|
777
823
|
} catch (error) {
|
|
778
824
|
this.emit("auth_test:error", { error });
|
|
779
825
|
throw error;
|
|
@@ -910,9 +956,9 @@ var LicenseSeatSDK = class {
|
|
|
910
956
|
startConnectivityPolling() {
|
|
911
957
|
if (this.connectivityTimer)
|
|
912
958
|
return;
|
|
913
|
-
const
|
|
959
|
+
const healthCheck = async () => {
|
|
914
960
|
try {
|
|
915
|
-
await fetch(`${this.config.apiBaseUrl}/
|
|
961
|
+
await fetch(`${this.config.apiBaseUrl}/health`, {
|
|
916
962
|
method: "GET",
|
|
917
963
|
credentials: "omit"
|
|
918
964
|
});
|
|
@@ -929,7 +975,7 @@ var LicenseSeatSDK = class {
|
|
|
929
975
|
}
|
|
930
976
|
};
|
|
931
977
|
this.connectivityTimer = setInterval(
|
|
932
|
-
|
|
978
|
+
healthCheck,
|
|
933
979
|
this.config.networkRecheckInterval
|
|
934
980
|
);
|
|
935
981
|
}
|
|
@@ -948,7 +994,7 @@ var LicenseSeatSDK = class {
|
|
|
948
994
|
// Offline License Management
|
|
949
995
|
// ============================================================
|
|
950
996
|
/**
|
|
951
|
-
* Fetch and cache offline
|
|
997
|
+
* Fetch and cache offline token and signing key
|
|
952
998
|
* Uses a lock to prevent concurrent calls from causing race conditions
|
|
953
999
|
* @returns {Promise<void>}
|
|
954
1000
|
* @private
|
|
@@ -960,19 +1006,19 @@ var LicenseSeatSDK = class {
|
|
|
960
1006
|
}
|
|
961
1007
|
this.syncingOfflineAssets = true;
|
|
962
1008
|
try {
|
|
963
|
-
const offline = await this.
|
|
964
|
-
this.cache.
|
|
965
|
-
const kid = offline.
|
|
1009
|
+
const offline = await this.getOfflineToken();
|
|
1010
|
+
this.cache.setOfflineToken(offline);
|
|
1011
|
+
const kid = offline.signature?.key_id || offline.token?.kid;
|
|
966
1012
|
if (kid) {
|
|
967
1013
|
const existingKey = this.cache.getPublicKey(kid);
|
|
968
1014
|
if (!existingKey) {
|
|
969
|
-
const
|
|
970
|
-
this.cache.setPublicKey(kid,
|
|
1015
|
+
const signingKey = await this.getSigningKey(kid);
|
|
1016
|
+
this.cache.setPublicKey(kid, signingKey.public_key);
|
|
971
1017
|
}
|
|
972
1018
|
}
|
|
973
|
-
this.emit("
|
|
974
|
-
kid
|
|
975
|
-
|
|
1019
|
+
this.emit("offlineToken:ready", {
|
|
1020
|
+
kid,
|
|
1021
|
+
exp: offline.token?.exp
|
|
976
1022
|
});
|
|
977
1023
|
const res = await this.quickVerifyCachedOfflineLocal();
|
|
978
1024
|
if (res) {
|
|
@@ -1002,39 +1048,40 @@ var LicenseSeatSDK = class {
|
|
|
1002
1048
|
);
|
|
1003
1049
|
}
|
|
1004
1050
|
/**
|
|
1005
|
-
* Verify cached offline
|
|
1051
|
+
* Verify cached offline token
|
|
1006
1052
|
* @returns {Promise<import('./types.js').ValidationResult>}
|
|
1007
1053
|
* @private
|
|
1008
1054
|
*/
|
|
1009
1055
|
async verifyCachedOffline() {
|
|
1010
|
-
const signed = this.cache.
|
|
1056
|
+
const signed = this.cache.getOfflineToken();
|
|
1011
1057
|
if (!signed) {
|
|
1012
|
-
return { valid: false, offline: true,
|
|
1058
|
+
return { valid: false, offline: true, code: "no_offline_token" };
|
|
1013
1059
|
}
|
|
1014
|
-
const kid = signed.
|
|
1060
|
+
const kid = signed.signature?.key_id || signed.token?.kid;
|
|
1015
1061
|
let pub = kid ? this.cache.getPublicKey(kid) : null;
|
|
1016
1062
|
if (!pub) {
|
|
1017
1063
|
try {
|
|
1018
|
-
|
|
1064
|
+
const signingKey = await this.getSigningKey(kid);
|
|
1065
|
+
pub = signingKey.public_key;
|
|
1019
1066
|
this.cache.setPublicKey(kid, pub);
|
|
1020
1067
|
} catch (e) {
|
|
1021
|
-
return { valid: false, offline: true,
|
|
1068
|
+
return { valid: false, offline: true, code: "no_public_key" };
|
|
1022
1069
|
}
|
|
1023
1070
|
}
|
|
1024
1071
|
try {
|
|
1025
|
-
const ok = await this.
|
|
1072
|
+
const ok = await this.verifyOfflineToken(signed, pub);
|
|
1026
1073
|
if (!ok) {
|
|
1027
|
-
return { valid: false, offline: true,
|
|
1074
|
+
return { valid: false, offline: true, code: "signature_invalid" };
|
|
1028
1075
|
}
|
|
1029
|
-
const
|
|
1076
|
+
const token = signed.token;
|
|
1030
1077
|
const cached = this.cache.getLicense();
|
|
1031
|
-
if (!cached || !constantTimeEqual(
|
|
1032
|
-
return { valid: false, offline: true,
|
|
1078
|
+
if (!cached || !constantTimeEqual(token.license_key || "", cached.license_key || "")) {
|
|
1079
|
+
return { valid: false, offline: true, code: "license_mismatch" };
|
|
1033
1080
|
}
|
|
1034
1081
|
const now = Date.now();
|
|
1035
|
-
const expAt =
|
|
1082
|
+
const expAt = token.exp ? token.exp * 1e3 : null;
|
|
1036
1083
|
if (expAt && expAt < now) {
|
|
1037
|
-
return { valid: false, offline: true,
|
|
1084
|
+
return { valid: false, offline: true, code: "expired" };
|
|
1038
1085
|
}
|
|
1039
1086
|
if (!expAt && this.config.maxOfflineDays > 0) {
|
|
1040
1087
|
const pivot = cached.last_validated || cached.activated_at;
|
|
@@ -1044,24 +1091,24 @@ var LicenseSeatSDK = class {
|
|
|
1044
1091
|
return {
|
|
1045
1092
|
valid: false,
|
|
1046
1093
|
offline: true,
|
|
1047
|
-
|
|
1094
|
+
code: "grace_period_expired"
|
|
1048
1095
|
};
|
|
1049
1096
|
}
|
|
1050
1097
|
}
|
|
1051
1098
|
}
|
|
1052
1099
|
const lastSeen = this.cache.getLastSeenTimestamp();
|
|
1053
1100
|
if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
|
|
1054
|
-
return { valid: false, offline: true,
|
|
1101
|
+
return { valid: false, offline: true, code: "clock_tamper" };
|
|
1055
1102
|
}
|
|
1056
1103
|
this.cache.setLastSeenTimestamp(now);
|
|
1057
|
-
const active = parseActiveEntitlements(
|
|
1104
|
+
const active = parseActiveEntitlements(token);
|
|
1058
1105
|
return {
|
|
1059
1106
|
valid: true,
|
|
1060
1107
|
offline: true,
|
|
1061
1108
|
...active.length ? { active_entitlements: active } : {}
|
|
1062
1109
|
};
|
|
1063
1110
|
} catch (e) {
|
|
1064
|
-
return { valid: false, offline: true,
|
|
1111
|
+
return { valid: false, offline: true, code: "verification_error" };
|
|
1065
1112
|
}
|
|
1066
1113
|
}
|
|
1067
1114
|
/**
|
|
@@ -1071,40 +1118,40 @@ var LicenseSeatSDK = class {
|
|
|
1071
1118
|
* @private
|
|
1072
1119
|
*/
|
|
1073
1120
|
async quickVerifyCachedOfflineLocal() {
|
|
1074
|
-
const signed = this.cache.
|
|
1121
|
+
const signed = this.cache.getOfflineToken();
|
|
1075
1122
|
if (!signed)
|
|
1076
1123
|
return null;
|
|
1077
|
-
const kid = signed.
|
|
1124
|
+
const kid = signed.signature?.key_id || signed.token?.kid;
|
|
1078
1125
|
const pub = kid ? this.cache.getPublicKey(kid) : null;
|
|
1079
1126
|
if (!pub)
|
|
1080
1127
|
return null;
|
|
1081
1128
|
try {
|
|
1082
|
-
const ok = await this.
|
|
1129
|
+
const ok = await this.verifyOfflineToken(signed, pub);
|
|
1083
1130
|
if (!ok) {
|
|
1084
|
-
return { valid: false, offline: true,
|
|
1131
|
+
return { valid: false, offline: true, code: "signature_invalid" };
|
|
1085
1132
|
}
|
|
1086
|
-
const
|
|
1133
|
+
const token = signed.token;
|
|
1087
1134
|
const cached = this.cache.getLicense();
|
|
1088
|
-
if (!cached || !constantTimeEqual(
|
|
1089
|
-
return { valid: false, offline: true,
|
|
1135
|
+
if (!cached || !constantTimeEqual(token.license_key || "", cached.license_key || "")) {
|
|
1136
|
+
return { valid: false, offline: true, code: "license_mismatch" };
|
|
1090
1137
|
}
|
|
1091
1138
|
const now = Date.now();
|
|
1092
|
-
const expAt =
|
|
1139
|
+
const expAt = token.exp ? token.exp * 1e3 : null;
|
|
1093
1140
|
if (expAt && expAt < now) {
|
|
1094
|
-
return { valid: false, offline: true,
|
|
1141
|
+
return { valid: false, offline: true, code: "expired" };
|
|
1095
1142
|
}
|
|
1096
1143
|
const lastSeen = this.cache.getLastSeenTimestamp();
|
|
1097
1144
|
if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
|
|
1098
|
-
return { valid: false, offline: true,
|
|
1145
|
+
return { valid: false, offline: true, code: "clock_tamper" };
|
|
1099
1146
|
}
|
|
1100
|
-
const active = parseActiveEntitlements(
|
|
1147
|
+
const active = parseActiveEntitlements(token);
|
|
1101
1148
|
return {
|
|
1102
1149
|
valid: true,
|
|
1103
1150
|
offline: true,
|
|
1104
1151
|
...active.length ? { active_entitlements: active } : {}
|
|
1105
1152
|
};
|
|
1106
1153
|
} catch (_) {
|
|
1107
|
-
return { valid: false, offline: true,
|
|
1154
|
+
return { valid: false, offline: true, code: "verification_error" };
|
|
1108
1155
|
}
|
|
1109
1156
|
}
|
|
1110
1157
|
// ============================================================
|
|
@@ -1146,11 +1193,14 @@ var LicenseSeatSDK = class {
|
|
|
1146
1193
|
});
|
|
1147
1194
|
const data = await response.json();
|
|
1148
1195
|
if (!response.ok) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
)
|
|
1196
|
+
const errorObj = data.error;
|
|
1197
|
+
let errorMessage = "Request failed";
|
|
1198
|
+
if (typeof errorObj === "object" && errorObj !== null) {
|
|
1199
|
+
errorMessage = errorObj.message || "Request failed";
|
|
1200
|
+
} else if (typeof errorObj === "string") {
|
|
1201
|
+
errorMessage = errorObj;
|
|
1202
|
+
}
|
|
1203
|
+
throw new APIError(errorMessage, response.status, data);
|
|
1154
1204
|
}
|
|
1155
1205
|
if (!this.online) {
|
|
1156
1206
|
this.online = true;
|