@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@licenseseat/js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Official JavaScript SDK for LicenseSeat – simple, secure software licensing.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
],
|
|
51
51
|
"repository": {
|
|
52
52
|
"type": "git",
|
|
53
|
-
"url": "https://github.com/licenseseat/licenseseat-js.git"
|
|
53
|
+
"url": "git+https://github.com/licenseseat/licenseseat-js.git"
|
|
54
54
|
},
|
|
55
55
|
"author": "LicenseSeat <hello@licenseseat.com>",
|
|
56
56
|
"license": "MIT",
|
package/src/LicenseSeat.js
CHANGED
|
@@ -37,7 +37,8 @@ import {
|
|
|
37
37
|
* @type {import('./types.js').LicenseSeatConfig}
|
|
38
38
|
*/
|
|
39
39
|
const DEFAULT_CONFIG = {
|
|
40
|
-
apiBaseUrl: "https://licenseseat.com/api",
|
|
40
|
+
apiBaseUrl: "https://licenseseat.com/api/v1",
|
|
41
|
+
productSlug: null, // Required: Product slug for API calls (e.g., "my-app")
|
|
41
42
|
storagePrefix: "licenseseat_",
|
|
42
43
|
autoValidateInterval: 3600000, // 1 hour
|
|
43
44
|
networkRecheckInterval: 30000, // 30 seconds
|
|
@@ -235,32 +236,40 @@ export class LicenseSeatSDK {
|
|
|
235
236
|
* @param {string} licenseKey - The license key to activate
|
|
236
237
|
* @param {import('./types.js').ActivationOptions} [options={}] - Activation options
|
|
237
238
|
* @returns {Promise<import('./types.js').CachedLicense>} Activation result with cached license data
|
|
239
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
238
240
|
* @throws {APIError} When the API request fails
|
|
239
241
|
*/
|
|
240
242
|
async activate(licenseKey, options = {}) {
|
|
241
|
-
|
|
243
|
+
if (!this.config.productSlug) {
|
|
244
|
+
throw new ConfigurationError("productSlug is required for activation");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const deviceId = options.deviceId || generateDeviceId();
|
|
242
248
|
const payload = {
|
|
243
|
-
|
|
244
|
-
device_identifier: deviceId,
|
|
249
|
+
device_id: deviceId,
|
|
245
250
|
metadata: options.metadata || {},
|
|
246
251
|
};
|
|
247
252
|
|
|
248
|
-
if (options.
|
|
249
|
-
payload.
|
|
253
|
+
if (options.deviceName) {
|
|
254
|
+
payload.device_name = options.deviceName;
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
try {
|
|
253
258
|
this.emit("activation:start", { licenseKey, deviceId });
|
|
254
259
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
// New v1 API: POST /products/{slug}/licenses/{key}/activate
|
|
261
|
+
const response = await this.apiCall(
|
|
262
|
+
`/products/${this.config.productSlug}/licenses/${encodeURIComponent(licenseKey)}/activate`,
|
|
263
|
+
{
|
|
264
|
+
method: "POST",
|
|
265
|
+
body: payload,
|
|
266
|
+
}
|
|
267
|
+
);
|
|
259
268
|
|
|
260
269
|
/** @type {import('./types.js').CachedLicense} */
|
|
261
270
|
const licenseData = {
|
|
262
271
|
license_key: licenseKey,
|
|
263
|
-
|
|
272
|
+
device_id: deviceId,
|
|
264
273
|
activation: response,
|
|
265
274
|
activated_at: new Date().toISOString(),
|
|
266
275
|
last_validated: new Date().toISOString(),
|
|
@@ -283,10 +292,15 @@ export class LicenseSeatSDK {
|
|
|
283
292
|
/**
|
|
284
293
|
* Deactivate the current license
|
|
285
294
|
* @returns {Promise<Object>} Deactivation result from the API
|
|
295
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
286
296
|
* @throws {LicenseError} When no active license is found
|
|
287
297
|
* @throws {APIError} When the API request fails
|
|
288
298
|
*/
|
|
289
299
|
async deactivate() {
|
|
300
|
+
if (!this.config.productSlug) {
|
|
301
|
+
throw new ConfigurationError("productSlug is required for deactivation");
|
|
302
|
+
}
|
|
303
|
+
|
|
290
304
|
const cachedLicense = this.cache.getLicense();
|
|
291
305
|
if (!cachedLicense) {
|
|
292
306
|
throw new LicenseError("No active license found", "no_license");
|
|
@@ -295,16 +309,19 @@ export class LicenseSeatSDK {
|
|
|
295
309
|
try {
|
|
296
310
|
this.emit("deactivation:start", cachedLicense);
|
|
297
311
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
312
|
+
// New v1 API: POST /products/{slug}/licenses/{key}/deactivate
|
|
313
|
+
const response = await this.apiCall(
|
|
314
|
+
`/products/${this.config.productSlug}/licenses/${encodeURIComponent(cachedLicense.license_key)}/deactivate`,
|
|
315
|
+
{
|
|
316
|
+
method: "POST",
|
|
317
|
+
body: {
|
|
318
|
+
device_id: cachedLicense.device_id,
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
);
|
|
305
322
|
|
|
306
323
|
this.cache.clearLicense();
|
|
307
|
-
this.cache.
|
|
324
|
+
this.cache.clearOfflineToken();
|
|
308
325
|
this.stopAutoValidation();
|
|
309
326
|
|
|
310
327
|
this.emit("deactivation:success", response);
|
|
@@ -320,26 +337,39 @@ export class LicenseSeatSDK {
|
|
|
320
337
|
* @param {string} licenseKey - License key to validate
|
|
321
338
|
* @param {import('./types.js').ValidationOptions} [options={}] - Validation options
|
|
322
339
|
* @returns {Promise<import('./types.js').ValidationResult>} Validation result
|
|
340
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
323
341
|
* @throws {APIError} When the API request fails and offline fallback is not available
|
|
324
342
|
*/
|
|
325
343
|
async validateLicense(licenseKey, options = {}) {
|
|
344
|
+
if (!this.config.productSlug) {
|
|
345
|
+
throw new ConfigurationError("productSlug is required for validation");
|
|
346
|
+
}
|
|
347
|
+
|
|
326
348
|
try {
|
|
327
349
|
this.emit("validation:start", { licenseKey });
|
|
328
350
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
351
|
+
// New v1 API: POST /products/{slug}/licenses/{key}/validate
|
|
352
|
+
const rawResponse = await this.apiCall(
|
|
353
|
+
`/products/${this.config.productSlug}/licenses/${encodeURIComponent(licenseKey)}/validate`,
|
|
354
|
+
{
|
|
355
|
+
method: "POST",
|
|
356
|
+
body: {
|
|
357
|
+
device_id: options.deviceId || this.cache.getDeviceId(),
|
|
358
|
+
},
|
|
359
|
+
}
|
|
360
|
+
);
|
|
337
361
|
|
|
338
|
-
// Normalize response: API returns { valid, license: {
|
|
339
|
-
// SDK
|
|
362
|
+
// Normalize response: API returns { object: "validation_result", valid, license: {...}, activation: {...} }
|
|
363
|
+
// SDK internal structure uses active_entitlements at the top level
|
|
340
364
|
const response = {
|
|
341
365
|
valid: rawResponse.valid,
|
|
342
|
-
|
|
366
|
+
code: rawResponse.code,
|
|
367
|
+
message: rawResponse.message,
|
|
368
|
+
warnings: rawResponse.warnings,
|
|
369
|
+
license: rawResponse.license,
|
|
370
|
+
activation: rawResponse.activation,
|
|
371
|
+
// Extract entitlements from license for easy access
|
|
372
|
+
active_entitlements: rawResponse.license?.active_entitlements || [],
|
|
343
373
|
};
|
|
344
374
|
|
|
345
375
|
// Preserve cached entitlements if server response omits them
|
|
@@ -394,11 +424,18 @@ export class LicenseSeatSDK {
|
|
|
394
424
|
}
|
|
395
425
|
}
|
|
396
426
|
|
|
397
|
-
// Persist invalid status
|
|
427
|
+
// Persist invalid status from error response
|
|
398
428
|
if (error instanceof APIError && error.data) {
|
|
399
429
|
const cachedLicense = this.cache.getLicense();
|
|
400
430
|
if (cachedLicense && cachedLicense.license_key === licenseKey) {
|
|
401
|
-
|
|
431
|
+
// Extract code from new error format: { error: { code, message } }
|
|
432
|
+
const errorCode = error.data.error?.code || error.data.code;
|
|
433
|
+
const errorMessage = error.data.error?.message || error.data.message;
|
|
434
|
+
this.cache.updateValidation({
|
|
435
|
+
valid: false,
|
|
436
|
+
code: errorCode,
|
|
437
|
+
message: errorMessage,
|
|
438
|
+
});
|
|
402
439
|
}
|
|
403
440
|
if (![0, 408, 429].includes(error.status)) {
|
|
404
441
|
this.stopAutoValidation();
|
|
@@ -456,37 +493,59 @@ export class LicenseSeatSDK {
|
|
|
456
493
|
}
|
|
457
494
|
|
|
458
495
|
/**
|
|
459
|
-
* Get offline
|
|
460
|
-
* @
|
|
496
|
+
* Get offline token data from the server
|
|
497
|
+
* @param {Object} [options={}] - Options for offline token generation
|
|
498
|
+
* @param {string} [options.deviceId] - Device ID to bind the token to (required for hardware_locked mode)
|
|
499
|
+
* @param {number} [options.ttlDays] - Token lifetime in days (default: 30, max: 90)
|
|
500
|
+
* @returns {Promise<import('./types.js').OfflineToken>} Offline token data
|
|
501
|
+
* @throws {ConfigurationError} When productSlug is not configured
|
|
461
502
|
* @throws {LicenseError} When no active license is found
|
|
462
503
|
* @throws {APIError} When the API request fails
|
|
463
504
|
*/
|
|
464
|
-
async
|
|
505
|
+
async getOfflineToken(options = {}) {
|
|
506
|
+
if (!this.config.productSlug) {
|
|
507
|
+
throw new ConfigurationError("productSlug is required for offline token");
|
|
508
|
+
}
|
|
509
|
+
|
|
465
510
|
const license = this.cache.getLicense();
|
|
466
511
|
if (!license || !license.license_key) {
|
|
467
512
|
const errorMsg =
|
|
468
|
-
"No active license key found in cache to fetch offline
|
|
513
|
+
"No active license key found in cache to fetch offline token.";
|
|
469
514
|
this.emit("sdk:error", { message: errorMsg });
|
|
470
515
|
throw new LicenseError(errorMsg, "no_license");
|
|
471
516
|
}
|
|
472
517
|
|
|
473
518
|
try {
|
|
474
|
-
this.emit("
|
|
475
|
-
const path = `/licenses/${license.license_key}/offline_license`;
|
|
519
|
+
this.emit("offlineToken:fetching", { licenseKey: license.license_key });
|
|
476
520
|
|
|
477
|
-
|
|
521
|
+
// Build request body
|
|
522
|
+
const body = {};
|
|
523
|
+
if (options.deviceId) {
|
|
524
|
+
body.device_id = options.deviceId;
|
|
525
|
+
}
|
|
526
|
+
if (options.ttlDays) {
|
|
527
|
+
body.ttl_days = options.ttlDays;
|
|
528
|
+
}
|
|
478
529
|
|
|
479
|
-
|
|
530
|
+
// New v1 API: POST /products/{slug}/licenses/{key}/offline-token
|
|
531
|
+
const path = `/products/${this.config.productSlug}/licenses/${encodeURIComponent(license.license_key)}/offline-token`;
|
|
532
|
+
|
|
533
|
+
const response = await this.apiCall(path, {
|
|
534
|
+
method: "POST",
|
|
535
|
+
body: Object.keys(body).length > 0 ? body : undefined,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
this.emit("offlineToken:fetched", {
|
|
480
539
|
licenseKey: license.license_key,
|
|
481
540
|
data: response,
|
|
482
541
|
});
|
|
483
542
|
return response;
|
|
484
543
|
} catch (error) {
|
|
485
544
|
this.log(
|
|
486
|
-
`Failed to get offline
|
|
545
|
+
`Failed to get offline token for ${license.license_key}:`,
|
|
487
546
|
error
|
|
488
547
|
);
|
|
489
|
-
this.emit("
|
|
548
|
+
this.emit("offlineToken:fetchError", {
|
|
490
549
|
licenseKey: license.license_key,
|
|
491
550
|
error: error,
|
|
492
551
|
});
|
|
@@ -495,50 +554,48 @@ export class LicenseSeatSDK {
|
|
|
495
554
|
}
|
|
496
555
|
|
|
497
556
|
/**
|
|
498
|
-
* Fetch a
|
|
499
|
-
* @param {string} keyId - The Key ID (kid) for which to fetch the
|
|
500
|
-
* @returns {Promise<
|
|
557
|
+
* Fetch a signing key from the server by key ID
|
|
558
|
+
* @param {string} keyId - The Key ID (kid) for which to fetch the signing key
|
|
559
|
+
* @returns {Promise<import('./types.js').SigningKey>} Signing key data
|
|
501
560
|
* @throws {Error} When keyId is not provided or the key is not found
|
|
502
561
|
*/
|
|
503
|
-
async
|
|
562
|
+
async getSigningKey(keyId) {
|
|
504
563
|
if (!keyId) {
|
|
505
|
-
throw new Error("Key ID is required to fetch a
|
|
564
|
+
throw new Error("Key ID is required to fetch a signing key.");
|
|
506
565
|
}
|
|
507
566
|
try {
|
|
508
|
-
this.log(`Fetching
|
|
509
|
-
|
|
567
|
+
this.log(`Fetching signing key for kid: ${keyId}`);
|
|
568
|
+
// New v1 API: GET /signing-keys/{key_id}
|
|
569
|
+
const response = await this.apiCall(`/signing-keys/${encodeURIComponent(keyId)}`, {
|
|
510
570
|
method: "GET",
|
|
511
571
|
});
|
|
512
|
-
if (response && response.
|
|
513
|
-
this.log(`Successfully fetched
|
|
514
|
-
return response
|
|
572
|
+
if (response && response.public_key) {
|
|
573
|
+
this.log(`Successfully fetched signing key for kid: ${keyId}`);
|
|
574
|
+
return response;
|
|
515
575
|
} else {
|
|
516
576
|
throw new Error(
|
|
517
|
-
`
|
|
577
|
+
`Signing key not found or invalid response for kid: ${keyId}`
|
|
518
578
|
);
|
|
519
579
|
}
|
|
520
580
|
} catch (error) {
|
|
521
|
-
this.log(`Failed to fetch
|
|
581
|
+
this.log(`Failed to fetch signing key for kid ${keyId}:`, error);
|
|
522
582
|
throw error;
|
|
523
583
|
}
|
|
524
584
|
}
|
|
525
585
|
|
|
526
586
|
/**
|
|
527
|
-
* Verify a signed offline
|
|
528
|
-
* @param {import('./types.js').
|
|
587
|
+
* Verify a signed offline token client-side using Ed25519
|
|
588
|
+
* @param {import('./types.js').OfflineToken} offlineTokenData - The offline token data
|
|
529
589
|
* @param {string} publicKeyB64 - Base64-encoded public Ed25519 key
|
|
530
590
|
* @returns {Promise<boolean>} True if verification is successful
|
|
531
591
|
* @throws {CryptoError} When crypto library is not available
|
|
532
592
|
* @throws {Error} When inputs are invalid
|
|
533
593
|
*/
|
|
534
|
-
async
|
|
535
|
-
this.log("Attempting to verify offline
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
!signedLicenseData.signature_b64u
|
|
540
|
-
) {
|
|
541
|
-
throw new Error("Invalid signedLicenseData object provided.");
|
|
594
|
+
async verifyOfflineToken(offlineTokenData, publicKeyB64) {
|
|
595
|
+
this.log("Attempting to verify offline token client-side.");
|
|
596
|
+
|
|
597
|
+
if (!offlineTokenData || !offlineTokenData.canonical || !offlineTokenData.signature) {
|
|
598
|
+
throw new Error("Invalid offline token object provided. Expected format: { token, signature, canonical }");
|
|
542
599
|
}
|
|
543
600
|
if (!publicKeyB64) {
|
|
544
601
|
throw new Error("Public key (Base64 encoded) is required.");
|
|
@@ -553,29 +610,22 @@ export class LicenseSeatSDK {
|
|
|
553
610
|
}
|
|
554
611
|
|
|
555
612
|
try {
|
|
556
|
-
const
|
|
557
|
-
const
|
|
613
|
+
const messageBytes = new TextEncoder().encode(offlineTokenData.canonical);
|
|
614
|
+
const signatureBytes = base64UrlDecode(offlineTokenData.signature.value);
|
|
558
615
|
const publicKeyBytes = base64UrlDecode(publicKeyB64);
|
|
559
|
-
const signatureBytes = base64UrlDecode(signedLicenseData.signature_b64u);
|
|
560
616
|
|
|
561
617
|
const isValid = ed.verify(signatureBytes, messageBytes, publicKeyBytes);
|
|
562
618
|
|
|
563
619
|
if (isValid) {
|
|
564
|
-
this.log(
|
|
565
|
-
|
|
566
|
-
);
|
|
567
|
-
this.emit("offlineLicense:verified", {
|
|
568
|
-
payload: signedLicenseData.payload,
|
|
569
|
-
});
|
|
620
|
+
this.log("Offline token signature VERIFIED successfully client-side.");
|
|
621
|
+
this.emit("offlineToken:verified", { token: offlineTokenData.token });
|
|
570
622
|
} else {
|
|
571
|
-
this.log("Offline
|
|
572
|
-
this.emit("
|
|
573
|
-
payload: signedLicenseData.payload,
|
|
574
|
-
});
|
|
623
|
+
this.log("Offline token signature INVALID client-side.");
|
|
624
|
+
this.emit("offlineToken:verificationFailed", { token: offlineTokenData.token });
|
|
575
625
|
}
|
|
576
626
|
return isValid;
|
|
577
627
|
} catch (error) {
|
|
578
|
-
this.log("Client-side offline
|
|
628
|
+
this.log("Client-side offline token verification error:", error);
|
|
579
629
|
this.emit("sdk:error", {
|
|
580
630
|
message: "Client-side verification failed.",
|
|
581
631
|
error: error,
|
|
@@ -603,12 +653,12 @@ export class LicenseSeatSDK {
|
|
|
603
653
|
if (validation.offline) {
|
|
604
654
|
return {
|
|
605
655
|
status: "offline-invalid",
|
|
606
|
-
message: validation.
|
|
656
|
+
message: validation.code || "License invalid (offline)",
|
|
607
657
|
};
|
|
608
658
|
}
|
|
609
659
|
return {
|
|
610
660
|
status: "invalid",
|
|
611
|
-
message: validation.
|
|
661
|
+
message: validation.message || validation.code || "License invalid",
|
|
612
662
|
};
|
|
613
663
|
}
|
|
614
664
|
|
|
@@ -616,7 +666,7 @@ export class LicenseSeatSDK {
|
|
|
616
666
|
return {
|
|
617
667
|
status: "offline-valid",
|
|
618
668
|
license: license.license_key,
|
|
619
|
-
device: license.
|
|
669
|
+
device: license.device_id,
|
|
620
670
|
activated_at: license.activated_at,
|
|
621
671
|
last_validated: license.last_validated,
|
|
622
672
|
entitlements: validation.active_entitlements || [],
|
|
@@ -626,7 +676,7 @@ export class LicenseSeatSDK {
|
|
|
626
676
|
return {
|
|
627
677
|
status: "active",
|
|
628
678
|
license: license.license_key,
|
|
629
|
-
device: license.
|
|
679
|
+
device: license.device_id,
|
|
630
680
|
activated_at: license.activated_at,
|
|
631
681
|
last_validated: license.last_validated,
|
|
632
682
|
entitlements: validation.active_entitlements || [],
|
|
@@ -802,9 +852,10 @@ export class LicenseSeatSDK {
|
|
|
802
852
|
startConnectivityPolling() {
|
|
803
853
|
if (this.connectivityTimer) return;
|
|
804
854
|
|
|
805
|
-
const
|
|
855
|
+
const healthCheck = async () => {
|
|
806
856
|
try {
|
|
807
|
-
|
|
857
|
+
// New v1 API: GET /health
|
|
858
|
+
await fetch(`${this.config.apiBaseUrl}/health`, {
|
|
808
859
|
method: "GET",
|
|
809
860
|
credentials: "omit",
|
|
810
861
|
});
|
|
@@ -824,7 +875,7 @@ export class LicenseSeatSDK {
|
|
|
824
875
|
};
|
|
825
876
|
|
|
826
877
|
this.connectivityTimer = setInterval(
|
|
827
|
-
|
|
878
|
+
healthCheck,
|
|
828
879
|
this.config.networkRecheckInterval
|
|
829
880
|
);
|
|
830
881
|
}
|
|
@@ -846,7 +897,7 @@ export class LicenseSeatSDK {
|
|
|
846
897
|
// ============================================================
|
|
847
898
|
|
|
848
899
|
/**
|
|
849
|
-
* Fetch and cache offline
|
|
900
|
+
* Fetch and cache offline token and signing key
|
|
850
901
|
* Uses a lock to prevent concurrent calls from causing race conditions
|
|
851
902
|
* @returns {Promise<void>}
|
|
852
903
|
* @private
|
|
@@ -860,21 +911,21 @@ export class LicenseSeatSDK {
|
|
|
860
911
|
|
|
861
912
|
this.syncingOfflineAssets = true;
|
|
862
913
|
try {
|
|
863
|
-
const offline = await this.
|
|
864
|
-
this.cache.
|
|
914
|
+
const offline = await this.getOfflineToken();
|
|
915
|
+
this.cache.setOfflineToken(offline);
|
|
865
916
|
|
|
866
|
-
const kid = offline.
|
|
917
|
+
const kid = offline.signature?.key_id || offline.token?.kid;
|
|
867
918
|
if (kid) {
|
|
868
919
|
const existingKey = this.cache.getPublicKey(kid);
|
|
869
920
|
if (!existingKey) {
|
|
870
|
-
const
|
|
871
|
-
this.cache.setPublicKey(kid,
|
|
921
|
+
const signingKey = await this.getSigningKey(kid);
|
|
922
|
+
this.cache.setPublicKey(kid, signingKey.public_key);
|
|
872
923
|
}
|
|
873
924
|
}
|
|
874
925
|
|
|
875
|
-
this.emit("
|
|
876
|
-
kid:
|
|
877
|
-
|
|
926
|
+
this.emit("offlineToken:ready", {
|
|
927
|
+
kid: kid,
|
|
928
|
+
exp: offline.token?.exp,
|
|
878
929
|
});
|
|
879
930
|
|
|
880
931
|
// Verify freshly-cached assets
|
|
@@ -907,50 +958,47 @@ export class LicenseSeatSDK {
|
|
|
907
958
|
}
|
|
908
959
|
|
|
909
960
|
/**
|
|
910
|
-
* Verify cached offline
|
|
961
|
+
* Verify cached offline token
|
|
911
962
|
* @returns {Promise<import('./types.js').ValidationResult>}
|
|
912
963
|
* @private
|
|
913
964
|
*/
|
|
914
965
|
async verifyCachedOffline() {
|
|
915
|
-
const signed = this.cache.
|
|
966
|
+
const signed = this.cache.getOfflineToken();
|
|
916
967
|
if (!signed) {
|
|
917
|
-
return { valid: false, offline: true,
|
|
968
|
+
return { valid: false, offline: true, code: "no_offline_token" };
|
|
918
969
|
}
|
|
919
970
|
|
|
920
|
-
const kid = signed.
|
|
971
|
+
const kid = signed.signature?.key_id || signed.token?.kid;
|
|
921
972
|
let pub = kid ? this.cache.getPublicKey(kid) : null;
|
|
922
973
|
if (!pub) {
|
|
923
974
|
try {
|
|
924
|
-
|
|
975
|
+
const signingKey = await this.getSigningKey(kid);
|
|
976
|
+
pub = signingKey.public_key;
|
|
925
977
|
this.cache.setPublicKey(kid, pub);
|
|
926
978
|
} catch (e) {
|
|
927
|
-
return { valid: false, offline: true,
|
|
979
|
+
return { valid: false, offline: true, code: "no_public_key" };
|
|
928
980
|
}
|
|
929
981
|
}
|
|
930
982
|
|
|
931
983
|
try {
|
|
932
|
-
const ok = await this.
|
|
984
|
+
const ok = await this.verifyOfflineToken(signed, pub);
|
|
933
985
|
if (!ok) {
|
|
934
|
-
return { valid: false, offline: true,
|
|
986
|
+
return { valid: false, offline: true, code: "signature_invalid" };
|
|
935
987
|
}
|
|
936
988
|
|
|
937
|
-
|
|
938
|
-
const payload = signed.payload || {};
|
|
989
|
+
const token = signed.token;
|
|
939
990
|
const cached = this.cache.getLicense();
|
|
940
991
|
|
|
941
992
|
// License key match
|
|
942
|
-
if (
|
|
943
|
-
|
|
944
|
-
!constantTimeEqual(payload.lic_k || "", cached.license_key || "")
|
|
945
|
-
) {
|
|
946
|
-
return { valid: false, offline: true, reason_code: "license_mismatch" };
|
|
993
|
+
if (!cached || !constantTimeEqual(token.license_key || "", cached.license_key || "")) {
|
|
994
|
+
return { valid: false, offline: true, code: "license_mismatch" };
|
|
947
995
|
}
|
|
948
996
|
|
|
949
|
-
// Expiry check
|
|
997
|
+
// Expiry check (exp is Unix timestamp in seconds)
|
|
950
998
|
const now = Date.now();
|
|
951
|
-
const expAt =
|
|
999
|
+
const expAt = token.exp ? token.exp * 1000 : null;
|
|
952
1000
|
if (expAt && expAt < now) {
|
|
953
|
-
return { valid: false, offline: true,
|
|
1001
|
+
return { valid: false, offline: true, code: "expired" };
|
|
954
1002
|
}
|
|
955
1003
|
|
|
956
1004
|
// Grace period check
|
|
@@ -962,7 +1010,7 @@ export class LicenseSeatSDK {
|
|
|
962
1010
|
return {
|
|
963
1011
|
valid: false,
|
|
964
1012
|
offline: true,
|
|
965
|
-
|
|
1013
|
+
code: "grace_period_expired",
|
|
966
1014
|
};
|
|
967
1015
|
}
|
|
968
1016
|
}
|
|
@@ -971,19 +1019,19 @@ export class LicenseSeatSDK {
|
|
|
971
1019
|
// Clock tamper detection
|
|
972
1020
|
const lastSeen = this.cache.getLastSeenTimestamp();
|
|
973
1021
|
if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
|
|
974
|
-
return { valid: false, offline: true,
|
|
1022
|
+
return { valid: false, offline: true, code: "clock_tamper" };
|
|
975
1023
|
}
|
|
976
1024
|
|
|
977
1025
|
this.cache.setLastSeenTimestamp(now);
|
|
978
1026
|
|
|
979
|
-
const active = parseActiveEntitlements(
|
|
1027
|
+
const active = parseActiveEntitlements(token);
|
|
980
1028
|
return {
|
|
981
1029
|
valid: true,
|
|
982
1030
|
offline: true,
|
|
983
1031
|
...(active.length ? { active_entitlements: active } : {}),
|
|
984
1032
|
};
|
|
985
1033
|
} catch (e) {
|
|
986
|
-
return { valid: false, offline: true,
|
|
1034
|
+
return { valid: false, offline: true, code: "verification_error" };
|
|
987
1035
|
}
|
|
988
1036
|
}
|
|
989
1037
|
|
|
@@ -994,51 +1042,48 @@ export class LicenseSeatSDK {
|
|
|
994
1042
|
* @private
|
|
995
1043
|
*/
|
|
996
1044
|
async quickVerifyCachedOfflineLocal() {
|
|
997
|
-
const signed = this.cache.
|
|
1045
|
+
const signed = this.cache.getOfflineToken();
|
|
998
1046
|
if (!signed) return null;
|
|
999
|
-
|
|
1047
|
+
|
|
1048
|
+
const kid = signed.signature?.key_id || signed.token?.kid;
|
|
1000
1049
|
const pub = kid ? this.cache.getPublicKey(kid) : null;
|
|
1001
1050
|
if (!pub) return null;
|
|
1002
1051
|
|
|
1003
1052
|
try {
|
|
1004
|
-
const ok = await this.
|
|
1053
|
+
const ok = await this.verifyOfflineToken(signed, pub);
|
|
1005
1054
|
if (!ok) {
|
|
1006
|
-
return { valid: false, offline: true,
|
|
1055
|
+
return { valid: false, offline: true, code: "signature_invalid" };
|
|
1007
1056
|
}
|
|
1008
1057
|
|
|
1009
|
-
|
|
1010
|
-
const payload = signed.payload || {};
|
|
1058
|
+
const token = signed.token;
|
|
1011
1059
|
const cached = this.cache.getLicense();
|
|
1012
1060
|
|
|
1013
1061
|
// License key match check
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
!constantTimeEqual(payload.lic_k || "", cached.license_key || "")
|
|
1017
|
-
) {
|
|
1018
|
-
return { valid: false, offline: true, reason_code: "license_mismatch" };
|
|
1062
|
+
if (!cached || !constantTimeEqual(token.license_key || "", cached.license_key || "")) {
|
|
1063
|
+
return { valid: false, offline: true, code: "license_mismatch" };
|
|
1019
1064
|
}
|
|
1020
1065
|
|
|
1021
|
-
// Expiry check
|
|
1066
|
+
// Expiry check (exp is Unix timestamp in seconds)
|
|
1022
1067
|
const now = Date.now();
|
|
1023
|
-
const expAt =
|
|
1068
|
+
const expAt = token.exp ? token.exp * 1000 : null;
|
|
1024
1069
|
if (expAt && expAt < now) {
|
|
1025
|
-
return { valid: false, offline: true,
|
|
1070
|
+
return { valid: false, offline: true, code: "expired" };
|
|
1026
1071
|
}
|
|
1027
1072
|
|
|
1028
1073
|
// Clock tamper detection
|
|
1029
1074
|
const lastSeen = this.cache.getLastSeenTimestamp();
|
|
1030
1075
|
if (lastSeen && now + this.config.maxClockSkewMs < lastSeen) {
|
|
1031
|
-
return { valid: false, offline: true,
|
|
1076
|
+
return { valid: false, offline: true, code: "clock_tamper" };
|
|
1032
1077
|
}
|
|
1033
1078
|
|
|
1034
|
-
const active = parseActiveEntitlements(
|
|
1079
|
+
const active = parseActiveEntitlements(token);
|
|
1035
1080
|
return {
|
|
1036
1081
|
valid: true,
|
|
1037
1082
|
offline: true,
|
|
1038
1083
|
...(active.length ? { active_entitlements: active } : {}),
|
|
1039
1084
|
};
|
|
1040
1085
|
} catch (_) {
|
|
1041
|
-
return { valid: false, offline: true,
|
|
1086
|
+
return { valid: false, offline: true, code: "verification_error" };
|
|
1042
1087
|
}
|
|
1043
1088
|
}
|
|
1044
1089
|
|
|
@@ -1087,11 +1132,16 @@ export class LicenseSeatSDK {
|
|
|
1087
1132
|
const data = await response.json();
|
|
1088
1133
|
|
|
1089
1134
|
if (!response.ok) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
)
|
|
1135
|
+
// Handle new error format: { error: { code, message, details } }
|
|
1136
|
+
// Also support legacy format: { error: "message", reason_code: "code" }
|
|
1137
|
+
const errorObj = data.error;
|
|
1138
|
+
let errorMessage = "Request failed";
|
|
1139
|
+
if (typeof errorObj === "object" && errorObj !== null) {
|
|
1140
|
+
errorMessage = errorObj.message || "Request failed";
|
|
1141
|
+
} else if (typeof errorObj === "string") {
|
|
1142
|
+
errorMessage = errorObj;
|
|
1143
|
+
}
|
|
1144
|
+
throw new APIError(errorMessage, response.status, data);
|
|
1095
1145
|
}
|
|
1096
1146
|
|
|
1097
1147
|
// Back online
|