@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 +22 -3
- package/dist/index.cjs +215 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +215 -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
|
@@ -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.
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
875
|
-
*
|
|
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
|
-
|
|
900
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|