@casazium/license-sdk
v0.5.1
Published
SDK and CLI for Casazium License Server. Supports activation, validation, and offline license checks.
Maintainers
Readme
@casazium/license-sdk
A lightweight JavaScript SDK for interacting with the Casazium License Server.
Supports online activation, key verification, license file validation, and optional offline enforcement via system fingerprinting.
Docs: docs.casazium.com · Guides: Licensing an Electron or Tauri desktop app, Limit a license key to a set number of computers, all guides
✨ Features
- 🔐 Activate and deactivate licenses on a specific server or device
- ✅ Verify license keys and signed license files
- 🖥️ Lock licenses to system fingerprints (
machineId) - 📁 Offline license file validation with RSA signatures
- 🧪 Fully tested and production-ready
📦 Installation
npm install @casazium/license-sdkServer version. SDK 0.5.0 and later need License Server 1.21.0 or
later: exportLicense() and exportOfflineLicense() send the license key
in the request body, which older servers don't accept (they answer
404). Hosted accounts already run 1.21.0. If you run the server
yourself, upgrade it before you ship an app built on 0.5.0. Apps on SDK
0.4.x and earlier keep working with 1.21.0.
To upgrade from 0.4.x: update the server to 1.21.0 or later, then change
your dependency to ^0.5.0 (^0.4.x won't pick it up). No code changes.
Against an older server, both export methods throw 404 with the message
exportLicense failed with 404: Not Found (or exportOfflineLicense …).
That means the server doesn't have the route, not that the key was
refused: an unknown key says License key not found. Don't treat it as a
refused license (see Errors); upgrade the server.
🚀 Usage
1. Instantiate the Client
import { CasaziumLicenseClient } from '@casazium/license-sdk';
const client = new CasaziumLicenseClient({
baseUrl: 'https://your-license-server.com/v1',
});Requests are retried on a network error or a 5xx, up to retries times
(default 3). When the retries run out, the error carries the server's
status and message. activate() and trackUsage() are never retried: if
the server saved the activation or counted the usage but the response was
lost, a retry would lose the activation token or count the usage twice.
The SDK sets no request timeout of its own. Pass a fetch that adds one,
so a server that never answers can't hold up your app's start:
const client = new CasaziumLicenseClient({
baseUrl: 'https://your-license-server.com/v1',
retries: 1,
fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(5000) }),
});2. Activate a License
import { getFingerprint } from '@casazium/license-sdk/fingerprint';
const key = 'your-license-key';
const instance_id = getFingerprint();
const { activated, token } = await client.activate(key, instance_id);
// Save `token` — it's required to deactivate this specific seat later.
// It's only returned once, when the activation is first created.If activate() fails with a network error, the activation may still have
been saved. Calling it again then returns alreadyActivated with no
token; an admin can issue a new one (POST /admin/reissue-token).
2a. Deactivate a License
Frees the seat so it can be activated elsewhere. Requires the token
returned by the activate() call that created it — not instance_id,
which is just a caller-supplied label and isn't accepted as the
deactivation credential.
const { deactivated } = await client.deactivate(key, token);3. Verify a License Key
expectedProductUuid is required — verify-license's response includes
product_uuid, the license's real, immutable product identity (a
product_id string alone is a per-tenant display label, not something
your app can trust to distinguish "my product" from someone else's).
Passing the wrong value, or a license issued for a different product,
throws rather than silently returning valid: true for a license you
shouldn't trust. Get your own product_uuid once (it's in the response of your first issue-license/
register-release admin call — on the hosted tier, also shown on that
license's/release's detail page in the console) and hard-code it in your
app — it never changes for a given product_id under your own tenant.
const result = await client.verifyKey('your-license-key', 'your-product-uuid');
if (result.valid) {
console.log('License is valid!');
}3a. Check This Machine Is Still Activated
verifyKey() checks the license, not this machine: it keeps returning
valid: true after this machine's seat was freed somewhere else (the
license portal, or a deactivate from another machine). For a launch
check, also call validate() with this machine's instance id (0.4.0+):
const license = await client.verifyKey(key, 'your-product-uuid');
const { valid } = await client.validate(key, instanceId);
if (license.valid && !valid) {
// This machine's seat was freed: ask the user to activate again.
}validate() resolves { valid: true } only when the license is active,
unexpired and activated for instanceId, and { valid: false }
otherwise, without a reason.
Errors
A method that gets a non-2xx response throws an Error whose message
names the method, the status and the server's own message (for example
verifyKey failed with 403: License is not active), and, from
0.4.0, whose status is the HTTP status. A network failure or a
timeout throws fetch's own error instead, with no status. So an app
can tell "refused" from "no answer":
// The server said no: a 4xx other than 429, or verifyKey's own refusal
// of a license for another product (which has no status).
function isRefusal(err) {
if (err.status === undefined) return /product_uuid mismatch/.test(err.message);
// A 404 ending ": Not Found" is a route the server doesn't have (one
// older than this SDK needs), not a refused key.
if (err.status === 404 && /: Not Found$/.test(err.message)) return false;
return err.status >= 400 && err.status < 500 && err.status !== 429;
}
try {
await client.verifyKey(key, productUuid);
} catch (err) {
if (isRefusal(err)) {
// Revoked, expired, unknown, or another product's: treat the license
// as invalid, and drop any offline file you keep for it.
} else {
// No answer: unreachable, timed out, rate limited (429) or a server
// error. Fall back to the offline file, within your own limit.
}
}A 429 means "slow down", not "no": its Retry-After header says how many
seconds until the rate-limit window resets, which can be up to 15
minutes.
4. Verify a Signed License File
For an app that may run offline. Export the license while online with
exportOfflineLicense(key) (POST /license-export/offline, the key in
the body), store
the result, and check it with only the public key:
import fs from 'node:fs/promises';
// { license, signature }, as exportOfflineLicense() returned it
const file = JSON.parse(await fs.readFile('license.json', 'utf8'));
const isValid = await client.verifySignedFile(file, 'your-product-uuid');
if (!isValid) {
// not a license for this product, tampered with, or expired
}verifySignedFile() resolves to true only when all of these hold, and
to false otherwise (it never throws on a malformed file):
- the signature is your server's, over these exact bytes;
- it's a license (
type: "license"and akey) — not, for example, a release manifest, which is signed with the same key; - its
product_uuidisexpectedProductUuid, which is required; - it hasn't expired:
expires_atisnull(perpetual) or a date in the future.
A file exported by License Server before 1.10.0 has no type, so it's
refused: export it again. Revocation can't be seen offline — a file stays
valid until its own expires_at — so check online (verifyKey()) when
you can, and export a fresh file after a renewal.
Fields after limits (License Server 1.20.0+). The file also signs
issued_at, exported_at (the server's clock when it signed the file),
livemode (true for a live purchase, false for a sandbox one) and
paid_through (the end of the latest period paid for with money). Each
is optional: older servers send none of them, and livemode and
paid_through are left out when unknown. Read them from file.license
once verifySignedFile() resolves true — they're covered by the
signature — and treat a missing one as "not known":
if (await client.verifySignedFile(file, 'your-product-uuid')) {
const { paid_through, exported_at, livemode } = file.license;
// e.g. keep the features of builds released by paid_through,
// and count offline time from exported_at rather than the local clock
}If you verify the file yourself, verify file.license exactly as
received: fields may be added after limits, so an object rebuilt from
the fields you know won't verify. verifyKey() returns livemode and
paid_through too (not signed there).
5. Check for a Software Update
Safe to call on every launch — same rate-limit tier and license validation
as verifyKey(), and no activation required first. Resolves to
{ update_available: false, release: null } rather than throwing when
there's nothing newer.
expectedProductUuid is required, same reasoning as verifyKey() above.
The server already scopes which releases you can see to your license's
own product_uuid — this is client-side defense-in-depth on top of that,
not a substitute for it. productId is optional now (it's still checked
as a convenience if you pass it, matched against either your product's
original or current display name so a rename never breaks this call),
but expectedProductUuid is what your app should actually rely on.
const { update_available, release } = await client.checkUpdate({
key: 'your-license-key',
expectedProductUuid: 'your-product-uuid',
platform: 'darwin-arm64', // must match what was used at release registration
currentVersion: '2.2.9',
// productId: 'your-product-id', // optional, defense-in-depth only
// channel: 'stable', // optional, defaults to 'stable'
});
if (update_available) {
console.log(`Update available: ${release.version}`);
console.log(release.artifact_url, release.checksum);
// release.signature/release.issued_at verify against GET /public-key —
// see the server's openapi.yaml for the exact signed field order.
}🧠 Fingerprinting (for License Locking)
import { getFingerprint } from '@casazium/license-sdk/fingerprint';
const fingerprint = getFingerprint(); // consistent per machineUses
node-machine-idto generate a unique machine identifier.
Safe for Linux, macOS, Windows, containers, and most VMs.
📁 Offline Usage Idea
You can store the following on disk:
{
"license": { ... },
"signature": "..."
}Then verify it at runtime with:
import { CasaziumLicenseClient } from '@casazium/license-sdk';
const client = new CasaziumLicenseClient({ publicKey }); // Required
// true only for an authentic, unexpired license for this product
await client.verifySignedFile({ license, signature }, 'your-product-uuid');🌐 Browser Usage
import { CasaziumLicenseClient } from '@casazium/license-sdk' resolves to
a browser-safe build automatically when bundled with a tool that sets the
browser platform/condition (esbuild --platform=browser, webpack,
Vite, etc.) — no separate import needed. It has zero Node-only
dependencies and no method that takes an admin API key.
If you want to be explicit, or load it directly without a bundler, import the browser entry point by name:
import { CasaziumLicenseClient } from '@casazium/license-sdk/browser';or drop in the prebuilt global script:
<script src="./node_modules/@casazium/license-sdk/dist/browser.global.js"></script>
<script>
const client = new CasaziumLicense.CasaziumLicenseClient({
baseUrl: 'https://your-license-server.com/v1',
});
</script>The browser build deliberately omits listLicenses() and revoke().
Both require an admin API key, and a license key embedded in page source
is necessarily public — an admin key must never be. Everything else
(verifyKey, activate, deactivate, trackUsage, getUsageReport,
exportLicense, exportOfflineLicense, getPublicKey,
verifySignedFile, checkUpdate) is available and safe to call from the
browser with just the license key. If your app needs license management
(issuing, listing, revoking), do that from a trusted server context using
the Node/CLI client, not from the browser.
🧪 Run Tests
npm install
npm test🛠 CLI (Optional)
If using the included CLI:
npx casazium-license verify --key YOUR-LICENSE-KEY --expected-product-uuid YOUR-PRODUCT-UUID--expected-product-uuid is required on verify, verify-file, and
check-update — same reasoning as the SDK methods above. Run
npx casazium-license --help for the full command list.
📄 License
MIT © 2025 Bob Casazza
