@antondziuin/talos-sdk 0.1.0 → 0.2.1
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 +90 -36
- package/dist/index.cjs +326 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +177 -1
- package/dist/index.d.ts +177 -1
- package/dist/index.js +318 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
3
|
-
import { createReadStream } from "fs";
|
|
3
|
+
import { createReadStream, readFileSync as readFileSync3 } from "fs";
|
|
4
4
|
import { open, rename, rm } from "fs/promises";
|
|
5
5
|
import os3 from "os";
|
|
6
6
|
import "crypto";
|
|
@@ -189,7 +189,13 @@ var StateStore = class {
|
|
|
189
189
|
if (pending && typeof pending["version"] === "string" && typeof pending["releaseSeq"] === "number") {
|
|
190
190
|
b.pendingRelease = { version: pending["version"], releaseSeq: pending["releaseSeq"] };
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
if (typeof o["machineKey"] === "string") b.machineKey = o["machineKey"];
|
|
193
|
+
if (typeof o["machinePublicKey"] === "string") {
|
|
194
|
+
b.machinePublicKey = o["machinePublicKey"];
|
|
195
|
+
}
|
|
196
|
+
if (typeof o["offlineToken"] === "string") b.offlineToken = o["offlineToken"];
|
|
197
|
+
if (typeof o["offlineSeen"] === "number") b.offlineSeen = o["offlineSeen"];
|
|
198
|
+
return b.activationSecret || b.offlineToken || b.machineKey ? b : null;
|
|
193
199
|
} catch {
|
|
194
200
|
return null;
|
|
195
201
|
}
|
|
@@ -230,9 +236,51 @@ var StateStore = class {
|
|
|
230
236
|
};
|
|
231
237
|
|
|
232
238
|
// src/version.ts
|
|
233
|
-
var SDK_VERSION = "0.1
|
|
239
|
+
var SDK_VERSION = "0.2.1";
|
|
234
240
|
var SDK_ID = `node/${SDK_VERSION}`;
|
|
235
241
|
|
|
242
|
+
// src/diagnostics.ts
|
|
243
|
+
var decisions = /* @__PURE__ */ new Set(["valid", "expired", "revoked", "suspended", "deactivated", "machine_revoked", "invalid", "grace_expired", "clock_tampered"]);
|
|
244
|
+
var errors = /* @__PURE__ */ new Set(["product_not_found", "license_not_found", "license_expired", "license_revoked", "license_suspended", "machine_revoked", "activation_limit_reached", "seat_limit_reached", "deactivation_limit_reached", "tenant_suspended", "tenant_closed", "invalid_activation_secret", "rate_limited"]);
|
|
245
|
+
function diagnosticReport(activated, anchorPinned, state, error) {
|
|
246
|
+
const code = error instanceof TalosApiError ? errors.has(error.errorCode ?? "") ? error.errorCode : "api_error" : error instanceof TalosSignatureError ? "signature_verification_failed" : error != null ? "client_error" : null;
|
|
247
|
+
const decision = state == null ? null : decisions.has(state.decision) ? state.decision : "unknown";
|
|
248
|
+
const advice = {
|
|
249
|
+
signature_verification_failed: "Check the pinned anchor, product configuration and system time. Keep signature verification enabled.",
|
|
250
|
+
activation_limit_reached: "Open the license's devices in the portal. Release an unused device or review its activation limit.",
|
|
251
|
+
seat_limit_reached: "Close an unused app session or review the concurrent seat limit.",
|
|
252
|
+
license_expired: "Renew the license in the portal, then validate again.",
|
|
253
|
+
expired: "Renew the license in the portal, then validate again.",
|
|
254
|
+
license_revoked: "Revocation is permanent. Ask the vendor for a replacement license.",
|
|
255
|
+
revoked: "Revocation is permanent. Ask the vendor for a replacement license.",
|
|
256
|
+
license_suspended: "Ask the vendor to review the suspension before activating a new device.",
|
|
257
|
+
suspended: "Ask the vendor to review the suspension.",
|
|
258
|
+
machine_revoked: "Ask the vendor to review this device in the portal.",
|
|
259
|
+
grace_expired: "Reconnect to the configured Talos server and validate. Do not extend the local grace deadline.",
|
|
260
|
+
clock_tampered: "Correct the system clock and validate online.",
|
|
261
|
+
client_error: "Check connectivity, server configuration and local state access. The exception text is intentionally excluded."
|
|
262
|
+
};
|
|
263
|
+
const action = advice[code ?? decision ?? ""] ?? (code ? "Review the licensing call and configuration with the vendor. HTTP errors are not signed license decisions." : state?.isOffline ? "The last decision was offline. Check connectivity and validate online; this snapshot cannot identify the transport failure." : !activated ? "Activate with a license key, then validate the saved activation." : decision === "valid" ? "The last licensing call returned valid. Check the portal for this device's recorded validation." : "Run a licensing check and generate a new report. Ask the vendor to review an invalid decision.");
|
|
264
|
+
const timestamp = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
265
|
+
return JSON.stringify({
|
|
266
|
+
schema_version: 1,
|
|
267
|
+
sdk: SDK_ID,
|
|
268
|
+
generated_at: Math.floor(Date.now() / 1e3),
|
|
269
|
+
activated,
|
|
270
|
+
anchor_pinned: anchorPinned,
|
|
271
|
+
decision,
|
|
272
|
+
is_offline: state?.isOffline ?? null,
|
|
273
|
+
license_expires_at: timestamp(state?.licenseExpiresAt),
|
|
274
|
+
grace_until: timestamp(state?.graceUntil),
|
|
275
|
+
error_code: code,
|
|
276
|
+
http_status: error instanceof TalosApiError && Number.isInteger(error.statusCode) && error.statusCode >= 100 && error.statusCode <= 599 ? error.statusCode : null,
|
|
277
|
+
next_action: action
|
|
278
|
+
}, null, 2);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/offline.ts
|
|
282
|
+
import { createPrivateKey, generateKeyPairSync, sign as cryptoSign } from "crypto";
|
|
283
|
+
|
|
236
284
|
// src/talos-compact.ts
|
|
237
285
|
import { createPublicKey, verify as cryptoVerify } from "crypto";
|
|
238
286
|
var SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
@@ -312,6 +360,36 @@ function verifyKeyset(anchorKey, expectedAud, token) {
|
|
|
312
360
|
return claims;
|
|
313
361
|
}
|
|
314
362
|
|
|
363
|
+
// src/offline.ts
|
|
364
|
+
var REQUEST_DOMAIN = "talos-offline-request";
|
|
365
|
+
var RECEIPT_DOMAIN = "talos-offline-deactivation";
|
|
366
|
+
var LICENSE_FILE_KIND = "talos-offline-license";
|
|
367
|
+
var DOC_VERSION = 1;
|
|
368
|
+
function generateMachineKey() {
|
|
369
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
370
|
+
return {
|
|
371
|
+
privateKey: privateKey.export({ format: "der", type: "pkcs8" }).toString("base64"),
|
|
372
|
+
// The raw key is the last 32 bytes of the SPKI DER; every other Ed25519
|
|
373
|
+
// implementation in this project speaks raw-32, including the server's.
|
|
374
|
+
publicKey: publicKey.export({ format: "der", type: "spki" }).subarray(-32).toString("base64")
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function privateKeyObject(pkcs8Base64) {
|
|
378
|
+
return createPrivateKey({
|
|
379
|
+
key: Buffer.from(pkcs8Base64, "base64"),
|
|
380
|
+
format: "der",
|
|
381
|
+
type: "pkcs8"
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function signedMessage(domain, payloadB64) {
|
|
385
|
+
return `${domain}.${DOC_VERSION}.${payloadB64}`;
|
|
386
|
+
}
|
|
387
|
+
function signMachineDoc(key, domain, payload) {
|
|
388
|
+
const req = b64urlEncode(Buffer.from(JSON.stringify(payload), "utf8"));
|
|
389
|
+
const sig = cryptoSign(null, Buffer.from(signedMessage(domain, req), "ascii"), privateKeyObject(key.privateKey));
|
|
390
|
+
return { v: DOC_VERSION, kind: domain, req, sig: b64urlEncode(sig) };
|
|
391
|
+
}
|
|
392
|
+
|
|
315
393
|
// src/client.ts
|
|
316
394
|
var CLOCK_SKEW_SECONDS2 = 60;
|
|
317
395
|
var DEFAULT_HEARTBEAT_SECONDS = 600;
|
|
@@ -398,6 +476,14 @@ var TalosClient = class {
|
|
|
398
476
|
/** Default instance id for floating leases: one per client object. */
|
|
399
477
|
instanceId = b64urlEncode(randomBytes2(9));
|
|
400
478
|
current = null;
|
|
479
|
+
/**
|
|
480
|
+
* Return a local JSON support report without credentials, identifiers or
|
|
481
|
+
* exception text. Pass a caught error for classification. Makes no requests
|
|
482
|
+
* and does not change licensing state; `decision` is the last known result.
|
|
483
|
+
*/
|
|
484
|
+
createDiagnosticReport(error) {
|
|
485
|
+
return diagnosticReport(this.isActivated, this.options.anchorPublicKey != null && this.options.anchorPublicKey !== "", this.current, error);
|
|
486
|
+
}
|
|
401
487
|
listeners = /* @__PURE__ */ new Map();
|
|
402
488
|
/** The newest release this client has already announced. */
|
|
403
489
|
announcedReleaseSeq = 0;
|
|
@@ -423,9 +509,9 @@ var TalosClient = class {
|
|
|
423
509
|
const stored = this.store?.load() ?? null;
|
|
424
510
|
if (stored) {
|
|
425
511
|
this.binding = stored;
|
|
426
|
-
this.licenseKeyHash = stored.licenseKeyHash;
|
|
427
|
-
this.activationSecret = stored.activationSecret;
|
|
428
|
-
this.machineId = stored.machineId;
|
|
512
|
+
this.licenseKeyHash = stored.licenseKeyHash || null;
|
|
513
|
+
this.activationSecret = stored.activationSecret || null;
|
|
514
|
+
this.machineId = stored.machineId || null;
|
|
429
515
|
if (stored.keyset) {
|
|
430
516
|
try {
|
|
431
517
|
this.adoptKeyset(stored.keyset);
|
|
@@ -473,8 +559,17 @@ var TalosClient = class {
|
|
|
473
559
|
* construction, so this is the startup question: prompt for a licence key, or
|
|
474
560
|
* go straight to {@link validate}?
|
|
475
561
|
*/
|
|
562
|
+
/**
|
|
563
|
+
* Whether this install holds a licence for this machine.
|
|
564
|
+
*
|
|
565
|
+
* True for an online activation (the activation secret is held) **and** for an
|
|
566
|
+
* air-gapped install that has loaded a licence file. Widened rather than
|
|
567
|
+
* split, because what a host does with this is decide whether to ask the user
|
|
568
|
+
* for a licence key — and an air-gapped machine that already holds a valid
|
|
569
|
+
* file must not be asked for one it has no way to redeem.
|
|
570
|
+
*/
|
|
476
571
|
get isActivated() {
|
|
477
|
-
return this.activationSecret !== null;
|
|
572
|
+
return this.activationSecret !== null || this.binding?.offlineToken != null;
|
|
478
573
|
}
|
|
479
574
|
/** The server's id for this machine, once activated. */
|
|
480
575
|
get currentMachineId() {
|
|
@@ -631,12 +726,221 @@ var TalosClient = class {
|
|
|
631
726
|
// A fresh activation invalidates any cached verdict for the previous
|
|
632
727
|
// binding; keep only the keyset, which is per product, not per machine.
|
|
633
728
|
keyset: typeof body["keyset"] === "string" ? body["keyset"] : this.binding?.keyset,
|
|
634
|
-
timeHighWater: this.binding?.timeHighWater
|
|
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".
|
|
635
740
|
};
|
|
636
741
|
this.advanceClock(claims);
|
|
637
742
|
this.store?.save(this.binding);
|
|
638
743
|
return this.adopt(stateFromClaims(claims, "valid", false));
|
|
639
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* This machine's own keypair, generated once and kept in the state file.
|
|
747
|
+
*
|
|
748
|
+
* The air-gapped counterpart of the activation secret: a machine that cannot
|
|
749
|
+
* reach a server is never issued one, so it makes an identity of its own and
|
|
750
|
+
* the vendor binds the file to it.
|
|
751
|
+
*/
|
|
752
|
+
machineKey() {
|
|
753
|
+
const held = this.binding?.machineKey;
|
|
754
|
+
const pub = this.binding?.machinePublicKey;
|
|
755
|
+
if (held && pub) return { privateKey: held, publicKey: pub };
|
|
756
|
+
const key = generateMachineKey();
|
|
757
|
+
this.binding = {
|
|
758
|
+
...this.bindingBase(),
|
|
759
|
+
machineKey: key.privateKey,
|
|
760
|
+
machinePublicKey: key.publicKey
|
|
761
|
+
};
|
|
762
|
+
this.store?.save(this.binding);
|
|
763
|
+
return key;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* The binding as it stands, or an empty one to build on.
|
|
767
|
+
*
|
|
768
|
+
* An air-gapped install reaches its first write with nothing stored — no
|
|
769
|
+
* licence key, no activation secret, no machine id — so there has to be
|
|
770
|
+
* something for the offline fields to be added to.
|
|
771
|
+
*/
|
|
772
|
+
bindingBase() {
|
|
773
|
+
return this.binding ?? {
|
|
774
|
+
licenseKeyHash: "",
|
|
775
|
+
activationSecret: "",
|
|
776
|
+
machineId: "",
|
|
777
|
+
fingerprint: this.fingerprint
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Write the request an air-gapped machine hands to its vendor.
|
|
782
|
+
*
|
|
783
|
+
* Save the returned object as `talos-request.json`, carry it to whoever sold
|
|
784
|
+
* the licence, and they upload it in the portal. What comes back is a
|
|
785
|
+
* `license.talos` for {@link loadOfflineLicense} — and only for *this*
|
|
786
|
+
* machine: the request carries the fingerprint and the public half of a
|
|
787
|
+
* keypair generated here, and the file is bound to both.
|
|
788
|
+
*
|
|
789
|
+
* Calling it twice produces two requests under the same identity. The vendor
|
|
790
|
+
* can issue against either; the second file supersedes the first without
|
|
791
|
+
* spending a second seat.
|
|
792
|
+
*
|
|
793
|
+
* ```ts
|
|
794
|
+
* writeFileSync("talos-request.json", JSON.stringify(talos.createOfflineRequest(), null, 2));
|
|
795
|
+
* ```
|
|
796
|
+
*
|
|
797
|
+
* @param licenseKey Optional. The operator picks the licence in the portal, so
|
|
798
|
+
* it is not needed — but when it is present the server checks it against the
|
|
799
|
+
* licence being issued against, which catches issuing from the wrong tab.
|
|
800
|
+
*/
|
|
801
|
+
createOfflineRequest(licenseKey) {
|
|
802
|
+
const key = this.machineKey();
|
|
803
|
+
return signMachineDoc(key, REQUEST_DOMAIN, {
|
|
804
|
+
product_token: this.options.productToken,
|
|
805
|
+
license_key: licenseKey,
|
|
806
|
+
fingerprint: { composite: this.fingerprint, components: this.fp.components },
|
|
807
|
+
machine_pubkey: key.publicKey,
|
|
808
|
+
nonce: newNonce(),
|
|
809
|
+
hostname: this.reportedHostname(),
|
|
810
|
+
os: `${os3.type()} ${os3.release()}`,
|
|
811
|
+
arch: os3.arch(),
|
|
812
|
+
app_version: this.options.appVersion,
|
|
813
|
+
sdk_version: SDK_ID,
|
|
814
|
+
created_at: nowSeconds()
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Adopt an air-gapped licence file issued for this machine.
|
|
819
|
+
*
|
|
820
|
+
* Everything is verified locally, because there is nothing to ask: the
|
|
821
|
+
* 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.
|
|
825
|
+
*
|
|
826
|
+
* **Throws** only for a file that is not a licence: unreadable, not one of
|
|
827
|
+
* ours, addressed to another product, or a broken signature. A file that *is*
|
|
828
|
+
* one but cannot license this machine right now comes back as a verdict —
|
|
829
|
+
* `machine_mismatch` for another machine's file, `offline_file_expired` for
|
|
830
|
+
* one past its horizon, `expired` for a licence that has ended, and
|
|
831
|
+
* `clock_tampered` for a clock wound back behind what this install has already
|
|
832
|
+
* seen. Those are things the user can be told and can act on, and turning
|
|
833
|
+
* them into exceptions would make the launch path throw for a reason nobody
|
|
834
|
+
* can catch usefully.
|
|
835
|
+
*/
|
|
836
|
+
async loadOfflineLicense(pathOrBuffer) {
|
|
837
|
+
const raw = typeof pathOrBuffer === "string" ? readFileSync3(pathOrBuffer) : Buffer.from(pathOrBuffer);
|
|
838
|
+
let doc;
|
|
839
|
+
try {
|
|
840
|
+
doc = JSON.parse(raw.toString("utf8"));
|
|
841
|
+
} catch {
|
|
842
|
+
throw new TalosError("not a Talos offline licence file");
|
|
843
|
+
}
|
|
844
|
+
if (doc["kind"] !== LICENSE_FILE_KIND || typeof doc["token"] !== "string") {
|
|
845
|
+
throw new TalosError("not a Talos offline licence file");
|
|
846
|
+
}
|
|
847
|
+
if (typeof doc["keyset"] === "string") this.adoptKeyset(doc["keyset"]);
|
|
848
|
+
return this.adoptOfflineToken(doc["token"], doc["keyset"]);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Hand this machine's air-gapped seat back.
|
|
852
|
+
*
|
|
853
|
+
* Save the returned object as `talos-deactivation.json` and send it to the
|
|
854
|
+
* vendor; they upload it in the portal and the seat returns to the pool.
|
|
855
|
+
* Signed by the same key the licence file is bound to, which is what makes it
|
|
856
|
+
* worth anything to a server that has never spoken to this machine.
|
|
857
|
+
*
|
|
858
|
+
* **The licence is dropped here, before the document is returned** — so an
|
|
859
|
+
* application that writes the file, or loses it, or never sends it has still
|
|
860
|
+
* stopped using the seat it is giving up. A receipt proves intent, not
|
|
861
|
+
* destruction: the vendor's copy of the file cannot be recalled, and if this
|
|
862
|
+
* machine is later restored from a snapshot it keeps working until that file
|
|
863
|
+
* expires. That is the accepted cost, and it is why the horizon is short.
|
|
864
|
+
*
|
|
865
|
+
* Throws when this install holds no offline licence — there is nothing to hand
|
|
866
|
+
* back, and a document saying otherwise would be a lie the vendor acts on.
|
|
867
|
+
*/
|
|
868
|
+
createOfflineDeactivation() {
|
|
869
|
+
const token = this.binding?.offlineToken;
|
|
870
|
+
const key = this.binding?.machineKey;
|
|
871
|
+
const pub = this.binding?.machinePublicKey;
|
|
872
|
+
if (!token || !key || !pub) {
|
|
873
|
+
throw new TalosError("no offline licence to hand back");
|
|
874
|
+
}
|
|
875
|
+
let claims;
|
|
876
|
+
try {
|
|
877
|
+
claims = verifyCachedToken(
|
|
878
|
+
this.licenseVerifyKeys(),
|
|
879
|
+
"offline",
|
|
880
|
+
this.options.productToken,
|
|
881
|
+
token
|
|
882
|
+
);
|
|
883
|
+
} catch {
|
|
884
|
+
throw new TalosError("the stored offline licence no longer verifies");
|
|
885
|
+
}
|
|
886
|
+
this.binding = { ...this.bindingBase(), offlineToken: void 0 };
|
|
887
|
+
this.current = null;
|
|
888
|
+
this.store?.save(this.binding);
|
|
889
|
+
return signMachineDoc({ privateKey: key, publicKey: pub }, RECEIPT_DOMAIN, {
|
|
890
|
+
fid: claims["fid"],
|
|
891
|
+
mid: claims["mid"],
|
|
892
|
+
fph: this.fingerprint,
|
|
893
|
+
local_time: nowSeconds()
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Verify the offline file against this machine and this clock, and adopt the
|
|
898
|
+
* verdict. Runs on load and again on every {@link validate}, because the two
|
|
899
|
+
* things that can change afterwards — the clock and the calendar — are
|
|
900
|
+
* exactly what it checks.
|
|
901
|
+
*/
|
|
902
|
+
adoptOfflineToken(token, keyset) {
|
|
903
|
+
const claims = verifyCachedToken(
|
|
904
|
+
this.licenseVerifyKeys(),
|
|
905
|
+
"offline",
|
|
906
|
+
this.options.productToken,
|
|
907
|
+
token
|
|
908
|
+
);
|
|
909
|
+
const mismatch = () => this.adopt(new LicenseState("machine_mismatch", {}, null, true, null, null));
|
|
910
|
+
if (typeof claims["mpk"] !== "string" || claims["mpk"] !== this.binding?.machinePublicKey) {
|
|
911
|
+
return mismatch();
|
|
912
|
+
}
|
|
913
|
+
if (claims["fph"] !== this.fingerprint) return mismatch();
|
|
914
|
+
const now = nowSeconds();
|
|
915
|
+
const mark = Math.max(this.binding?.timeHighWater ?? 0, this.binding?.offlineSeen ?? 0);
|
|
916
|
+
if (mark > 0 && now + CLOCK_SKEW_SECONDS2 < mark) {
|
|
917
|
+
return this.adopt(new LicenseState("clock_tampered", {}, null, true, null, null));
|
|
918
|
+
}
|
|
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;
|
|
924
|
+
const base = this.bindingBase();
|
|
925
|
+
this.binding = {
|
|
926
|
+
...base,
|
|
927
|
+
machineId: this.machineId ?? base.machineId,
|
|
928
|
+
offlineToken: token,
|
|
929
|
+
offlineSeen: Math.max(base.offlineSeen ?? 0, now),
|
|
930
|
+
...typeof keyset === "string" ? { keyset } : {}
|
|
931
|
+
};
|
|
932
|
+
this.advanceClock(claims);
|
|
933
|
+
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
|
+
return this.adopt(new LicenseState("valid", features, licExp, true, null, exp));
|
|
943
|
+
}
|
|
640
944
|
/**
|
|
641
945
|
* The heartbeat cadence the server's policy asks for, in seconds, taken from
|
|
642
946
|
* the last heartbeat's **signed** claims — so it cannot be lengthened in
|
|
@@ -776,6 +1080,9 @@ var TalosClient = class {
|
|
|
776
1080
|
}
|
|
777
1081
|
}
|
|
778
1082
|
async validate() {
|
|
1083
|
+
if (!this.activationSecret && this.binding?.offlineToken) {
|
|
1084
|
+
return this.adoptOfflineToken(this.binding.offlineToken);
|
|
1085
|
+
}
|
|
779
1086
|
this.requireActivated();
|
|
780
1087
|
try {
|
|
781
1088
|
await this.ensureKeyset();
|
|
@@ -1444,7 +1751,10 @@ async function verifyAppliedUpdate(filePath, delta) {
|
|
|
1444
1751
|
}
|
|
1445
1752
|
}
|
|
1446
1753
|
export {
|
|
1754
|
+
LICENSE_FILE_KIND,
|
|
1447
1755
|
LicenseState,
|
|
1756
|
+
RECEIPT_DOMAIN,
|
|
1757
|
+
REQUEST_DOMAIN,
|
|
1448
1758
|
StateStore,
|
|
1449
1759
|
TalosApiError,
|
|
1450
1760
|
TalosClient,
|