@key-warden/sdk
v1.3.0
Published
Official Node.js client for Key-Warden - validate software licences online (seat- and revocation-aware) or verify signed tokens offline against your embedded public key.
Downloads
782
Maintainers
Readme
@key-warden/sdk
The official Node.js client for Key-Warden. Validate a software licence online — seat-aware, revocation-aware — or verify a signed token offline against your embedded public key, with no network round-trip.
No dependencies. Node 16+.
npm install @key-warden/sdkValidate online
The authoritative check. Ask the platform whether a licence is good right now.
const kw = require('@key-warden/sdk');
const res = await kw.validate(customerLicenceKey, {
apimKey: process.env.KW_APIM_KEY, // your APIM subscription key
clientKey: process.env.KW_CLIENT_KEY, // your validation key
machineId: kw.machineIdFrom(os.hostname(), userId), // stable, hashed on your side
});
if (!res.valid) throw new Error(`licence not valid: ${res.reason}`);
// res.token is a freshly signed proof — cache it for the offline path below.A valid: false (e.g. revoked, expired, seat_limit_exceeded) is data, not
an error. A wrong clientKey throws a KeyWardenError with code:
'unauthorized_client' — that's your auth failing, and your customer should
never see it as a licence problem.
Verify offline
No connection? Verify a token you already hold against your public key — the 32-byte raw key from your vendor console. Pure and synchronous.
const check = kw.verifyToken(cachedToken, process.env.KW_PUBLIC_KEY);
if (!check.valid) lockFeatures(check.reason); // 'bad_signature' | 'expired' | ...The token is header.body.signature (compact JWT style) and the Ed25519
signature covers the exact bytes header.body. This SDK verifies over those
bytes — you never decode-then-reverify, which is the one mistake that silently
breaks offline checks. Expiry is honoured within the offline grace window you set
at mint time.
Online, with an offline fallback
The pattern most desktop apps want: online is authoritative; if the network is down, keep working within grace.
const res = await kw.validateOrVerify(customerLicenceKey, {
apimKey, clientKey, machineId,
cachedToken: lastGoodToken, // from a previous validate()
publicKey: process.env.KW_PUBLIC_KEY,
});
// res.source === 'online' | 'offline'A rejected clientKey (401) is never masked by the offline path — only a genuine
reachability failure falls back.
Free trials
A trial licence is an ordinary Key-Warden key — you validate it exactly like any
other. It just carries two extra claims: trial: true and an exp (unix
seconds). When the trial ends, verifyToken()/validate() refuse it as
expired on their own; you don't enforce anything yourself. The trial helpers
are for display — showing "N days left" and switching to an expired state:
const res = kw.verifyToken(cachedToken, process.env.KW_PUBLIC_KEY);
if (res.valid) {
const t = kw.trialInfo(res); // { isTrial, expired, expiresAt, secondsRemaining, daysRemaining }
if (t.isTrial) {
showBanner(`Trial — ${t.daysRemaining} day(s) left`);
}
runApp();
} else if (res.reason === 'expired') {
// trial (or paid licence) has run out — prompt for a purchased key
showPaywall('Your trial has ended. Enter a licence key to continue.');
}trialInfo() accepts a verifyToken()/validate() result or a raw claims
object. isTrial(x) and daysRemaining(x) are shortcuts. daysRemaining is
rounded up (so the last partial day still reads "1 day left") and is 0 once
expired, null for a key with no exp. These helpers never grant access —
always gate on verifyToken()/validate() first.
Trial keys are minted node-locked to one device (sites: 1), so use the same
machineId you pass to validate().
API
| Function | Purpose |
|---|---|
| validate(key, opts) | Online check. Returns { valid, reason?, activeSeats?, token? }. |
| verifyToken(token, rawPubB64) | Offline check. Returns { valid, reason?, claims? }. |
| validateOrVerify(key, opts) | Online, falling back to a cached token when unreachable. |
| machineIdFrom(...parts) | A stable SHA-256 machine id; raw parts never leave the machine. |
| trialInfo(x, opts?) | Trial facts for display: { isTrial, expired, expiresAt, secondsRemaining, daysRemaining }. |
| isTrial(x) | true when the licence carries trial: true. |
| daysRemaining(x, opts?) | Whole days left (rounded up); 0 once expired; null if no exp. |
TypeScript definitions are bundled.
Security notes
- Your private signing key never leaves Key-Warden's Key Vault. You embed only the 32-byte public half.
machineIdis hashed by the platform, but send an opaque, stable id — not a raw MAC address or a hostname you wouldn't want logged.machineIdFrom()hashes on your side too.- Two independent credentials gate every online call: the APIM subscription key gets you to the gateway, the validation key authenticates you as the vendor. A leaked validation key can be rotated without reissuing a single customer licence.
Code protection (seal / unlock / unseal)
Lock part of your product so it only runs for a valid, activated licence. Turn it on and get your content key (base64) from the vendor console → Protect your code.
Build time — seal a file once:
const kw = require('@key-warden/sdk');
const fs = require('fs');
const blob = kw.seal(fs.readFileSync('secret-module.js'), MY_CONTENT_KEY_B64);
fs.writeFileSync('secret-module.sealed', blob); // ship this insteadRuntime — get the key and decrypt. The key rides in the validate token as
ck, machine-bound, so unlock with the SAME machineId you validate with:
const res = await kw.validate(licence, { apimKey, clientKey, machineId });
const key = kw.unlockFromToken(res.token, machineId); // Buffer: content key
const code = kw.unseal(sealedBlob, key); // your decrypted filePrefer a live check every time (real-time revocation)? Use the online path instead of the token:
const key = await kw.unsealOnline(licence, { apimKey, machineId /*, product */ });All AES-256-GCM. A revoked or lapsed licence stops getting the key.
Licence
MIT.
