{"_id":"@antondziuin/talos-sdk","_rev":"2-309bf8e9164032f3e1f65ae8ceaafc07","name":"@antondziuin/talos-sdk","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@antondziuin/talos-sdk","version":"0.1.0","license":"MIT","_id":"@antondziuin/talos-sdk@0.1.0","maintainers":[{"name":"antondziuin","email":"anton.wvk@gmail.com"}],"homepage":"https://github.com/antondziuin/talos-sdk#readme","bugs":{"url":"https://github.com/antondziuin/talos-sdk/issues"},"dist":{"shasum":"3179a004e7a8f57c2f65aff920b1f7bd98873c71","tarball":"https://registry.npmjs.org/@antondziuin/talos-sdk/-/talos-sdk-0.1.0.tgz","fileCount":9,"integrity":"sha512-JDN76vHlsbO3hcM94HRvKAk5gCzfGWMZxUMQkxbKIyCF+6VJenZLf1XkFF7mpjOZIDDCqmMoWSxbKx6spCBVGA==","signatures":[{"sig":"MEUCIQD5jNPVzUbDQwYeN7ZWgClB1SOeFG91aFqsfNhFhfPpgAIgHY+xqZaeXK0IddCX4X10nwtTc8aoCfi85lQylkFSynA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@antondziuin%2ftalos-sdk@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":500407},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"20e87b258b6103d755c7bef551a5f6574367f6fb","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"antondziuin","email":"anton.wvk@gmail.com"},"repository":{"url":"git+https://github.com/antondziuin/talos-sdk.git","type":"git"},"_npmVersion":"10.9.8","description":"Client SDK for the Talos licensing & software-protection platform.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.6.3","@types/node":"^22.9.0"},"_npmOperationalInternal":{"tmp":"tmp/talos-sdk_0.1.0_1788845602413_0.8690939749374875","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@antondziuin/talos-sdk@0.2.0","bugs":{"url":"https://github.com/antondziuin/talos-sdk/issues"},"dist":{"shasum":"1a6b4cfa57b2f27048a680427f4fcd82fb2c975a","tarball":"https://registry.npmjs.org/@antondziuin/talos-sdk/-/talos-sdk-0.2.0.tgz","fileCount":9,"integrity":"sha512-bnmaJx7sJ4LCQOOoRNJTjo3KgMrv/PJd5HKgt8EXDg4J3x+I152BaGKNmUr7elUpAWkNQlD5bMwuoW87mkFM0w==","signatures":[{"sig":"MEUCIQCQIBRpBgr3zlX2RtmL7Hoeq/Xb30u/H8SlKTlVvmPuFQIgbSs/1J5v4657qdREL6FpB2Av3tyGAWQYKUDSBKOZ93E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBdAb60wFMaQ5DEdNWbxmb8RBhr5dQFLPS4TixVq3KUWAiBQcOUgmJz7fMCpLxmk7Z1ohNqhyyRiAMkcrSfLjwXtsA=="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@antondziuin%2ftalos-sdk@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":614560},"main":"./dist/index.cjs","name":"@antondziuin/talos-sdk","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"9b35b74ba0f8597af4e83c3c79131f8eb0dc20c5","license":"MIT","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"version":"0.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:bea416dd-819f-46a6-9829-fda6f7163910"}},"homepage":"https://github.com/antondziuin/talos-sdk#readme","repository":{"url":"git+https://github.com/antondziuin/talos-sdk.git","type":"git"},"_npmVersion":"11.19.0","description":"Talos SDK for Node.js and Electron: license activation and validation, offline licensing, feature entitlements, floating seats, and signed software updates.","directories":{},"maintainers":[{"name":"antondziuin","email":"anton.wvk@gmail.com"}],"sideEffects":false,"_nodeVersion":"24.20.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.6.3","@types/node":"^22.9.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/talos-sdk_0.2.0_1789248516436_0.8511582866542264"}}},"time":{"created":"2026-09-08T05:33:22.246Z","modified":"2026-09-12T21:28:36.818Z","0.1.0":"2026-09-08T05:33:22.590Z","0.2.0":"2026-09-12T21:28:36.518Z"},"bugs":{"url":"https://github.com/antondziuin/talos-sdk/issues"},"license":"MIT","homepage":"https://github.com/antondziuin/talos-sdk#readme","repository":{"url":"git+https://github.com/antondziuin/talos-sdk.git","type":"git"},"description":"Talos SDK for Node.js and Electron: license activation and validation, offline licensing, feature entitlements, floating seats, and signed software updates.","maintainers":[{"name":"antondziuin","email":"anton.wvk@gmail.com"}],"readme":"# @antondziuin/talos-sdk (Node.js)\n\nTalos SDK for Node.js and Electron applications. Activate and validate licenses,\nread feature entitlements, manage floating seats, and download signed software\nupdates. Supports offline grace periods and license files for air-gapped machines.\n\nRequires Node.js 18 or later. Includes TypeScript declarations, ESM and CommonJS\nbuilds, and **zero runtime dependencies**. Uses `node:crypto` for Ed25519\nverification and the built-in `fetch` for HTTP requests.\n\n```sh\nnpm install @antondziuin/talos-sdk\n```\n\nMIT-licensed.\n\n## Quickstart\n\n```ts\nimport { TalosClient } from \"@antondziuin/talos-sdk\";\n\nconst talos = new TalosClient({\n  productToken: \"tpt_…\",               // from the portal's Integration page\n  anchorPublicKey: \"base64-anchor-key\", // pin THIS — the rotation-safe root\n  serverUrl: \"https://api.talos.dev\",\n  appVersion: \"1.4.2\",\n});\n\n// The activation is stored on this machine, so the second launch onwards has\n// nothing to ask the user for.\nconst state = talos.isActivated\n  ? await talos.validate()              // machine-bound signed decision\n  : await talos.activate(userEnteredLicenseKey);\nif (!state.isValid) promptForKey();\n\nif (talos.isFeatureEnabled(\"pdf-export\")) enablePdfMenu();\nconst seats = Number(talos.current?.getEntitlement(\"seats\") ?? 1);\n```\n\n## Local diagnostic reports\n\nAfter a licensing call, `talos.createDiagnosticReport()` returns JSON describing\nthe last decision, offline grace deadline, SDK version and whether an activation\nis held. If the call throws, pass the caught error to classify it and get a next\naction:\n\n```ts\ntry {\n  await talos.validate();\n  console.log(talos.createDiagnosticReport());\n} catch (error) {\n  console.log(talos.createDiagnosticReport(error));\n}\n```\n\nThe report is a local snapshot: it makes no requests, writes no files and changes\nno licensing state. It excludes keys, tokens, server URLs, machine identifiers,\nentitlements and exception messages/stacks. Unknown error codes are replaced with\n`api_error`. An HTTP error is not a signed license decision. An offline result\nalone cannot identify the original transport or verification failure.\n\nIn the portal, select the same license on **Integration → Connection progress**\nto see its devices, recorded validation, seat usage and recent activation refusals.\nUse the report for failures that the server cannot see. Do not use this report\nas authorization; continue to gate access on the SDK's verified license state.\n\n## Supplying your own `fetch`\n\n`options.fetch` replaces the global one for **every** network call, artifact\ndownloads included — so a corporate proxy, a custom CA bundle, request logging\nor a test double applies uniformly. It must return `{ status, text(),\narrayBuffer() }`; `headers` and `body` are optional, and when the implementation\nprovides a streaming `body` the artifact download meters it against the signed\nmanifest's size instead of buffering first.\n\n## Licensing calls have a deadline\n\n`options.httpTimeoutMs` (default 10000, `0` disables) bounds every licensing\ncall. Node's `fetch` has none of its own, so without it a server that accepts\nthe connection and then says nothing leaves `activate()` or `validate()` pending\nforever — which in a desktop application is a splash screen that never goes\naway, on the one code path a user cannot skip.\n\nA timeout surfaces as a `TalosError`, deliberately: `validate()` treats network\nfailure as the grace-period case, and an abort arriving as something else would\nstop the application instead of letting it keep running offline.\n\nIt does **not** cap `downloadUpdate`. A total-duration limit on a\nmulti-megabyte artifact is a size limit disguised as a safeguard — the same ten\nseconds that is generous for a licensing call fails every download on a slow\nconnection. That one is bounded by the signed manifest's `size`.\n\nAn injected `fetch` receives the `signal` and is expected to honour it; one that\nignores it has no timeout.\n\n## The licence key is not kept\n\nAfter activation the SDK holds no licence key — not in memory, not in the state\nfile. The server resolves the licence from the per-machine activation secret,\nwhich is what these calls authenticate with anyway, so the key was only a lookup\nvalue travelling beside the credential.\n\nThat matters because the state file lives on a disk the user does not solely\ncontrol. It is encrypted, but with material any process on that machine can\nread; a plaintext licence key in it is a credential-shaped file, and a licence\nkey works anywhere. A machine-bound secret does not.\n\nWhat is kept is a SHA-256 of the key, for one thing only: activating twice with\nthe same key validates instead of spending a second seat. It is accepted by\nnothing — the server matches an HMAC under a pepper no client has seen.\n\nA state file written by an older version is upgraded on first read, so nobody\nre-activates to get this. And `license_key` is still accepted by every route, so\nan application shipped against an older SDK keeps working unchanged.\n\n## The machine binding is persistent\n\n`activate` binds this machine and the SDK writes that binding to a per-product\nfile under the OS user-data directory. A `TalosClient` restores it in its\nconstructor, so `talos.isActivated` answers the startup question — prompt for a\nkey, or go straight to `validate()` — and the user enters their key once, not\nonce per launch.\n\nThat file is `%LOCALAPPDATA%\\Talos\\<slot>\\state.bin` on Windows,\n`~/Library/Application Support/Talos/<slot>/state.bin` on macOS,\n`$XDG_DATA_HOME/talos/<slot>/state.bin` (or `~/.local/share/...`) on Linux —\nwhere `<slot>` is a hash of the product token, and the same three paths the C#\nSDK uses, so one product cannot end up with two bindings in two places.\n\n`activate()` is safe to call on every start: when a binding for the same key is\nalready held it **re-validates** instead of re-activating. That is not just an\noptimisation. Re-activating rotates the activation secret, so a second copy of\nyour app on the same machine would knock the first one out; and `activate` is\nthe one client route refused while the vendor's own Talos account is suspended,\nso re-activating on every launch would take *your* paying users offline over\n*your* billing dispute. It falls back to a real activation when the stored\nbinding is genuinely gone — the seat was released from the portal, or the secret\nwas rotated elsewhere.\n\n`deactivate()` forgets the binding on disk as well as in memory.\n\nSet `statePath` to choose the file, or `persistState: false` for a process that\nshould not leave a licence on disk (a test, a short-lived worker) — at the cost\nof everything in the paragraph above.\n\nThe file is encrypted with a key derived from a stable machine identifier, but\ntreat that as tamper-evidence and resistance to casual copying, **not** secrecy:\nwhoever owns the machine can derive the same key. What actually stops a copied\nstate file is the server — it resolves a machine by fingerprint and demands the\nactivation secret, so the file does not work on a different computer. A corrupt\nor unreadable file is treated as \"not activated yet\", never as an error: a\ndamaged cache costs a re-activation, not a failed launch.\n\n## Offline and grace\n\n`validate()` is online-first and offline-tolerant. When the server answers, that\nanswer wins and is cached. When it cannot be reached, the last verified verdict\nis honoured until the policy's `offline_grace_hours` runs out, so a laptop on a\nplane keeps working instead of losing the licensed application the moment the\nnetwork does.\n\n```ts\nconst state = await talos.validate();\nif (state.isOffline) showBanner(\"Working offline — checked in \" + ago(state.graceUntil));\nswitch (state.decision) {\n  case \"valid\": break;\n  case \"grace_expired\": requireConnection(); break;   // offline too long\n  case \"clock_tampered\": requireConnection(); break;  // the clock moved back\n  default: showLicenceProblem(state.decision);\n}\n```\n\nWhat is *not* cached matters as much. **A status code from the server is an\nanswer** — \"revoked\", \"no such machine\", \"tenant suspended\" — and is never\nmasked by the cache; falling back there would turn every negative verdict into\nhours of grace, which is the opposite of its purpose. Only the absence of an\nanswer falls back: a dead network, or a reply that fails signature verification\n(a garbled or forged response tells you nothing, so it is treated as\nunreachable — never as valid).\n\nFour things bound the cached verdict, and each closes a specific hole:\n\n- it is **re-verified under the pinned key** on every use, so editing the state\n  file achieves nothing;\n- its `fph` claim must be *this* machine, so copying the file to a second\n  computer and pulling the network cable does not licence it;\n- the licence's own expiry is re-checked, so a cached \"valid\" cannot outlive\n  the subscription it was issued under;\n- and a **persistent server-time high-water mark** — the highest signed server\n  clock ever seen — is compared against the local clock. Without it, winding the\n  system clock back would renew the grace window forever. A monotonic timer is\n  not a substitute: it resets on reboot and on a VM snapshot restore, which is\n  exactly the case that matters. A clock behind the mark reports\n  `clock_tampered`, which is deliberately distinct from `expired` — nothing has\n  run out, the machine is lying about the time.\n\nThe anchor-signed keyset is cached alongside it, so the whole chain verifies\noffline against a root the network cannot influence: the anchor is compiled into\nyour app, the keyset is signed by it, and the validation token is signed by a key\nit lists.\n\nEvery server response is a Talos-compact token signed by the product key and\nverified against the pinned key, so a fake/MITM server cannot forge an acceptable\nanswer. The SDK ignores any `kid` and enforces `typ`/`aud`/nonce/`exp`/`nbf`. In\nElectron, run this in the **main process** and bridge to the renderer over IPC.\n\n**Key rotation (anti-lock-in).** Pin `anchorPublicKey` — the product's immutable\nroot. The SDK fetches the anchor-signed *keyset* (delivered with activation, or\n`await talos.refreshKeyset()`) and learns the current license/release keys from\nit. When you rotate a signing key in the portal, deployed apps keep working with\nno reconfiguration. (`publicKey`/`releasePublicKey` are still accepted as direct\npins, but they break on the first rotation — prefer the anchor.)\n\nAnti-tamper honesty: JS is patchable — gate premium value on server-returned\n*data* (entitlements/config), not client booleans.\n\n## Air-gapped machines\n\nGrace above is for an install that has been online. This is for one that never\nwill be — an isolated network, no route out at all.\n\n```ts\n// On the machine. Write this out and carry it to your vendor:\nwriteFileSync(\"talos-request.json\", JSON.stringify(talos.createOfflineRequest(), null, 2));\n\n// They upload it in the portal and hand you back license.talos:\nconst state = await talos.loadOfflineLicense(\"license.talos\");\nstate.decision; // \"valid\"\n```\n\nThe first call generates an **Ed25519 keypair for this machine**, keeps the\nprivate half in the same encrypted state file as everything else, and puts the\npublic half in the request beside the fingerprint. The file you get back is bound\nto both, so a file copied off another machine does not license this one and this\nmachine's state directory does not license another. Calling it twice reuses the\nsame identity — only the nonce changes.\n\nEverything is verified locally: the anchor-signed keyset travels inside the file,\nso an anchor-only pin still works with no server to fetch one from. The verdict\nis kept, so later starts need nothing — `validate()` re-checks the stored file\ninstead of reaching for the network, and `isActivated` is true.\n\n`loadOfflineLicense` **throws** only for a file that is not a licence: unreadable,\nnot one of ours, addressed to another product, or a signature that does not\nverify. A file that is one but cannot license this machine comes back as a\nverdict you can show the user — `machine_mismatch`, `offline_file_expired` (get a\nnew file), `expired` (renew the licence), `clock_tampered`.\n\nRetiring the machine? `talos.createOfflineDeactivation()` writes a\n`talos-deactivation.json` for the vendor to upload, and **drops the licence\nbefore returning it** — so an app that loses the file has still stopped using the\nseat. Credited once per file, so a machine restored from a snapshot replaying its\nreceipt changes nothing.\n\nTwo things to know before you offer this to customers. A file **cannot be\nrecalled** — revoking it in the portal frees the seat so you can issue to a\nreplacement machine, but the machine holding it keeps working until the file's\nown expiry, which is why the policy's horizon is short. And hardware changes are\nnot forgiven offline: the drift matcher lives on the server, so a replaced disk\nmeans `machine_mismatch` and a new request.\n\n## Heartbeats, monitoring, and cadence\n\n```ts\ntalos.startCheckIns({\n  onState: (s) => updateUi(s),\n  onError: (e) => console.warn(\"check-in failed\", e),\n});\n// On shutdown:\ntalos.stopCheckIns();\n```\n\nOne background loop, driven by the **policy** rather than by a number you pick:\neach tick re-validates once the signed `reval_after` has passed, and otherwise\nsends a heartbeat at the signed `heartbeat_interval_s`. Change the policy in the\nportal and deployed apps follow — which is the point, because that interval is\nboth the telemetry rate and the upper bound on how long a revocation takes to\nreach an install. Both values come from the signed claims, never the response\nenvelope, so nobody in the middle can tell a fleet to check in less often.\n\nRunning the loop is also what keeps the offline cache fresh, so an app that\nstarts it stays inside its grace window without scheduling anything itself.\n\nA heartbeat refreshes this machine's last-seen state, returns the current\n**signed** decision, and feeds the developer portal's monitoring — active\ninstalls and version adoption. It reports only the app version and OS/arch; no\nIP address is stored. Call `talos.heartbeat()` directly if you would rather own\nthe schedule; `talos.heartbeatIntervalSeconds` is the policy's answer once the\nfirst one has been sent.\n\n## Events: the transitions, not the level\n\n```ts\ntalos.on(\"gracePeriodStarted\", (s) => showOfflineBanner(s.graceUntil));\ntalos.on(\"gracePeriodExpired\", () => requireConnection());\ntalos.on(\"licenseInvalid\", (s) => showLicenceProblem(s.decision));\ntalos.on(\"clockTamperDetected\", () => showClockWarning());\ntalos.on(\"updateAvailable\", (info) => offerUpdate(info.version));\n```\n\nFour of them are licence transitions, each raised **once** on the way in and not again while nothing\nchanges: an application offline for a week gets one `gracePeriodStarted`, not\none per check-in. `licenseInvalid` covers the server's own verdicts (`expired`,\n`revoked`, `suspended`, `deactivated`, `machine_revoked`, `invalid`) and fires\nagain when the *reason* changes, because \"your subscription lapsed\" and \"this\nmachine was revoked\" are different things to put in front of a user. The two\ngrace verdicts are deliberately not folded into it: what ran out there is\npermission to keep believing a cached answer, and reporting it as a licence\nproblem sends the user to support instead of to their network.\n\n`on` returns a function that unsubscribes. Listeners are called synchronously,\nfrom every path that adopts a verdict — `activate`, `validate`, `heartbeat` and\nthe offline fallback — so a host running its own timer gets the same\ntransitions. A listener that throws is ignored: these are raised from inside\n`validate`, whose own catch treats a throw as an unreachable server, and a\nbroken banner must not turn a live \"revoked\" answer into offline grace.\n\nThere is deliberately no \"grace ended\" or \"licence recovered\" event. That is a\nlevel, and `onState` (or `talos.current`) already carries it on every check-in;\na second way to learn one fact is a second thing to keep in step.\n\n`updateAvailable` is the odd one out: it carries an `UpdateInfo`, not a licence\nstate, and it is how an application learns about a release **without asking**.\nSubscribing to it makes each heartbeat name your `updateChannel` (`stable`\nunless you set one), which is what asks the server whether anything is waiting;\non a yes, the SDK runs a real `checkForUpdate` and hands you what that verified\n— the signed manifest and this install's own anti-downgrade floor, never the\nhint's word. Nothing is asked for and nothing is spent while no listener is\nattached, and each release is announced once however long the user puts off\ninstalling it.\n\n## When updates are refused\n\nA maintenance licence that has run out is **not** a licence that has failed.\n`validate()` still returns `valid` — the customer keeps the version they bought,\nand their application must keep working — and only the update check refuses:\n\n```ts\ntry {\n  const upd = await talos.checkForUpdate();\n  if (upd) offerUpdate(upd);\n} catch (e) {\n  if (e instanceof TalosApiError && e.errorCode === \"updates_not_entitled\") {\n    // Not a licensing failure. Say what it is and what fixes it.\n    showNotice(\"Your maintenance period has ended. The app keeps working; renew to get updates.\");\n  } else {\n    throw e;\n  }\n}\n```\n\nBoth mistakes here cost something. Treating the 403 as a licensing error locks a\npaying customer out of software they own. Treating it as \"no update available\"\nnever tells them their maintenance lapsed, so they find out when they ask why\nthey are three versions behind — and nobody renews for a reason they were not\ngiven.\n\nThe same code arrives on the artifact download, for the same reason: the gate is\nthe licence's `updates` entitlement, checked on both routes.\n\n## Electron integration\n\nKeep `TalosClient` in the main process and expose the license state to the\nrenderer through IPC. Keep the activation secret in the main process. Use\nserver-verified entitlements to control licensed features; client-side code\ncan be modified by the person running the application.\n\n## Hardware changes\n\nThe SDK identifies the machine by a set of component hashes (never the raw\nidentifiers — each is HMAC'd with a product-scoped key before it is sent). If\nthe policy uses tolerant matching, a machine that replaces a disk or reinstalls\nthe OS keeps its seat instead of forcing the user through a re-activation.\n\n**Node's reach is limited.** With no native dependencies and no subprocesses,\nthis SDK can read a stable machine identity on Linux but not on Windows or\nmacOS, so those hosts fall back to exact matching: change a NIC and the user\nre-activates. If your application can do better — an Electron native module, or\nan id you already hold — pass it in and the server will use it:\n\n```ts\nconst talos = new TalosClient({\n  productToken: \"tpt_…\",\n  anchorPublicKey: \"…\",\n  serverUrl: \"https://api.talos.dev\",\n  fingerprintComponents: { machine_guid: myNativeModule.machineId() },\n});\n```\n\nThe value must be stable for the life of the install — one that changes per\nlaunch reports a hardware change on every launch. The product's fingerprint\npolicy controls which hardware changes are tolerated.\n\n## Floating (concurrent) licenses\n\n```ts\n// Policy kind \"floating\" with max_concurrent_seats = N.\ntry {\n  const lease = await talos.acquireLease({\n    onLeaseLost: (why) => showBanner(`Seat lost: ${why}`),\n  });\n  console.log(`seat ${lease.seatsInUse}/${lease.maxSeats}`);\n} catch (e) {\n  if (e instanceof TalosApiError && e.statusCode === 409) showBusyDialog();\n}\n\n// On clean exit:\nawait talos.releaseLease();\n```\n\nThe server arbitrates seats atomically, so two instances can never both take the\nlast one. The seat auto-renews in the background; a crash needs no cleanup —\nthe lease simply expires and the seat returns to the pool. Re-acquiring with the\nsame `instanceId` renews rather than consuming a second seat.\n\n## Updates\n\n```ts\nconst talos = new TalosClient({\n  productToken: \"tpt_…\",\n  publicKey: \"…\",             // license key (activation/validation tokens)\n  releasePublicKey: \"…\",      // release key (update manifests) — separate custody\n  serverUrl: \"https://api.talos.dev\",\n});\nawait talos.activate(licenseKey);\n\nconst upd = await talos.checkForUpdate();\nif (upd) {\n  // `upd` is built from a manifest signed by the pinned release key. Metadata\n  // (version, sha256, size, filename) is trusted; the download URL is not.\n  const dest = `/tmp/${upd.artifacts[0].filename}`;\n  await talos.downloadUpdate(upd, dest, upd.artifacts[0], ({ bytesReceived, totalBytes }) => {\n    showProgress(bytesReceived / totalBytes); // optional; called as the bytes arrive\n  });\n  // Hand `dest` to your installer / electron-updater. Applying is the app's job.\n}\n```\n\nThe bytes stream to a `.talos-part` file beside the destination, hashed as they\narrive, and are renamed into place only once the hash matches — so a\nmulti-gigabyte installer is never held in memory, and a download that fails\nverification leaves whatever was at `dest` before rather than a truncated file\nyour updater might run. `totalBytes` is the signed manifest's size, never the\nserver's `content-length`: a progress bar the download host can drive can be\nparked at 100% while bytes are still arriving.\n\nThe manifest carries a monotonic `releaseSeq`, and the SDK refuses to surface a\nrelease at or below the highest one this install has been seen running\n(**anti-downgrade**). It keeps that mark itself, in the state file: when\n`downloadUpdate` fetches a release, the version is remembered, and the next time\na client is constructed with `appVersion` equal to it the mark moves up. So a\nserver that offers an older build — a rollback, a stale replica, or somebody in\nthe middle — is answered with `null` and nothing else is needed from you.\n\nPass `currentReleaseSeq` only when you know something the SDK cannot: a build\ninstalled by a package manager, or a first run after adopting the SDK on a fleet\nthat is already updated.\n\nDownloaded bytes are verified against the signed manifest's SHA-256, so the\nCDN/mirror serving them is untrusted infrastructure.\n","readmeFilename":"README.md"}