@antondziuin/talos-sdk 0.2.1 → 0.4.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 CHANGED
@@ -240,6 +240,25 @@ so an anchor-only pin still works with no server to fetch one from. The verdict
240
240
  is kept, so later starts need nothing — `validate()` re-checks the stored file
241
241
  instead of reaching for the network, and `isActivated` is true.
242
242
 
243
+ A **portable** permanent file is issued directly by the vendor. Import it with
244
+ `loadOfflineLicense("license.talos")` on each computer; no request or machine
245
+ key is needed. Its signed `binding: "portable"` explicitly permits use on
246
+ different computers. Each computer keeps its own local state; carry the licence
247
+ file rather than copying machine-encrypted state. Portable files can be copied
248
+ and remain usable indefinitely. `createOfflineDeactivation()` refuses them
249
+ because they have no bound seat to return.
250
+
251
+ A valid offline import replaces any existing online activation locally, so
252
+ later `validate()` calls and restarts use the file without contacting Talos.
253
+ Rejected imports preserve the previous stored activation. Keep state
254
+ persistence enabled for automatic restore.
255
+
256
+ For trusted users, a perpetual policy can opt into permanent offline files with
257
+ `offline_checkout_max_days: 0`. The issued file has a signed `perpetual: true`
258
+ and explicit null file and licence expiries. It needs no renewal or server
259
+ connection; `graceUntil` and `licenseExpiresAt` are null. Import it using the
260
+ same calls above. Older SDK versions reject these files and must be upgraded.
261
+
243
262
  `loadOfflineLicense` **throws** only for a file that is not a licence: unreadable,
244
263
  not one of ours, addressed to another product, or a signature that does not
245
264
  verify. A file that is one but cannot license this machine comes back as a
@@ -255,9 +274,9 @@ receipt changes nothing.
255
274
  Two things to know before you offer this to customers. A file **cannot be
256
275
  recalled** — revoking it in the portal frees the seat so you can issue to a
257
276
  replacement machine, but the machine holding it keeps working until the file's
258
- own expiry, which is why the policy's horizon is short. And hardware changes are
259
- not forgiven offline: the drift matcher lives on the server, so a replaced disk
260
- means `machine_mismatch` and a new request.
277
+ own expiry, or indefinitely for a permanent file. Hardware changes to a
278
+ machine-bound install are not forgiven offline: the drift matcher lives on the
279
+ server, so a replaced disk means `machine_mismatch` and a new request.
261
280
 
262
281
  ## Heartbeats, monitoring, and cadence
263
282
 
package/dist/index.cjs CHANGED
@@ -247,6 +247,9 @@ var StateStore = class {
247
247
  }
248
248
  if (typeof o["offlineToken"] === "string") b.offlineToken = o["offlineToken"];
249
249
  if (typeof o["offlineSeen"] === "number") b.offlineSeen = o["offlineSeen"];
250
+ if (typeof o["registryManifest"] === "string" && o["registryManifest"].length <= 65536) b.registryManifest = o["registryManifest"];
251
+ if (typeof o["registryActivationId"] === "string" && o["registryActivationId"].length <= 36) b.registryActivationId = o["registryActivationId"];
252
+ if (typeof o["registryChainState"] === "string") b.registryChainState = o["registryChainState"];
250
253
  return b.activationSecret || b.offlineToken || b.machineKey ? b : null;
