@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/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.
@@ -1088,6 +1093,12 @@ interface StoredBinding {
1088
1093
  * a clock its owner controls, and it is the price of offline.
1089
1094
  */
1090
1095
  offlineSeen?: number;
1096
+ /** Public on-chain registry manifest; never a license code or an authorization cache. */
1097
+ registryManifest?: string;
1098
+ /** Public identifier of a device-authenticated activation job that can be resumed without a code. */
1099
+ registryActivationId?: string;
1100
+ /** Public canonical head and per-license revision/revocation observations; never an authorization cache. */
1101
+ registryChainState?: string;
1091
1102
  }
1092
1103
  /**
1093
1104
  * Where the binding lives, per product, following each OS's convention.
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.
@@ -1088,6 +1093,12 @@ interface StoredBinding {
1088
1093
  * a clock its owner controls, and it is the price of offline.
1089
1094
  */
1090
1095
  offlineSeen?: number;
1096
+ /** Public on-chain registry manifest; never a license code or an authorization cache. */
1097
+ registryManifest?: string;
1098
+ /** Public identifier of a device-authenticated activation job that can be resumed without a code. */
1099
+ registryActivationId?: string;
1100
+ /** Public canonical head and per-license revision/revocation observations; never an authorization cache. */
1101
+ registryChainState?: string;
1091
1102
  }
1092
1103
  /**
1093
1104
  * Where the binding lives, per product, following each OS's convention.
package/dist/index.js CHANGED
@@ -195,6 +195,9 @@ var StateStore = class {
195
195
  }
196
196
  if (typeof o["offlineToken"] === "string") b.offlineToken = o["offlineToken"];
197
197
  if (typeof o["offlineSeen"] === "number") b.offlineSeen = o["offlineSeen"];
198
+ if (typeof o["registryManifest"] === "string" && o["registryManifest"].length <= 65536) b.registryManifest = o["registryManifest"];
199
+ if (typeof o["registryActivationId"] === "string" && o["registryActivationId"].length <= 36) b.registryActivationId = o["registryActivationId"];
200
+ if (typeof o["registryChainState"] === "string") b.registryChainState = o["registryChainState"];
198
201
  return b.activationSecret || b.offlineToken || b.machineKey ? b : null;
199
202
  } catch {
200
203
  return null;
@@ -236,7 +239,7 @@ var StateStore = class {
236
239
  };
237
240
 
238
241
  // src/version.ts
239
- var SDK_VERSION = "0.2.1";
242
+ var SDK_VERSION = "0.4.0";
240
243
  var SDK_ID = `node/${SDK_VERSION}`;
241
244
 
242
245
  // src/diagnostics.ts
@@ -464,6 +467,9 @@ var TalosClient = class {
464
467
  * for weeks. A key revoked in the portal would never reach it.
465
468
  */
466
469
  keysetIssuedAt = 0;
470
+ keysetToken = null;
471
+ /** Invalidates online work started before a local licensing-mode change. */
472
+ licensingGeneration = 0;
467
473
  /** The policy's heartbeat cadence, from the last heartbeat's signed claims. */
468
474
  heartbeatInterval = null;
469
475
  checkInTimer = null;
