@antondziuin/talos-sdk 0.2.0 → 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/dist/index.d.cts CHANGED
@@ -457,6 +457,9 @@ declare class TalosClient {
457
457
  * for weeks. A key revoked in the portal would never reach it.
458
458
  */
459
459
  private keysetIssuedAt;
460
+ private keysetToken;
461
+ /** Invalidates online work started before a local licensing-mode change. */
462
+ private licensingGeneration;
460
463
  /** The policy's heartbeat cadence, from the last heartbeat's signed claims. */
461
464
  private heartbeatInterval;
462
465
  private checkInTimer;
@@ -623,13 +626,13 @@ declare class TalosClient {
623
626
  */
624
627
  createOfflineRequest(licenseKey?: string): SignedDoc;
625
628
  /**
626
- * Adopt an air-gapped licence file issued for this machine.
629
+ * Adopt a machine-bound or portable offline licence file.
627
630
  *
628
631
  * Everything is verified locally, because there is nothing to ask: the
629
632
  * anchor-signed keyset travels inside the file, the verdict is signed by a key
630
- * that keyset lists, and the file names both halves of this machine's identity
631
- * — its fingerprint and the public key it generated. The verdict is then kept,
632
- * so later starts are licensed with no further ceremony.
633
+ * that keyset lists. Machine-bound files name this machine's fingerprint and
634
+ * public key; explicitly portable files need no machine request. The verdict
635
+ * is kept, so later starts are licensed with no further ceremony.
633
636
  *
634
637
  * **Throws** only for a file that is not a licence: unreadable, not one of
635
638
  * ours, addressed to another product, or a broken signature. A file that *is*
@@ -881,6 +884,8 @@ declare class TalosClient {
881
884
  */
882
885
  private fetchVerified;
883
886
  private requireActivated;
887
+ /** Keep a newly imported offline licence authoritative over older requests. */
888
+ private offlineSince;
884
889
  private post;
885
890
  /**
886
891
  * The abort signal for one licensing call, or `undefined` when disabled.
package/dist/index.d.ts CHANGED
@@ -457,6 +457,9 @@ declare class TalosClient {
457
457
  * for weeks. A key revoked in the portal would never reach it.
458
458
  */
459
459
  private keysetIssuedAt;
460
+ private keysetToken;
461
+ /** Invalidates online work started before a local licensing-mode change. */
462
+ private licensingGeneration;
460
463
  /** The policy's heartbeat cadence, from the last heartbeat's signed claims. */
461
464
  private heartbeatInterval;
462
465
  private checkInTimer;
@@ -623,13 +626,13 @@ declare class TalosClient {
623
626
  */
624
627
  createOfflineRequest(licenseKey?: string): SignedDoc;
625
628
  /**
626
- * Adopt an air-gapped licence file issued for this machine.
629
+ * Adopt a machine-bound or portable offline licence file.
627
630
  *
628
631
  * Everything is verified locally, because there is nothing to ask: the
629
632
  * anchor-signed keyset travels inside the file, the verdict is signed by a key
630
- * that keyset lists, and the file names both halves of this machine's identity
631
- * — its fingerprint and the public key it generated. The verdict is then kept,
632
- * so later starts are licensed with no further ceremony.
633
+ * that keyset lists. Machine-bound files name this machine's fingerprint and
634
+ * public key; explicitly portable files need no machine request. The verdict
635
+ * is kept, so later starts are licensed with no further ceremony.
633
636
  *
634
637
  * **Throws** only for a file that is not a licence: unreadable, not one of
635
638
  * ours, addressed to another product, or a broken signature. A file that *is*
@@ -881,6 +884,8 @@ declare class TalosClient {
881
884
  */
882
885
  private fetchVerified;
883
886
  private requireActivated;
887
+ /** Keep a newly imported offline licence authoritative over older requests. */
888
+ private offlineSince;
884
889
  private post;
885
890
  /**
886
891
  * The abort signal for one licensing call, or `undefined` when disabled.
package/dist/index.js CHANGED
@@ -236,7 +236,7 @@ var StateStore = class {
236
236
  };
237
237
 
238
238
  // src/version.ts
239
- var SDK_VERSION = "0.2.0";
239
+ var SDK_VERSION = "0.3.0";
240
240
  var SDK_ID = `node/${SDK_VERSION}`;
241
241
 
242
242
  // src/diagnostics.ts
@@ -464,6 +464,9 @@ var TalosClient = class {
464
464
  * for weeks. A key revoked in the portal would never reach it.
465
465
  */
466
466
  keysetIssuedAt = 0;
467
+ keysetToken = null;
468
+ /** Invalidates online work started before a local licensing-mode change. */
469
+ licensingGeneration = 0;
467
470
  /** The policy's heartbeat cadence, from the last heartbeat's signed claims. */
468
471
  heartbeatInterval = null;
469
472
  checkInTimer = null;
@@ -586,12 +589,14 @@ var TalosClient = class {
586
589
  */
587
590
  async refreshKeyset() {
588
591
  if (!this.anchorKey) return;
592
+ const generation = this.licensingGeneration;
589
593
  const body = await this.post("/v1/client/keyset", {
590
594
  product_token: this.options.productToken
591
595
  });
596
+ if (generation !== this.licensingGeneration) return;
592
597
  this.adoptKeyset(body["keyset"]);
593
598
  this.keysetFromCache = false;
594
- if (typeof body["keyset"] === "string") this.persist({ keyset: body["keyset"] });
599
+ if (this.keysetToken) this.persist({ keyset: this.keysetToken });
595
600
  }
596
601
  /**
597
602
  * Make sure a verification key exists before a response has to be verified.
@@ -642,7 +647,8 @@ var TalosClient = class {
642
647
  if (!this.anchorKey || typeof token !== "string") return;
643
648
  const claims = verifyKeyset(this.anchorKey, this.options.productToken, token);
644
649
  const iat = claims["iat"];
645
- this.keysetIssuedAt = typeof iat === "number" ? iat : 0;
650
+ const issuedAt = typeof iat === "number" ? iat : 0;
651
+ if (issuedAt < this.keysetIssuedAt || issuedAt === this.keysetIssuedAt && this.keysetToken !== null && token !== this.keysetToken) return;
646
652
  const licenseKeys = [];
647
653
  const releaseKeys = [];
648
654
  for (const entry of asArray(claims["keys"])) {
@@ -654,6 +660,8 @@ var TalosClient = class {
654
660
  }
655
661
  this.keysetLicenseKeys = licenseKeys;
656
662
  this.keysetReleaseKeys = releaseKeys;
663
+ this.keysetIssuedAt = issuedAt;
664
+ this.keysetToken = token;
657
665
  }
658
666
  /** Keys to verify license tokens with: the keyset if loaded, else the direct pin. */
659
667
  licenseVerifyKeys() {
@@ -685,62 +693,71 @@ var TalosClient = class {
685
693
  * elsewhere.
686
694
  */
687
695
  async activate(licenseKey) {
688
- if (this.isActivated && this.licenseKeyHash === hashLicenseKey(licenseKey)) {
689
- try {
690
- const state = await this.validate();
691
- if (state.decision !== "deactivated") return state;
692
- } catch (e) {
693
- const stale = e instanceof TalosApiError && (e.statusCode === 401 || e.statusCode === 404);
694
- if (!stale) throw e;
696
+ const generation = this.licensingGeneration;
697
+ try {
698
+ if (this.isActivated && this.licenseKeyHash === hashLicenseKey(licenseKey)) {
699
+ try {
700
+ const state = await this.validate();
701
+ if (state.decision !== "deactivated") return state;
702
+ } catch (e) {
703
+ const stale = e instanceof TalosApiError && (e.statusCode === 401 || e.statusCode === 404);
704
+ if (!stale) throw e;
705
+ }
706
+ this.forgetBinding();
695
707
  }
696
- this.forgetBinding();
708
+ const nonce = newNonce();
709
+ const body = await this.post("/v1/client/activate", {
710
+ product_token: this.options.productToken,
711
+ license_key: licenseKey,
712
+ fingerprint: { composite: this.fingerprint, components: this.fp.components },
713
+ nonce,
714
+ hostname: this.reportedHostname(),
715
+ os: `${os3.type()} ${os3.release()}`,
716
+ arch: os3.arch(),
717
+ app_version: this.options.appVersion,
718
+ sdk_version: SDK_ID
719
+ });
720
+ const switched = this.offlineSince(generation);
721
+ if (switched) return switched;
722
+ this.adoptKeyset(body["keyset"]);
723
+ const claims = verifyToken(
724
+ this.licenseVerifyKeys(),
725
+ "activation",
726
+ this.options.productToken,
727
+ nonce,
728
+ body["activation_token"]
729
+ );
730
+ this.licenseKeyHash = hashLicenseKey(licenseKey);
731
+ this.activationSecret = body["activation_secret"];
732
+ this.machineId = body["machine_id"] ?? null;
733
+ this.binding = {
734
+ licenseKeyHash: this.licenseKeyHash,
735
+ activationSecret: this.activationSecret,
736
+ machineId: this.machineId ?? "",
737
+ fingerprint: this.fingerprint,
738
+ // A fresh activation invalidates any cached verdict for the previous
739
+ // binding; keep only the keyset, which is per product, not per machine.
740
+ keyset: this.keysetToken ?? this.binding?.keyset,
741
+ timeHighWater: this.binding?.timeHighWater,
742
+ // The machine's own identity survives an online activation, because it is
743
+ // an identity and not a verdict: an install that reaches the network once
744
+ // must not have to repeat the whole air-gapped ceremony with its vendor
745
+ // the next time it is unplugged. The clock mark survives for the same
746
+ // reason a mark exists at all — it only moves forward.
747
+ machineKey: this.binding?.machineKey,
748
+ machinePublicKey: this.binding?.machinePublicKey,
749
+ offlineSeen: this.binding?.offlineSeen
750
+ // The offline *file* does not: a live activation supersedes it, and
751
+ // keeping it would leave two answers to "what licenses this machine".
752
+ };
753
+ this.advanceClock(claims);
754
+ this.store?.save(this.binding);
755
+ return this.adopt(stateFromClaims(claims, "valid", false));
756
+ } catch (e) {
757
+ const switched = this.offlineSince(generation);
758
+ if (switched) return switched;
759
+ throw e;
697
760
  }
698
- const nonce = newNonce();
699
- const body = await this.post("/v1/client/activate", {
700
- product_token: this.options.productToken,
701
- license_key: licenseKey,
702
- fingerprint: { composite: this.fingerprint, components: this.fp.components },
703
- nonce,
704
- hostname: this.reportedHostname(),
705
- os: `${os3.type()} ${os3.release()}`,
706
- arch: os3.arch(),
707
- app_version: this.options.appVersion,
708
- sdk_version: SDK_ID
709
- });
710
- this.adoptKeyset(body["keyset"]);
711
- const claims = verifyToken(
712
- this.licenseVerifyKeys(),
713
- "activation",
714
- this.options.productToken,
715
- nonce,
716
- body["activation_token"]
717
- );
718
- this.licenseKeyHash = hashLicenseKey(licenseKey);
719
- this.activationSecret = body["activation_secret"];
720
- this.machineId = body["machine_id"] ?? null;
721
- this.binding = {
722
- licenseKeyHash: this.licenseKeyHash,
723
- activationSecret: this.activationSecret,
724
- machineId: this.machineId ?? "",
725
- fingerprint: this.fingerprint,
726
- // A fresh activation invalidates any cached verdict for the previous
727
- // binding; keep only the keyset, which is per product, not per machine.
728
- keyset: typeof body["keyset"] === "string" ? body["keyset"] : this.binding?.keyset,
729
- timeHighWater: this.binding?.timeHighWater,
730
- // The machine's own identity survives an online activation, because it is
731
- // an identity and not a verdict: an install that reaches the network once
732
- // must not have to repeat the whole air-gapped ceremony with its vendor
733
- // the next time it is unplugged. The clock mark survives for the same
734
- // reason a mark exists at all — it only moves forward.
735
- machineKey: this.binding?.machineKey,
736
- machinePublicKey: this.binding?.machinePublicKey,
737
- offlineSeen: this.binding?.offlineSeen
738
- // The offline *file* does not: a live activation supersedes it, and
739
- // keeping it would leave two answers to "what licenses this machine".
740
- };
741
- this.advanceClock(claims);
742
- this.store?.save(this.binding);
743
- return this.adopt(stateFromClaims(claims, "valid", false));
744
761
  }
745
762
  /**
746
763
  * This machine's own keypair, generated once and kept in the state file.
@@ -815,13 +832,13 @@ var TalosClient = class {
815
832
  });
816
833
  }
817
834
  /**
818
- * Adopt an air-gapped licence file issued for this machine.
835
+ * Adopt a machine-bound or portable offline licence file.
819
836
  *
820
837
  * Everything is verified locally, because there is nothing to ask: the
821
838
  * anchor-signed keyset travels inside the file, the verdict is signed by a key
822
- * that keyset lists, and the file names both halves of this machine's identity
823
- * — its fingerprint and the public key it generated. The verdict is then kept,
824
- * so later starts are licensed with no further ceremony.
839
+ * that keyset lists. Machine-bound files name this machine's fingerprint and
840
+ * public key; explicitly portable files need no machine request. The verdict
841
+ * is kept, so later starts are licensed with no further ceremony.
825
842
  *
826
843
  * **Throws** only for a file that is not a licence: unreadable, not one of
827
844
  * ours, addressed to another product, or a broken signature. A file that *is*
@@ -844,8 +861,27 @@ var TalosClient = class {
844
861
  if (doc["kind"] !== LICENSE_FILE_KIND || typeof doc["token"] !== "string") {
845
862
  throw new TalosError("not a Talos offline licence file");
846
863
  }
847
- if (typeof doc["keyset"] === "string") this.adoptKeyset(doc["keyset"]);
848
- return this.adoptOfflineToken(doc["token"], doc["keyset"]);
864
+ const previous = {
865
+ license: this.keysetLicenseKeys,
866
+ release: this.keysetReleaseKeys,
867
+ iat: this.keysetIssuedAt,
868
+ token: this.keysetToken
869
+ };
870
+ const restore = () => {
871
+ this.keysetLicenseKeys = previous.license;
872
+ this.keysetReleaseKeys = previous.release;
873
+ this.keysetIssuedAt = previous.iat;
874
+ this.keysetToken = previous.token;
875
+ };
876
+ try {
877
+ if (typeof doc["keyset"] === "string") this.adoptKeyset(doc["keyset"]);
878
+ const state = this.adoptOfflineToken(doc["token"], this.keysetToken);
879
+ if (!state.isValid) restore();
880
+ return state;
881
+ } catch (e) {
882
+ restore();
883
+ throw e;
884
+ }
849
885
  }
850
886
  /**
851
887
  * Hand this machine's air-gapped seat back.
@@ -867,9 +903,7 @@ var TalosClient = class {
867
903
  */
868
904
  createOfflineDeactivation() {
869
905
  const token = this.binding?.offlineToken;
870
- const key = this.binding?.machineKey;
871
- const pub = this.binding?.machinePublicKey;
872
- if (!token || !key || !pub) {
906
+ if (!token) {
873
907
  throw new TalosError("no offline licence to hand back");
874
908
  }
875
909
  let claims;
@@ -883,6 +917,12 @@ var TalosClient = class {
883
917
  } catch {
884
918
  throw new TalosError("the stored offline licence no longer verifies");
885
919
  }
920
+ if (claims["binding"] === "portable") {
921
+ throw new TalosError("portable offline licences have no bound seat to return");
922
+ }
923
+ const key = this.binding?.machineKey;
924
+ const pub = this.binding?.machinePublicKey;
925
+ if (!key || !pub) throw new TalosError("no offline licence to hand back");
886
926
  this.binding = { ...this.bindingBase(), offlineToken: void 0 };
887
927
  this.current = null;
888
928
  this.store?.save(this.binding);
@@ -907,38 +947,58 @@ var TalosClient = class {
907
947
  token
908
948
  );
909
949
  const mismatch = () => this.adopt(new LicenseState("machine_mismatch", {}, null, true, null, null));
910
- if (typeof claims["mpk"] !== "string" || claims["mpk"] !== this.binding?.machinePublicKey) {
950
+ const lic = claims["lic"] ?? {};
951
+ const features = lic["features"] ?? {};
952
+ const licExp = num(lic["lic_exp"]);
953
+ const exp = num(claims["exp"]);
954
+ const perpetual = claims["perpetual"] === true && claims["exp"] === null && lic["lic_exp"] === null;
955
+ const portable = claims["binding"] === "portable";
956
+ if (portable) {
957
+ const present = (key) => Object.hasOwn(claims, key);
958
+ const named = (key) => typeof claims[key] === "string" && claims[key].trim().length > 0;
959
+ if (!perpetual || claims["decision"] !== "valid" || present("fph") || present("mpk") || present("mid") || !named("fid") || !named("sub")) return mismatch();
960
+ } else if (claims["binding"] !== void 0 && claims["binding"] !== "machine") {
961
+ return mismatch();
962
+ }
963
+ if (!portable && (typeof claims["mpk"] !== "string" || claims["mpk"] !== this.binding?.machinePublicKey)) {
911
964
  return mismatch();
912
965
  }
913
- if (claims["fph"] !== this.fingerprint) return mismatch();
966
+ if (!portable && claims["fph"] !== this.fingerprint) return mismatch();
914
967
  const now = nowSeconds();
915
968
  const mark = Math.max(this.binding?.timeHighWater ?? 0, this.binding?.offlineSeen ?? 0);
916
969
  if (mark > 0 && now + CLOCK_SKEW_SECONDS2 < mark) {
917
970
  return this.adopt(new LicenseState("clock_tampered", {}, null, true, null, null));
918
971
  }
919
- const lic = claims["lic"] ?? {};
920
- const features = lic["features"] ?? {};
921
- const licExp = num(lic["lic_exp"]);
922
- const exp = num(claims["exp"]);
923
- this.machineId = typeof claims["mid"] === "string" ? claims["mid"] : this.machineId;
972
+ if (!perpetual && (exp === null || now > exp)) {
973
+ return this.adopt(
974
+ new LicenseState("offline_file_expired", features, licExp, true, null, exp)
975
+ );
976
+ }
977
+ if (licExp !== null && now > licExp) {
978
+ return this.adopt(new LicenseState("expired", features, licExp, true, null, exp));
979
+ }
980
+ this.licensingGeneration += 1;
981
+ this.licenseKeyHash = null;
982
+ this.activationSecret = null;
983
+ this.heartbeatInterval = null;
984
+ this.leaseGeneration += 1;
985
+ this.stopRenew();
986
+ this.lease = null;
987
+ this.machineId = !portable && typeof claims["mid"] === "string" ? claims["mid"] : null;
924
988
  const base = this.bindingBase();
925
989
  this.binding = {
926
990
  ...base,
927
- machineId: this.machineId ?? base.machineId,
991
+ licenseKeyHash: "",
992
+ activationSecret: "",
993
+ validation: void 0,
994
+ machineId: this.machineId ?? "",
995
+ fingerprint: this.fingerprint,
928
996
  offlineToken: token,
929
997
  offlineSeen: Math.max(base.offlineSeen ?? 0, now),
930
998
  ...typeof keyset === "string" ? { keyset } : {}
931
999
  };
932
1000
  this.advanceClock(claims);
933
1001
  this.store?.save(this.binding);
934
- if (exp === null || now > exp) {
935
- return this.adopt(
936
- new LicenseState("offline_file_expired", features, licExp, true, null, exp)
937
- );
938
- }
939
- if (licExp !== null && now > licExp) {
940
- return this.adopt(new LicenseState("expired", features, licExp, true, null, exp));
941
- }
942
1002
  return this.adopt(new LicenseState("valid", features, licExp, true, null, exp));
943
1003
  }
944
1004
  /**
@@ -1051,6 +1111,7 @@ var TalosClient = class {
1051
1111
  }
1052
1112
  /** Drop the machine binding, in memory and on disk. */
1053
1113
  forgetBinding() {
1114
+ this.licensingGeneration += 1;
1054
1115
  this.licenseKeyHash = null;
1055
1116
  this.activationSecret = null;
1056
1117
  this.machineId = null;
@@ -1084,8 +1145,11 @@ var TalosClient = class {
1084
1145
  return this.adoptOfflineToken(this.binding.offlineToken);
1085
1146
  }
1086
1147
  this.requireActivated();
1148
+ const generation = this.licensingGeneration;
1087
1149
  try {
1088
1150
  await this.ensureKeyset();
1151
+ const switchedBeforeRequest = this.offlineSince(generation);
1152
+ if (switchedBeforeRequest) return switchedBeforeRequest;
1089
1153
  const nonce = newNonce();
1090
1154
  const body = await this.post("/v1/client/validate", {
1091
1155
  product_token: this.options.productToken,
@@ -1099,6 +1163,8 @@ var TalosClient = class {
1099
1163
  app_version: this.options.appVersion,
1100
1164
  sdk_version: SDK_ID
1101
1165
  });
1166
+ const switched = this.offlineSince(generation);
1167
+ if (switched) return switched;
1102
1168
  const token = body["validation_token"];
1103
1169
  const claims = verifyToken(
1104
1170
  this.licenseVerifyKeys(),
@@ -1112,6 +1178,8 @@ var TalosClient = class {
1112
1178
  this.persist({ validation: token, timeHighWater: this.binding?.timeHighWater });
1113
1179
  return this.adopt(stateFromClaims(claims, decision, false));
1114
1180
  } catch (e) {
1181
+ const switched = this.offlineSince(generation);
1182
+ if (switched) return switched;
1115
1183
  if (e instanceof TalosApiError) throw e;
1116
1184
  const offline = this.offlineDecision();
1117
1185
  if (!offline) throw e;
@@ -1184,12 +1252,13 @@ var TalosClient = class {
1184
1252
  }
1185
1253
  async deactivate() {
1186
1254
  this.requireActivated();
1255
+ const generation = this.licensingGeneration;
1187
1256
  await this.post("/v1/client/deactivate", {
1188
1257
  product_token: this.options.productToken,
1189
1258
  fingerprint: { composite: this.fingerprint, components: this.fp.components },
1190
1259
  activation_secret: this.activationSecret
1191
1260
  });
1192
- this.forgetBinding();
1261
+ if (generation === this.licensingGeneration) this.forgetBinding();
1193
1262
  }
1194
1263
  /**
1195
1264
  * Send a heartbeat: refreshes this machine's last-seen state, feeds the
@@ -1213,46 +1282,59 @@ var TalosClient = class {
1213
1282
  }
1214
1283
  async heartbeat() {
1215
1284
  this.requireActivated();
1216
- await this.ensureKeyset();
1217
- const nonce = newNonce();
1218
- const body = await this.post("/v1/client/heartbeat", {
1219
- product_token: this.options.productToken,
1220
- fingerprint: { composite: this.fingerprint, components: this.fp.components },
1221
- activation_secret: this.activationSecret,
1222
- nonce,
1223
- app_version: this.options.appVersion,
1224
- os: `${os3.type()} ${os3.release()}`,
1225
- arch: os3.arch(),
1226
- sdk_version: SDK_ID,
1227
- // Naming the channel is what asks the server for the update hint, and it
1228
- // costs two queries there — so it is asked for only when somebody is
1229
- // listening for the answer.
1230
- update_channel: this.listeners.get("updateAvailable")?.size ? this.updateChannel() : void 0
1231
- });
1232
- const claims = verifyToken(
1233
- this.licenseVerifyKeys(),
1234
- "heartbeat",
1235
- this.options.productToken,
1236
- nonce,
1237
- body["heartbeat_token"]
1238
- );
1239
- const decision = typeof claims["decision"] === "string" ? claims["decision"] : "invalid";
1240
- const interval = num(claims["heartbeat_interval_s"]);
1241
- if (interval !== null && interval > 0) this.heartbeatInterval = interval;
1242
- this.advanceClock(claims);
1243
- this.persist({ timeHighWater: this.binding?.timeHighWater });
1244
- const state = this.adopt(
1245
- new LicenseState(
1246
- decision,
1247
- this.current?.features ?? {},
1248
- this.current?.licenseExpiresAt ?? null,
1249
- false,
1250
- this.current?.revalidateAfter ?? null,
1251
- this.current?.graceUntil ?? null
1252
- )
1253
- );
1254
- await this.announceUpdate(claims);
1255
- return state;
1285
+ const generation = this.licensingGeneration;
1286
+ try {
1287
+ await this.ensureKeyset();
1288
+ const switchedBeforeRequest = this.offlineSince(generation);
1289
+ if (switchedBeforeRequest) return switchedBeforeRequest;
1290
+ const nonce = newNonce();
1291
+ const body = await this.post("/v1/client/heartbeat", {
1292
+ product_token: this.options.productToken,
1293
+ fingerprint: { composite: this.fingerprint, components: this.fp.components },
1294
+ activation_secret: this.activationSecret,
1295
+ nonce,
1296
+ app_version: this.options.appVersion,
1297
+ os: `${os3.type()} ${os3.release()}`,
1298
+ arch: os3.arch(),
1299
+ sdk_version: SDK_ID,
1300
+ // Naming the channel is what asks the server for the update hint, and it
1301
+ // costs two queries there — so it is asked for only when somebody is
1302
+ // listening for the answer.
1303
+ update_channel: this.listeners.get("updateAvailable")?.size ? this.updateChannel() : void 0
1304
+ });
1305
+ const switched = this.offlineSince(generation);
1306
+ if (switched) return switched;
1307
+ const claims = verifyToken(
1308
+ this.licenseVerifyKeys(),
1309
+ "heartbeat",
1310
+ this.options.productToken,
1311
+ nonce,
1312
+ body["heartbeat_token"]
1313
+ );
1314
+ const decision = typeof claims["decision"] === "string" ? claims["decision"] : "invalid";
1315
+ const interval = num(claims["heartbeat_interval_s"]);
1316
+ if (interval !== null && interval > 0) this.heartbeatInterval = interval;
1317
+ this.advanceClock(claims);
1318
+ this.persist({ timeHighWater: this.binding?.timeHighWater });
1319
+ const state = this.adopt(
1320
+ new LicenseState(
1321
+ decision,
1322
+ this.current?.features ?? {},
1323
+ this.current?.licenseExpiresAt ?? null,
1324
+ false,
1325
+ this.current?.revalidateAfter ?? null,
1326
+ this.current?.graceUntil ?? null
1327
+ )
1328
+ );
1329
+ await this.announceUpdate(claims);
1330
+ const switchedAfterUpdate = this.offlineSince(generation);
1331
+ if (switchedAfterUpdate) return switchedAfterUpdate;
1332
+ return state;
1333
+ } catch (e) {
1334
+ const switched = this.offlineSince(generation);
1335
+ if (switched) return switched;
1336
+ throw e;
1337
+ }
1256
1338
  }
1257
1339
  /** The channel this install follows; `stable` unless set. */
1258
1340
  updateChannel() {
@@ -1637,6 +1719,13 @@ var TalosClient = class {
1637
1719
  throw new TalosError("not activated: call activate() first");
1638
1720
  }
1639
1721
  }
1722
+ /** Keep a newly imported offline licence authoritative over older requests. */
1723
+ offlineSince(generation) {
1724
+ if (generation === this.licensingGeneration || this.activationSecret || !this.binding?.offlineToken) {
1725
+ return null;
1726
+ }
1727
+ return this.adoptOfflineToken(this.binding.offlineToken);
1728
+ }
1640
1729
  post(path, body) {
1641
1730
  return this.request("POST", path, body);
1642
1731
  }