251
254
  } catch {
252
255
  return null;
@@ -288,7 +291,7 @@ var StateStore = class {
288
291
  };
289
292
 
290
293
  // src/version.ts
291
- var SDK_VERSION = "0.2.1";
294
+ var SDK_VERSION = "0.4.0";
292
295
  var SDK_ID = `node/${SDK_VERSION}`;
293
296
 
294
297
  // src/diagnostics.ts
@@ -516,6 +519,9 @@ var TalosClient = class {
516
519
  * for weeks. A key revoked in the portal would never reach it.
517
520
  */
518
521
  keysetIssuedAt = 0;
522
+ keysetToken = null;
523
+ /** Invalidates online work started before a local licensing-mode change. */
524
+ licensingGeneration = 0;
519
525
  /** The policy's heartbeat cadence, from the last heartbeat's signed claims. */
520
526
  heartbeatInterval = null;
521
527
  checkInTimer = null;
@@ -638,12 +644,14 @@ var TalosClient = class {
638
644
  */
639
645
  async refreshKeyset() {
640
646
  if (!this.anchorKey) return;
647
+ const generation = this.licensingGeneration;
641
648
  const body = await this.post("/v1/client/keyset", {
642
649
  product_token: this.options.productToken
643
650
  });
651
+ if (generation !== this.licensingGeneration) return;
644
652
  this.adoptKeyset(body["keyset"]);
645
653
  this.keysetFromCache = false;
646
- if (typeof body["keyset"] === "string") this.persist({ keyset: body["keyset"] });
654
+ if (this.keysetToken) this.persist({ keyset: this.keysetToken });
647
655
  }
648
656
  /**
649
657
  * Make sure a verification key exists before a response has to be verified.
@@ -694,7 +702,8 @@ var TalosClient = class {
694
702
  if (!this.anchorKey || typeof token !== "string") return;
695
703
  const claims = verifyKeyset(this.anchorKey, this.options.productToken, token);
696
704
  const iat = claims["iat"];
697
- this.keysetIssuedAt = typeof iat === "number" ? iat : 0;
705
+ const issuedAt = typeof iat === "number" ? iat : 0;
706
+ if (issuedAt < this.keysetIssuedAt || issuedAt === this.keysetIssuedAt && this.keysetToken !== null && token !== this.keysetToken) return;
698
707
  const licenseKeys = [];
699
708
  const releaseKeys = [];
700
709
  for (const entry of asArray(claims["keys"])) {
@@ -706,6 +715,8 @@ var TalosClient = class {
706
715
  }
707
716
  this.keysetLicenseKeys = licenseKeys;
708
717
  this.keysetReleaseKeys = releaseKeys;
718
+ this.keysetIssuedAt = issuedAt;
719
+ this.keysetToken = token;
709
720
  }
710
721
  /** Keys to verify license tokens with: the keyset if loaded, else the direct pin. */
711
722
  licenseVerifyKeys() {
@@ -737,62 +748,71 @@ var TalosClient = class {
737
748
  * elsewhere.
738
749
  */
739
750
  async activate(licenseKey) {
740
- if (this.isActivated && this.licenseKeyHash === hashLicenseKey(licenseKey)) {
741
- try {
742
- const state = await this.validate();
743
- if (state.decision !== "deactivated") return state;
744
- } catch (e) {
745
- const stale = e instanceof TalosApiError && (e.statusCode === 401 || e.statusCode === 404);
746
- if (!stale) throw e;
751
+ const generation = this.licensingGeneration;
752
+ try {
753
+ if (this.isActivated && this.licenseKeyHash === hashLicenseKey(licenseKey)) {
754
+ try {
755
+ const state = await this.validate();
756
+ if (state.decision !== "deactivated") return state;
757
+ } catch (e) {
758
+ const stale = e instanceof TalosApiError && (e.statusCode === 401 || e.statusCode === 404);
759
+ if (!stale) throw e;
760
+ }
761
+ this.forgetBinding();
747
762
  }
748
- this.forgetBinding();
763
+ const nonce = newNonce();
764
+ const body = await this.post("/v1/client/activate", {
765
+ product_token: this.options.productToken,
766
+ license_key: licenseKey,
767
+ fingerprint: { composite: this.fingerprint, components: this.fp.components },
768
+ nonce,
769
+ hostname: this.reportedHostname(),
770
+ os: `${import_node_os3.default.type()} ${import_node_os3.default.release()}`,
771
+ arch: import_node_os3.default.arch(),
772
+ app_version: this.options.appVersion,
773
+ sdk_version: SDK_ID
774
+ });
775
+ const switched = this.offlineSince(generation);
776
+ if (switched) return switched;
777
+ this.adoptKeyset(body["keyset"]);
778
+ const claims = verifyToken(
779
+ this.licenseVerifyKeys(),
780
+ "activation",
781
+ this.options.productToken,
782
+ nonce,
783
+ body["activation_token"]
784
+ );
785
+ this.licenseKeyHash = hashLicenseKey(licenseKey);
786
+ this.activationSecret = body["activation_secret"];
787
+ this.machineId = body["machine_id"] ?? null;
788
+ this.binding = {
789
+ licenseKeyHash: this.licenseKeyHash,
790
+ activationSecret: this.activationSecret,
791
+ machineId: this.machineId ?? "",
792
+ fingerprint: this.fingerprint,
793
+ // A fresh activation invalidates any cached verdict for the previous
794
+ // binding; keep only the keyset, which is per product, not per machine.
795
+ keyset: this.keysetToken ?? this.binding?.keyset,
796
+ timeHighWater: this.binding?.timeHighWater,
797
+ // The machine's own identity survives an online activation, because it is
798
+ // an identity and not a verdict: an install that reaches the network once
799
+ // must not have to repeat the whole air-gapped ceremony with its vendor
800
+ // the next time it is unplugged. The clock mark survives for the same
801
+ // reason a mark exists at all — it only moves forward.
802
+ machineKey: this.binding?.machineKey,
803
+ machinePublicKey: this.binding?.machinePublicKey,
804
+ offlineSeen: this.binding?.offlineSeen
805
+ // The offline *file* does not: a live activation supersedes it, and
806
+ // keeping it would leave two answers to "what licenses this machine".
807
+ };
808
+ this.advanceClock(claims);
809
+ this.store?.save(this.binding);
810
+ return this.adopt(stateFromClaims(claims, "valid", false));
811
+ } catch (e) {
812
+ const switched = this.offlineSince(generation);
813
+ if (switched) return switched;
814
+ throw e;
749
815
  }
750
- const nonce = newNonce();
751
- const body = await this.post("/v1/client/activate", {
752
- product_token: this.options.productToken,
753
- license_key: licenseKey,
754
- fingerprint: { composite: this.fingerprint, components: this.fp.components },
755
- nonce,
756
- hostname: this.reportedHostname(),
757
- os: `${import_node_os3.default.type()} ${import_node_os3.default.release()}`,
758
- arch: import_node_os3.default.arch(),
759
- app_version: this.options.appVersion,
760
- sdk_version: SDK_ID
761
- });
762
- this.adoptKeyset(body["keyset"]);
763
- const claims = verifyToken(
764
- this.licenseVerifyKeys(),
765
- "activation",
766
- this.options.productToken,
767
- nonce,
768
- body["activation_token"]
769
- );
770
- this.licenseKeyHash = hashLicenseKey(licenseKey);
771
- this.activationSecret = body["activation_secret"];
772
- this.machineId = body["machine_id"] ?? null;
773
- this.binding = {
774
- licenseKeyHash: this.licenseKeyHash,
775
- activationSecret: this.activationSecret,
776
- machineId: this.machineId ?? "",
777
- fingerprint: this.fingerprint,
778
- // A fresh activation invalidates any cached verdict for the previous
779
- // binding; keep only the keyset, which is per product, not per machine.
780
- keyset: typeof body["keyset"] === "string" ? body["keyset"] : this.binding?.keyset,
781
- timeHighWater: this.binding?.timeHighWater,
782
- // The machine's own identity survives an online activation, because it is
783
- // an identity and not a verdict: an install that reaches the network once
784
- // must not have to repeat the whole air-gapped ceremony with its vendor
785
- // the next time it is unplugged. The clock mark survives for the same
786
- // reason a mark exists at all — it only moves forward.
787
- machineKey: this.binding?.machineKey,
788
- machinePublicKey: this.binding?.machinePublicKey,
789
- offlineSeen: this.binding?.offlineSeen
790
- // The offline *file* does not: a live activation supersedes it, and
791
- // keeping it would leave two answers to "what licenses this machine".
792
- };
793
- this.advanceClock(claims);
794
- this.store?.save(this.binding);
795
- return this.adopt(stateFromClaims(claims, "valid", false));
796
816
  }
797
817
  /**
798
818
  * This machine's own keypair, generated once and kept in the state file.
@@ -867,13 +887,13 @@ var TalosClient = class {
867
887
  });
868
888
  }
869
889
  /**
870
- * Adopt an air-gapped licence file issued for this machine.
890
+ * Adopt a machine-bound or portable offline licence file.
871
891
  *
872
892
  * Everything is verified locally, because there is nothing to ask: the
873
893
  * anchor-signed keyset travels inside the file, the verdict is signed by a key
874
- * that keyset lists, and the file names both halves of this machine's identity
875
- * — its fingerprint and the public key it generated. The verdict is then kept,
876
- * so later starts are licensed with no further ceremony.
894
+ * that keyset lists. Machine-bound files name this machine's fingerprint and
895
+ * public key; explicitly portable files need no machine request. The verdict
896
+ * is kept, so later starts are licensed with no further ceremony.
877
897
  *
878
898
  * **Throws** only for a file that is not a licence: unreadable, not one of
879
899
  * ours, addressed to another product, or a broken signature. A file that *is*
@@ -896,8 +916,27 @@ var TalosClient = class {
896
916
  if (doc["kind"] !== LICENSE_FILE_KIND || typeof doc["token"] !== "string") {
897
917
  throw new TalosError("not a Talos offline licence file");
898
918
  }
899
- if (typeof doc["keyset"] === "string") this.adoptKeyset(doc["keyset"]);
900
- return this.adoptOfflineToken(doc["token"], doc["keyset"]);
919
+ const previous = {
920
+ license: this.keysetLicenseKeys,
921
+ release: this.keysetReleaseKeys,
922
+ iat: this.keysetIssuedAt,
923
+ token: this.keysetToken
924
+ };
925
+ const restore = () => {
926
+ this.keysetLicenseKeys = previous.license;
927
+ this.keysetReleaseKeys = previous.release;
928
+ this.keysetIssuedAt = previous.iat;
929
+ this.keysetToken = previous.token;
930
+ };
931
+ try {
932
+ if (typeof doc["keyset"] === "string") this.adoptKeyset(doc["keyset"]);
933
+ const state = this.adoptOfflineToken(doc["token"], this.keysetToken);
934
+ if (!state.isValid) restore();
935
+ return state;
936
+ } catch (e) {
937
+ restore();
938
+ throw e;
939
+ }
901
940
  }
902
941
  /**
903
942
  * Hand this machine's air-gapped seat back.
@@ -919,9 +958,7 @@ var TalosClient = class {
919
958
  */
920
959
  createOfflineDeactivation() {
921
960
  const token = this.binding?.offlineToken;
922
- const key = this.binding?.machineKey;
923
- const pub = this.binding?.machinePublicKey;
924
- if (!token || !key || !pub) {
961
+ if (!token) {
925
962
  throw new TalosError("no offline licence to hand back");
926
963
  }
927
964
  let claims;
@@ -935,6 +972,12 @@ var TalosClient = class {
935
972
  } catch {
936
973
  throw new TalosError("the stored offline licence no longer verifies");
937
974
  }
975
+ if (claims["binding"] === "portable") {
976
+ throw new TalosError("portable offline licences have no bound seat to return");
977
+ }
978
+ const key = this.binding?.machineKey;
979
+ const pub = this.binding?.machinePublicKey;
980
+ if (!key || !pub) throw new TalosError("no offline licence to hand back");
938
981
  this.binding = { ...this.bindingBase(), offlineToken: void 0 };
939
982
  this.current = null;
940
983
  this.store?.save(this.binding);
@@ -959,38 +1002,58 @@ var TalosClient = class {
959
1002
  token
960
1003
  );
961
1004
  const mismatch = () => this.adopt(new LicenseState("machine_mismatch", {}, null, true, null, null));
962
- if (typeof claims["mpk"] !== "string" || claims["mpk"] !== this.binding?.machinePublicKey) {
1005
+ const lic = claims["lic"] ?? {};
1006
+ const features = lic["features"] ?? {};
1007
+ const licExp = num(lic["lic_exp"]);
1008
+ const exp = num(claims["exp"]);
1009
+ const perpetual = claims["perpetual"] === true && claims["exp"] === null && lic["lic_exp"] === null;
1010
+ const portable = claims["binding"] === "portable";
1011
+ if (portable) {
1012
+ const present = (key) => Object.hasOwn(claims, key);
1013
+ const named = (key) => typeof claims[key] === "string" && claims[key].trim().length > 0;
1014
+ if (!perpetual || claims["decision"] !== "valid" || present("fph") || present("mpk") || present("mid") || !named("fid") || !named("sub")) return mismatch();
1015
+ } else if (claims["binding"] !== void 0 && claims["binding"] !== "machine") {
1016
+ return mismatch();
1017
+ }
1018
+ if (!portable && (typeof claims["mpk"] !== "string" || claims["mpk"] !== this.binding?.machinePublicKey)) {
963
1019
  return mismatch();
964
1020
  }
965
- if (claims["fph"] !== this.fingerprint) return mismatch();
1021
+ if (!portable && claims["fph"] !== this.fingerprint) return mismatch();
966
1022
  const now = nowSeconds();
967
1023
  const mark = Math.max(this.binding?.timeHighWater ?? 0, this.binding?.offlineSeen ?? 0);
968
1024
  if (mark > 0 && now + CLOCK_SKEW_SECONDS2 < mark) {
969
1025
  return this.adopt(new LicenseState("clock_tampered", {}, null, true, null, null));
970
1026
  }
971
- const lic = claims["lic"] ?? {};
972
- const features = lic["features"] ?? {};
973
- const licExp = num(lic["lic_exp"]);
974
- const exp = num(claims["exp"]);
975
- this.machineId = typeof claims["mid"] === "string" ? claims["mid"] : this.machineId;
1027
+ if (!perpetual && (exp === null || now > exp)) {
1028
+ return this.adopt(
1029
+ new LicenseState("offline_file_expired", features, licExp, true, null, exp)
1030
+ );
1031
+ }
1032
+ if (licExp !== null && now > licExp) {
1033
+ return this.adopt(new LicenseState("expired", features, licExp, true, null, exp));
1034
+ }
1035
+ this.licensingGeneration += 1;
1036
+ this.licenseKeyHash = null;
1037
+ this.activationSecret = null;
1038
+ this.heartbeatInterval = null;
1039
+ this.leaseGeneration += 1;
1040
+ this.stopRenew();
1041
+ this.lease = null;
1042
+ this.machineId = !portable && typeof claims["mid"] === "string" ? claims["mid"] : null;
976
1043
  const base = this.bindingBase();
977
1044
  this.binding = {
978
1045
  ...base,
979
- machineId: this.machineId ?? base.machineId,
1046
+ licenseKeyHash: "",
1047
+ activationSecret: "",
1048
+ validation: void 0,
1049
+ machineId: this.machineId ?? "",
1050
+ fingerprint: this.fingerprint,
980
1051
  offlineToken: token,
981
1052
  offlineSeen: Math.max(base.offlineSeen ?? 0, now),
982
1053
  ...typeof keyset === "string" ? { keyset } : {}
983
1054
  };
984
1055
  this.advanceClock(claims);
985
1056
  this.store?.save(this.binding);
986
- if (exp === null || now > exp) {
987
- return this.adopt(
988
- new LicenseState("offline_file_expired", features, licExp, true, null, exp)
989
- );
990
- }
991
- if (licExp !== null && now > licExp) {
992
- return this.adopt(new LicenseState("expired", features, licExp, true, null, exp));
993
- }
994
1057
  return this.adopt(new LicenseState("valid", features, licExp, true, null, exp));
995
1058
  }
996
1059
  /**
@@ -1103,6 +1166,7 @@ var TalosClient = class {
1103
1166
  }
1104
1167
  /** Drop the machine binding, in memory and on disk. */
1105
1168
  forgetBinding() {
1169
+ this.licensingGeneration += 1;
1106
1170
  this.licenseKeyHash = null;
1107
1171
  this.activationSecret = null;
1108
1172
  this.machineId = null;
@@ -1136,8 +1200,11 @@ var TalosClient = class {
1136
1200
  return this.adoptOfflineToken(this.binding.offlineToken);
1137
1201
  }
1138
1202
  this.requireActivated();
1203
+ const generation = this.licensingGeneration;
1139
1204
  try {
1140
1205
  await this.ensureKeyset();
1206
+ const switchedBeforeRequest = this.offlineSince(generation);
1207
+ if (switchedBeforeRequest) return switchedBeforeRequest;
1141
1208
  const nonce = newNonce();
1142
1209
  const body = await this.post("/v1/client/validate", {
1143
1210
  product_token: this.options.productToken,
@@ -1151,6 +1218,8 @@ var TalosClient = class {
1151
1218
  app_version: this.options.appVersion,
1152
1219
  sdk_version: SDK_ID
1153
1220
  });
1221
+ const switched = this.offlineSince(generation);
1222
+ if (switched) return switched;
1154
1223
  const token = body["validation_token"];
1155
1224
  const claims = verifyToken(
1156
1225
  this.licenseVerifyKeys(),
@@ -1164,6 +1233,8 @@ var TalosClient = class {
1164
1233
  this.persist({ validation: token, timeHighWater: this.binding?.timeHighWater });
1165
1234
  return this.adopt(stateFromClaims(claims, decision, false));
1166
1235
  } catch (e) {
1236
+ const switched = this.offlineSince(generation);
1237
+ if (switched) return switched;
1167
1238
  if (e instanceof TalosApiError) throw e;
1168
1239
  const offline = this.offlineDecision();
1169
1240
  if (!offline) throw e;
@@ -1236,12 +1307,13 @@ var TalosClient = class {
1236
1307
  }
1237
1308
  async deactivate() {
1238
1309
  this.requireActivated();
1310
+ const generation = this.licensingGeneration;
1239
1311
  await this.post("/v1/client/deactivate", {
1240
1312
  product_token: this.options.productToken,
1241
1313
  fingerprint: { composite: this.fingerprint, components: this.fp.components },
1242
1314
  activation_secret: this.activationSecret
1243
1315
  });
1244
- this.forgetBinding();
1316
+ if (generation === this.licensingGeneration) this.forgetBinding();
1245
1317
  }
1246
1318
  /**
1247
1319
  * Send a heartbeat: refreshes this machine's last-seen state, feeds the
@@ -1265,46 +1337,59 @@ var TalosClient = class {
1265
1337
  }
1266
1338
  async heartbeat() {
1267
1339
  this.requireActivated();
1268
- await this.ensureKeyset();
1269
- const nonce = newNonce();
1270
- const body = await this.post("/v1/client/heartbeat", {
1271
- product_token: this.options.productToken,
1272
- fingerprint: { composite: this.fingerprint, components: this.fp.components },
1273
- activation_secret: this.activationSecret,
1274
- nonce,
1275
- app_version: this.options.appVersion,
1276
- os: `${import_node_os3.default.type()} ${import_node_os3.default.release()}`,
1277
- arch: import_node_os3.default.arch(),
1278
- sdk_version: SDK_ID,
1279
- // Naming the channel is what asks the server for the update hint, and it
1280
- // costs two queries there — so it is asked for only when somebody is
1281
- // listening for the answer.
1282
- update_channel: this.listeners.get("updateAvailable")?.size ? this.updateChannel() : void 0
1283
- });
1284
- const claims = verifyToken(
1285
- this.licenseVerifyKeys(),
1286
- "heartbeat",
1287
- this.options.productToken,
1288
- nonce,
1289
- body["heartbeat_token"]
1290
- );
1291
- const decision = typeof claims["decision"] === "string" ? claims["decision"] : "invalid";
1292
- const interval = num(claims["heartbeat_interval_s"]);
1293
- if (interval !== null && interval > 0) this.heartbeatInterval = interval;
1294
- this.advanceClock(claims);
1295
- this.persist({ timeHighWater: this.binding?.timeHighWater });
1296
- const state = this.adopt(
1297
- new LicenseState(
1298
- decision,
1299
- this.current?.features ?? {},
1300
- this.current?.licenseExpiresAt ?? null,
1301
- false,
1302
- this.current?.revalidateAfter ?? null,
1303
- this.current?.graceUntil ?? null
1304
- )
1305
- );
1306
- await this.announceUpdate(claims);
1307
- return state;
1340
+ const generation = this.licensingGeneration;
1341
+ try {
1342
+ await this.ensureKeyset();
1343
+ const switchedBeforeRequest = this.offlineSince(generation);
1344
+ if (switchedBeforeRequest) return switchedBeforeRequest;
1345
+ const nonce = newNonce();
1346
+ const body = await this.post("/v1/client/heartbeat", {
1347
+ product_token: this.options.productToken,
1348
+ fingerprint: { composite: this.fingerprint, components: this.fp.components },
1349
+ activation_secret: this.activationSecret,
1350
+ nonce,
1351
+ app_version: this.options.appVersion,
1352
+ os: `${import_node_os3.default.type()} ${import_node_os3.default.release()}`,
1353
+ arch: import_node_os3.default.arch(),
1354
+ sdk_version: SDK_ID,
1355
+ // Naming the channel is what asks the server for the update hint, and it
1356
+ // costs two queries there — so it is asked for only when somebody is
1357
+ // listening for the answer.
1358
+ update_channel: this.listeners.get("updateAvailable")?.size ? this.updateChannel() : void 0
1359
+ });
1360
+ const switched = this.offlineSince(generation);
1361
+ if (switched) return switched;
1362
+ const claims = verifyToken(
1363
+ this.licenseVerifyKeys(),
1364
+ "heartbeat",
1365
+ this.options.productToken,
1366
+ nonce,
1367
+ body["heartbeat_token"]
1368
+ );
1369
+ const decision = typeof claims["decision"] === "string" ? claims["decision"] : "invalid";
1370
+ const interval = num(claims["heartbeat_interval_s"]);
1371
+ if (interval !== null && interval > 0) this.heartbeatInterval = interval;
1372
+ this.advanceClock(claims);
1373
+ this.persist({ timeHighWater: this.binding?.timeHighWater });
1374
+ const state = this.adopt(
1375
+ new LicenseState(
1376
+ decision,
1377
+ this.current?.features ?? {},
1378
+ this.current?.licenseExpiresAt ?? null,
1379
+ false,
1380
+ this.current?.revalidateAfter ?? null,
1381
+ this.current?.graceUntil ?? null
1382
+ )
1383
+ );
1384
+ await this.announceUpdate(claims);
1385
+ const switchedAfterUpdate = this.offlineSince(generation);
1386
+ if (switchedAfterUpdate) return switchedAfterUpdate;
1387
+ return state;
1388
+ } catch (e) {
1389
+ const switched = this.offlineSince(generation);
1390
+ if (switched) return switched;
1391
+ throw e;
1392
+ }
1308
1393
  }
1309
1394
  /** The channel this install follows; `stable` unless set. */
1310
1395
  updateChannel() {
@@ -1689,6 +1774,13 @@ var TalosClient = class {
1689
1774
  throw new TalosError("not activated: call activate() first");
1690
1775
  }
1691
1776
  }
1777
+ /** Keep a newly imported offline licence authoritative over older requests. */
1778
+ offlineSince(generation) {
1779
+ if (generation === this.licensingGeneration || this.activationSecret || !this.binding?.offlineToken) {
1780
+ return null;
1781
+ }
1782
+ return this.adoptOfflineToken(this.binding.offlineToken);
1783
+ }
1692
1784
  post(path, body) {
1693
1785
  return this.request("POST", path, body);
1694
1786
  }