@@ -586,12 +592,14 @@ var TalosClient = class {
586
592
  */
587
593
  async refreshKeyset() {
588
594
  if (!this.anchorKey) return;
595
+ const generation = this.licensingGeneration;
589
596
  const body = await this.post("/v1/client/keyset", {
590
597
  product_token: this.options.productToken
591
598
  });
599
+ if (generation !== this.licensingGeneration) return;
592
600
  this.adoptKeyset(body["keyset"]);
593
601
  this.keysetFromCache = false;
594
- if (typeof body["keyset"] === "string") this.persist({ keyset: body["keyset"] });
602
+ if (this.keysetToken) this.persist({ keyset: this.keysetToken });
595
603
  }
596
604
  /**
597
605
  * Make sure a verification key exists before a response has to be verified.
@@ -642,7 +650,8 @@ var TalosClient = class {
642
650
  if (!this.anchorKey || typeof token !== "string") return;
643
651
  const claims = verifyKeyset(this.anchorKey, this.options.productToken, token);
644
652
  const iat = claims["iat"];
645
- this.keysetIssuedAt = typeof iat === "number" ? iat : 0;
653
+ const issuedAt = typeof iat === "number" ? iat : 0;
654
+ if (issuedAt < this.keysetIssuedAt || issuedAt === this.keysetIssuedAt && this.keysetToken !== null && token !== this.keysetToken) return;
646
655
  const licenseKeys = [];
647
656
  const releaseKeys = [];
648
657
  for (const entry of asArray(claims["keys"])) {
@@ -654,6 +663,8 @@ var TalosClient = class {
654
663
  }
655
664
  this.keysetLicenseKeys = licenseKeys;
656
665
  this.keysetReleaseKeys = releaseKeys;
666
+ this.keysetIssuedAt = issuedAt;
667
+ this.keysetToken = token;
657
668
  }
658
669
  /** Keys to verify license tokens with: the keyset if loaded, else the direct pin. */
659
670
  licenseVerifyKeys() {
@@ -685,62 +696,71 @@ var TalosClient = class {
685
696
  * elsewhere.
686
697
  */
687
698
  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;
699
+ const generation = this.licensingGeneration;
700
+ try {
701
+ if (this.isActivated && this.licenseKeyHash === hashLicenseKey(licenseKey)) {
702
+ try {
703
+ const state = await this.validate();
704
+ if (state.decision !== "deactivated") return state;
705
+ } catch (e) {
706
+ const stale = e instanceof TalosApiError && (e.statusCode === 401 || e.statusCode === 404);
707
+ if (!stale) throw e;
708
+ }
709
+ this.forgetBinding();
695
710
  }
696
- this.forgetBinding();
711
+ const nonce = newNonce();
712
+ const body = await this.post("/v1/client/activate", {
713
+ product_token: this.options.productToken,
714
+ license_key: licenseKey,
715
+ fingerprint: { composite: this.fingerprint, components: this.fp.components },
716
+ nonce,
717
+ hostname: this.reportedHostname(),
718
+ os: `${os3.type()} ${os3.release()}`,
719
+ arch: os3.arch(),
720
+ app_version: this.options.appVersion,
721
+ sdk_version: SDK_ID
722
+ });
723
+ const switched = this.offlineSince(generation);
724
+ if (switched) return switched;
725
+ this.adoptKeyset(body["keyset"]);
726
+ const claims = verifyToken(
727
+ this.licenseVerifyKeys(),
728
+ "activation",
729
+ this.options.productToken,
730
+ nonce,
731
+ body["activation_token"]
732
+ );
733
+ this.licenseKeyHash = hashLicenseKey(licenseKey);
734
+ this.activationSecret = body["activation_secret"];
735
+ this.machineId = body["machine_id"] ?? null;
736
+ this.binding = {
737
+ licenseKeyHash: this.licenseKeyHash,
738
+ activationSecret: this.activationSecret,
739
+ machineId: this.machineId ?? "",
740
+ fingerprint: this.fingerprint,
741
+ // A fresh activation invalidates any cached verdict for the previous
742
+ // binding; keep only the keyset, which is per product, not per machine.
743
+ keyset: this.keysetToken ?? this.binding?.keyset,
744
+ timeHighWater: this.binding?.timeHighWater,
745
+ // The machine's own identity survives an online activation, because it is
746
+ // an identity and not a verdict: an install that reaches the network once
747
+ // must not have to repeat the whole air-gapped ceremony with its vendor
748
+ // the next time it is unplugged. The clock mark survives for the same
749
+ // reason a mark exists at all — it only moves forward.
750
+ machineKey: this.binding?.machineKey,
751
+ machinePublicKey: this.binding?.machinePublicKey,
752
+ offlineSeen: this.binding?.offlineSeen
753
+ // The offline *file* does not: a live activation supersedes it, and
754
+ // keeping it would leave two answers to "what licenses this machine".
755
+ };
756
+ this.advanceClock(claims);
757
+ this.store?.save(this.binding);
758
+ return this.adopt(stateFromClaims(claims, "valid", false));
759
+ } catch (e) {
760
+ const switched = this.offlineSince(generation);
761
+ if (switched) return switched;
762
+ throw e;
697
763
  }
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
764
  }
745
765
  /**
746
766
  * This machine's own keypair, generated once and kept in the state file.
@@ -815,13 +835,13 @@ var TalosClient = class {
815
835
  });
816
836
  }
817
837
  /**
818
- * Adopt an air-gapped licence file issued for this machine.
838
+ * Adopt a machine-bound or portable offline licence file.
819
839
  *
820
840
  * Everything is verified locally, because there is nothing to ask: the
821
841
  * 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.
842
+ * that keyset lists. Machine-bound files name this machine's fingerprint and
843
+ * public key; explicitly portable files need no machine request. The verdict
844
+ * is kept, so later starts are licensed with no further ceremony.
825
845
  *
826
846
  * **Throws** only for a file that is not a licence: unreadable, not one of
827
847
  * ours, addressed to another product, or a broken signature. A file that *is*
@@ -844,8 +864,27 @@ var TalosClient = class {
844
864
  if (doc["kind"] !== LICENSE_FILE_KIND || typeof doc["token"] !== "string") {
845
865
  throw new TalosError("not a Talos offline licence file");
846
866
  }
847
- if (typeof doc["keyset"] === "string") this.adoptKeyset(doc["keyset"]);
848
- return this.adoptOfflineToken(doc["token"], doc["keyset"]);
867
+ const previous = {
868
+ license: this.keysetLicenseKeys,
869
+ release: this.keysetReleaseKeys,
870
+ iat: this.keysetIssuedAt,
871
+ token: this.keysetToken
872
+ };
873
+ const restore = () => {
874
+ this.keysetLicenseKeys = previous.license;
875
+ this.keysetReleaseKeys = previous.release;
876
+ this.keysetIssuedAt = previous.iat;
877
+ this.keysetToken = previous.token;
878
+ };
879
+ try {
880
+ if (typeof doc["keyset"] === "string") this.adoptKeyset(doc["keyset"]);
881
+ const state = this.adoptOfflineToken(doc["token"], this.keysetToken);
882
+ if (!state.isValid) restore();
883
+ return state;
884
+ } catch (e) {
885
+ restore();
886
+ throw e;
887
+ }
849
888
  }
850
889
  /**
851
890
  * Hand this machine's air-gapped seat back.
@@ -867,9 +906,7 @@ var TalosClient = class {
867
906
  */
868
907
  createOfflineDeactivation() {
869
908
  const token = this.binding?.offlineToken;
870
- const key = this.binding?.machineKey;
871
- const pub = this.binding?.machinePublicKey;
872
- if (!token || !key || !pub) {
909
+ if (!token) {
873
910
  throw new TalosError("no offline licence to hand back");
874
911
  }
875
912
  let claims;
@@ -883,6 +920,12 @@ var TalosClient = class {
883
920
  } catch {
884
921
  throw new TalosError("the stored offline licence no longer verifies");
885
922
  }
923
+ if (claims["binding"] === "portable") {
924
+ throw new TalosError("portable offline licences have no bound seat to return");
925
+ }
926
+ const key = this.binding?.machineKey;
927
+ const pub = this.binding?.machinePublicKey;
928
+ if (!key || !pub) throw new TalosError("no offline licence to hand back");
886
929
  this.binding = { ...this.bindingBase(), offlineToken: void 0 };
887
930
  this.current = null;
888
931
  this.store?.save(this.binding);
@@ -907,38 +950,58 @@ var TalosClient = class {
907
950
  token
908
951
  );
909
952
  const mismatch = () => this.adopt(new LicenseState("machine_mismatch", {}, null, true, null, null));
910
- if (typeof claims["mpk"] !== "string" || claims["mpk"] !== this.binding?.machinePublicKey) {
953
+ const lic = claims["lic"] ?? {};
954
+ const features = lic["features"] ?? {};
955
+ const licExp = num(lic["lic_exp"]);
956
+ const exp = num(claims["exp"]);
957
+ const perpetual = claims["perpetual"] === true && claims["exp"] === null && lic["lic_exp"] === null;
958
+ const portable = claims["binding"] === "portable";
959
+ if (portable) {
960
+ const present = (key) => Object.hasOwn(claims, key);
961
+ const named = (key) => typeof claims[key] === "string" && claims[key].trim().length > 0;
962
+ if (!perpetual || claims["decision"] !== "valid" || present("fph") || present("mpk") || present("mid") || !named("fid") || !named("sub")) return mismatch();
963
+ } else if (claims["binding"] !== void 0 && claims["binding"] !== "machine") {
964
+ return mismatch();
965
+ }
966
+ if (!portable && (typeof claims["mpk"] !== "string" || claims["mpk"] !== this.binding?.machinePublicKey)) {
911
967
  return mismatch();
912
968
  }
913
- if (claims["fph"] !== this.fingerprint) return mismatch();
969
+ if (!portable && claims["fph"] !== this.fingerprint) return mismatch();
914
970
  const now = nowSeconds();
915
971
  const mark = Math.max(this.binding?.timeHighWater ?? 0, this.binding?.offlineSeen ?? 0);
916
972
  if (mark > 0 && now + CLOCK_SKEW_SECONDS2 < mark) {
917
973
  return this.adopt(new LicenseState("clock_tampered", {}, null, true, null, null));
918
974
  }
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;
975
+ if (!perpetual && (exp === null || now > exp)) {
976
+ return this.adopt(
977
+ new LicenseState("offline_file_expired", features, licExp, true, null, exp)
978
+ );
979
+ }
980
+ if (licExp !== null && now > licExp) {
981
+ return this.adopt(new LicenseState("expired", features, licExp, true, null, exp));
982
+ }
983
+ this.licensingGeneration += 1;
984
+ this.licenseKeyHash = null;
985
+ this.activationSecret = null;
986
+ this.heartbeatInterval = null;
987
+ this.leaseGeneration += 1;
988
+ this.stopRenew();
989
+ this.lease = null;
990
+ this.machineId = !portable && typeof claims["mid"] === "string" ? claims["mid"] : null;
924
991
  const base = this.bindingBase();
925
992
  this.binding = {
926
993
  ...base,
927
- machineId: this.machineId ?? base.machineId,
994
+ licenseKeyHash: "",
995
+ activationSecret: "",
996
+ validation: void 0,
997
+ machineId: this.machineId ?? "",
998
+ fingerprint: this.fingerprint,
928
999
  offlineToken: token,
929
1000
  offlineSeen: Math.max(base.offlineSeen ?? 0, now),
930
1001
  ...typeof keyset === "string" ? { keyset } : {}
931
1002
  };
932
1003
  this.advanceClock(claims);
933
1004
  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
1005
  return this.adopt(new LicenseState("valid", features, licExp, true, null, exp));
943
1006
  }
944
1007
  /**
@@ -1051,6 +1114,7 @@ var TalosClient = class {
1051
1114
  }
1052
1115
  /** Drop the machine binding, in memory and on disk. */
1053
1116
  forgetBinding() {
1117
+ this.licensingGeneration += 1;
1054
1118
  this.licenseKeyHash = null;
1055
1119
  this.activationSecret = null;
1056
1120
  this.machineId = null;
@@ -1084,8 +1148,11 @@ var TalosClient = class {
1084
1148
  return this.adoptOfflineToken(this.binding.offlineToken);
1085
1149
  }
1086
1150
  this.requireActivated();
1151
+ const generation = this.licensingGeneration;
1087
1152
  try {
1088
1153
  await this.ensureKeyset();
1154
+ const switchedBeforeRequest = this.offlineSince(generation);
1155
+ if (switchedBeforeRequest) return switchedBeforeRequest;
1089
1156
  const nonce = newNonce();
1090
1157
  const body = await this.post("/v1/client/validate", {
1091
1158
  product_token: this.options.productToken,
@@ -1099,6 +1166,8 @@ var TalosClient = class {
1099
1166
  app_version: this.options.appVersion,
1100
1167
  sdk_version: SDK_ID
1101
1168
  });
1169
+ const switched = this.offlineSince(generation);
1170
+ if (switched) return switched;
1102
1171
  const token = body["validation_token"];
1103
1172
  const claims = verifyToken(
1104
1173
  this.licenseVerifyKeys(),
@@ -1112,6 +1181,8 @@ var TalosClient = class {
1112
1181
  this.persist({ validation: token, timeHighWater: this.binding?.timeHighWater });
1113
1182
  return this.adopt(stateFromClaims(claims, decision, false));
1114
1183
  } catch (e) {
1184
+ const switched = this.offlineSince(generation);
1185
+ if (switched) return switched;
1115
1186
  if (e instanceof TalosApiError) throw e;
1116
1187
  const offline = this.offlineDecision();
1117
1188
  if (!offline) throw e;
@@ -1184,12 +1255,13 @@ var TalosClient = class {
1184
1255
  }
1185
1256
  async deactivate() {
1186
1257
  this.requireActivated();
1258
+ const generation = this.licensingGeneration;
1187
1259
  await this.post("/v1/client/deactivate", {
1188
1260
  product_token: this.options.productToken,
1189
1261
  fingerprint: { composite: this.fingerprint, components: this.fp.components },
1190
1262
  activation_secret: this.activationSecret
1191
1263
  });
1192
- this.forgetBinding();
1264
+ if (generation === this.licensingGeneration) this.forgetBinding();
1193
1265
  }
1194
1266
  /**
1195
1267
  * Send a heartbeat: refreshes this machine's last-seen state, feeds the
@@ -1213,46 +1285,59 @@ var TalosClient = class {
1213
1285
  }
1214
1286
  async heartbeat() {
1215
1287
  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;
1288
+ const generation = this.licensingGeneration;
1289
+ try {
1290
+ await this.ensureKeyset();
1291
+ const switchedBeforeRequest = this.offlineSince(generation);
1292
+ if (switchedBeforeRequest) return switchedBeforeRequest;
1293
+ const nonce = newNonce();
1294
+ const body = await this.post("/v1/client/heartbeat", {
1295
+ product_token: this.options.productToken,
1296
+ fingerprint: { composite: this.fingerprint, components: this.fp.components },
1297
+ activation_secret: this.activationSecret,
1298
+ nonce,
1299
+ app_version: this.options.appVersion,
1300
+ os: `${os3.type()} ${os3.release()}`,
1301
+ arch: os3.arch(),
1302
+ sdk_version: SDK_ID,
1303
+ // Naming the channel is what asks the server for the update hint, and it
1304
+ // costs two queries there — so it is asked for only when somebody is
1305
+ // listening for the answer.
1306
+ update_channel: this.listeners.get("updateAvailable")?.size ? this.updateChannel() : void 0
1307
+ });
1308
+ const switched = this.offlineSince(generation);
1309
+ if (switched) return switched;
1310
+ const claims = verifyToken(
1311
+ this.licenseVerifyKeys(),
1312
+ "heartbeat",
1313
+ this.options.productToken,
1314
+ nonce,
1315
+ body["heartbeat_token"]
1316
+ );
1317
+ const decision = typeof claims["decision"] === "string" ? claims["decision"] : "invalid";
1318
+ const interval = num(claims["heartbeat_interval_s"]);
1319
+ if (interval !== null && interval > 0) this.heartbeatInterval = interval;
1320
+ this.advanceClock(claims);
1321
+ this.persist({ timeHighWater: this.binding?.timeHighWater });
1322
+ const state = this.adopt(
1323
+ new LicenseState(
1324
+ decision,
1325
+ this.current?.features ?? {},
1326
+ this.current?.licenseExpiresAt ?? null,
1327
+ false,
1328
+ this.current?.revalidateAfter ?? null,
1329
+ this.current?.graceUntil ?? null
1330
+ )
1331
+ );
1332
+ await this.announceUpdate(claims);
1333
+ const switchedAfterUpdate = this.offlineSince(generation);
1334
+ if (switchedAfterUpdate) return switchedAfterUpdate;
1335
+ return state;
1336
+ } catch (e) {
1337
+ const switched = this.offlineSince(generation);
1338
+ if (switched) return switched;
1339
+ throw e;
1340
+ }
1256
1341
  }
1257
1342
  /** The channel this install follows; `stable` unless set. */
1258
1343
  updateChannel() {
@@ -1637,6 +1722,13 @@ var TalosClient = class {
1637
1722
  throw new TalosError("not activated: call activate() first");
1638
1723
  }
1639
1724
  }
1725
+ /** Keep a newly imported offline licence authoritative over older requests. */
1726
+ offlineSince(generation) {
1727
+ if (generation === this.licensingGeneration || this.activationSecret || !this.binding?.offlineToken) {
1728
+ return null;
1729
+ }
1730
+ return this.adoptOfflineToken(this.binding.offlineToken);
1731
+ }
1640
1732
  post(path, body) {
1641
1733
  return this.request("POST", path, body);
1642
1734
  }