@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@licenseseat/js",
3
- "version": "0.2.2",
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",
@@ -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
- const deviceId = options.deviceIdentifier || generateDeviceId();
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
- license_key: licenseKey,
244
- device_identifier: deviceId,
249
+ device_id: deviceId,
245
250
  metadata: options.metadata || {},
246
251
  };
247
252
 
248
- if (options.softwareReleaseDate) {
249
- payload.software_release_date = options.softwareReleaseDate;
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
- const response = await this.apiCall("/activations/activate", {
256
- method: "POST",
257
- body: payload,
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
- device_identifier: deviceId,
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
- const response = await this.apiCall("/activations/deactivate", {
299
- method: "POST",
300
- body: {
301
- license_key: cachedLicense.license_key,
302
- device_identifier: cachedLicense.device_identifier,
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.clearOfflineLicense();
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
- const rawResponse = await this.apiCall("/licenses/validate", {
330
- method: "POST",
331
- body: {
332
- license_key: licenseKey,
333
- device_identifier: options.deviceIdentifier || this.cache.getDeviceId(),
334
- product_slug: options.productSlug,
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: { active_entitlements, ... } }
339
- // SDK expects flat structure { valid, active_entitlements, ... }
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
- ...(rawResponse.license || {}),
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
- this.cache.updateValidation({ valid: false, ...error.data });
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 license data from the server
460
- * @returns {Promise<import('./types.js').SignedOfflineLicense>} Signed offline license data
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 getOfflineLicense() {
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 license.";
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("offlineLicense:fetching", { licenseKey: license.license_key });
475
- const path = `/licenses/${license.license_key}/offline_license`;
519
+ this.emit("offlineToken:fetching", { licenseKey: license.license_key });
476
520
 
477
- const response = await this.apiCall(path, { method: "POST" });
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
- this.emit("offlineLicense:fetched", {
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 license for ${license.license_key}:`,
545
+ `Failed to get offline token for ${license.license_key}:`,
487
546
  error
488
547
  );
489
- this.emit("offlineLicense:fetchError", {
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 public key from the server by key ID
499
- * @param {string} keyId - The Key ID (kid) for which to fetch the public key
500
- * @returns {Promise<string>} Base64-encoded public key
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 getPublicKey(keyId) {
562
+ async getSigningKey(keyId) {
504
563
  if (!keyId) {
505
- throw new Error("Key ID is required to fetch a public key.");
564
+ throw new Error("Key ID is required to fetch a signing key.");
506
565
  }
507
566
  try {
508
- this.log(`Fetching public key for kid: ${keyId}`);
509
- const response = await this.apiCall(`/public_keys/${keyId}`, {
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.public_key_b64) {
513
- this.log(`Successfully fetched public key for kid: ${keyId}`);
514
- return response.public_key_b64;
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
- `Public key not found or invalid response for kid: ${keyId}`
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 public key for kid ${keyId}:`, error);
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 license client-side using Ed25519
528
- * @param {import('./types.js').SignedOfflineLicense} signedLicenseData - The signed license data
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 verifyOfflineLicense(signedLicenseData, publicKeyB64) {
535
- this.log("Attempting to verify offline license client-side.");
536
- if (
537
- !signedLicenseData ||
538
- !signedLicenseData.payload ||
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 payloadString = canonicalJsonStringify(signedLicenseData.payload);
557
- const messageBytes = new TextEncoder().encode(payloadString);
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
- "Offline license signature VERIFIED successfully client-side."
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 license signature INVALID client-side.");
572
- this.emit("offlineLicense:verificationFailed", {
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 license verification error:", error);
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.reason_code || "License invalid (offline)",
656
+ message: validation.code || "License invalid (offline)",
607
657
  };
608
658
  }
609
659
  return {
610
660
  status: "invalid",
611
- message: validation.reason || "License invalid",
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.device_identifier,
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.device_identifier,
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 heartbeat = async () => {
855
+ const healthCheck = async () => {
806
856
  try {
807
- await fetch(`${this.config.apiBaseUrl}/heartbeat`, {
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
- heartbeat,
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 license and public key
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.getOfflineLicense();
864
- this.cache.setOfflineLicense(offline);
914
+ const offline = await this.getOfflineToken();
915
+ this.cache.setOfflineToken(offline);
865
916
 
866
- const kid = offline.kid || offline.payload?.kid;
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 pub = await this.getPublicKey(kid);
871
- this.cache.setPublicKey(kid, pub);
921
+ const signingKey = await this.getSigningKey(kid);
922
+ this.cache.setPublicKey(kid, signingKey.public_key);
872
923
  }
873
924
  }
874
925
 
875
- this.emit("offlineLicense:ready", {
876
- kid: offline.kid || offline.payload?.kid,
877
- exp_at: offline.payload?.exp_at,
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 license
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.getOfflineLicense();
966
+ const signed = this.cache.getOfflineToken();
916
967
  if (!signed) {
917
- return { valid: false, offline: true, reason_code: "no_offline_license" };
968
+ return { valid: false, offline: true, code: "no_offline_token" };
918
969
  }
919
970
 
920
- const kid = signed.kid || signed.payload?.kid;
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
- pub = await this.getPublicKey(kid);
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, reason_code: "no_public_key" };
979
+ return { valid: false, offline: true, code: "no_public_key" };
928
980
  }
929
981
  }
930
982
 
931
983
  try {
932
- const ok = await this.verifyOfflineLicense(signed, pub);
984
+ const ok = await this.verifyOfflineToken(signed, pub);
933
985
  if (!ok) {
934
- return { valid: false, offline: true, reason_code: "signature_invalid" };
986
+ return { valid: false, offline: true, code: "signature_invalid" };
935
987
  }
936
988
 
937
- /** @type {import('./types.js').OfflineLicensePayload} */
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
- !cached ||
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 = payload.exp_at ? Date.parse(payload.exp_at) : null;
999
+ const expAt = token.exp ? token.exp * 1000 : null;
952
1000
  if (expAt && expAt < now) {
953
- return { valid: false, offline: true, reason_code: "expired" };
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
- reason_code: "grace_period_expired",
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, reason_code: "clock_tamper" };
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(payload);
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, reason_code: "verification_error" };
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.getOfflineLicense();
1045
+ const signed = this.cache.getOfflineToken();
998
1046
  if (!signed) return null;
999
- const kid = signed.kid || signed.payload?.kid;
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.verifyOfflineLicense(signed, pub);
1053
+ const ok = await this.verifyOfflineToken(signed, pub);
1005
1054
  if (!ok) {
1006
- return { valid: false, offline: true, reason_code: "signature_invalid" };
1055
+ return { valid: false, offline: true, code: "signature_invalid" };
1007
1056
  }
1008
1057
 
1009
- /** @type {import('./types.js').OfflineLicensePayload} */
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
- !cached ||
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 = payload.exp_at ? Date.parse(payload.exp_at) : null;
1068
+ const expAt = token.exp ? token.exp * 1000 : null;
1024
1069
  if (expAt && expAt < now) {
1025
- return { valid: false, offline: true, reason_code: "expired" };
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, reason_code: "clock_tamper" };
1076
+ return { valid: false, offline: true, code: "clock_tamper" };
1032
1077
  }
1033
1078
 
1034
- const active = parseActiveEntitlements(payload);
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, reason_code: "verification_error" };
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
- throw new APIError(
1091
- data.error || "Request failed",
1092
- response.status,
1093
- data
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