Docs · SDK reference

The client, method by method.

BindsealClient runs the whole license lifecycle — activation, the offline lease, protected features, grants and tamper reporting — behind one small API over a single transport.

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

license.js
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.

license.js
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)

FieldTypeRequiredMeaning
post(path, body) => Promise<object|null>yesTransport. 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).
productstringyesProduct id compiled into this build.
trustStoreMapyesEmbedded trust anchors, kidHex -> { alg, publicKey }. Every signed container is verified against this before any field is trusted.
fingerprintobject—This machine’s fingerprint-v1 object, sent on activate().
buildIdstring | number | null— (default null)The build_id this client reports (version gating).
buildSecretstring | 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.
clockSkewSecondsnumber— (default policy value)Tolerated clock drift for the rollback guard.
acceptReleaseboolean— (default false)Whether this build honors a contract-end release grant at all. Compile this true only in your offboarding build.
compileEpochnumber— (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/activate with { lid, product, fingerprint, nonce, build_id, build_proof }. Verifies the signed activation_response container against the trust store (type, product and nonce must match), caches machineId, fpId and 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 of status(): "valid" | "grace" | "expired" | "inactive").
  • Throws BindsealClientError when the server is unreachable (post returned null), when the server returns an error (e.g. update_required is 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. A heartbeat() with a corrected clock recovers.
  • Throws: nothing.

clockRollbackSuspected() offline

  • Returns: boolean — true when the local clock reads earlier than time this client has already seen (beyond clockSkewSeconds). 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/heartbeat with { 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 new status(). On a transient outage (post returned null) it returns the current status() unchanged, so the game keeps running on the cached lease.
  • Throws BindsealClientError when not activated, when the server returns an error (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, default null).
  • Returns: Promise of the operation’s result (the server-side data.result).
  • Throws BindsealClientError when 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 asset input is, so distinct assets never share a keystream — input (Buffer), attestation (object | null, default null).
  • 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 FusedError when asset is missing, when the server declines (no operand), or when the capability’s operand is for a different op / asset; otherwise BindsealClientError when 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, default null).
  • Returns: Promise of 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 BindsealClientError if the release grant carries no key for that section.
  • Throws BindsealClientError when 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: Promise of a session handle that drives /v1/session/open and /v1/session/step.
  • Throws BindsealClientError when 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 signed offline_grant container, 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 BindsealClientError when not activated, when the server is unreachable, when the server returns an error, or when the response carried no signed container; OfflineGrantError when 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 signed offline_grant container; machineId (string, default client.machineId) — the machine id saved at activation.
  • Does: verifies and loads the grant, and sets offlineGrant, offlineGrantContainer and machineId.
  • Returns: number — the grant’s expiry (Unix seconds).
  • Throws BindsealClientError when no machine id is available; OfflineGrantError when 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 FusedError when asset is missing; BindsealClientError when 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 signed release_grant container you shipped as an asset.
  • Returns: object — the release-grant provider (also stored as client.releaseGrant): { released: true, enforcement, released_at, operandFor(op, asset), unsealKeyFor(section) }.
  • Throws ReleaseGrantError when the signature fails, when the grant is for a different product, when this build was not compiled with acceptRelease: true, or when the grant’s released_at predates this build’s compileEpoch — 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, default null) — optional detail.
  • Does: POSTs /v1/adjudicate with { machine_id, nonce, evidence, report }.
  • Returns: Promise<{ action, evidence }> — the server’s verdict, where action is one of "observe" | "flag" | "degrade" | "block". On a failed report (no response or an error) 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 BindsealClientError when not activated (and not released).

Which calls need the server

CallServer?
activateYes
heartbeatYes (fails open to the cached lease on an outage)
featureYes (no offline / released fallback — stops at release)
fusedFeatureYes, unless released (then uses the release grant, no server)
sealedSectionYes, unless released (then uses the release grant, no server)
sessionYes (no offline / released fallback — stops at release)
fetchOfflineGrantYes (one online call)
reportTamperYes (no-op when released)
statusNo (reads the cached lease)
clockRollbackSuspectedNo
loadStoredOfflineGrantNo (reloads a persisted grant)
fusedFeatureOfflineNo (uses the loaded offline / release grant)
loadReleaseGrantNo (verifies a shipped grant locally)

Error classes

All are exported from @bindseal/sdk for instanceof handling.

ClassThrown by
BindsealClientErrorClient methods — bad config, server unreachable, server-returned error, verification failure, not activated, no offline / released fallback.
OfflineGrantErrorloadOfflineGrant — wrong type, wrong product, wrong machine, expired, or wrong build.
ReleaseGrantErrorloadReleaseGrant — wrong type, wrong product, build not acceptRelease, or grant older than the compile epoch.
FusedErrorFused 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 }, where operandFor yields the operand { k, asset, tweak?, epoch? } or null.
  • 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.