@antondziuin/talos-sdk 0.2.1 → 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/README.md +22 -3
- package/dist/index.cjs +212 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +212 -123
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
259
|
-
not forgiven offline: the drift matcher lives on the
|
|
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
|
@@ -288,7 +288,7 @@ var StateStore = class {
|
|
|
288
288
|
};
|
|
289
289
|
|
|
290
290
|
// src/version.ts
|
|
291
|
-
var SDK_VERSION = "0.
|
|
291
|
+
var SDK_VERSION = "0.3.0";
|
|
292
292
|
var SDK_ID = `node/${SDK_VERSION}`;
|
|
293
293
|
|
|
294
294
|
// src/diagnostics.ts
|
|
@@ -516,6 +516,9 @@ var TalosClient = class {
|
|
|
516
516
|
* for weeks. A key revoked in the portal would never reach it.
|
|
517
517
|
*/
|
|
518
518
|
keysetIssuedAt = 0;
|
|
519
|
+
keysetToken = null;
|
|
520
|
+
/** Invalidates online work started before a local licensing-mode change. */
|
|
521
|
+
licensingGeneration = 0;
|
|
519
522
|
/** The policy's heartbeat cadence, from the last heartbeat's signed claims. */
|
|
520
523
|
heartbeatInterval = null;
|
|
521
524
|
checkInTimer = null;
|
|
@@ -638,12 +641,14 @@ var TalosClient = class {
|
|
|
638
641
|
*/
|
|
639
642
|
async refreshKeyset() {
|
|
640
643
|
if (!this.anchorKey) return;
|
|
644
|
+
const generation = this.licensingGeneration;
|
|
641
645
|
const body = await this.post("/v1/client/keyset", {
|
|
642
646
|
product_token: this.options.productToken
|
|
643
647
|
});
|
|
648
|
+
if (generation !== this.licensingGeneration) return;
|
|
644
649
|
this.adoptKeyset(body["keyset"]);
|
|
645
650
|
this.keysetFromCache = false;
|
|
646
|
-
if (
|
|
651
|
+
if (this.keysetToken) this.persist({ keyset: this.keysetToken });
|
|
647
652
|
}
|
|
648
653
|
/**
|
|
649
654
|
* Make sure a verification key exists before a response has to be verified.
|
|
@@ -694,7 +699,8 @@ var TalosClient = class {
|
|
|
694
699
|
if (!this.anchorKey || typeof token !== "string") return;
|
|
695
700
|
const claims = verifyKeyset(this.anchorKey, this.options.productToken, token);
|
|
696
701
|
const iat = claims["iat"];
|
|
697
|
-
|
|
702
|
+
const issuedAt = typeof iat === "number" ? iat : 0;
|
|
703
|
+
if (issuedAt < this.keysetIssuedAt || issuedAt === this.keysetIssuedAt && this.keysetToken !== null && token !== this.keysetToken) return;
|
|
698
704
|
const licenseKeys = [];
|
|
699
705
|
const releaseKeys = [];
|
|
700
706
|
for (const entry of asArray(claims["keys"])) {
|
|
@@ -706,6 +712,8 @@ var TalosClient = class {
|
|
|
706
712
|
}
|
|
707
713
|
this.keysetLicenseKeys = licenseKeys;
|
|
708
714
|
this.keysetReleaseKeys = releaseKeys;
|
|
715
|
+
this.keysetIssuedAt = issuedAt;
|
|
716
|
+
this.keysetToken = token;
|
|
709
717
|
}
|
|
710
718
|
/** Keys to verify license tokens with: the keyset if loaded, else the direct pin. */
|
|
711
719
|
licenseVerifyKeys() {
|
|
@@ -737,62 +745,71 @@ var TalosClient = class {
|
|
|
737
745
|
* elsewhere.
|
|
738
746
|
*/
|
|
739
747
|
async activate(licenseKey) {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
748
|
+
const generation = this.licensingGeneration;
|
|
749
|
+
try {
|
|
750
|
+
if (this.isActivated && this.licenseKeyHash === hashLicenseKey(licenseKey)) {
|
|
751
|
+
try {
|
|
752
|
+
const state = await this.validate();
|
|
753
|
+
if (state.decision !== "deactivated") return state;
|
|
754
|
+
} catch (e) {
|
|
755
|
+
const stale = e instanceof TalosApiError && (e.statusCode === 401 || e.statusCode === 404);
|
|
756
|
+
if (!stale) throw e;
|
|
757
|
+
}
|
|
758
|
+
this.forgetBinding();
|
|
747
759
|
}
|
|
748
|
-
|
|
760
|
+
const nonce = newNonce();
|
|
761
|
+
const body = await this.post("/v1/client/activate", {
|
|
762
|
+
product_token: this.options.productToken,
|
|
763
|
+
license_key: licenseKey,
|
|
764
|
+
fingerprint: { composite: this.fingerprint, components: this.fp.components },
|
|
765
|
+
nonce,
|
|
766
|
+
hostname: this.reportedHostname(),
|
|
767
|
+
os: `${import_node_os3.default.type()} ${import_node_os3.default.release()}`,
|
|
768
|
+
arch: import_node_os3.default.arch(),
|
|
769
|
+
app_version: this.options.appVersion,
|
|
770
|
+
sdk_version: SDK_ID
|
|
771
|
+
});
|
|
772
|
+
const switched = this.offlineSince(generation);
|
|
773
|
+
if (switched) return switched;
|
|
774
|
+
this.adoptKeyset(body["keyset"]);
|
|
775
|
+
const claims = verifyToken(
|
|
776
|
+
this.licenseVerifyKeys(),
|
|
777
|
+
"activation",
|
|
778
|
+
this.options.productToken,
|
|
779
|
+
nonce,
|
|
780
|
+
body["activation_token"]
|
|
781
|
+
);
|
|
782
|
+
this.licenseKeyHash = hashLicenseKey(licenseKey);
|
|
783
|
+
this.activationSecret = body["activation_secret"];
|
|
784
|
+
this.machineId = body["machine_id"] ?? null;
|
|
785
|
+
this.binding = {
|
|
786
|
+
licenseKeyHash: this.licenseKeyHash,
|
|
787
|
+
activationSecret: this.activationSecret,
|
|
788
|
+
machineId: this.machineId ?? "",
|
|
789
|
+
fingerprint: this.fingerprint,
|
|
790
|
+
// A fresh activation invalidates any cached verdict for the previous
|
|
791
|
+
// binding; keep only the keyset, which is per product, not per machine.
|
|
792
|
+
keyset: this.keysetToken ?? this.binding?.keyset,
|
|
793
|
+
timeHighWater: this.binding?.timeHighWater,
|
|
794
|
+
// The machine's own identity survives an online activation, because it is
|
|
795
|
+
// an identity and not a verdict: an install that reaches the network once
|
|
796
|
+
// must not have to repeat the whole air-gapped ceremony with its vendor
|
|
797
|
+
// the next time it is unplugged. The clock mark survives for the same
|
|
798
|
+
// reason a mark exists at all — it only moves forward.
|
|
799
|
+
machineKey: this.binding?.machineKey,
|
|
800
|
+
machinePublicKey: this.binding?.machinePublicKey,
|
|
801
|
+
offlineSeen: this.binding?.offlineSeen
|
|
802
|
+
// The offline *file* does not: a live activation supersedes it, and
|
|
803
|
+
// keeping it would leave two answers to "what licenses this machine".
|
|
804
|
+
};
|
|
805
|
+
this.advanceClock(claims);
|
|
806
|
+
this.store?.save(this.binding);
|
|
807
|
+
return this.adopt(stateFromClaims(claims, "valid", false));
|
|
808
|
+
} catch (e) {
|
|
809
|
+
const switched = this.offlineSince(generation);
|
|
810
|
+
if (switched) return switched;
|
|
811
|
+
throw e;
|
|
749
812
|
}
|
|
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
813
|
}
|
|
797
814
|
/**
|
|
798
815
|
* This machine's own keypair, generated once and kept in the state file.
|
|
@@ -867,13 +884,13 @@ var TalosClient = class {
|
|
|
867
884
|
});
|
|
868
885
|
}
|
|
869
886
|
/**
|
|
870
|
-
* Adopt
|
|
887
|
+
* Adopt a machine-bound or portable offline licence file.
|
|
871
888
|
*
|
|
872
889
|
* Everything is verified locally, because there is nothing to ask: the
|
|
873
890
|
* anchor-signed keyset travels inside the file, the verdict is signed by a key
|
|
874
|
-
* that keyset lists
|
|
875
|
-
*
|
|
876
|
-
* so later starts are licensed with no further ceremony.
|
|
891
|
+
* that keyset lists. Machine-bound files name this machine's fingerprint and
|
|
892
|
+
* public key; explicitly portable files need no machine request. The verdict
|
|
893
|
+
* is kept, so later starts are licensed with no further ceremony.
|
|
877
894
|
*
|
|
878
895
|
* **Throws** only for a file that is not a licence: unreadable, not one of
|
|
879
896
|
* ours, addressed to another product, or a broken signature. A file that *is*
|
|
@@ -896,8 +913,27 @@ var TalosClient = class {
|
|
|
896
913
|
if (doc["kind"] !== LICENSE_FILE_KIND || typeof doc["token"] !== "string") {
|
|
897
914
|
throw new TalosError("not a Talos offline licence file");
|
|
898
915
|
}
|
|
899
|
-
|
|
900
|
-
|
|
916
|
+
const previous = {
|
|
917
|
+
license: this.keysetLicenseKeys,
|
|
918
|
+
release: this.keysetReleaseKeys,
|
|
919
|
+
iat: this.keysetIssuedAt,
|
|
920
|
+
token: this.keysetToken
|
|
921
|
+
};
|
|
922
|
+
const restore = () => {
|
|
923
|
+
this.keysetLicenseKeys = previous.license;
|
|
924
|
+
this.keysetReleaseKeys = previous.release;
|
|
925
|
+
this.keysetIssuedAt = previous.iat;
|
|
926
|
+
this.keysetToken = previous.token;
|
|
927
|
+
};
|
|
928
|
+
try {
|
|
929
|
+
if (typeof doc["keyset"] === "string") this.adoptKeyset(doc["keyset"]);
|
|
930
|
+
const state = this.adoptOfflineToken(doc["token"], this.keysetToken);
|
|
931
|
+
if (!state.isValid) restore();
|
|
932
|
+
return state;
|
|
933
|
+
} catch (e) {
|
|
934
|
+
restore();
|
|
935
|
+
throw e;
|
|
936
|
+
}
|
|
901
937
|
}
|
|
902
938
|
/**
|
|
903
939
|
* Hand this machine's air-gapped seat back.
|
|
@@ -919,9 +955,7 @@ var TalosClient = class {
|
|
|
919
955
|
*/
|
|
920
956
|
createOfflineDeactivation() {
|
|
921
957
|
const token = this.binding?.offlineToken;
|
|
922
|
-
|
|
923
|
-
const pub = this.binding?.machinePublicKey;
|
|
924
|
-
if (!token || !key || !pub) {
|
|
958
|
+
if (!token) {
|
|
925
959
|
throw new TalosError("no offline licence to hand back");
|
|
926
960
|
}
|
|
927
961
|
let claims;
|
|
@@ -935,6 +969,12 @@ var TalosClient = class {
|
|
|
935
969
|
} catch {
|
|
936
970
|
throw new TalosError("the stored offline licence no longer verifies");
|
|
937
971
|
}
|
|
972
|
+
if (claims["binding"] === "portable") {
|
|
973
|
+
throw new TalosError("portable offline licences have no bound seat to return");
|
|
974
|
+
}
|
|
975
|
+
const key = this.binding?.machineKey;
|
|
976
|
+
const pub = this.binding?.machinePublicKey;
|
|
977
|
+
if (!key || !pub) throw new TalosError("no offline licence to hand back");
|
|
938
978
|
this.binding = { ...this.bindingBase(), offlineToken: void 0 };
|
|
939
979
|
this.current = null;
|
|
940
980
|
this.store?.save(this.binding);
|
|
@@ -959,38 +999,58 @@ var TalosClient = class {
|
|
|
959
999
|
token
|
|
960
1000
|
);
|
|
961
1001
|
const mismatch = () => this.adopt(new LicenseState("machine_mismatch", {}, null, true, null, null));
|
|
962
|
-
|
|
1002
|
+
const lic = claims["lic"] ?? {};
|
|
1003
|
+
const features = lic["features"] ?? {};
|
|
1004
|
+
const licExp = num(lic["lic_exp"]);
|
|
1005
|
+
const exp = num(claims["exp"]);
|
|
1006
|
+
const perpetual = claims["perpetual"] === true && claims["exp"] === null && lic["lic_exp"] === null;
|
|
1007
|
+
const portable = claims["binding"] === "portable";
|
|
1008
|
+
if (portable) {
|
|
1009
|
+
const present = (key) => Object.hasOwn(claims, key);
|
|
1010
|
+
const named = (key) => typeof claims[key] === "string" && claims[key].trim().length > 0;
|
|
1011
|
+
if (!perpetual || claims["decision"] !== "valid" || present("fph") || present("mpk") || present("mid") || !named("fid") || !named("sub")) return mismatch();
|
|
1012
|
+
} else if (claims["binding"] !== void 0 && claims["binding"] !== "machine") {
|
|
1013
|
+
return mismatch();
|
|
1014
|
+
}
|
|
1015
|
+
if (!portable && (typeof claims["mpk"] !== "string" || claims["mpk"] !== this.binding?.machinePublicKey)) {
|
|
963
1016
|
return mismatch();
|
|
964
1017
|
}
|
|
965
|
-
if (claims["fph"] !== this.fingerprint) return mismatch();
|
|
1018
|
+
if (!portable && claims["fph"] !== this.fingerprint) return mismatch();
|
|
966
1019
|
const now = nowSeconds();
|
|
967
1020
|
const mark = Math.max(this.binding?.timeHighWater ?? 0, this.binding?.offlineSeen ?? 0);
|
|
968
1021
|
if (mark > 0 && now + CLOCK_SKEW_SECONDS2 < mark) {
|
|
969
1022
|
return this.adopt(new LicenseState("clock_tampered", {}, null, true, null, null));
|
|
970
1023
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1024
|
+
if (!perpetual && (exp === null || now > exp)) {
|
|
1025
|
+
return this.adopt(
|
|
1026
|
+
new LicenseState("offline_file_expired", features, licExp, true, null, exp)
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
if (licExp !== null && now > licExp) {
|
|
1030
|
+
return this.adopt(new LicenseState("expired", features, licExp, true, null, exp));
|
|
1031
|
+
}
|
|
1032
|
+
this.licensingGeneration += 1;
|
|
1033
|
+
this.licenseKeyHash = null;
|
|
1034
|
+
this.activationSecret = null;
|
|
1035
|
+
this.heartbeatInterval = null;
|
|
1036
|
+
this.leaseGeneration += 1;
|
|
1037
|
+
this.stopRenew();
|
|
1038
|
+
this.lease = null;
|
|
1039
|
+
this.machineId = !portable && typeof claims["mid"] === "string" ? claims["mid"] : null;
|
|
976
1040
|
const base = this.bindingBase();
|
|
977
1041
|
this.binding = {
|
|
978
1042
|
...base,
|
|
979
|
-
|
|
1043
|
+
licenseKeyHash: "",
|
|
1044
|
+
activationSecret: "",
|
|
1045
|
+
validation: void 0,
|
|
1046
|
+
machineId: this.machineId ?? "",
|
|
1047
|
+
fingerprint: this.fingerprint,
|
|
980
1048
|
offlineToken: token,
|
|
981
1049
|
offlineSeen: Math.max(base.offlineSeen ?? 0, now),
|
|
982
1050
|
...typeof keyset === "string" ? { keyset } : {}
|
|
983
1051
|
};
|
|
984
1052
|
this.advanceClock(claims);
|
|
985
1053
|
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
1054
|
return this.adopt(new LicenseState("valid", features, licExp, true, null, exp));
|
|
995
1055
|
}
|
|
996
1056
|
/**
|
|
@@ -1103,6 +1163,7 @@ var TalosClient = class {
|
|
|
1103
1163
|
}
|
|
1104
1164
|
/** Drop the machine binding, in memory and on disk. */
|
|
1105
1165
|
forgetBinding() {
|
|
1166
|
+
this.licensingGeneration += 1;
|
|
1106
1167
|
this.licenseKeyHash = null;
|
|
1107
1168
|
this.activationSecret = null;
|
|
1108
1169
|
this.machineId = null;
|
|
@@ -1136,8 +1197,11 @@ var TalosClient = class {
|
|
|
1136
1197
|
return this.adoptOfflineToken(this.binding.offlineToken);
|
|
1137
1198
|
}
|
|
1138
1199
|
this.requireActivated();
|
|
1200
|
+
const generation = this.licensingGeneration;
|
|
1139
1201
|
try {
|
|
1140
1202
|
await this.ensureKeyset();
|
|
1203
|
+
const switchedBeforeRequest = this.offlineSince(generation);
|
|
1204
|
+
if (switchedBeforeRequest) return switchedBeforeRequest;
|
|
1141
1205
|
const nonce = newNonce();
|
|
1142
1206
|
const body = await this.post("/v1/client/validate", {
|
|
1143
1207
|
product_token: this.options.productToken,
|
|
@@ -1151,6 +1215,8 @@ var TalosClient = class {
|
|
|
1151
1215
|
app_version: this.options.appVersion,
|
|
1152
1216
|
sdk_version: SDK_ID
|
|
1153
1217
|
});
|
|
1218
|
+
const switched = this.offlineSince(generation);
|
|
1219
|
+
if (switched) return switched;
|
|
1154
1220
|
const token = body["validation_token"];
|
|
1155
1221
|
const claims = verifyToken(
|
|
1156
1222
|
this.licenseVerifyKeys(),
|
|
@@ -1164,6 +1230,8 @@ var TalosClient = class {
|
|
|
1164
1230
|
this.persist({ validation: token, timeHighWater: this.binding?.timeHighWater });
|
|
1165
1231
|
return this.adopt(stateFromClaims(claims, decision, false));
|
|
1166
1232
|
} catch (e) {
|
|
1233
|
+
const switched = this.offlineSince(generation);
|
|
1234
|
+
if (switched) return switched;
|
|
1167
1235
|
if (e instanceof TalosApiError) throw e;
|
|
1168
1236
|
const offline = this.offlineDecision();
|
|
1169
1237
|
if (!offline) throw e;
|
|
@@ -1236,12 +1304,13 @@ var TalosClient = class {
|
|
|
1236
1304
|
}
|
|
1237
1305
|
async deactivate() {
|
|
1238
1306
|
this.requireActivated();
|
|
1307
|
+
const generation = this.licensingGeneration;
|
|
1239
1308
|
await this.post("/v1/client/deactivate", {
|
|
1240
1309
|
product_token: this.options.productToken,
|
|
1241
1310
|
fingerprint: { composite: this.fingerprint, components: this.fp.components },
|
|
1242
1311
|
activation_secret: this.activationSecret
|
|
1243
1312
|
});
|
|
1244
|
-
this.forgetBinding();
|
|
1313
|
+
if (generation === this.licensingGeneration) this.forgetBinding();
|
|
1245
1314
|
}
|
|
1246
1315
|
/**
|
|
1247
1316
|
* Send a heartbeat: refreshes this machine's last-seen state, feeds the
|
|
@@ -1265,46 +1334,59 @@ var TalosClient = class {
|
|
|
1265
1334
|
}
|
|
1266
1335
|
async heartbeat() {
|
|
1267
1336
|
this.requireActivated();
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1337
|
+
const generation = this.licensingGeneration;
|
|
1338
|
+
try {
|
|
1339
|
+
await this.ensureKeyset();
|
|
1340
|
+
const switchedBeforeRequest = this.offlineSince(generation);
|
|
1341
|
+
if (switchedBeforeRequest) return switchedBeforeRequest;
|
|
1342
|
+
const nonce = newNonce();
|
|
1343
|
+
const body = await this.post("/v1/client/heartbeat", {
|
|
1344
|
+
product_token: this.options.productToken,
|
|
1345
|
+
fingerprint: { composite: this.fingerprint, components: this.fp.components },
|
|
1346
|
+
activation_secret: this.activationSecret,
|
|
1347
|
+
nonce,
|
|
1348
|
+
app_version: this.options.appVersion,
|
|
1349
|
+
os: `${import_node_os3.default.type()} ${import_node_os3.default.release()}`,
|
|
1350
|
+
arch: import_node_os3.default.arch(),
|
|
1351
|
+
sdk_version: SDK_ID,
|
|
1352
|
+
// Naming the channel is what asks the server for the update hint, and it
|
|
1353
|
+
// costs two queries there — so it is asked for only when somebody is
|
|
1354
|
+
// listening for the answer.
|
|
1355
|
+
update_channel: this.listeners.get("updateAvailable")?.size ? this.updateChannel() : void 0
|
|
1356
|
+
});
|
|
1357
|
+
const switched = this.offlineSince(generation);
|
|
1358
|
+
if (switched) return switched;
|
|
1359
|
+
const claims = verifyToken(
|
|
1360
|
+
this.licenseVerifyKeys(),
|
|
1361
|
+
"heartbeat",
|
|
1362
|
+
this.options.productToken,
|
|
1363
|
+
nonce,
|
|
1364
|
+
body["heartbeat_token"]
|
|
1365
|
+
);
|
|
1366
|
+
const decision = typeof claims["decision"] === "string" ? claims["decision"] : "invalid";
|
|
1367
|
+
const interval = num(claims["heartbeat_interval_s"]);
|
|
1368
|
+
if (interval !== null && interval > 0) this.heartbeatInterval = interval;
|
|
1369
|
+
this.advanceClock(claims);
|
|
1370
|
+
this.persist({ timeHighWater: this.binding?.timeHighWater });
|
|
1371
|
+
const state = this.adopt(
|
|
1372
|
+
new LicenseState(
|
|
1373
|
+
decision,
|
|
1374
|
+
this.current?.features ?? {},
|
|
1375
|
+
this.current?.licenseExpiresAt ?? null,
|
|
1376
|
+
false,
|
|
1377
|
+
this.current?.revalidateAfter ?? null,
|
|
1378
|
+
this.current?.graceUntil ?? null
|
|
1379
|
+
)
|
|
1380
|
+
);
|
|
1381
|
+
await this.announceUpdate(claims);
|
|
1382
|
+
const switchedAfterUpdate = this.offlineSince(generation);
|
|
1383
|
+
if (switchedAfterUpdate) return switchedAfterUpdate;
|
|
1384
|
+
return state;
|
|
1385
|
+
} catch (e) {
|
|
1386
|
+
const switched = this.offlineSince(generation);
|
|
1387
|
+
if (switched) return switched;
|
|
1388
|
+
throw e;
|
|
1389
|
+
}
|
|
1308
1390
|
}
|
|
1309
1391
|
/** The channel this install follows; `stable` unless set. */
|
|
1310
1392
|
updateChannel() {
|
|
@@ -1689,6 +1771,13 @@ var TalosClient = class {
|
|
|
1689
1771
|
throw new TalosError("not activated: call activate() first");
|
|
1690
1772
|
}
|
|
1691
1773
|
}
|
|
1774
|
+
/** Keep a newly imported offline licence authoritative over older requests. */
|
|
1775
|
+
offlineSince(generation) {
|
|
1776
|
+
if (generation === this.licensingGeneration || this.activationSecret || !this.binding?.offlineToken) {
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
return this.adoptOfflineToken(this.binding.offlineToken);
|
|
1780
|
+
}
|
|
1692
1781
|
post(path, body) {
|
|
1693
1782
|
return this.request("POST", path, body);
|
|
1694
1783
|
}
|