@licenseseat/js 0.3.1 → 0.4.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/dist/index.js CHANGED
@@ -300,12 +300,12 @@ function generateDeviceId() {
300
300
  return `node-${hashCode(os + "|" + arch)}`;
301
301
  }
302
302
  const nav = window.navigator;
303
- const screen = window.screen;
303
+ const screen2 = window.screen;
304
304
  const data = [
305
305
  nav.userAgent,
306
306
  nav.language,
307
- screen.colorDepth,
308
- screen.width + "x" + screen.height,
307
+ screen2.colorDepth,
308
+ screen2.width + "x" + screen2.height,
309
309
  (/* @__PURE__ */ new Date()).getTimezoneOffset(),
310
310
  nav.hardwareConcurrency,
311
311
  getCanvasFingerprint()
@@ -320,7 +320,366 @@ function getCsrfToken() {
320
320
  return token ? token.content : "";
321
321
  }
322
322
 
323
+ // src/telemetry.js
324
+ function detectOSName() {
325
+ if (typeof process !== "undefined" && process.platform) {
326
+ const map = {
327
+ darwin: "macOS",
328
+ win32: "Windows",
329
+ linux: "Linux",
330
+ freebsd: "FreeBSD",
331
+ sunos: "SunOS"
332
+ };
333
+ return map[process.platform] || process.platform;
334
+ }
335
+ if (typeof navigator !== "undefined") {
336
+ if (navigator.userAgentData && navigator.userAgentData.platform) {
337
+ return navigator.userAgentData.platform;
338
+ }
339
+ const ua = navigator.userAgent || "";
340
+ if (/Android/i.test(ua))
341
+ return "Android";
342
+ if (/iPhone|iPad|iPod/i.test(ua))
343
+ return "iOS";
344
+ if (/Mac/i.test(ua))
345
+ return "macOS";
346
+ if (/Win/i.test(ua))
347
+ return "Windows";
348
+ if (/Linux/i.test(ua))
349
+ return "Linux";
350
+ }
351
+ return "Unknown";
352
+ }
353
+ function detectOSVersion() {
354
+ if (typeof process !== "undefined" && process.version) {
355
+ try {
356
+ const os = await_free_os_release();
357
+ if (os)
358
+ return os;
359
+ } catch (_) {
360
+ }
361
+ return process.version;
362
+ }
363
+ if (typeof navigator !== "undefined") {
364
+ const ua = navigator.userAgent || "";
365
+ const macMatch = ua.match(/Mac OS X\s+([\d._]+)/);
366
+ if (macMatch)
367
+ return macMatch[1].replace(/_/g, ".");
368
+ const winMatch = ua.match(/Windows NT\s+([\d.]+)/);
369
+ if (winMatch)
370
+ return winMatch[1];
371
+ const androidMatch = ua.match(/Android\s+([\d.]+)/);
372
+ if (androidMatch)
373
+ return androidMatch[1];
374
+ const iosMatch = ua.match(/OS\s+([\d._]+)/);
375
+ if (iosMatch)
376
+ return iosMatch[1].replace(/_/g, ".");
377
+ }
378
+ return null;
379
+ }
380
+ function await_free_os_release() {
381
+ try {
382
+ const os = new Function("try { return require('os') } catch(e) { return null }")();
383
+ if (os && os.release)
384
+ return os.release();
385
+ } catch (_) {
386
+ }
387
+ return null;
388
+ }
389
+ function dynamicRequire(moduleName) {
390
+ try {
391
+ return new Function("m", "try { return require(m) } catch(e) { return null }")(moduleName);
392
+ } catch (_) {
393
+ return null;
394
+ }
395
+ }
396
+ function detectPlatform() {
397
+ if (typeof process !== "undefined") {
398
+ if (process.versions && process.versions.electron)
399
+ return "electron";
400
+ if (process.versions && process.versions.bun)
401
+ return "bun";
402
+ if (process.versions && process.versions.node)
403
+ return "node";
404
+ }
405
+ if (typeof Deno !== "undefined")
406
+ return "deno";
407
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative")
408
+ return "react-native";
409
+ if (typeof window !== "undefined")
410
+ return "browser";
411
+ return "unknown";
412
+ }
413
+ function detectDeviceModel() {
414
+ try {
415
+ if (typeof navigator !== "undefined" && navigator.userAgentData) {
416
+ return navigator.userAgentData.model || null;
417
+ }
418
+ } catch (_) {
419
+ }
420
+ return null;
421
+ }
422
+ function detectLocale() {
423
+ if (typeof navigator !== "undefined" && navigator.language) {
424
+ return navigator.language;
425
+ }
426
+ if (typeof Intl !== "undefined") {
427
+ try {
428
+ return Intl.DateTimeFormat().resolvedOptions().locale || null;
429
+ } catch (_) {
430
+ }
431
+ }
432
+ if (typeof process !== "undefined" && process.env) {
433
+ return process.env.LANG || process.env.LC_ALL || null;
434
+ }
435
+ return null;
436
+ }
437
+ function detectTimezone() {
438
+ if (typeof Intl !== "undefined") {
439
+ try {
440
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
441
+ } catch (_) {
442
+ }
443
+ }
444
+ return null;
445
+ }
446
+ function detectDeviceType() {
447
+ try {
448
+ const platform = detectPlatform();
449
+ if (platform === "node" || platform === "bun" || platform === "deno")
450
+ return "server";
451
+ if (platform === "electron")
452
+ return "desktop";
453
+ if (platform === "react-native") {
454
+ if (typeof screen !== "undefined" && screen.width) {
455
+ return screen.width < 768 ? "phone" : "tablet";
456
+ }
457
+ return "phone";
458
+ }
459
+ if (typeof navigator !== "undefined") {
460
+ if (navigator.userAgentData && typeof navigator.userAgentData.mobile === "boolean") {
461
+ if (navigator.userAgentData.mobile) {
462
+ if (typeof screen !== "undefined" && screen.width >= 768)
463
+ return "tablet";
464
+ return "phone";
465
+ }
466
+ return "desktop";
467
+ }
468
+ if (navigator.maxTouchPoints > 0) {
469
+ if (typeof screen !== "undefined" && screen.width >= 768)
470
+ return "tablet";
471
+ return "phone";
472
+ }
473
+ return "desktop";
474
+ }
475
+ } catch (_) {
476
+ }
477
+ return "unknown";
478
+ }
479
+ function detectArchitecture() {
480
+ try {
481
+ if (typeof process !== "undefined" && process.arch) {
482
+ const map = { ia32: "x86", x64: "x64", arm: "arm", arm64: "arm64" };
483
+ return map[process.arch] || process.arch;
484
+ }
485
+ if (typeof navigator !== "undefined" && navigator.userAgentData) {
486
+ if (navigator.userAgentData.architecture) {
487
+ return navigator.userAgentData.architecture;
488
+ }
489
+ }
490
+ } catch (_) {
491
+ }
492
+ return null;
493
+ }
494
+ function detectCpuCores() {
495
+ try {
496
+ if (typeof navigator !== "undefined" && navigator.hardwareConcurrency) {
497
+ return navigator.hardwareConcurrency;
498
+ }
499
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
500
+ const os = dynamicRequire("os");
501
+ if (os && os.cpus) {
502
+ const cpus = os.cpus();
503
+ if (cpus && cpus.length)
504
+ return cpus.length;
505
+ }
506
+ }
507
+ } catch (_) {
508
+ }
509
+ return null;
510
+ }
511
+ function detectMemoryGb() {
512
+ try {
513
+ if (typeof navigator !== "undefined" && navigator.deviceMemory) {
514
+ return navigator.deviceMemory;
515
+ }
516
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
517
+ const os = dynamicRequire("os");
518
+ if (os && os.totalmem) {
519
+ return Math.round(os.totalmem() / (1024 * 1024 * 1024));
520
+ }
521
+ }
522
+ } catch (_) {
523
+ }
524
+ return null;
525
+ }
526
+ function detectLanguage() {
527
+ try {
528
+ const locale = detectLocale();
529
+ if (locale) {
530
+ const lang = locale.split(/[-_]/)[0];
531
+ if (lang && lang.length >= 2)
532
+ return lang.toLowerCase();
533
+ }
534
+ } catch (_) {
535
+ }
536
+ return null;
537
+ }
538
+ function detectScreenResolution() {
539
+ try {
540
+ if (typeof screen !== "undefined" && screen.width && screen.height) {
541
+ return `${screen.width}x${screen.height}`;
542
+ }
543
+ } catch (_) {
544
+ }
545
+ return null;
546
+ }
547
+ function detectDisplayScale() {
548
+ try {
549
+ if (typeof window !== "undefined" && window.devicePixelRatio) {
550
+ return window.devicePixelRatio;
551
+ }
552
+ } catch (_) {
553
+ }
554
+ return null;
555
+ }
556
+ function detectBrowserName() {
557
+ try {
558
+ if (typeof navigator === "undefined")
559
+ return null;
560
+ if (navigator.userAgentData && navigator.userAgentData.brands) {
561
+ const brands = navigator.userAgentData.brands;
562
+ for (const b of brands) {
563
+ const name = b.brand || "";
564
+ if (/^(Google Chrome|Microsoft Edge|Opera|Brave|Vivaldi|Samsung Internet)$/i.test(name)) {
565
+ return name;
566
+ }
567
+ }
568
+ for (const b of brands) {
569
+ if ((b.brand || "").toLowerCase() === "chromium")
570
+ return "Chrome";
571
+ }
572
+ }
573
+ const ua = navigator.userAgent || "";
574
+ if (/Edg\//i.test(ua))
575
+ return "Edge";
576
+ if (/OPR\//i.test(ua) || /Opera/i.test(ua))
577
+ return "Opera";
578
+ if (/Brave/i.test(ua))
579
+ return "Brave";
580
+ if (/Vivaldi/i.test(ua))
581
+ return "Vivaldi";
582
+ if (/Firefox/i.test(ua))
583
+ return "Firefox";
584
+ if (/SamsungBrowser/i.test(ua))
585
+ return "Samsung Internet";
586
+ if (/CriOS/i.test(ua))
587
+ return "Chrome";
588
+ if (/Chrome/i.test(ua))
589
+ return "Chrome";
590
+ if (/Safari/i.test(ua))
591
+ return "Safari";
592
+ } catch (_) {
593
+ }
594
+ return null;
595
+ }
596
+ function detectBrowserVersion() {
597
+ try {
598
+ if (typeof navigator === "undefined")
599
+ return null;
600
+ if (navigator.userAgentData && navigator.userAgentData.brands) {
601
+ const brands = navigator.userAgentData.brands;
602
+ for (const b of brands) {
603
+ const name = b.brand || "";
604
+ if (/^(Google Chrome|Microsoft Edge|Opera|Brave|Vivaldi|Samsung Internet)$/i.test(name)) {
605
+ return b.version || null;
606
+ }
607
+ }
608
+ for (const b of brands) {
609
+ if ((b.brand || "").toLowerCase() === "chromium")
610
+ return b.version || null;
611
+ }
612
+ }
613
+ const ua = navigator.userAgent || "";
614
+ const patterns = [
615
+ /Edg\/([\d.]+)/,
616
+ /OPR\/([\d.]+)/,
617
+ /Firefox\/([\d.]+)/,
618
+ /SamsungBrowser\/([\d.]+)/,
619
+ /CriOS\/([\d.]+)/,
620
+ /Chrome\/([\d.]+)/,
621
+ /Version\/([\d.]+).*Safari/
622
+ ];
623
+ for (const re of patterns) {
624
+ const m = ua.match(re);
625
+ if (m)
626
+ return m[1];
627
+ }
628
+ } catch (_) {
629
+ }
630
+ return null;
631
+ }
632
+ function detectRuntimeVersion() {
633
+ try {
634
+ if (typeof process !== "undefined" && process.versions) {
635
+ if (process.versions.bun)
636
+ return process.versions.bun;
637
+ if (process.versions.electron)
638
+ return process.versions.electron;
639
+ if (process.versions.node)
640
+ return process.versions.node;
641
+ }
642
+ if (typeof Deno !== "undefined" && Deno.version)
643
+ return Deno.version.deno;
644
+ } catch (_) {
645
+ }
646
+ return null;
647
+ }
648
+ function collectTelemetry(sdkVersion, options) {
649
+ const locale = detectLocale();
650
+ const raw = {
651
+ sdk_version: sdkVersion,
652
+ sdk_name: "js",
653
+ os_name: detectOSName(),
654
+ os_version: detectOSVersion(),
655
+ platform: detectPlatform(),
656
+ device_model: detectDeviceModel(),
657
+ device_type: detectDeviceType(),
658
+ locale,
659
+ timezone: detectTimezone(),
660
+ language: detectLanguage(),
661
+ architecture: detectArchitecture(),
662
+ cpu_cores: detectCpuCores(),
663
+ memory_gb: detectMemoryGb(),
664
+ screen_resolution: detectScreenResolution(),
665
+ display_scale: detectDisplayScale(),
666
+ browser_name: detectBrowserName(),
667
+ browser_version: detectBrowserVersion(),
668
+ runtime_version: detectRuntimeVersion(),
669
+ app_version: options && options.appVersion || null,
670
+ app_build: options && options.appBuild || null
671
+ };
672
+ const result = {};
673
+ for (const [key, value] of Object.entries(raw)) {
674
+ if (value != null) {
675
+ result[key] = value;
676
+ }
677
+ }
678
+ return result;
679
+ }
680
+
323
681
  // src/LicenseSeat.js
682
+ var SDK_VERSION = "0.4.1";
324
683
  var DEFAULT_CONFIG = {
325
684
  apiBaseUrl: "https://licenseseat.com/api/v1",
326
685
  productSlug: null,
@@ -328,6 +687,8 @@ var DEFAULT_CONFIG = {
328
687
  storagePrefix: "licenseseat_",
329
688
  autoValidateInterval: 36e5,
330
689
  // 1 hour
690
+ heartbeatInterval: 3e5,
691
+ // 5 minutes
331
692
  networkRecheckInterval: 3e4,
332
693
  // 30 seconds
333
694
  maxRetries: 3,
@@ -342,7 +703,13 @@ var DEFAULT_CONFIG = {
342
703
  // 0 = disabled
343
704
  maxClockSkewMs: 5 * 60 * 1e3,
344
705
  // 5 minutes
345
- autoInitialize: true
706
+ autoInitialize: true,
707
+ telemetryEnabled: true,
708
+ // Set false to disable telemetry (e.g. for GDPR compliance)
709
+ appVersion: null,
710
+ // User-provided app version, sent as app_version in telemetry
711
+ appBuild: null
712
+ // User-provided app build, sent as app_build in telemetry
346
713
  };
347
714
  var LicenseSeatSDK = class {
348
715
  /**
@@ -356,6 +723,7 @@ var LicenseSeatSDK = class {
356
723
  };
357
724
  this.eventListeners = {};
358
725
  this.validationTimer = null;
726
+ this.heartbeatTimer = null;
359
727
  this.cache = new LicenseCache(this.config.storagePrefix);
360
728
  this.online = true;
361
729
  this.currentAutoLicenseKey = null;
@@ -402,6 +770,7 @@ var LicenseSeatSDK = class {
402
770
  }
403
771
  if (this.config.apiKey) {
404
772
  this.startAutoValidation(cachedLicense.license_key);
773
+ this.startHeartbeat();
405
774
  this.validateLicense(cachedLicense.license_key).catch((err) => {
406
775
  this.log("Background validation failed:", err);
407
776
  if (err instanceof APIError && (err.status === 401 || err.status === 501)) {
@@ -457,6 +826,7 @@ var LicenseSeatSDK = class {
457
826
  this.cache.setLicense(licenseData);
458
827
  this.cache.updateValidation({ valid: true, optimistic: true });
459
828
  this.startAutoValidation(licenseKey);
829
+ this.startHeartbeat();
460
830
  this.syncOfflineAssets();
461
831
  this.scheduleOfflineRefresh();
462
832
  this.emit("activation:success", licenseData);
@@ -495,6 +865,7 @@ var LicenseSeatSDK = class {
495
865
  this.cache.clearLicense();
496
866
  this.cache.clearOfflineToken();
497
867
  this.stopAutoValidation();
868
+ this.stopHeartbeat();
498
869
  this.emit("deactivation:success", response);
499
870
  return response;
500
871
  } catch (error) {
@@ -825,12 +1196,41 @@ var LicenseSeatSDK = class {
825
1196
  throw error;
826
1197
  }
827
1198
  }
1199
+ /**
1200
+ * Send a heartbeat for the current license.
1201
+ * Heartbeats let the server know the device is still active.
1202
+ * @returns {Promise<Object|undefined>} Heartbeat response, or undefined if no active license
1203
+ * @throws {ConfigurationError} When productSlug is not configured
1204
+ * @throws {APIError} When the API request fails
1205
+ */
1206
+ async heartbeat() {
1207
+ if (!this.config.productSlug) {
1208
+ throw new ConfigurationError("productSlug is required for heartbeat");
1209
+ }
1210
+ const cached = this.cache.getLicense();
1211
+ if (!cached) {
1212
+ this.log("No active license for heartbeat");
1213
+ return;
1214
+ }
1215
+ const body = { device_id: cached.device_id };
1216
+ const response = await this.apiCall(
1217
+ `/products/${this.config.productSlug}/licenses/${encodeURIComponent(cached.license_key)}/heartbeat`,
1218
+ {
1219
+ method: "POST",
1220
+ body
1221
+ }
1222
+ );
1223
+ this.emit("heartbeat:success", response);
1224
+ this.log("Heartbeat sent successfully");
1225
+ return response;
1226
+ }
828
1227
  /**
829
1228
  * Clear all data and reset SDK state
830
1229
  * @returns {void}
831
1230
  */
832
1231
  reset() {
833
1232
  this.stopAutoValidation();
1233
+ this.stopHeartbeat();
834
1234
  this.stopConnectivityPolling();
835
1235
  if (this.offlineRefreshTimer) {
836
1236
  clearInterval(this.offlineRefreshTimer);
@@ -850,6 +1250,7 @@ var LicenseSeatSDK = class {
850
1250
  destroy() {
851
1251
  this.destroyed = true;
852
1252
  this.stopAutoValidation();
1253
+ this.stopHeartbeat();
853
1254
  this.stopConnectivityPolling();
854
1255
  if (this.offlineRefreshTimer) {
855
1256
  clearInterval(this.offlineRefreshTimer);
@@ -922,8 +1323,14 @@ var LicenseSeatSDK = class {
922
1323
  this.stopAutoValidation();
923
1324
  this.currentAutoLicenseKey = licenseKey;
924
1325
  const validationInterval = this.config.autoValidateInterval;
1326
+ if (!validationInterval || validationInterval <= 0) {
1327
+ this.log("Auto-validation disabled (interval:", validationInterval, ")");
1328
+ return;
1329
+ }
925
1330
  const performAndReschedule = () => {
926
- this.validateLicense(licenseKey).catch((err) => {
1331
+ this.validateLicense(licenseKey).then(() => {
1332
+ this.heartbeat().catch((err) => this.log("Heartbeat failed:", err));
1333
+ }).catch((err) => {
927
1334
  this.log("Auto-validation failed:", err);
928
1335
  this.emit("validation:auto-failed", { licenseKey, error: err });
929
1336
  });
@@ -948,6 +1355,35 @@ var LicenseSeatSDK = class {
948
1355
  this.emit("autovalidation:stopped");
949
1356
  }
950
1357
  }
1358
+ /**
1359
+ * Start separate heartbeat timer
1360
+ * Sends periodic heartbeats between auto-validation cycles.
1361
+ * @returns {void}
1362
+ * @private
1363
+ */
1364
+ startHeartbeat() {
1365
+ this.stopHeartbeat();
1366
+ const interval = this.config.heartbeatInterval;
1367
+ if (!interval || interval <= 0) {
1368
+ this.log("Heartbeat timer disabled (interval:", interval, ")");
1369
+ return;
1370
+ }
1371
+ this.heartbeatTimer = setInterval(() => {
1372
+ this.heartbeat().then(() => this.emit("heartbeat:cycle", { nextRunAt: new Date(Date.now() + interval) })).catch((err) => this.log("Heartbeat timer failed:", err));
1373
+ }, interval);
1374
+ this.log("Heartbeat timer started (interval:", interval, "ms)");
1375
+ }
1376
+ /**
1377
+ * Stop the separate heartbeat timer
1378
+ * @returns {void}
1379
+ * @private
1380
+ */
1381
+ stopHeartbeat() {
1382
+ if (this.heartbeatTimer) {
1383
+ clearInterval(this.heartbeatTimer);
1384
+ this.heartbeatTimer = null;
1385
+ }
1386
+ }
951
1387
  /**
952
1388
  * Start connectivity polling (when offline)
953
1389
  * @returns {void}
@@ -958,10 +1394,12 @@ var LicenseSeatSDK = class {
958
1394
  return;
959
1395
  const healthCheck = async () => {
960
1396
  try {
961
- await fetch(`${this.config.apiBaseUrl}/health`, {
1397
+ const res = await fetch(`${this.config.apiBaseUrl}/health`, {
962
1398
  method: "GET",
963
1399
  credentials: "omit"
964
1400
  });
1401
+ await res.text().catch(() => {
1402
+ });
965
1403
  if (!this.online) {
966
1404
  this.online = true;
967
1405
  this.emit("network:online");
@@ -994,10 +1432,10 @@ var LicenseSeatSDK = class {
994
1432
  // Offline License Management
995
1433
  // ============================================================
996
1434
  /**
997
- * Fetch and cache offline token and signing key
998
- * Uses a lock to prevent concurrent calls from causing race conditions
1435
+ * Download and cache the offline token and its corresponding public signing key.
1436
+ * Emits `offlineToken:ready` on success. Safe to call multiple times concurrent
1437
+ * calls are deduplicated automatically.
999
1438
  * @returns {Promise<void>}
1000
- * @private
1001
1439
  */
1002
1440
  async syncOfflineAssets() {
1003
1441
  if (this.syncingOfflineAssets || this.destroyed) {
@@ -1048,9 +1486,10 @@ var LicenseSeatSDK = class {
1048
1486
  );
1049
1487
  }
1050
1488
  /**
1051
- * Verify cached offline token
1489
+ * Verify the cached offline token and return a validation result.
1490
+ * Use this to validate the license when the device is offline.
1491
+ * The offline token must have been previously downloaded via {@link syncOfflineAssets}.
1052
1492
  * @returns {Promise<import('./types.js').ValidationResult>}
1053
- * @private
1054
1493
  */
1055
1494
  async verifyCachedOffline() {
1056
1495
  const signed = this.cache.getOfflineToken();
@@ -1183,12 +1622,20 @@ var LicenseSeatSDK = class {
1183
1622
  "[Warning] No API key configured for LicenseSeat SDK. Authenticated endpoints will fail."
1184
1623
  );
1185
1624
  }
1625
+ const method = options.method || "GET";
1626
+ let body = options.body;
1627
+ if (method === "POST" && body && this.config.telemetryEnabled !== false) {
1628
+ body = { ...body, telemetry: collectTelemetry(SDK_VERSION, {
1629
+ appVersion: this.config.appVersion,
1630
+ appBuild: this.config.appBuild
1631
+ }) };
1632
+ }
1186
1633
  for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
1187
1634
  try {
1188
1635
  const response = await fetch(url, {
1189
- method: options.method || "GET",
1636
+ method,
1190
1637
  headers,
1191
- body: options.body ? JSON.stringify(options.body) : void 0,
1638
+ body: body ? JSON.stringify(body) : void 0,
1192
1639
  credentials: "omit"
1193
1640
  });
1194
1641
  const data = await response.json();
@@ -1312,8 +1759,10 @@ export {
1312
1759
  LicenseCache,
1313
1760
  LicenseError,
1314
1761
  LicenseSeatSDK,
1762
+ SDK_VERSION,
1315
1763
  base64UrlDecode,
1316
1764
  canonicalJsonStringify,
1765
+ collectTelemetry,
1317
1766
  configure,
1318
1767
  constantTimeEqual,
1319
1768
  src_default as default,
@@ -16,6 +16,11 @@ export function configure(config: import("./types.js").LicenseSeatConfig, force?
16
16
  * @returns {void}
17
17
  */
18
18
  export function resetSharedInstance(): void;
19
+ /**
20
+ * SDK version constant
21
+ * @type {string}
22
+ */
23
+ export const SDK_VERSION: string;
19
24
  /**
20
25
  * LicenseSeat SDK Main Class
21
26
  *
@@ -61,6 +66,12 @@ export class LicenseSeatSDK {
61
66
  * @private
62
67
  */
63
68
  private validationTimer;
69
+ /**
70
+ * Heartbeat timer ID (separate from auto-validation)
71
+ * @type {ReturnType<typeof setInterval>|null}
72
+ * @private
73
+ */
74
+ private heartbeatTimer;
64
75
  /**
65
76
  * License cache manager
66
77
  * @type {LicenseCache}
@@ -204,6 +215,14 @@ export class LicenseSeatSDK {
204
215
  healthy: boolean;
205
216
  api_version: string;
206
217
  }>;
218
+ /**
219
+ * Send a heartbeat for the current license.
220
+ * Heartbeats let the server know the device is still active.
221
+ * @returns {Promise<Object|undefined>} Heartbeat response, or undefined if no active license
222
+ * @throws {ConfigurationError} When productSlug is not configured
223
+ * @throws {APIError} When the API request fails
224
+ */
225
+ heartbeat(): Promise<any | undefined>;
207
226
  /**
208
227
  * Clear all data and reset SDK state
209
228
  * @returns {void}
@@ -251,6 +270,19 @@ export class LicenseSeatSDK {
251
270
  * @private
252
271
  */
253
272
  private stopAutoValidation;
273
+ /**
274
+ * Start separate heartbeat timer
275
+ * Sends periodic heartbeats between auto-validation cycles.
276
+ * @returns {void}
277
+ * @private
278
+ */
279
+ private startHeartbeat;
280
+ /**
281
+ * Stop the separate heartbeat timer
282
+ * @returns {void}
283
+ * @private
284
+ */
285
+ private stopHeartbeat;
254
286
  /**
255
287
  * Start connectivity polling (when offline)
256
288
  * @returns {void}
@@ -264,12 +296,12 @@ export class LicenseSeatSDK {
264
296
  */
265
297
  private stopConnectivityPolling;
266
298
  /**
267
- * Fetch and cache offline token and signing key
268
- * Uses a lock to prevent concurrent calls from causing race conditions
299
+ * Download and cache the offline token and its corresponding public signing key.
300
+ * Emits `offlineToken:ready` on success. Safe to call multiple times concurrent
301
+ * calls are deduplicated automatically.
269
302
  * @returns {Promise<void>}
270
- * @private
271
303
  */
272
- private syncOfflineAssets;
304
+ syncOfflineAssets(): Promise<void>;
273
305
  /**
274
306
  * Schedule periodic offline license refresh
275
307
  * @returns {void}
@@ -277,11 +309,12 @@ export class LicenseSeatSDK {
277
309
  */
278
310
  private scheduleOfflineRefresh;
279
311
  /**
280
- * Verify cached offline token
312
+ * Verify the cached offline token and return a validation result.
313
+ * Use this to validate the license when the device is offline.
314
+ * The offline token must have been previously downloaded via {@link syncOfflineAssets}.
281
315
  * @returns {Promise<import('./types.js').ValidationResult>}
282
- * @private
283
316
  */
284
- private verifyCachedOffline;
317
+ verifyCachedOffline(): Promise<import("./types.js").ValidationResult>;
285
318
  /**
286
319
  * Quick offline verification using only local data (no network)
287
320
  * Performs signature verification plus basic validity checks (expiry, license key match)