Browse the library
Documentation libraryIntegration
JavaScript SDK
`@clavisauh/sdk` is the official SDK your software uses to talk to Clavis Authentication Engine: activate keys, sign people in, keep sessions alive, read variables, receive signed updates and run offline. Every request is signed on the machine, and every answer is verified before you get to use it.
14 min read
What the SDK does for you
- Signs each request with an Ed25519 device key, plus a single-use nonce and a timestamp, so no request can be played back a second time.
- Verifies each answer under protocol CLV1.1: the signature must come from your app's public key, be bound to the request just sent (HTTP status, endpoint, nonce) and be no more than 60 seconds off. No endpoint is exempt, error answers included.
- Corrects its clock when the server answers
clock_skew(a signed answer) and retries exactly once. - Tries again by itself when the server cannot be reached or asks to wait, and only when repeating the call is safe.
- Checks offline tokens on the machine itself, without the server, and checks update manifest signatures and downloaded file checksums before handing them to you.
The SDK runs in Node.js, Electron or the browser, wherever fetch exists (or you pass your own). The package is plain JavaScript ES modules with TypeScript declarations; types such as Grant and ClavisOptions are exported too. Software written in another language has its own SDK (Python, C#, C++, Go, Rust, Java) or the C library, with the same protocol and the same signature checks: see Downloads and the Get started guides. The protections that need native code, such as TLS certificate pinning and hardware-backed keys (TPM, Secure Enclave), are coming soon to those SDKs.
Install
Download clavisauh-sdk-js-0.1.0.tgz from Downloads, check its SHA-256, and install the file into your project:
npm install ./clavisauh-sdk-js-0.1.0.tgz
The SDK is not on npm yet. Step by step, from install to release: Get started with Node.js and Electron.
Get baseUrl, the App ID and the public key from the dashboard:
- Open the app and go to its Overview page. The Public key card shows the App ID (
app_…) and the public key. - Go to Integration › Connect software and pick Node.js. The Your code step has the API address, App ID and public key filled in. Copy the API address from there exactly as
baseUrl. - Want an AI to do the wiring? Click Copy prompt for your AI agent at the top of the page.
The public key only verifies and cannot sign anything, so it is safe to ship. Never embed a secret key of any kind.
Creating a ClavisClient
import { ClavisClient, hashComponent } from "@clavisauh/sdk";
const clavis = new ClavisClient({
baseUrl: "https://api.example.com",
appId: "app_…",
appPublicKey: "<your app public key>",
device: {
fingerprint: hashComponent(machineId),
components: { cpu: hashComponent(cpuId), disk: hashComponent(diskSerial) },
platform: "windows",
},
deviceKey: await loadDeviceKey(),
appVersion: "1.4.0",
});
| Option | Required | What it does |
|---|---|---|
baseUrl | Yes | The API address; the SDK appends /v1/client/<endpoint> |
appId | Yes | Your app id, app_… |
appPublicKey | One of the two | The public key (base64url). Answers not signed by it are refused |
appPublicKeys | One of the two | A list of { kid?, key }, every key this build trusts, for key rotation. Without kid the SDK derives it from the key |
device | Yes | Describes the machine, see below |
deviceKey | No | The device key pair { secretKey, publicKey }. Left out, the SDK generates a new pair |
appVersion | No | The running version, semver, up to 32 characters |
buildHash | No | SHA-256 of the running file, exactly 64 lowercase hex characters |
security | No | This build's own maxOfflineSec, heartbeatSec and reportTamperSignals |
timeoutMs | No | Milliseconds to wait for an answer before failing with timeout, 15 000 by default |
maxRetries | No | Tries after the first when the server cannot be reached or asks to wait. 2 by default, 0 turns retries off |
fallbackUrls | No | Other addresses of the same server, tried in turn. Safe: an answer counts only if your app's key signed it |
fetch | No | A replacement fetch |
With neither appPublicKey nor appPublicKeys, the constructor throws an Error right away. Pass both and the SDK trusts every key in both. The fetch you pass is also used by download(), so a very short timeout can cut off a large update file.
Device details and hashComponent
fingerprint(required): a stable hash that stands for the whole machine, 16 to 128 characters of letters, digits,_or-.components: up to 12 hashed hardware parts, names up to 32 characters, values up to 128. They let the server recognise the machine after an OS reinstall or a part swap.name,platform: up to 64 characters, shown in the dashboard.
hashComponent(value) trims, lowercases, hashes with SHA-256 and keeps the first 32 hex characters, so " ABC " and "abc" give the same result. Hash every hardware identifier with it: raw values never leave the machine. How the server matches machines is in Devices, HWID and transfers.
Keeping the device key across launches
deviceKey signs every request, sessions are tied to it, and offline tokens are only valid with it. With a new key on every launch the machine is still recognised by its fingerprint, but every stored offline token is refused. Read the generated pair from clavis.deviceKey and keep it in the OS keychain, DPAPI or another secret store:
const toB64 = (b: Uint8Array) => Buffer.from(b).toString("base64");
const fromB64 = (s: string) => new Uint8Array(Buffer.from(s, "base64"));
async function loadDeviceKey() {
const saved = await readSecret("clavis.deviceKey"); // your function
if (!saved) return undefined;
const { s, p } = JSON.parse(saved) as { s: string; p: string };
return { secretKey: fromB64(s), publicKey: fromB64(p) };
}
async function saveDeviceKey(key: { secretKey: Uint8Array; publicKey: Uint8Array }) {
await writeSecret("clavis.deviceKey", JSON.stringify({ s: toB64(key.secretKey), p: toB64(key.publicKey) }));
}
Public keys during a key rotation
In Advanced Protection, the Signing keys section of App security › Protection, tab Methods always shows an appPublicKeys block under Keys for your next build, paste into ClavisClient, listing every key in play (current, next and previous). To rotate: click Prepare a new key, copy that block into your build, ship it, and only then click Activate the new key. A build that only knows the old key refuses answers signed by the new one with unknown_key. The full procedure is in Signing keys and rotation.
appVersion and buildHash
Both travel with init() and every sign-in. The server uses them to refuse builds below the minimum version (update_required), builds that belong to a blocked release (build_blocked) and, when Build Integrity Verification is set to Block, builds that match no release (unknown_build). Without appVersion the minimum version check is skipped. See Sign-in modes and trials.
The life of a session
The SDK keeps the session in memory only. Each time your software starts, call activate(key) or login(...) again; on the same machine the server reuses the existing seat, so no extra seat is spent. The usual order:
init(): read the app's configuration and apply the security hints.- One sign-in call, matching the app's sign-in mode.
heartbeat()on a schedule to keep the session alive. A session expires after 60 minutes without a heartbeat, or after three times Pulse every when Short-Lived Session Lease is on. The SDK does not send heartbeats by itself.logout()when your software closes, to free its place under any concurrent session cap.
Methods
| Method | Needs a session | Returns |
|---|---|---|
init() | No | { app, securityHints, serverTime } |
activate(key) | No | Grant |
trial() | No | Grant with trial: { endsAt, fallback } |
register(key, username, password, email?) | No | Grant |
login(username, password) | No | Grant |
redeem(key) | Yes, an account session | Grant |
heartbeat() | Yes | { session, license, entitlements, offlineToken } |
variables() | Yes | The variables by name, { [key]: value } |
transfer() | Yes | { ok, remaining } |
checkUpdates({ channel?, platform? }) | Yes | A verified manifest, or null |
download(file) | A live link | Uint8Array |
logout() | Yes | Nothing |
verifyOffline(token?) | No, runs locally | The token payload, or null |
nextCheckIn() | No, runs locally | Seconds to wait before the next heartbeat |
Calling a session method before signing in returns session_required.
init()
Returns app with: name, version (the newest release on the stable channel, or Current version from Integration › App settings when nothing is released yet), forceUpdate, downloadUrl and notice (that page's Require update, Download URL and Message to users fields), loginMode (key, account or account_key), trial (whether a trial is on offer), maintenance (the maintenance message, or null), minVersion and update. When you pass appVersion, update has required: true if the running build must update before it can sign in, or required: false with latest if a newer build simply exists. The SDK only passes forceUpdate, downloadUrl and notice through; how to show them is up to your software.
init() is optional, but the server's security hints only take effect after it succeeds. Call it first on every launch.
activate, trial, register, login, redeem
These are the five ways to open a session, each tied to a sign-in mode. Call the wrong one and the server answers mode_not_allowed.
activate(key)(License key mode): activates a key on this machine. Keys are case-insensitive, whitespace is ignored, and a valid length is 10 to 80 characters.trial()(License key mode): a machine-bound trial, one per machine per app.register(key, username, password, email?)(Account and Account + license key modes): passnullaskeyto create an account on the default plan.login(username, password): signs in an existing account.redeem(key)(Account + license key mode): upgrades the signed-in account with a key, keeping its username, password and machine.
Each one returns a Grant:
| Field | Contents |
|---|---|
session | id, expiresAt, and heartbeatSeconds when Short-Lived Session Lease is on |
license | id, status, state (active, expiring, grace), expiresAt (null for lifetime), maxDevices, metadata, mark (the buyer watermark, when Forensic Watermarking is on) |
entitlements | The license plan's entitlements |
device | The machine's id on the license |
variables | The app variables for this session |
offlineToken | An offline token, on every plan. null only while Online-Only Mode is on |
state is expiring when the license ends within the plan's Notice before the end (days) (7 by default), and grace once it is past its end date but still inside Grace after the end (days). Use it to remind people to renew before the software stops.
heartbeat() and variables()
heartbeat() extends the session and returns the latest license state. The SDK refreshes its entitlements from the answer and keeps any new offlineToken. Schedule the next one with clavis.nextCheckIn(): the seconds to wait, from the signed nextCheckSeconds in the last sign-in or heartbeat answer, plus a spread of up to a fifth that stays the same for this machine, so thousands of copies do not call at once.
variables() reads the app variables again mid-session, for example after you change a value in the dashboard. Variables scoped to one user or one license override a global variable with the same key. See App variables.
transfer() and logout()
transfer() frees this machine's seat so the buyer can move to another machine, and counts against the plan's move rules. The result's remaining is the number of moves left. A refusal is HTTP 429 with transfer_limited or transfer_not_allowed. On success the SDK drops its session.
logout() ends the session on the server and in the SDK.
checkUpdates() and download()
checkUpdates() asks for the newest release on a channel the license plan allows. If the channel you ask for is not on that list, the server uses the first allowed channel (stable by default). platform (for example win-x64) filters files by platform; any files are always included. With nothing released, the result is null.
The SDK only returns a manifest signed by one of the app's trusted keys and still fresh; otherwise it throws a ClavisVerificationError with the code invalid_manifest. A manifest lives 10 minutes, and its download links live 5 minutes and are tied to the session that asked. The SDK does not compare versions for you: compare manifest.version with the running build.
download(file) fetches one file of the manifest into memory and only returns the bytes when their SHA-256 matches the manifest. Details are in Signed releases and updates.
const manifest = await clavis.checkUpdates({ platform: "win-x64" });
if (manifest && manifest.version !== currentVersion) {
const file = manifest.files[0];
if (file) await installUpdate(file.fileName, await clavis.download(file));
}
verifyOffline()
verifyOffline(token?) checks an offline token on the machine and returns its payload (app, lic, dev, lexp, iat, exp, possibly nbf, kid, wm) or null. Without an argument it uses the latest token the SDK holds in memory. A token is refused when the key or machine is wrong, it has expired, the clock looks turned back, it exceeds maxOfflineSec, or the license was reported revoked. All the rules are in Offline licenses.
Offline tokens carry no entitlements. In a process that has not had a single online answer, has() always returns false, so decide up front which features an offline run unlocks.
State properties
| Property | Meaning |
|---|---|
has(name) | true when the latest verified answer granted the entitlement name |
entitlements | A copy of the current entitlements |
session | The current session id, or null |
security | The security settings in force |
revoked | true once a verified answer carried license_revoked or license_suspended |
deviceKey | The device key pair in use |
security holds heartbeatSec, maxOfflineSec, reportTamperSignals, pinning, spkiPins and requireHardwareKey. What you set in the security option can only be tightened by the server, never loosened: the smaller number wins for heartbeatSec and maxOfflineSec, and a flag that is on stays on. A fake server sending softer hints therefore changes nothing. pinning, spkiPins and requireHardwareKey are for the native SDKs; the JavaScript SDK cannot pin TLS.
revoked lives in memory only: when you see it set, delete any stored offline token.
Handling errors
Every server refusal and every verification failure is a ClavisError, with status (HTTP), code and message. ClavisVerificationError extends it: the answer could not be proven to come from your app's server for this very request, so treat it as no answer at all. Check for the subclass first.
| Verification code | When |
|---|---|
unsigned_response | The answer has no signature, no kid or no timestamp |
unknown_key | Signed by a key this build does not trust |
invalid_response_signature | The signature does not match: a fake key, an answer to another request, a changed status, an edited body |
stale_response | Signed more than 60 seconds away from the corrected clock |
invalid_manifest | An invalid update manifest (from checkUpdates() only) |
For the first four codes, right after the next verified answer the SDK sends a background report with the number of failures. It shows up as a Forged signature signal in App security › Protection.
The SDK raises a few codes of its own, with status 0 when no answer arrived: network_error (offline, DNS, firewall, TLS), timeout (no answer within timeoutMs, 15 seconds by default) and invalid_response (an answer that is not JSON, often a proxy page). That is the moment to fall back to verifyOffline(). When downloading it adds download_failed (status is the download's HTTP status) and checksum_mismatch.
Every code, from the server or from the SDK, is in one table shared by every official SDK: Error codes. Two helpers come with it:
err.userMessage("vi")(or"en") is a sentence you can show the person using your software, for every code.err.retryablesays whether trying again later, unchanged, may work (network, rate limits, maintenance).err.retryAfteris the seconds the server asked to wait, when it asked.
To see what the SDK sends and receives, create the client with debug: true (or a function that receives each line). License keys are cut to their first and last four characters and passwords never appear in that log.
Common server codes: invalid_credentials, too_many_attempts, device_limit, session_expired, license_expired, maintenance, update_required. This SDK speaks CLV1.1, so a key that is unknown, suspended, revoked or expired always answers invalid_credentials, unless this very machine already holds a seat on the license. What each code means and what to do is in Error codes.
Automatic retries
The SDK tries again by itself, up to maxRetries times, with a growing random wait. No retry starts more than 20 seconds after the first try.
network_error,timeoutor an unsigned 5xx page: only for calls that are safe to repeat:init,activate,login,heartbeat,variables,checkUpdates,logoutand reports.trial,register,redeemandtransferare never repeated after a lost answer, because they may already have happened.- A signed
rate_limited, or an unsigned 429 or 503: on any call, obeyingRetry-Afterup to 30 seconds. A longer wait is not retried: you get the error, with the seconds inerr.retryAfter.
Each try is signed again with a new nonce, and moves to the next address when you set fallbackUrls. Only a verified signature changes license state, so a failed try changes nothing. You see the error only after the last try.
A complete example
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { ClavisClient, ClavisError, ClavisVerificationError, hashComponent } from "@clavisauh/sdk";
const deviceKey = await loadDeviceKey();
const clavis = new ClavisClient({
baseUrl: "https://api.example.com",
appId: "app_…",
appPublicKey: "<your app public key>",
device: {
fingerprint: hashComponent(machineId),
components: { cpu: hashComponent(cpuId), disk: hashComponent(diskSerial), board: hashComponent(boardSerial) },
platform: process.platform,
},
deviceKey,
appVersion: "1.4.0",
// exePath: your executable
buildHash: createHash("sha256").update(await readFile(exePath)).digest("hex"),
fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(15_000) }),
});
if (!deviceKey) await saveDeviceKey(clavis.deviceKey);
export async function start(licenseKey: string) {
try {
const { app } = await clavis.init();
if (app.maintenance) return showMessage(app.maintenance.message);
if (app.update?.required) return showUpdate(app.update.minVersion);
const grant = await clavis.activate(licenseKey);
if (grant.offlineToken) await saveToken(grant.offlineToken);
if (grant.license.state !== "active") warnRenewal(grant.license.expiresAt);
} catch (e) {
if (e instanceof ClavisVerificationError) return showMessage("Could not verify the server");
// status 0 means no answer arrived: network_error or timeout
if (e instanceof ClavisError && e.status !== 0) {
if (clavis.revoked) await deleteToken();
return showMessage(e.code);
}
// Network error: run offline on the stored token, until its exp
const offline = clavis.verifyOffline(await loadToken());
return offline ? runOffline(offline.exp) : showMessage("An internet connection is required");
}
if (clavis.has("export_pdf")) enableExport();
const every = clavis.nextCheckIn() * 1000;
setInterval(async () => {
try {
const hb = await clavis.heartbeat();
if (hb.offlineToken) await saveToken(hb.offlineToken);
} catch (e) {
// The session ended or lapsed: sign in again on the same machine
if (e instanceof ClavisError && e.code === "session_expired") {
await clavis.activate(licenseKey).catch(() => lockApp());
}
}
}, every);
onQuit(() => clavis.logout());
}
loadDeviceKey, saveDeviceKey, saveToken, loadToken, deleteToken, runOffline, the hardware identifiers and the UI functions are yours to write for your platform. Keep the key the user entered (in the keychain, for example) so the next launch can call activate again.