@antondziuin/talos-sdk 0.1.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/LICENSE +21 -0
- package/README.md +406 -0
- package/dist/index.cjs +1512 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +985 -0
- package/dist/index.d.ts +985 -0
- package/dist/index.js +1462 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
import { KeyObject } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a response has to offer this SDK.
|
|
5
|
+
*
|
|
6
|
+
* `headers` and `body` are optional because a test double need not implement a
|
|
7
|
+
* `ReadableStream`; `arrayBuffer` is not, because the artifact download has to
|
|
8
|
+
* read bytes and an optional method there just moves the failure to run time —
|
|
9
|
+
* which is exactly what this shape is being widened to prevent.
|
|
10
|
+
*/
|
|
11
|
+
interface FetchResponseLike {
|
|
12
|
+
status: number;
|
|
13
|
+
text(): Promise<string>;
|
|
14
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
15
|
+
headers?: {
|
|
16
|
+
get(name: string): string | null;
|
|
17
|
+
};
|
|
18
|
+
/** A streaming body, when the implementation has one (the global `fetch`
|
|
19
|
+
* does). Used to enforce the size cap without buffering first. */
|
|
20
|
+
body?: {
|
|
21
|
+
getReader(): {
|
|
22
|
+
read(): Promise<{
|
|
23
|
+
done: boolean;
|
|
24
|
+
value?: Uint8Array;
|
|
25
|
+
}>;
|
|
26
|
+
cancel(): Promise<unknown>;
|
|
27
|
+
};
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
/** Minimal fetch signature (Node 18+ global fetch, or an injected one). */
|
|
31
|
+
type FetchLike = (url: string, init: {
|
|
32
|
+
method: string;
|
|
33
|
+
headers: Record<string, string>;
|
|
34
|
+
body?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Aborts the request once {@link TalosOptions.httpTimeoutMs} elapses.
|
|
37
|
+
*
|
|
38
|
+
* Optional so an injected `fetch` that predates this field still satisfies
|
|
39
|
+
* the type — but one that ignores the signal has no timeout, and the
|
|
40
|
+
* licensing call it drops is on somebody's launch path.
|
|
41
|
+
*/
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}) => Promise<FetchResponseLike>;
|
|
44
|
+
interface TalosOptions {
|
|
45
|
+
/** The product's public token (`tpt_…`) from the portal. */
|
|
46
|
+
productToken: string;
|
|
47
|
+
/**
|
|
48
|
+
* The product's **anchor** public key, base64 (from the portal's Integration
|
|
49
|
+
* page). The rotation-safe root: the SDK fetches the anchor-signed keyset and
|
|
50
|
+
* learns the current license/release keys from it, so rotating a signing key
|
|
51
|
+
* never breaks a deployed app. Strongly recommended — pin this. If set,
|
|
52
|
+
* {@link publicKey} is optional (a fallback for when the keyset is unavailable).
|
|
53
|
+
*/
|
|
54
|
+
anchorPublicKey?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The product's Ed25519 license public key, base64. A direct pin used to
|
|
57
|
+
* verify activation/validation tokens when no keyset is loaded. Optional if
|
|
58
|
+
* {@link anchorPublicKey} is set.
|
|
59
|
+
*/
|
|
60
|
+
publicKey?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The product's **release**-signing public key, base64. Direct pin used to
|
|
63
|
+
* verify update manifests when no keyset is loaded. Falls back to
|
|
64
|
+
* {@link publicKey} when omitted.
|
|
65
|
+
*/
|
|
66
|
+
releasePublicKey?: string;
|
|
67
|
+
/** Base URL of the Talos (or self-hosted) server. */
|
|
68
|
+
serverUrl: string;
|
|
69
|
+
/** Optional app version reported in telemetry/heartbeats. */
|
|
70
|
+
appVersion?: string;
|
|
71
|
+
/**
|
|
72
|
+
* The name shown for this seat in the developer's portal. Defaults to
|
|
73
|
+
* `os.hostname()`.
|
|
74
|
+
*
|
|
75
|
+
* Unlike the hostname the SDK folds into the fingerprint, this one is stored
|
|
76
|
+
* and displayed as written — it is what a support agent reads when a customer
|
|
77
|
+
* says "release the one on my old laptop", and without it every seat on a
|
|
78
|
+
* licence is an indistinguishable hash prefix. Pass `""` to send nothing if a
|
|
79
|
+
* machine name would be more identifying than your users expect.
|
|
80
|
+
*/
|
|
81
|
+
hostname?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Optional fetch implementation (defaults to the global `fetch`).
|
|
84
|
+
*
|
|
85
|
+
* Used for **every** network call, artifact downloads included. It did not
|
|
86
|
+
* used to be: `downloadUpdate` reached for `globalThis.fetch` directly, so a
|
|
87
|
+
* proxy, a custom CA bundle, request logging or a test double applied to
|
|
88
|
+
* every licensing call and was silently bypassed by the one call that pulls a
|
|
89
|
+
* multi-megabyte binary and is likeliest to need it.
|
|
90
|
+
*/
|
|
91
|
+
fetch?: FetchLike;
|
|
92
|
+
/**
|
|
93
|
+
* The update channel this install follows (`stable` by default, `beta` and
|
|
94
|
+
* whatever else the product publishes).
|
|
95
|
+
*
|
|
96
|
+
* The default for {@link TalosClient.checkForUpdate}, and the channel a
|
|
97
|
+
* heartbeat names when something is subscribed to `updateAvailable` — a beta
|
|
98
|
+
* install has to ask about its own channel or it would be told about
|
|
99
|
+
* stable's releases.
|
|
100
|
+
*/
|
|
101
|
+
updateChannel?: string;
|
|
102
|
+
/**
|
|
103
|
+
* How long a licensing call may take before it is abandoned, in milliseconds.
|
|
104
|
+
* Default 10000; `0` disables it.
|
|
105
|
+
*
|
|
106
|
+
* Node's `fetch` has **no** default timeout, so before this existed a server
|
|
107
|
+
* that accepted the connection and never answered left `validate()` pending
|
|
108
|
+
* forever — which in a desktop application is a splash screen that never goes
|
|
109
|
+
* away, on the one code path a user cannot skip. Passing a custom `fetch` was
|
|
110
|
+
* the only way to bound it, which is a lot of ceremony for one number.
|
|
111
|
+
*
|
|
112
|
+
* It does **not** apply to `downloadUpdate`. A total-duration cap on a
|
|
113
|
+
* multi-megabyte artifact is not a safeguard, it is a size limit disguised as
|
|
114
|
+
* one: the same 30 seconds that is generous for a licensing call fails every
|
|
115
|
+
* download on a slow connection. The download is bounded by the manifest's
|
|
116
|
+
* signed `size` instead, which is the bound that actually matters there.
|
|
117
|
+
*/
|
|
118
|
+
httpTimeoutMs?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Extra fingerprint components, merged over the ones the SDK collects.
|
|
121
|
+
*
|
|
122
|
+
* Node cannot read hardware identifiers on Windows or macOS without native
|
|
123
|
+
* modules, so on those hosts the server falls back to exact matching and a
|
|
124
|
+
* NIC change forces a re-activation. If your app can supply a stable
|
|
125
|
+
* identifier — an Electron native module, or a value you already hold — pass
|
|
126
|
+
* it here (e.g. `{ machine_guid: "<value>" }`) and the server can keep the
|
|
127
|
+
* seat across hardware changes instead. Values are hashed like any other
|
|
128
|
+
* component, so raw identifiers never leave the machine. See
|
|
129
|
+
* `docs/FINGERPRINT.md` for the recognised ids and their weights.
|
|
130
|
+
*/
|
|
131
|
+
fingerprintComponents?: Record<string, string>;
|
|
132
|
+
/**
|
|
133
|
+
* Where to keep this machine's activation across restarts. Defaults to a
|
|
134
|
+
* per-product file under the OS user-data directory (see
|
|
135
|
+
* {@link defaultStatePath}).
|
|
136
|
+
*/
|
|
137
|
+
statePath?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Persist the machine binding (default `true`).
|
|
140
|
+
*
|
|
141
|
+
* Turn it off only for a process that should not leave a licence on disk — a
|
|
142
|
+
* test, or a short-lived worker. The cost is that every start must
|
|
143
|
+
* `activate` again, which rotates the activation secret and is refused while
|
|
144
|
+
* the tenant is suspended, so a long-lived desktop app wants this on.
|
|
145
|
+
*/
|
|
146
|
+
persistState?: boolean;
|
|
147
|
+
}
|
|
148
|
+
/** One downloadable artifact of an available update (from a verified manifest). */
|
|
149
|
+
interface UpdateArtifact {
|
|
150
|
+
platform: string;
|
|
151
|
+
arch: string;
|
|
152
|
+
kind: string;
|
|
153
|
+
filename: string;
|
|
154
|
+
/** Expected size in bytes (from the signed manifest). */
|
|
155
|
+
size: number;
|
|
156
|
+
/** Expected SHA-256, lowercase hex (from the signed manifest). */
|
|
157
|
+
sha256: string;
|
|
158
|
+
/** Untrusted transport path; the downloaded bytes are verified against `sha256`. */
|
|
159
|
+
downloadPath?: string;
|
|
160
|
+
}
|
|
161
|
+
/** What applying a patch has to produce. */
|
|
162
|
+
interface UpdateDeltaTarget {
|
|
163
|
+
/** The `kind` of the full artifact this rebuilds (`installer`, `archive`, …). */
|
|
164
|
+
kind: string;
|
|
165
|
+
filename: string;
|
|
166
|
+
/** Expected size of the rebuilt file, in bytes (from the signed manifest). */
|
|
167
|
+
size: number;
|
|
168
|
+
/**
|
|
169
|
+
* Expected SHA-256 of the rebuilt file, lowercase hex (from the signed
|
|
170
|
+
* manifest). **Check it after applying** — see {@link verifyAppliedUpdate}.
|
|
171
|
+
* Verifying only the patch leaves whatever comes out of the patcher
|
|
172
|
+
* unverified, which is the whole thing the signed manifest exists to prevent.
|
|
173
|
+
*/
|
|
174
|
+
sha256: string;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A patch that turns one already-installed version into this release, from the
|
|
178
|
+
* signed manifest.
|
|
179
|
+
*
|
|
180
|
+
* Talos hosts and signs patches; it does not produce or apply them. The diff
|
|
181
|
+
* format belongs to whatever applies it (Velopack, Squirrel, MSIX, bsdiff,
|
|
182
|
+
* `zstd --patch-from`), and this SDK deliberately does not run installers.
|
|
183
|
+
*/
|
|
184
|
+
interface UpdateDelta {
|
|
185
|
+
platform: string;
|
|
186
|
+
arch: string;
|
|
187
|
+
/** The installed version this patch applies to, exactly as published. */
|
|
188
|
+
from: string;
|
|
189
|
+
/** `release_seq` of the version it applies to (always below this release's). */
|
|
190
|
+
fromSeq: number;
|
|
191
|
+
filename: string;
|
|
192
|
+
/** Expected size of the patch itself, in bytes. */
|
|
193
|
+
size: number;
|
|
194
|
+
/** Expected SHA-256 of the patch itself, lowercase hex. */
|
|
195
|
+
sha256: string;
|
|
196
|
+
target: UpdateDeltaTarget;
|
|
197
|
+
/** Untrusted transport path; the bytes are verified against `sha256`. */
|
|
198
|
+
downloadPath?: string;
|
|
199
|
+
}
|
|
200
|
+
/** A verified available update (its metadata all comes from the signed manifest). */
|
|
201
|
+
interface UpdateInfo {
|
|
202
|
+
version: string;
|
|
203
|
+
releaseSeq: number;
|
|
204
|
+
mandatory: boolean;
|
|
205
|
+
notesUrl: string | null;
|
|
206
|
+
artifacts: UpdateArtifact[];
|
|
207
|
+
/**
|
|
208
|
+
* Patches this release ships, if any. Never mixed into {@link artifacts}: a
|
|
209
|
+
* patch is not something a host can install, and an SDK that picks "the
|
|
210
|
+
* artifact for this platform" would hand one over as if it were a build.
|
|
211
|
+
*/
|
|
212
|
+
deltas: UpdateDelta[];
|
|
213
|
+
}
|
|
214
|
+
/** Which patch to use, when a release ships more than one. */
|
|
215
|
+
interface DeltaOptions {
|
|
216
|
+
/**
|
|
217
|
+
* The version currently installed. Defaults to `appVersion` from the client
|
|
218
|
+
* options — the same value the update check reports.
|
|
219
|
+
*/
|
|
220
|
+
from?: string;
|
|
221
|
+
/**
|
|
222
|
+
* Which shipped file to rebuild, when a release carries patches for several
|
|
223
|
+
* (an installer and a portable archive, say). Defaults to the only one, and
|
|
224
|
+
* is required when there is more than one.
|
|
225
|
+
*/
|
|
226
|
+
targetKind?: string;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* How far a download has got, passed to the `onProgress` callback of
|
|
230
|
+
* {@link TalosClient.downloadUpdate} and {@link TalosClient.downloadDelta}.
|
|
231
|
+
*
|
|
232
|
+
* `totalBytes` is the size the **signed manifest** names, not the server's
|
|
233
|
+
* `content-length`. The download host is untrusted, and a bar driven by a header
|
|
234
|
+
* it controls can be parked at 100% while bytes are still arriving. It is also
|
|
235
|
+
* the cap the read is bounded by, so `bytesReceived` never exceeds it.
|
|
236
|
+
*/
|
|
237
|
+
interface DownloadProgress {
|
|
238
|
+
/** Bytes verified and written so far. */
|
|
239
|
+
bytesReceived: number;
|
|
240
|
+
/** Total size, in bytes, from the signed manifest. */
|
|
241
|
+
totalBytes: number;
|
|
242
|
+
}
|
|
243
|
+
/** Called as an artifact download advances. */
|
|
244
|
+
type DownloadProgressCallback = (progress: DownloadProgress) => void;
|
|
245
|
+
/**
|
|
246
|
+
* The licence transitions a host can subscribe to with {@link TalosClient.on}.
|
|
247
|
+
*
|
|
248
|
+
* Edges, not levels. `onState` already delivers the verdict on every check-in,
|
|
249
|
+
* and an application that wants "tell me when grace starts" has to diff those
|
|
250
|
+
* itself — getting that right (not re-warning every ten minutes for the whole
|
|
251
|
+
* outage) is exactly the bookkeeping a library should do once instead of every
|
|
252
|
+
* host doing it slightly wrong.
|
|
253
|
+
*/
|
|
254
|
+
interface TalosEventMap {
|
|
255
|
+
/** The licence stopped being usable; the payload says why. */
|
|
256
|
+
licenseInvalid: LicenseState;
|
|
257
|
+
/** Running on a cached verdict because the server could not be reached. */
|
|
258
|
+
gracePeriodStarted: LicenseState;
|
|
259
|
+
/** The offline window closed: the cached verdict may no longer be honoured. */
|
|
260
|
+
gracePeriodExpired: LicenseState;
|
|
261
|
+
/** The system clock moved back behind a signed server time already seen. */
|
|
262
|
+
clockTamperDetected: LicenseState;
|
|
263
|
+
/**
|
|
264
|
+
* A release newer than this install is waiting, noticed by a heartbeat rather
|
|
265
|
+
* than by being asked. The payload is the same verified {@link UpdateInfo}
|
|
266
|
+
* {@link TalosClient.checkForUpdate} returns.
|
|
267
|
+
*/
|
|
268
|
+
updateAvailable: UpdateInfo;
|
|
269
|
+
}
|
|
270
|
+
type TalosEventName = keyof TalosEventMap;
|
|
271
|
+
/** Handler for a licence transition; receives the state that was adopted. */
|
|
272
|
+
type LicenseListener = (state: LicenseState) => void;
|
|
273
|
+
/** A held floating seat. Renew before {@link expiresAt} or it is reclaimed. */
|
|
274
|
+
interface Lease {
|
|
275
|
+
/** Server-side lease id (from the signed token). */
|
|
276
|
+
id: string;
|
|
277
|
+
/** This process's instance id. */
|
|
278
|
+
instanceId: string;
|
|
279
|
+
/** Unix seconds when the seat is reclaimed unless renewed. */
|
|
280
|
+
expiresAt: number;
|
|
281
|
+
/** Renew after this many seconds (server-chosen, ttl/3). */
|
|
282
|
+
renewAfterSeconds: number;
|
|
283
|
+
seatsInUse: number;
|
|
284
|
+
maxSeats: number;
|
|
285
|
+
}
|
|
286
|
+
interface AcquireLeaseOptions {
|
|
287
|
+
/**
|
|
288
|
+
* Identifies this running process. Re-acquiring with the same value renews
|
|
289
|
+
* instead of taking a second seat. Defaults to a random per-client id.
|
|
290
|
+
*/
|
|
291
|
+
instanceId?: string;
|
|
292
|
+
/** Auto-renew in the background (default true). Call {@link TalosClient.releaseLease} to stop. */
|
|
293
|
+
autoRenew?: boolean;
|
|
294
|
+
/** Called if a renewal fails and the seat is lost (e.g. revoked, server down). */
|
|
295
|
+
onLeaseLost?: (reason: string) => void;
|
|
296
|
+
}
|
|
297
|
+
interface CheckInOptions {
|
|
298
|
+
/** Called after every successful check-in, with the fresh verdict. */
|
|
299
|
+
onState?: (state: LicenseState) => void;
|
|
300
|
+
/**
|
|
301
|
+
* Called when a check-in fails outright — the server answered with an error,
|
|
302
|
+
* or the network is down *and* there is nothing left to fall back on.
|
|
303
|
+
*/
|
|
304
|
+
onError?: (error: unknown) => void;
|
|
305
|
+
}
|
|
306
|
+
interface CheckForUpdateOptions {
|
|
307
|
+
/** Currently installed version (informational). */
|
|
308
|
+
currentVersion?: string;
|
|
309
|
+
/**
|
|
310
|
+
* Override the anti-downgrade floor for this call.
|
|
311
|
+
*
|
|
312
|
+
* Optional, and normally omitted: the SDK persists the highest `release_seq`
|
|
313
|
+
* it has seen this install running and uses that. Pass one when the host
|
|
314
|
+
* knows something the SDK cannot — a build installed by a package manager, or
|
|
315
|
+
* a first run after adopting the SDK on an already-updated fleet.
|
|
316
|
+
*/
|
|
317
|
+
currentReleaseSeq?: number;
|
|
318
|
+
/** Update channel; defaults to `stable`. */
|
|
319
|
+
channel?: string;
|
|
320
|
+
}
|
|
321
|
+
/** The current, cryptographically-verified license verdict. */
|
|
322
|
+
declare class LicenseState {
|
|
323
|
+
/**
|
|
324
|
+
* `valid | expired | revoked | suspended | deactivated | machine_revoked |
|
|
325
|
+
* invalid`, plus two verdicts only the client can reach:
|
|
326
|
+
* `grace_expired` (offline past the licence's grace window) and
|
|
327
|
+
* `clock_tampered` (the system clock moved back behind a signed server
|
|
328
|
+
* time we had already seen).
|
|
329
|
+
*/
|
|
330
|
+
readonly decision: string;
|
|
331
|
+
/** Entitlement bag (server-defined). */
|
|
332
|
+
readonly features: Record<string, unknown>;
|
|
333
|
+
/** Business expiry (unix seconds), or null for perpetual. */
|
|
334
|
+
readonly licenseExpiresAt: number | null;
|
|
335
|
+
/**
|
|
336
|
+
* True when this verdict came from the local cache rather than a live
|
|
337
|
+
* server — the licence is being honoured on trust, inside the window the
|
|
338
|
+
* policy allows.
|
|
339
|
+
*/
|
|
340
|
+
readonly isOffline: boolean;
|
|
341
|
+
/**
|
|
342
|
+
* Unix seconds after which the app should validate again. Chosen by the
|
|
343
|
+
* server and carried in a signed claim, so it cannot be lengthened in
|
|
344
|
+
* transit.
|
|
345
|
+
*/
|
|
346
|
+
readonly revalidateAfter: number | null;
|
|
347
|
+
/** Unix seconds until which this verdict may be honoured offline. */
|
|
348
|
+
readonly graceUntil: number | null;
|
|
349
|
+
constructor(
|
|
350
|
+
/**
|
|
351
|
+
* `valid | expired | revoked | suspended | deactivated | machine_revoked |
|
|
352
|
+
* invalid`, plus two verdicts only the client can reach:
|
|
353
|
+
* `grace_expired` (offline past the licence's grace window) and
|
|
354
|
+
* `clock_tampered` (the system clock moved back behind a signed server
|
|
355
|
+
* time we had already seen).
|
|
356
|
+
*/
|
|
357
|
+
decision: string,
|
|
358
|
+
/** Entitlement bag (server-defined). */
|
|
359
|
+
features: Record<string, unknown>,
|
|
360
|
+
/** Business expiry (unix seconds), or null for perpetual. */
|
|
361
|
+
licenseExpiresAt: number | null,
|
|
362
|
+
/**
|
|
363
|
+
* True when this verdict came from the local cache rather than a live
|
|
364
|
+
* server — the licence is being honoured on trust, inside the window the
|
|
365
|
+
* policy allows.
|
|
366
|
+
*/
|
|
367
|
+
isOffline?: boolean,
|
|
368
|
+
/**
|
|
369
|
+
* Unix seconds after which the app should validate again. Chosen by the
|
|
370
|
+
* server and carried in a signed claim, so it cannot be lengthened in
|
|
371
|
+
* transit.
|
|
372
|
+
*/
|
|
373
|
+
revalidateAfter?: number | null,
|
|
374
|
+
/** Unix seconds until which this verdict may be honoured offline. */
|
|
375
|
+
graceUntil?: number | null);
|
|
376
|
+
get isValid(): boolean;
|
|
377
|
+
isFeatureEnabled(name: string): boolean;
|
|
378
|
+
getEntitlement(name: string): unknown;
|
|
379
|
+
}
|
|
380
|
+
declare class TalosClient {
|
|
381
|
+
private readonly options;
|
|
382
|
+
private readonly anchorKey;
|
|
383
|
+
private readonly publicKey;
|
|
384
|
+
private readonly releaseKey;
|
|
385
|
+
private readonly fingerprint;
|
|
386
|
+
private readonly fp;
|
|
387
|
+
private readonly doFetch;
|
|
388
|
+
private keysetLicenseKeys;
|
|
389
|
+
private keysetReleaseKeys;
|
|
390
|
+
/**
|
|
391
|
+
* SHA-256 of the key this machine activated with, or null.
|
|
392
|
+
*
|
|
393
|
+
* The key itself is deliberately not held: after activation the server
|
|
394
|
+
* resolves the licence from the activation secret, so keeping the key would
|
|
395
|
+
* mean a plaintext credential in memory and on disk for the sake of one
|
|
396
|
+
* string comparison in `activate`.
|
|
397
|
+
*/
|
|
398
|
+
private licenseKeyHash;
|
|
399
|
+
private activationSecret;
|
|
400
|
+
private machineId;
|
|
401
|
+
private readonly store;
|
|
402
|
+
/** Everything persisted for this product; written back as one file. */
|
|
403
|
+
private binding;
|
|
404
|
+
/**
|
|
405
|
+
* True while the adopted keyset came off disk. One refresh per process
|
|
406
|
+
* upgrades it, so a rotation is picked up, but a client that cannot reach the
|
|
407
|
+
* server keeps verifying with what it already trusts.
|
|
408
|
+
*/
|
|
409
|
+
private keysetFromCache;
|
|
410
|
+
/**
|
|
411
|
+
* The `iat` of the adopted keyset, in unix seconds, or 0 if unknown.
|
|
412
|
+
*
|
|
413
|
+
* Kept so a keyset can go stale. Without it the refresh happens once per
|
|
414
|
+
* process, which is fine for a desktop app that restarts and useless for the
|
|
415
|
+
* process this SDK is most often *in* — a server or an Electron main that runs
|
|
416
|
+
* for weeks. A key revoked in the portal would never reach it.
|
|
417
|
+
*/
|
|
418
|
+
private keysetIssuedAt;
|
|
419
|
+
/** The policy's heartbeat cadence, from the last heartbeat's signed claims. */
|
|
420
|
+
private heartbeatInterval;
|
|
421
|
+
private checkInTimer;
|
|
422
|
+
/** Bumped on stop so an in-flight tick cannot re-arm a cancelled loop. */
|
|
423
|
+
private checkInGeneration;
|
|
424
|
+
private lease;
|
|
425
|
+
private renewTimer;
|
|
426
|
+
/** Bumped on release so an in-flight renewal cannot resurrect the seat. */
|
|
427
|
+
private leaseGeneration;
|
|
428
|
+
/** Default instance id for floating leases: one per client object. */
|
|
429
|
+
private readonly instanceId;
|
|
430
|
+
current: LicenseState | null;
|
|
431
|
+
private readonly listeners;
|
|
432
|
+
/** The newest release this client has already announced. */
|
|
433
|
+
private announcedReleaseSeq;
|
|
434
|
+
constructor(options: TalosOptions);
|
|
435
|
+
/**
|
|
436
|
+
* A release that was downloaded and is now the version running: promote its
|
|
437
|
+
* `release_seq` to the anti-downgrade mark.
|
|
438
|
+
*
|
|
439
|
+
* This is the whole of "the SDK knows what is installed". The SDK downloads
|
|
440
|
+
* and verifies bytes; the host installs them, its own way, usually across a
|
|
441
|
+
* restart — so installation is not an event the SDK can observe. What it can
|
|
442
|
+
* observe is the result, because `appVersion` is the host saying which
|
|
443
|
+
* version it is. When that equals the version whose bytes were fetched, the
|
|
444
|
+
* download became an install.
|
|
445
|
+
*
|
|
446
|
+
* Only forward, and that is the point: yanking a release or lowering its
|
|
447
|
+
* rollout stops it being *offered*, and must not walk an install that already
|
|
448
|
+
* applied it back onto an older build.
|
|
449
|
+
*/
|
|
450
|
+
private promoteDownloadedRelease;
|
|
451
|
+
/**
|
|
452
|
+
* The `release_seq` no update may go at or below.
|
|
453
|
+
*
|
|
454
|
+
* The host's explicit value wins when it gives one — it knows things the SDK
|
|
455
|
+
* cannot, such as a build installed by a package manager — and the persisted
|
|
456
|
+
* mark is the default that makes anti-downgrade work for the hosts that give
|
|
457
|
+
* nothing, which is every example and most applications.
|
|
458
|
+
*/
|
|
459
|
+
private releaseFloor;
|
|
460
|
+
/**
|
|
461
|
+
* Whether this machine already holds an activation — restored from disk on
|
|
462
|
+
* construction, so this is the startup question: prompt for a licence key, or
|
|
463
|
+
* go straight to {@link validate}?
|
|
464
|
+
*/
|
|
465
|
+
get isActivated(): boolean;
|
|
466
|
+
/** The server's id for this machine, once activated. */
|
|
467
|
+
get currentMachineId(): string | null;
|
|
468
|
+
isFeatureEnabled(name: string): boolean;
|
|
469
|
+
/**
|
|
470
|
+
* Fetch and verify the product's keyset (signed by the pinned anchor) and
|
|
471
|
+
* adopt its current license/release keys. Called automatically during
|
|
472
|
+
* {@link activate}; call it again after a server-side key rotation to keep
|
|
473
|
+
* verifying without any config change. No-op if no `anchorPublicKey` is pinned.
|
|
474
|
+
*/
|
|
475
|
+
refreshKeyset(): Promise<void>;
|
|
476
|
+
/**
|
|
477
|
+
* Make sure a verification key exists before a response has to be verified.
|
|
478
|
+
*
|
|
479
|
+
* A restored binding goes straight to `validate`, skipping `activate` — which
|
|
480
|
+
* is the only place the keyset used to be adopted. An anchor-only client (the
|
|
481
|
+
* recommended pinning, and the one that survives key rotation) therefore had
|
|
482
|
+
* nothing to verify with on the app's *second* launch, and every call failed
|
|
483
|
+
* with "keyset unavailable". Only persistence made that reachable: before it,
|
|
484
|
+
* `activate` always ran first.
|
|
485
|
+
*
|
|
486
|
+
* The keyset is also cached on disk, so the *first* call of a launch with no
|
|
487
|
+
* network can still verify a cached decision. A cached one is refreshed once
|
|
488
|
+
* per process so a rotation is picked up, but a client that cannot reach the
|
|
489
|
+
* server keeps verifying with what it already trusts.
|
|
490
|
+
*/
|
|
491
|
+
private ensureKeyset;
|
|
492
|
+
/**
|
|
493
|
+
* Whether the adopted keyset is old enough to be worth re-fetching.
|
|
494
|
+
*
|
|
495
|
+
* This is what makes revoking a compromised signing key mean anything to an
|
|
496
|
+
* install: a revoked key is omitted from the keyset, so a client only stops
|
|
497
|
+
* trusting it once it fetches a newer one. Refreshing only on the first call
|
|
498
|
+
* of a process bounds that by "next restart", which for a long-running host is
|
|
499
|
+
* not a bound at all.
|
|
500
|
+
*
|
|
501
|
+
* A day, not an hour: the endpoint is public and unauthenticated, and every
|
|
502
|
+
* install on earth hitting it hourly is a self-inflicted load pattern for a
|
|
503
|
+
* document that changes about once a year. The honest description of the
|
|
504
|
+
* guarantee is in `docs/SECURITY.md` — revocation is bounded, not instant.
|
|
505
|
+
*/
|
|
506
|
+
private keysetIsStale;
|
|
507
|
+
/** Verify a keyset token against the anchor and adopt its keys. */
|
|
508
|
+
private adoptKeyset;
|
|
509
|
+
/** Keys to verify license tokens with: the keyset if loaded, else the direct pin. */
|
|
510
|
+
private licenseVerifyKeys;
|
|
511
|
+
/** Keys to verify manifests with: the keyset if loaded, else the direct pin. */
|
|
512
|
+
private releaseVerifyKeys;
|
|
513
|
+
/**
|
|
514
|
+
* Ensure this machine is activated on `licenseKey`, and return the verified
|
|
515
|
+
* state. Safe to call on every start.
|
|
516
|
+
*
|
|
517
|
+
* When a binding for the same key is already held, this **re-validates**
|
|
518
|
+
* instead of re-activating. Same one round trip, three different outcomes:
|
|
519
|
+
* the activation secret is not rotated (so a second copy of the app on the
|
|
520
|
+
* same machine does not knock the first one out), no seat accounting is
|
|
521
|
+
* touched, and — because `activate` is the only client route refused for a
|
|
522
|
+
* suspended tenant — the developer's own paying users keep working while
|
|
523
|
+
* their vendor's account is suspended, which is what "graduated suspension"
|
|
524
|
+
* was supposed to mean.
|
|
525
|
+
*
|
|
526
|
+
* It falls back to a real activation when the stored binding is no longer
|
|
527
|
+
* usable: the seat was released from the portal, or the secret was rotated
|
|
528
|
+
* elsewhere.
|
|
529
|
+
*/
|
|
530
|
+
activate(licenseKey: string): Promise<LicenseState>;
|
|
531
|
+
/**
|
|
532
|
+
* The heartbeat cadence the server's policy asks for, in seconds, taken from
|
|
533
|
+
* the last heartbeat's **signed** claims — so it cannot be lengthened in
|
|
534
|
+
* transit by anyone wanting a fleet to check in less often.
|
|
535
|
+
*
|
|
536
|
+
* `null` until the first heartbeat of this process.
|
|
537
|
+
*/
|
|
538
|
+
get heartbeatIntervalSeconds(): number | null;
|
|
539
|
+
/**
|
|
540
|
+
* Subscribe to a licence transition. Returns a function that unsubscribes.
|
|
541
|
+
*
|
|
542
|
+
* ```ts
|
|
543
|
+
* const off = talos.on("gracePeriodStarted", (s) => showOfflineBanner(s.graceUntil));
|
|
544
|
+
* ```
|
|
545
|
+
*
|
|
546
|
+
* Fires from every path that adopts a verdict — `activate`, `validate`,
|
|
547
|
+
* `heartbeat`, and the offline fallback — not only from the background loop,
|
|
548
|
+
* so a host that runs its own timer gets the same transitions. The listener
|
|
549
|
+
* runs synchronously inside that call; a listener that throws is ignored (see
|
|
550
|
+
* {@link emit}).
|
|
551
|
+
*
|
|
552
|
+
* `updateAvailable` is the one event that is not a licence transition: it
|
|
553
|
+
* carries an {@link UpdateInfo}, and subscribing to it is what makes the
|
|
554
|
+
* heartbeat ask the server whether a release is waiting.
|
|
555
|
+
*/
|
|
556
|
+
on<E extends TalosEventName>(event: E, listener: (payload: TalosEventMap[E]) => void): () => void;
|
|
557
|
+
/**
|
|
558
|
+
* Adopt a verdict as the current one, announcing the transition it makes.
|
|
559
|
+
*
|
|
560
|
+
* The single funnel every path goes through, which is what makes the events
|
|
561
|
+
* trustworthy: a verdict reached anywhere else would be a transition nobody
|
|
562
|
+
* hears about.
|
|
563
|
+
*/
|
|
564
|
+
private adopt;
|
|
565
|
+
private emit;
|
|
566
|
+
/**
|
|
567
|
+
* Run the check-ins the server's policy asks for, in the background.
|
|
568
|
+
*
|
|
569
|
+
* Two knobs have existed on the wire since Phase 1 and reached no client:
|
|
570
|
+
* `heartbeat_interval_s` (how often an install reports in — the telemetry
|
|
571
|
+
* rate, and the upper bound on how long a revocation takes to arrive) and
|
|
572
|
+
* `reval_after` (when the entitlement bag and the offline cache should be
|
|
573
|
+
* refreshed). Both are signed. This loop is what makes them a lever the
|
|
574
|
+
* developer can actually pull: change the policy, and deployed apps follow.
|
|
575
|
+
*
|
|
576
|
+
* One timer serves both. Each tick re-validates if the signed `reval_after`
|
|
577
|
+
* has passed, and otherwise sends a heartbeat — which is also what renews the
|
|
578
|
+
* cached verdict that {@link validate} falls back on, so an app that runs
|
|
579
|
+
* this stays inside its grace window without the host writing any scheduling
|
|
580
|
+
* of its own.
|
|
581
|
+
*
|
|
582
|
+
* Opt-in, and idempotent: calling it again replaces the running loop.
|
|
583
|
+
*/
|
|
584
|
+
startCheckIns(opts?: CheckInOptions): void;
|
|
585
|
+
/** Stop the background check-ins started by {@link startCheckIns}. */
|
|
586
|
+
stopCheckIns(): void;
|
|
587
|
+
/** Drop the machine binding, in memory and on disk. */
|
|
588
|
+
private forgetBinding;
|
|
589
|
+
/** Merge fields into the persisted binding and write it back. */
|
|
590
|
+
private persist;
|
|
591
|
+
/**
|
|
592
|
+
* Advance the server-time high-water mark from a signed claim.
|
|
593
|
+
*
|
|
594
|
+
* Only ever forward: this is the mark that keeps offline grace bounded when
|
|
595
|
+
* the user owns the clock. `iat` is used when a token carries no explicit
|
|
596
|
+
* `server_time` (activation does not) — both are the server's clock, signed.
|
|
597
|
+
*/
|
|
598
|
+
private advanceClock;
|
|
599
|
+
validate(): Promise<LicenseState>;
|
|
600
|
+
/**
|
|
601
|
+
* The verdict from the local cache, or null when there is nothing usable.
|
|
602
|
+
*
|
|
603
|
+
* This is the whole offline story, and every check here is load-bearing:
|
|
604
|
+
*
|
|
605
|
+
* - the token is re-verified under the pinned key, so a cache edited on disk
|
|
606
|
+
* is worthless;
|
|
607
|
+
* - `fph` must be this machine, so copying a state file to a second computer
|
|
608
|
+
* and pulling the network cable does not licence it;
|
|
609
|
+
* - `grace_until` — the policy's `offline_grace_hours`, signed — bounds how
|
|
610
|
+
* long a verdict may outlive its issuance;
|
|
611
|
+
* - the licence's own expiry is re-checked, because a cached "valid" must not
|
|
612
|
+
* outlive the subscription it was issued under;
|
|
613
|
+
* - and the clock is checked against a mark that only moves forward, since
|
|
614
|
+
* every deadline above is evaluated against a clock the user controls.
|
|
615
|
+
*/
|
|
616
|
+
private offlineDecision;
|
|
617
|
+
deactivate(): Promise<void>;
|
|
618
|
+
/**
|
|
619
|
+
* Send a heartbeat: refreshes this machine's last-seen state, feeds the
|
|
620
|
+
* developer's monitoring (version adoption / active installs), and returns the
|
|
621
|
+
* current signed decision — so a revocation reaches the app within one
|
|
622
|
+
* heartbeat interval. Call it periodically while the app runs.
|
|
623
|
+
*
|
|
624
|
+
* The response is a signed `typ=heartbeat` token, so a MITM cannot forge the
|
|
625
|
+
* status or lengthen the interval.
|
|
626
|
+
*/
|
|
627
|
+
/**
|
|
628
|
+
* The machine name to report, or `undefined` to report none.
|
|
629
|
+
*
|
|
630
|
+
* An explicit `""` means "do not send": omitting the field leaves whatever
|
|
631
|
+
* the server already holds, because the server coalesces, so sending an empty
|
|
632
|
+
* string would not clear it either — suppression has to happen here.
|
|
633
|
+
*/
|
|
634
|
+
private reportedHostname;
|
|
635
|
+
heartbeat(): Promise<LicenseState>;
|
|
636
|
+
/** The channel this install follows; `stable` unless set. */
|
|
637
|
+
private updateChannel;
|
|
638
|
+
/**
|
|
639
|
+
* Act on the heartbeat's `upd` hint: confirm it with a real update check and
|
|
640
|
+
* emit `updateAvailable` for a release not announced yet.
|
|
641
|
+
*
|
|
642
|
+
* The hint is read from the **signed** claims, not the response envelope, and
|
|
643
|
+
* it is only ever a prompt to go and look: the check applies this install's
|
|
644
|
+
* anti-downgrade floor and verifies a manifest, neither of which the hint
|
|
645
|
+
* does. A hint that turns out to be wrong costs one request and emits
|
|
646
|
+
* nothing.
|
|
647
|
+
*
|
|
648
|
+
* A failing check is swallowed. The heartbeat it rode in on has already
|
|
649
|
+
* succeeded, and a licensing call must not start failing because the update
|
|
650
|
+
* service is having a bad day.
|
|
651
|
+
*/
|
|
652
|
+
private announceUpdate;
|
|
653
|
+
/**
|
|
654
|
+
* Acquire a floating seat (concurrent-use license). The server arbitrates the
|
|
655
|
+
* cap atomically, so this either returns a seat or throws
|
|
656
|
+
* {@link TalosApiError} with `seat_limit_reached` (HTTP 409).
|
|
657
|
+
*
|
|
658
|
+
* By default the seat is auto-renewed in the background at the server-chosen
|
|
659
|
+
* interval; a lost renewal invokes `onLeaseLost`. Always
|
|
660
|
+
* {@link releaseLease} on clean exit — a crash simply lets the seat expire.
|
|
661
|
+
*/
|
|
662
|
+
acquireLease(opts?: AcquireLeaseOptions): Promise<Lease>;
|
|
663
|
+
/** Best-effort DELETE of one instance's seat (used for stale-renewal cleanup). */
|
|
664
|
+
private releaseInstance;
|
|
665
|
+
/** The currently held floating seat, if any. */
|
|
666
|
+
get currentLease(): Lease | null;
|
|
667
|
+
/** Release the floating seat (clean exit) and stop auto-renewal. */
|
|
668
|
+
releaseLease(): Promise<void>;
|
|
669
|
+
private scheduleRenew;
|
|
670
|
+
private stopRenew;
|
|
671
|
+
/**
|
|
672
|
+
* Ask whether a newer release is available. Returns a verified {@link UpdateInfo}
|
|
673
|
+
* (all metadata comes from the manifest signed by the pinned release key), or
|
|
674
|
+
* `null` when up to date.
|
|
675
|
+
*
|
|
676
|
+
* Enforces anti-downgrade: a manifest whose `release_seq` is at or below the
|
|
677
|
+
* floor is rejected as no update. The floor is the persisted high-water mark
|
|
678
|
+
* — the highest `release_seq` this install has been seen running — unless
|
|
679
|
+
* {@link CheckForUpdateOptions.currentReleaseSeq} overrides it.
|
|
680
|
+
*/
|
|
681
|
+
checkForUpdate(opts?: CheckForUpdateOptions): Promise<UpdateInfo | null>;
|
|
682
|
+
/**
|
|
683
|
+
* The patch that upgrades this machine, or `null` if there is none.
|
|
684
|
+
*
|
|
685
|
+
* Matches on the *exact* published version string, not on version ordering: a
|
|
686
|
+
* patch applies to the bytes it was built against, and "close enough" produces
|
|
687
|
+
* a corrupt file rather than an error. So a machine whose installed version is
|
|
688
|
+
* not one the release ships a patch from simply downloads the full build.
|
|
689
|
+
*/
|
|
690
|
+
selectDelta(info: UpdateInfo, opts?: DeltaOptions): UpdateDelta | null;
|
|
691
|
+
/**
|
|
692
|
+
* Download a patch and verify its bytes against the manifest's SHA-256, then
|
|
693
|
+
* write it to `destPath`. Returns the patch, whose `target.sha256` is what the
|
|
694
|
+
* rebuilt file must hash to.
|
|
695
|
+
*
|
|
696
|
+
* Does NOT apply it. Applying is the host's job — and so is checking the
|
|
697
|
+
* result, which {@link verifyAppliedUpdate} does. A patch verified on the way
|
|
698
|
+
* in and a result nobody checks is a signed chain that stops one step short of
|
|
699
|
+
* the binary that runs.
|
|
700
|
+
*/
|
|
701
|
+
downloadDelta(info: UpdateInfo, destPath: string, opts?: DeltaOptions & {
|
|
702
|
+
delta?: UpdateDelta;
|
|
703
|
+
onProgress?: DownloadProgressCallback;
|
|
704
|
+
}): Promise<UpdateDelta>;
|
|
705
|
+
/**
|
|
706
|
+
* Download one artifact of an update and verify its bytes against the manifest
|
|
707
|
+
* SHA-256, then write it to `destPath`. Returns the chosen artifact. Does NOT
|
|
708
|
+
* apply or execute anything — verification and installation are the host app's
|
|
709
|
+
* responsibility.
|
|
710
|
+
*
|
|
711
|
+
* `onProgress` is called as the bytes arrive, which is the only way a host can
|
|
712
|
+
* show a bar: an installer takes minutes on a domestic connection, and a
|
|
713
|
+
* promise that says nothing until it settles is indistinguishable from one that
|
|
714
|
+
* has hung. Totals come from the signed manifest — see {@link DownloadProgress}.
|
|
715
|
+
*/
|
|
716
|
+
downloadUpdate(info: UpdateInfo, destPath: string, artifact?: UpdateArtifact, onProgress?: DownloadProgressCallback): Promise<UpdateArtifact>;
|
|
717
|
+
/**
|
|
718
|
+
* Note which release's bytes are now on disk, so the next start can tell
|
|
719
|
+
* whether they were installed.
|
|
720
|
+
*
|
|
721
|
+
* Written after the hash check, never before: a download that failed
|
|
722
|
+
* verification is not a release this install is about to be running, and
|
|
723
|
+
* recording it would let a corrupt or substituted file raise the
|
|
724
|
+
* anti-downgrade floor past a good build.
|
|
725
|
+
*/
|
|
726
|
+
private rememberDownloaded;
|
|
727
|
+
/**
|
|
728
|
+
* Fetch one file over the entitled download route and put it at `destPath`
|
|
729
|
+
* only once its bytes match the size and hash the signed manifest names.
|
|
730
|
+
* Shared by full builds and patches, which differ in what they are for and not
|
|
731
|
+
* at all in how their bytes are checked.
|
|
732
|
+
*
|
|
733
|
+
* The bytes go straight to a `.talos-part` file beside the destination, hashed
|
|
734
|
+
* as they arrive, and are renamed into place only once the hash matches. Two
|
|
735
|
+
* things follow. A multi-gigabyte installer is never held in memory — the whole
|
|
736
|
+
* body used to be buffered before it was checked, so the largest artifact a
|
|
737
|
+
* product could ship was bounded by the smallest machine it ran on. And
|
|
738
|
+
* `destPath` never holds unverified bytes, not even briefly: a download that
|
|
739
|
+
* fails leaves whatever was there before rather than a truncated file a host
|
|
740
|
+
* might run.
|
|
741
|
+
*/
|
|
742
|
+
private fetchVerified;
|
|
743
|
+
private requireActivated;
|
|
744
|
+
private post;
|
|
745
|
+
/**
|
|
746
|
+
* The abort signal for one licensing call, or `undefined` when disabled.
|
|
747
|
+
*
|
|
748
|
+
* A fresh one per request rather than one shared: `AbortSignal.timeout`
|
|
749
|
+
* starts counting when it is created, so a signal made once in the
|
|
750
|
+
* constructor would already have fired by the second call.
|
|
751
|
+
*/
|
|
752
|
+
private timeoutSignal;
|
|
753
|
+
private request;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Check that applying a patch produced the file the signed manifest said it
|
|
757
|
+
* would. Throws {@link TalosSignatureError} if not.
|
|
758
|
+
*
|
|
759
|
+
* This is the last link in the chain and the easiest one to leave out. The patch
|
|
760
|
+
* itself was verified on the way in, but a patcher can fail halfway, apply to
|
|
761
|
+
* the wrong base file, or be handed bytes from somewhere else entirely — and
|
|
762
|
+
* every one of those produces a file that nothing has vouched for. Reads the
|
|
763
|
+
* file in chunks, so a multi-gigabyte install is not brought into memory.
|
|
764
|
+
*
|
|
765
|
+
* Needs no licence and no activation: run it from the updater process, after the
|
|
766
|
+
* application it belongs to has exited.
|
|
767
|
+
*/
|
|
768
|
+
declare function verifyAppliedUpdate(filePath: string, delta: UpdateDelta): Promise<void>;
|
|
769
|
+
|
|
770
|
+
/** Base class for all Talos SDK errors. */
|
|
771
|
+
declare class TalosError extends Error {
|
|
772
|
+
constructor(message: string);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* A server response failed cryptographic verification against the pinned
|
|
776
|
+
* product key (bad signature, wrong nonce/audience/type, or expired). Treated
|
|
777
|
+
* like a network failure — never as "valid".
|
|
778
|
+
*/
|
|
779
|
+
declare class TalosSignatureError extends TalosError {
|
|
780
|
+
constructor(message: string);
|
|
781
|
+
}
|
|
782
|
+
/** A non-success HTTP response from the Talos runtime API. */
|
|
783
|
+
declare class TalosApiError extends TalosError {
|
|
784
|
+
readonly statusCode: number;
|
|
785
|
+
readonly errorCode: string | undefined;
|
|
786
|
+
constructor(statusCode: number, errorCode: string | undefined);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** A machine fingerprint: the composite plus the per-component hashes. */
|
|
790
|
+
interface FingerprintData {
|
|
791
|
+
/** What the server matches on. Derived from {@link components}. */
|
|
792
|
+
composite: string;
|
|
793
|
+
/** `{component_id: hash}` — lets the server tolerate hardware drift. */
|
|
794
|
+
components: Record<string, string>;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Collect a machine fingerprint.
|
|
798
|
+
*
|
|
799
|
+
* Each component is HMAC'd with a product-scoped key before it leaves the
|
|
800
|
+
* machine, so the server stores hashes and never the underlying identifiers,
|
|
801
|
+
* and the same hardware yields different hashes for different products.
|
|
802
|
+
*
|
|
803
|
+
* **Coverage is thin here, and honestly so.** Node has no hardware access
|
|
804
|
+
* without native modules, and this SDK ships with zero dependencies and never
|
|
805
|
+
* spawns a subprocess. On Linux it can read `/etc/machine-id`, which is enough
|
|
806
|
+
* for the server to recognise a machine across a hardware change. On Windows and
|
|
807
|
+
* macOS it cannot reach anything comparable, so those hosts stay on exact
|
|
808
|
+
* matching: change a NIC and the user re-activates. If your app can do better —
|
|
809
|
+
* Electron with a native module, or a value you already have — pass
|
|
810
|
+
* `fingerprintComponents` in the options and the server will use it. See
|
|
811
|
+
* `docs/FINGERPRINT.md` for the recognised ids and their weights.
|
|
812
|
+
*
|
|
813
|
+
* None of this resists a determined attacker: these values are produced on the
|
|
814
|
+
* client and can be forged. They exist to recognise honest hardware churn.
|
|
815
|
+
*/
|
|
816
|
+
declare function collectFingerprint(productToken: string, extra?: Record<string, string>): FingerprintData;
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The machine binding the server hands back at activation, held across process
|
|
820
|
+
* restarts.
|
|
821
|
+
*
|
|
822
|
+
* Without this the binding died with the process and every application start
|
|
823
|
+
* had to call `activate` again — which burns a round trip the protocol was
|
|
824
|
+
* designed to avoid, rotates the activation secret out from under any other
|
|
825
|
+
* running copy, and (because `activate` is the one client route refused for a
|
|
826
|
+
* suspended tenant) turns the platform owner's deliberately graduated
|
|
827
|
+
* suspension into a hard kill switch for the developer's own paying users.
|
|
828
|
+
*/
|
|
829
|
+
interface StoredBinding {
|
|
830
|
+
/**
|
|
831
|
+
* SHA-256 of the licence key as it was typed, hex — **not** the key.
|
|
832
|
+
*
|
|
833
|
+
* The key itself used to live here, so that the next call could send it. It
|
|
834
|
+
* no longer has to: the server resolves a licence from the activation secret,
|
|
835
|
+
* which is the credential these calls authenticate with anyway. What was left
|
|
836
|
+
* was a plaintext licence key in a file on every customer's machine, which is
|
|
837
|
+
* a credential-shaped file and exactly what an infostealer collects.
|
|
838
|
+
*
|
|
839
|
+
* A hash is kept because one thing still needs it: `activate()` called twice
|
|
840
|
+
* with the same key must validate rather than spend a second seat, and that
|
|
841
|
+
* comparison is all the stored value was ever used for. The hash cannot be
|
|
842
|
+
* replayed to the server — the server matches an HMAC under its own pepper —
|
|
843
|
+
* so this is a value with exactly one use and no resale.
|
|
844
|
+
*
|
|
845
|
+
* Hashed as typed, without normalising, because the comparison it replaces
|
|
846
|
+
* was a string equality on the same input: normalising here would change when
|
|
847
|
+
* a re-activation is avoided, which is not what this change is about.
|
|
848
|
+
*/
|
|
849
|
+
licenseKeyHash: string;
|
|
850
|
+
activationSecret: string;
|
|
851
|
+
machineId: string;
|
|
852
|
+
/**
|
|
853
|
+
* The fingerprint composite at activation time. Informational: it is *not*
|
|
854
|
+
* required to match on restore, because a machine whose hardware drifted is
|
|
855
|
+
* exactly the case the server's fuzzy matcher exists to resolve. Discarding
|
|
856
|
+
* the binding here would re-activate the customer whose disk was replaced —
|
|
857
|
+
* the support ticket the drift budget was built to prevent.
|
|
858
|
+
*/
|
|
859
|
+
fingerprint: string;
|
|
860
|
+
/**
|
|
861
|
+
* The last anchor-signed keyset token.
|
|
862
|
+
*
|
|
863
|
+
* Cached because an anchor-only client — the pinning that survives key
|
|
864
|
+
* rotation — otherwise has no verification key at all until it can reach the
|
|
865
|
+
* server, which is exactly the situation the offline path exists for. The
|
|
866
|
+
* anchor is compiled into the app, the keyset is signed by it, and the
|
|
867
|
+
* validation token is signed by a key it lists: the whole chain verifies
|
|
868
|
+
* offline against a root the network cannot influence.
|
|
869
|
+
*/
|
|
870
|
+
keyset?: string;
|
|
871
|
+
/** The last verified `typ=validation` token — the offline verdict. */
|
|
872
|
+
validation?: string;
|
|
873
|
+
/**
|
|
874
|
+
* The highest server clock ever seen, in unix seconds, taken from a signed
|
|
875
|
+
* claim.
|
|
876
|
+
*
|
|
877
|
+
* This is what makes offline grace bounded. Grace is evaluated against the
|
|
878
|
+
* local clock, which the user controls; with no mark that only moves forward,
|
|
879
|
+
* winding the clock back would renew a 72-hour window indefinitely. A
|
|
880
|
+
* monotonic timer is not a substitute — it resets on reboot and on a VM
|
|
881
|
+
* snapshot restore, which is the case that matters.
|
|
882
|
+
*/
|
|
883
|
+
timeHighWater?: number;
|
|
884
|
+
/**
|
|
885
|
+
* The highest `release_seq` this install is known to be running, and the
|
|
886
|
+
* floor `checkForUpdate` refuses to go below.
|
|
887
|
+
*
|
|
888
|
+
* Anti-downgrade only works against a mark that outlives the process, and
|
|
889
|
+
* until this existed there was none: the floor came entirely from
|
|
890
|
+
* `currentReleaseSeq`, which the host had to compute and pass, and which no
|
|
891
|
+
* example passed. `docs/SECURITY.md` described a high-water mark that did not
|
|
892
|
+
* exist, so "yanking a release does not recall it from installs that already
|
|
893
|
+
* applied it" rested on the host having implemented the mark itself.
|
|
894
|
+
*/
|
|
895
|
+
releaseSeqHighWater?: number;
|
|
896
|
+
/**
|
|
897
|
+
* A release whose bytes were downloaded and verified but which is not
|
|
898
|
+
* running yet: the version string from its signed manifest, and its
|
|
899
|
+
* `release_seq`.
|
|
900
|
+
*
|
|
901
|
+
* The SDK downloads; the host installs, in its own way, on its own schedule,
|
|
902
|
+
* usually after a restart. So "installed" is not an event the SDK can see —
|
|
903
|
+
* but the *result* is, because the host tells it which version it is on every
|
|
904
|
+
* time a client is constructed. When `appVersion` matches what was downloaded,
|
|
905
|
+
* the download became an install and the mark advances.
|
|
906
|
+
*
|
|
907
|
+
* The alternative was a `markInstalled()` for the host to call, and the whole
|
|
908
|
+
* defect here is a mark that depended on the host doing something nobody
|
|
909
|
+
* documented and no example did.
|
|
910
|
+
*/
|
|
911
|
+
pendingRelease?: {
|
|
912
|
+
version: string;
|
|
913
|
+
releaseSeq: number;
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Where the binding lives, per product, following each OS's convention.
|
|
918
|
+
*
|
|
919
|
+
* The directory is named by a hash of the product token rather than the token
|
|
920
|
+
* itself: it keeps the path filesystem-safe without assuming anything about the
|
|
921
|
+
* token's alphabet, and two products never collide.
|
|
922
|
+
*/
|
|
923
|
+
declare function defaultStatePath(productToken: string): string;
|
|
924
|
+
/**
|
|
925
|
+
* SHA-256 of a licence key, hex.
|
|
926
|
+
*
|
|
927
|
+
* Used only to recognise "the same key as last time" without keeping the key.
|
|
928
|
+
* Not a credential and not accepted by anything: the server matches an HMAC
|
|
929
|
+
* computed under a pepper this process has never seen.
|
|
930
|
+
*/
|
|
931
|
+
declare function hashLicenseKey(key: string): string;
|
|
932
|
+
/** Reads and writes one product's machine binding. */
|
|
933
|
+
declare class StateStore {
|
|
934
|
+
readonly path: string;
|
|
935
|
+
private readonly productToken;
|
|
936
|
+
private readonly key;
|
|
937
|
+
constructor(path: string, productToken: string, keyMaterial: string);
|
|
938
|
+
/**
|
|
939
|
+
* Load the binding, or null if there is none.
|
|
940
|
+
*
|
|
941
|
+
* Never throws. A state file that is missing, truncated, from another
|
|
942
|
+
* product, written by a future version, or simply corrupt must degrade to
|
|
943
|
+
* "not activated yet" — the app then activates again. Throwing here would
|
|
944
|
+
* turn a damaged cache into a launch failure.
|
|
945
|
+
*/
|
|
946
|
+
load(): StoredBinding | null;
|
|
947
|
+
/**
|
|
948
|
+
* Persist the binding. Best-effort by design: a read-only or full disk costs
|
|
949
|
+
* the user a re-activation next launch, and must not fail the activation
|
|
950
|
+
* that just succeeded.
|
|
951
|
+
*/
|
|
952
|
+
save(b: StoredBinding): boolean;
|
|
953
|
+
/** Forget the binding (deactivation). */
|
|
954
|
+
clear(): void;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** Wrap a base64-encoded raw 32-byte Ed25519 public key as a KeyObject. */
|
|
958
|
+
declare function publicKeyFromBase64(b64: string): KeyObject;
|
|
959
|
+
type Keys = KeyObject | KeyObject[];
|
|
960
|
+
/**
|
|
961
|
+
* Verify a Talos-compact token
|
|
962
|
+
* (`talos.<ver>.<typ>.<b64url(payload)>.<b64url(sig)>`, Ed25519 over the exact
|
|
963
|
+
* ASCII prefix). `keys` may be a single pinned key or a keyset (any of which may
|
|
964
|
+
* have signed it). IGNORES any `kid` and enforces `typ`, `aud`, the echoed
|
|
965
|
+
* nonce, and `nbf`/`exp`.
|
|
966
|
+
*/
|
|
967
|
+
declare function verifyToken(keys: Keys, expectedTyp: string, expectedAud: string, expectedNonce: string, token: string): Record<string, unknown>;
|
|
968
|
+
/**
|
|
969
|
+
* Verify a signed update **manifest** (`typ=manifest`) against the pinned
|
|
970
|
+
* release key(s). Unlike per-request tokens a manifest is a long-lived, static
|
|
971
|
+
* artifact, so there is no nonce and no short `exp`; instead it carries a
|
|
972
|
+
* `not_before` gate. Enforces scheme/version/`typ`, the signature, `aud`, and
|
|
973
|
+
* `not_before`.
|
|
974
|
+
*/
|
|
975
|
+
declare function verifyManifest(releaseKeys: Keys, expectedAud: string, token: string): Record<string, unknown>;
|
|
976
|
+
/**
|
|
977
|
+
* Verify a signed **keyset** (`typ=keyset`) against the pinned anchor key. The
|
|
978
|
+
* keyset lists the product's current, rotatable license/release public keys, so
|
|
979
|
+
* an SDK that pins only the anchor learns the signing keys from it — a rotated
|
|
980
|
+
* key never breaks a deployed app. Enforces scheme/version/`typ`, the anchor
|
|
981
|
+
* signature, and `aud`.
|
|
982
|
+
*/
|
|
983
|
+
declare function verifyKeyset(anchorKey: KeyObject, expectedAud: string, token: string): Record<string, unknown>;
|
|
984
|
+
|
|
985
|
+
export { type AcquireLeaseOptions, type CheckForUpdateOptions, type CheckInOptions, type DeltaOptions, type DownloadProgress, type DownloadProgressCallback, type FetchLike, type FetchResponseLike, type FingerprintData, type Lease, type LicenseListener, LicenseState, StateStore, type StoredBinding, TalosApiError, TalosClient, TalosError, type TalosEventMap, type TalosEventName, type TalosOptions, TalosSignatureError, type UpdateArtifact, type UpdateDelta, type UpdateDeltaTarget, type UpdateInfo, collectFingerprint, defaultStatePath, hashLicenseKey, publicKeyFromBase64, verifyAppliedUpdate, verifyKeyset, verifyManifest, verifyToken };
|