@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.
@@ -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 };