5.3 KiB
@keysat/licensing-client
TypeScript / JavaScript client for Keysat — a Bitcoin-native self-hosted software licensing service that runs on Start9.
Works in modern browsers and Node 18+. No native dependencies; signature verification is done in pure JS via @noble/ed25519.
What you get
- Offline verification: check a license key with just the issuing server's public key. No network. Optional local fingerprint and expiry enforcement.
- Online validation: live revocation check and fingerprint binding via the service's
/v1/validateendpoint. - Purchase flow: kick off a BTCPay checkout and poll for the issued key.
- Free licenses: redeem a free-license code, no payment.
- Tiers: list a product's public tiers for an in-app picker.
- Machine/seat management: activate, heartbeat, and deactivate seats.
Install
npm install @keysat/licensing-client
5-line offline check
import { Verifier, PublicKey } from '@keysat/licensing-client'
const verifier = new Verifier(PublicKey.fromPem(ISSUER_PUBKEY_PEM))
const ok = verifier.verify(keyFromUser)
console.log('licensed for product', ok.productId)
That's the whole integration. Embed your public key as a string at build time (e.g. Vite's ?raw import, webpack raw-loader, or just a const). If the verifier returns without throwing, the key is real and was issued by you.
Local fingerprint and expiry enforcement
Two offline variants enforce more without a network call:
// Throws if the key is fingerprint-bound and the machine doesn't match.
// (Unbound keys ignore the fingerprint.)
verifier.verifyWithFingerprint(keyFromUser, machineFingerprint)
// Throws `expired` if now is at or past the key's expiry. Perpetual
// keys (expiresAt === 0) always pass. No grace-window logic offline;
// use `Client.validate` for that.
verifier.verifyWithTime(keyFromUser, Math.floor(Date.now() / 1000))
10-line online check (with revocation + fingerprint)
import { Client } from '@keysat/licensing-client'
const client = new Client('https://license.example.com')
// Current form: pass an options object. The positional form
// `validate(key, productSlug, fingerprint)` still works for backwards compatibility.
const result = await client.validate(keyFromUser, { productSlug: 'my-product', fingerprint: machineFingerprint })
if (!result.ok) {
console.error('rejected:', result.reason)
process.exit(1)
}
The server enforces revocation live and does trust-on-first-use fingerprint binding, so the same key used from a second machine gets rejected.
Purchase flow
const session = await client.startPurchase('my-product')
console.log('pay at:', session.checkoutUrl)
const key = await client.waitForLicense(session.invoiceId)
console.log('got license:', key)
waitForLicense polls until the BTCPay invoice settles and the service issues a key. It throws if the invoice expires or becomes invalid.
Free licenses
Redeem a free-license code (the Creator-tier onboarding path) to get a signed key directly, with no BTCPay checkout:
const { licenseKey } = await client.redeemFreeLicense('my-product', 'CODE-1234')
verifier.verify(licenseKey) // offline-verifiable like any issued key
Throws if the code is unknown, disabled, expired, for another product, not a free-license code, or capped out. Optional { buyerEmail, buyerNote } third argument is recorded on the issued license.
Tiers
List a product's public tiers (no auth) to build an in-app tier picker that stays in sync with the operator's admin setup:
const { product, policies } = await client.listPublicPolicies('my-product')
for (const p of policies) {
console.log(p.name, p.slug, p.priceSats, p.entitlements)
}
Each policy carries slug, name, price (in the product currency's smallest unit, sats or cents), duration, seat cap, entitlements, and trial/recurring flags. Pass the chosen slug to startPurchase(slug, { policySlug }) so the invoice is priced and the issued license is provisioned for that tier.
Machine/seat management
For per-seat enforcement, manage machines explicitly. All three return a MachineResponse ({ ok, reason?, machineId?, activeCount?, maxMachines? }):
await client.activate(key, fingerprint, { hostname, platform }) // claim a seat
await client.heartbeat(key, fingerprint) // mark the seat alive
await client.deactivate(key, fingerprint, 'user signed out') // free the seat
validate already binds a seat on first use when you pass a fingerprint; reach for these when you want explicit activate/deactivate lifecycle or periodic liveness pings.
Examples
Runnable end-to-end scripts live in examples/: offline-verify.ts and online-validate.ts (the latter walks purchase to waitForLicense to validate).
Browser usage
Everything here works in the browser too. Drop the library into your React/Svelte/Vue app and run offline verification client-side — no server call needed for the common case.
// Vite: import the PEM as a raw string at build time
import issuerPem from './issuer.pub?raw'
import { Verifier, PublicKey } from '@keysat/licensing-client'
const verifier = new Verifier(PublicKey.fromPem(issuerPem))
License
MIT.