Overview
The SDK ships as the @bindseal/sdk package: plain JavaScript ES modules for Node.js 20+ and Node-based runtimes such as Electron, with no native bindings and zero runtime dependencies. It is the only SDK available today.
The client composes activation, the lease and heartbeat, the protected-feature tiers (hollow, fused, sealed), live co-processor sessions, the offline grant, the contract-end release grant and tamper reporting behind one API. The same client drives an in-memory service in your tests and the HTTPS endpoint at https://api.bindseal.com in production.
Every signed container the server returns is verified against your embedded trust anchors before any field is trusted.
Construction
import { BindsealClient } from "@bindseal/sdk"; const client = new BindsealClient({ post, product, trustStore, fingerprint, /* …options */ });
createClient(config)
A one-call helper that removes the two pieces of boilerplate: it builds the trustStore Map from a plain trustAnchors array, and — when given a baseUrl and no post — supplies a fetch-based transport that already follows the transport contract below. It returns an ordinary BindsealClient.
import { createClient } from "@bindseal/sdk"; const client = createClient({ baseUrl: "https://api.bindseal.com", product: "my-game", trustAnchors: [{ kidHex, alg: 0x01 /* Ed25519 */, publicKey }], fingerprint, buildId: 1, });
Accepted fields: baseUrl, post (takes precedence over baseUrl), product, trustAnchors (non-empty array of { kidHex, alg, publicKey }), fingerprint, buildId, buildSecret, acceptRelease, compileEpoch. To inject a clock, a nonce factory or a persistent clockStore, use the constructor directly.
Throws BindsealClientError when trustAnchors is empty, an anchor lacks a string kidHex, or neither baseUrl nor post is given.
new BindsealClient(config)
| Field | Type | Required | Meaning |
|---|---|---|---|
post | (path, body) => Promise<object|null> | yes | Transport. Returns parsed JSON for any HTTP status (so update_required and declines are visible), or null on a network failure (so the client can fail open on the cached lease). |
product | string | yes | Product id compiled into this build. |
trustStore | Map | yes | Embedded trust anchors, kidHex -> { alg, publicKey }. Every signed container is verified against this before any field is trusted. |
fingerprint | object | — | This machine’s fingerprint-v1 object, sent on activate(). |
buildId | string | number | null | — (default null) | The build_id this client reports (version gating). |
buildSecret | string | null | — (default null) | This build’s proof-of-possession secret (hex), embedded at seal time. When set and the product requires proof, the client presents a build_proof MAC over (product, build_id, machine_id, nonce). Honest limit: the secret ships in the binary, so it raises re-crack cost per release — it is not cryptographic proof of the running version. |
now | () => number | — (default current time) | Clock, in Unix seconds. |
newNonce | () => string | — (default random) | Fresh-nonce factory (anti-replay). |
clockStore | { load(): {server?, local?}|null, save(s): void } | — (default null) | Where the clock-rollback high-water marks persist across restarts. The default is in-memory only (one process lifetime). Anyone who can wipe it can reset the rollback guard. |
clockSkewSeconds | number | — (default policy value) | Tolerated clock drift for the rollback guard. |
acceptRelease | boolean | — (default false) | Whether this build honors a contract-end release grant at all. Compile this true only in your offboarding build. |
compileEpoch | number | — (default 0) | This build line’s compile epoch: a release grant is honored only when its released_at >= compileEpoch. |
Throws BindsealClientError when post is not a function, or when product or trustStore is missing.
After construction the client holds no session: machineId, fpId and lease are null until activate() (or loadStoredOfflineGrant() / loadReleaseGrant()) populates them.
Lifecycle: activate / heartbeat / status
activate(lid) needs server
Activate against a license. lid is the license key the player entered.
- Parameters:
lid(string) — the license key. - Does: POSTs
/v1/activatewith{ lid, product, fingerprint, nonce, build_id, build_proof }. Verifies the signedactivation_responsecontainer against the trust store (type, product and nonce must match), cachesmachineId,fpIdand the lease window{ lease_expires, grace_until }, and records the server’s signed time as a clock-rollback high-water mark. - Returns:
Promise<string>— the current offline status (the result ofstatus():"valid" | "grace" | "expired" | "inactive"). - Throws
BindsealClientErrorwhen the server is unreachable (postreturnednull), when the server returns anerror(e.g.update_requiredis surfaced verbatim as an update prompt, not a lockout), or when the signed response fails verification.
status() offline
Fail-open status read from the cached lease, with no server contact.
- Returns:
string—"inactive"(never activated / no lease),"valid","grace"or"expired". If a release grant has been loaded it always returns"valid"(enforcement off). - Behavior: rollback-guarded. If the local clock reads earlier than the persisted high-water mark (beyond
clockSkewSeconds), the lease is reported"expired". Each accepted reading ratchets the local high-water mark forward. Aheartbeat()with a corrected clock recovers. - Throws: nothing.
clockRollbackSuspected() offline
- Returns:
boolean—truewhen the local clock reads earlier than time this client has already seen (beyondclockSkewSeconds). Use it to explain to the player an"expired"status caused by a turned-back clock. - Throws: nothing.
heartbeat() needs server fails open
Refresh the lease online; propagates revoke / suspend / expiry and update_required.
- Parameters: none. Requires a prior
activate(). - Does: POSTs
/v1/heartbeatwith{ machine_id, nonce, build_id, build_proof }, verifies the signed response, and refreshes the cached lease window and clock high-water mark. - Returns:
Promise<string>— the newstatus(). On a transient outage (postreturnednull) it returns the currentstatus()unchanged, so the game keeps running on the cached lease. - Throws
BindsealClientErrorwhen not activated, when the server returns anerror(revoked, suspended, expired,update_required, …), or when the signed response fails verification.
Protected features
These calls fetch a server-signed value and fold it into the result, so a cracked client that strips the calls gets wrong output or nothing rather than a working bypass of that feature. Each requires an activated client unless a release grant is loaded.
feature({ op, scope, input, attestation }) needs server hollow
The server computes the whole result; the client only consumes it.
- Parameters:
op(string),scope(string[]),input(object),attestation(object | null, defaultnull). - Returns:
Promiseof the operation’s result (the server-sidedata.result). - Throws
BindsealClientErrorwhen the client is released — a hollow feature has no released or offline fallback (its value lives only on the server, so it genuinely stops at contract end) — or when not activated. Transport and verification errors propagate.
fusedFeature({ op, scope, asset, input, attestation }) needs server fused
Fold a server operand into local computation; the wrong token yields the wrong output.
- Parameters:
op(string),scope(string[]),asset(string, mandatory) — which shipped assetinputis, so distinct assets never share a keystream —input(Buffer),attestation(object | null, defaultnull). - Returns:
Promise<Buffer>— the transformed bytes (correct only with the genuine server operand). - When released: resolves the operand from the loaded release grant and folds it with no server contact.
- Throws
FusedErrorwhenassetis missing, when the server declines (no operand), or when the capability’s operand is for a different op / asset; otherwiseBindsealClientErrorwhen not activated.
sealedSection({ op, scope, sealed, section, attestation }) needs server sealed
Unseal a shipped sealed section with a server-granted key bound to the machine and epoch.
- Parameters:
op(string),scope(string[]),sealed(the sealed blob),section(string),attestation(object | null, defaultnull). - Returns:
Promiseof the unsealed section bytes. - When released: unseals locally with the grant’s perpetual key for that section and a product-wide binding (no machine / epoch), with no server. Throws
BindsealClientErrorif the release grant carries no key for that section. - Throws
BindsealClientErrorwhen not activated; verification and decrypt errors propagate.
session({ op, scope, input }) needs server co-processor
Open a live co-processor session — server-authoritative, stateful compute.
- Parameters:
op(string),scope(string[]),input(object, default{}). - Returns:
Promiseof a session handle that drives/v1/session/openand/v1/session/step. - Throws
BindsealClientErrorwhen the client is released — co-processor state lives only on the server, so it has no released / offline fallback and stops at contract end — or when not activated.
Offline grant
Lets a game that runs offline for long stretches keep fused correctness with no per-feature server round-trip. Fetch once while online, persist the container, reload it at the next launch.
fetchOfflineGrant(ops, ttlSeconds) needs server (once)
- Parameters:
ops(Array<{ op, asset }>) — which fused operands to bundle;ttlSeconds(number, optional) — requested lifetime (the server clamps it). - Does: POSTs
/v1/offline-grant, verifies the signedoffline_grantcontainer, and stores the operand provider (client.offlineGrant) and the raw signed container (client.offlineGrantContainer, for you to persist to disk). - Returns:
Promise<number>— the grant’s expiry (Unix seconds). - Throws
BindsealClientErrorwhen not activated, when the server is unreachable, when the server returns anerror, or when the response carried no signed container;OfflineGrantErrorwhen the grant is forged, for another product / machine / build, or expired.
loadStoredOfflineGrant(container, machineId) offline
Reload a persisted offline grant at startup, with no server.
- Parameters:
container(Buffer) — the saved signedoffline_grantcontainer;machineId(string, defaultclient.machineId) — the machine id saved at activation. - Does: verifies and loads the grant, and sets
offlineGrant,offlineGrantContainerandmachineId. - Returns:
number— the grant’s expiry (Unix seconds). - Throws
BindsealClientErrorwhen no machine id is available;OfflineGrantErrorwhen the grant is forged, for another machine / build, or expired (surface this as “reactivate online”).
fusedFeatureOffline({ op, scope, asset, input }) offline
Fold a grant operand into local computation with no server.
- Parameters:
op(string),scope(string, ignored),asset(string, mandatory),input(Buffer). - Returns:
Buffer— the transformed bytes. Correct only when the loaded grant carries the genuine operand for(op, asset); a wrong, absent, expired or forged grant yields deterministically wrong output, never an unlock. - When released: resolves the operand from the release grant instead.
- Throws
FusedErrorwhenassetis missing;BindsealClientErrorwhen no offline grant is loaded (and not released).
Contract-end release grant
At contract end you can ship an offboarding build that turns enforcement off, so the game keeps working without the license server.
loadReleaseGrant(container) offline
Load the contract-end release grant shipped in this offboarding build. On success it latches “released”: status() is perpetually "valid", fusedFeature / sealedSection / fusedFeatureOffline resolve from the grant with no server, and reportTamper() goes inert. No activation is required — the grant is product-wide and perpetual.
- Parameters:
container(Buffer) — the signedrelease_grantcontainer you shipped as an asset. - Returns:
object— the release-grant provider (also stored asclient.releaseGrant):{ released: true, enforcement, released_at, operandFor(op, asset), unsealKeyFor(section) }. - Throws
ReleaseGrantErrorwhen the signature fails, when the grant is for a different product, when this build was not compiled withacceptRelease: true, or when the grant’sreleased_atpredates this build’scompileEpoch— so a leaked grant is inert on a normally built, still-protected copy.
Tamper reporting
reportTamper({ evidence, report }) needs server no-op when released
Report tamper evidence (for example from the SDK’s DefensePipeline) to the server for adjudication. The hard decision is the server’s; the client never bricks itself on its own evidence.
- Parameters:
evidence(number in [0, 1]) — the confidence summary;report(object | null, defaultnull) — optional detail. - Does: POSTs
/v1/adjudicatewith{ machine_id, nonce, evidence, report }. - Returns:
Promise<{ action, evidence }>— the server’s verdict, whereactionis one of"observe" | "flag" | "degrade" | "block". On a failed report (no response or anerror) it returns{ action: "observe", evidence }, so a failed report never bricks the client. When the client is released it returns{ action: "observe" }without contacting the server. - Throws
BindsealClientErrorwhen not activated (and not released).
Which calls need the server
| Call | Server? |
|---|---|
activate | Yes |
heartbeat | Yes (fails open to the cached lease on an outage) |
feature | Yes (no offline / released fallback — stops at release) |
fusedFeature | Yes, unless released (then uses the release grant, no server) |
sealedSection | Yes, unless released (then uses the release grant, no server) |
session | Yes (no offline / released fallback — stops at release) |
fetchOfflineGrant | Yes (one online call) |
reportTamper | Yes (no-op when released) |
status | No (reads the cached lease) |
clockRollbackSuspected | No |
loadStoredOfflineGrant | No (reloads a persisted grant) |
fusedFeatureOffline | No (uses the loaded offline / release grant) |
loadReleaseGrant | No (verifies a shipped grant locally) |
Error classes
All are exported from @bindseal/sdk for instanceof handling.
| Class | Thrown by |
|---|---|
BindsealClientError | Client methods — bad config, server unreachable, server-returned error, verification failure, not activated, no offline / released fallback. |
OfflineGrantError | loadOfflineGrant — wrong type, wrong product, wrong machine, expired, or wrong build. |
ReleaseGrantError | loadReleaseGrant — wrong type, wrong product, build not acceptRelease, or grant older than the compile epoch. |
FusedError | Fused features — missing asset, server declined (no operand), update_required, or an operand bound to a different op / asset. |
The package also exports UpdateRequiredError, HollowClientError, SealedSectionError, CapabilityError and CoprocessorClientError, thrown through the feature methods — for example, to tell “an update is available” apart from a real decline.
Signature verification runs before any field of a container is trusted, so a forged, tampered or truncated grant surfaces as a signature-verification error rather than the grant-specific one.
Standalone helpers
The grant loaders are also exported for direct use.
loadOfflineGrant(container, trustStore, { product, machineId, buildId?, now? })
Verifies a signed offline grant (signature first) and returns an operand provider. The grant must be type offline_grant, for this product, bound to this machineId, not expired, and — when both sides know it — for this buildId.
- Returns:
{ exp: number, build_id: number|null, operandFor(op, asset): object|null }, whereoperandForyields the operand{ k, asset, tweak?, epoch? }ornull. - Throws:
OfflineGrantError, or a signature-verification error on a forged container.
loadReleaseGrant(container, trustStore, { product, compileEpoch?, acceptRelease?, now? })
Verifies a signed release grant (signature first) and returns a released-state provider. The grant must be type release_grant and for this product, and the acceptRelease / compileEpoch controls must hold. There is no expiry and no machine binding — a release grant is perpetual and product-wide (now is accepted but unused).
- Returns:
{ released: true, enforcement: string, released_at: number, operandFor(op, asset): object|null, unsealKeyFor(section): string|null }. - Throws:
ReleaseGrantError, or a signature-verification error on a forged container.