Electron / TypeScript

The reference for @keygrant/electron 0.9.0, the KeyGrant SDK for Electron and Node: every option, call and status. The rules every SDK shares are in How every SDK behaves.

  • Node.js 20 or later.
  • Runs in Electron's main process or any Node process, not in a renderer: it uses Node's file system, child processes and WebCrypto.
  • Depends on zod only. Nothing in it is specific to Electron.

Release notes

Add it

@keygrant/electron is on npm.

Terminal
npm install @keygrant/electron

Use it

The quick start, after Add it: embed your product's public key, activate a key, and check the license on every launch. You need a product with a license model and a key to try (see Getting started).

1. Get the public key

The SDK verifies each lease offline with your product's public keys. Fetch them once and ship them with your app. The request needs no API key:

Terminal
curl https://api.keygrant.dev/v1/products/my-product/pubkey
Response
{  "publicJwk": {    "kty": "OKP",    "key_ops": ["verify"],    "alg": "EdDSA",    "ext": true,    "crv": "Ed25519",    "x": "…"  },  "keys": [    { "kty": "OKP", "crv": "Ed25519", "x": "…", "kid": "…" }  ]}
  • my-product is your product's API ID, the last part of the product page's URL in the dashboard (/app/products/my-product).
  • Pass the keys array to the SDK's publicJwks option unchanged: it is every key a build of your app should carry, each with its id (kid).
  • Copy key set, on the product's Signing key tab, copies the same set as {"keys": [...]}.
  • The SPKI and .pem forms, and when the keys change, are in Get your product's public key.

2. Activate a key

Create one License for the process, and activate the key your customer enters:

Main process
import { License } from "@keygrant/electron";
const license = new License({ product: "my-product", apiBaseUrl: "https://api.keygrant.dev", publicJwks: PUBLIC_KEYS, // the keys from step 1 major: 1, // this build's major version});
const result = await license.activate(userEnteredKey);if (!result.ok) showActivationError(result.error);
  • activate sends the key to KeyGrant to be checked, takes one of the key's device seats for this machine, and stores the signed lease KeyGrant returns, a JWT that your app verifies offline.
  • When it fails, result.error says why, such as invalid-key or limit. Every value is in Activating.

3. Check the license on launch

Main process
// Run on every launch.async function checkLicense() {  const status = await license.status();  if (status.state === "licensed" || status.state === "trial") return; // run  if (status.state === "unknown") {    // The license file could not be read: keep running and check again shortly.    setTimeout(checkLicense, 5_000);    return;  }  // "expired" or "invalid": status.reason says why. "unlicensed": no key is stored yet.  showActivationWindow(status);}
checkLicense();

status() verifies the stored lease on the device, and usually needs no network.

  • A lease lasts the key's Offline days, and never runs past the key's expiry plus its grace period: see How long a lease lasts.
  • A device trial started with startTrial() gets one lease that runs to the trial's end.
  • When the lease nears its end, status() renews it first, waiting at most 5 seconds for KeyGrant: see When the SDK goes online.
  • When KeyGrant cannot be reached, your app keeps working until the lease runs out. See Offline validation & grace.

The rest of this page is every option, call and status.

new License(options)

The constructor checks the public keys and revokedLicenseFiles, and no other option, and does no I/O. It throws when:

  • neither publicJwks nor publicJwk is given, or both are;
  • publicJwks is empty;
  • a key is not an Ed25519 public JWK: its kty is not OKP, its crv not Ed25519, it holds the private d, its use is not sig, its key_ops do not include verify, its alg is not EdDSA or Ed25519, or its x is not 32 bytes of base64url;
  • revokedLicenseFiles is given and is not a list of texts.

An x pasted with its padding is read as the same key.

product

Required. The product's API ID, as it appears in the request path.

apiBaseUrl

Required. Set it to https://api.keygrant.dev. A single trailing slash is removed.

publicJwks

The product's public keys, an array of JWKs used to verify leases offline: the keys from /pubkey. Give this or publicJwk. See Rotating the signing key.

  • A lease names the key that signed it (kid) and is verified by that key.
  • A lease signed before leases named their key names none, and is verified by each key in turn.
publicJwk

One Ed25519 public key as a JWK: a key set of one, as an app configured before key sets has it. Give this or publicJwks.

major

The major version of this build. The SDK uses its whole number (2.7 is 2). See Licensing by version.

  • Anything below 1, or anything that is not a finite number, reads as 1, as KeyGrant reads a 0.x version, so a 0.x build can set 0 or 1.
  • KeyGrant rejects a major above 99,999, and activate() then fails with network.

Default 1

deviceName

What the license's device list calls this machine. Sent on activation, truncated to 120 characters.

deviceHold

false turns device hold off, so no license is held to its device. See Device management.

Default true

httpTimeoutMs

How long one request may take, in milliseconds. The SDK enforces this limit even when a custom HTTP adapter does not.

  • The checks status() makes while the lease is still valid have their own 5-second limit.

Default 15000 (15 seconds)

revokedLicenseFiles

The ids of the license files this build refuses (each file's fid), matched exactly: how an app update stops an offline license file that leaked. See License files.

  • A stored file listed here reads as invalid with reason revoked-file, and importing one is refused with revoked-file.

Since 0.6.0

adapters

Your own storage, registry, clock, fingerprint or http, to replace the defaults. See Adapters.

Methods

Calls on one License run one at a time, except has, require, upgradeUrl and the two pickUp calls, which only read and run beside any call. Create one License per process.

activate(key, options?)

Activates the key on this device and stores its lease. Resolves { ok: true } or { ok: false, error } (below). The key is trimmed and uppercased first.

  • options is { email?, code? }, for a key whose license model locks who can activate (below). Any other key ignores it, and nothing in it is stored.
  • Unlike status(), it does not use the stored lease: it contacts KeyGrant every time, unless the key cannot be valid, the device cannot be identified, or an earlier call is stuck.
has(code)

Resolves whether the license grants the entitlement code: a flag that is on, a number that is not zero, or a text value that is not empty. The code is matched exactly, case included. See Check an entitlement.

  • Answered offline from what the device holds, as status() would answer it, with no request and no write. So a license that does not license this build grants nothing, and a running trial grants what its trial lease does.
  • It never rejects: an unreadable license file grants nothing.
require(code)

Like has(code), but resolves nothing, and rejects with MissingEntitlementError, whose entitlement is code, when the license does not grant it.

importLicenseFile(content, options?)

Takes the content of a .kglicense file the customer was sent: text, a byte array (a Node Buffer too) or an ArrayBuffer, never a path. Resolves { ok: true, kind, licensedTo } or { ok: false, error } (License files).

  • It verifies the file offline and stores an offline file. It activates a key file's key as activate() does, and options is then the same { email?, code? }.

Since 0.6.0 ; options since 0.7.0

removeLicenseFile()

Drops the stored offline license file, if there is one. A key this device holds keeps working, and deactivate() leaves the file alone. Resolves nothing.

  • Rejects when an earlier call has not finished or when the change cannot be saved.

Since 0.6.0

use(meter, n?)

Spends n uses (1 by default) of the key's meter meter, before the work they count. Resolves { ok: true, left } or { ok: false, error } and never rejects (Metered uses).

Since 0.8.0

pickUp(email)

Asks KeyGrant to email a six-digit code to the address, for key pickup. Resolves { ok: true, sent: true } whether or not the address has a key, or { ok: false, error }. Stores nothing, and never rejects.

Since 0.9.0

pickUp(email, code)

Resolves { ok: true, keys }, the address's live keys of the product for the code that came, or { ok: false, error }. Stores nothing, and never rejects.

  • Each key is a PickedKey: key, model, modelName, state, entitlements, expiresAt, versions, deviceLimit, activeDevices, held (this device already holds a seat on it), sale and activationCode.

Since 0.9.0

restoreByEmail(email, code)

Confirms the code and activates the key that fits this build, never handing it to your app: the one this device holds a seat on, else one with a free seat, else the first. Resolves { ok: true } or { ok: false, error }.

Since 0.9.0

status()

Verifies what the device holds, offline first, and resolves a status. It contacts KeyGrant only in the cases listed in Offline validation & grace.

refresh()

Like status(), but contacts KeyGrant immediately, even during the wait after a failed check-in. Call it when the customer says they have paid. status(true) is the same call.

  • It contacts KeyGrant only when a license key is stored, or when a device trial whose lease no longer verifies still has time left.
  • If KeyGrant does not respond, it resolves the status from what the device holds.
startTrial()

Starts this device's trial with KeyGrant and resolves the trial's status: trial, expired with reason trial, or unlicensed. It resolves unknown when the license file cannot be read. See Trials.

deactivate()

Releases this device's seat and removes the license from this device. Resolves { released }, explained below.

upgradeUrl(link)

Your Stripe Payment Link with client_reference_id set to this device's activation ID (never the key), which names the key to upgrade (How KeyGrant finds the key). See Sell an upgrade.

  • Resolves null when no key and activation are stored, without reading link.
  • Otherwise it rejects when link is not an absolute URL. It also rejects when the license file cannot be read.

Activating

  • Activating the same key again on the same device keeps its seat.
  • Activating a different key replaces the stored license without releasing the old key's seat: call deactivate() first to free it.
  • activate() rejects when the license file cannot be read or the activation cannot be saved.

activate() answers one of:

invalid-key

The key is empty or longer than 120 characters (no request is made), or KeyGrant does not recognize it.

limit

The key is already active on as many devices as its device limit allows, or more after its limit was lowered.

revoked

The key is revoked, or past its expiry and grace period.

upgrade

This build is a newer major version than the key covers.

outdated

This build is an older major version than the key covers.

invalid-lease

KeyGrant returned a lease that does not verify on this device, for example one signed by a key this build does not carry. KeyGrant has assigned the seat, but nothing is stored on the device.

network

KeyGrant did not respond (offline, or a timeout), or returned any other response.

  • Any other response: a server error, a rate limit, a product ID it does not know, an archived product (on a device that holds no seat on the key), or a request it rejected as malformed.
  • activate() also fails with it, without a request, while an earlier call is stuck, or when the device cannot be identified, such as when this device activates the same key again and cannot read its machine ID in time.

Keys only their buyer can activate

When a key is locked to its buyer (Who can activate), pass what the customer typed as activate(key, { email, code }), asking for each only when an error names it. The steps and rules are in How every SDK behaves. Since 0.7.0.

App
let result = await license.activate(key)if (!result.ok && result.error === 'user-email-required') {  // Ask for the email the key was sold to.  result = await license.activate(key, { email })}if (!result.ok && result.error === 'user-code-required') {  // A code was just emailed to that address: ask for it.  result = await license.activate(key, { email, code })}if (!result.ok && result.error === 'try-again') {  // The code could not be emailed. Nothing was activated; offer to try again.}
  • The options argument comes in 0.7.0: in 0.6.0 and earlier, activate(key) takes none, and answers a locked key network.
  • importLicenseFile(content, { email, code }) passes them on to a key file's key.

The errors it answers:

user-email-required

The key is locked to its buyer and this device is new: ask for the email the key was sold to and call activate(key, { email }).

user-email-mismatch

That is not the email the key was sold to (or it is longer than any email, and no request is made). KeyGrant never says what the right one is.

user-code-required

The email was right and KeyGrant has just emailed a code to it: ask for the code and call activate(key, { email, code }).

user-code-invalid

The code is wrong, expired (it lasts 10 minutes) or already used, or is not six digits (no request is made). Ask again, or call activate(key, { email }) for a new code.

too-many-codes

Three codes have been emailed for this key this hour. Use the last one, or wait.

too-many-attempts

Too many wrong emails or codes for this key this hour. Try again later.

try-again

The code could not be emailed just now, and nothing was activated. Try again; it is nobody's mistake.

Restoring a license without the key

For an App Store build, which may not take a typed key: pickUp(email) emails the customer a six-digit code, and restoreByEmail(email, code) activates the key that fits this build. How a pickup works is in How every SDK behaves. Since 0.9.0.

App
// An App Store build: no key field. Ask for the email the customer bought with.await license.pickUp(email)// They type the code from the email:const result = await license.restoreByEmail(email, code)if (!result.ok && result.error === 'wrong-code') {  // Ask for the code again.} else if (!result.ok && result.error === 'code-expired') {  // Offer to send a new code: license.pickUp(email).} else if (!result.ok && result.error === 'no-key') {  // No purchase on that email.}
  • To let the customer choose, call pickUp(email, code), show each key by its modelName, and activate the one chosen with activate(picked.key, { email, code: picked.activationCode }).
  • restoreByEmail with the same email and code after pickUp(email, code), or again after an activation that got no answer, needs no new code (What is kept).
  • The requests are on the endpoints page.

The errors a pickup answers:

invalid-email

No email, one longer than any email (no request is made), or one KeyGrant could not read.

wrong-code

Not the code KeyGrant emailed, or not six digits (no request is made, and it costs no try). Ask again.

code-expired

The address has no live code: it expired (it lasts 10 minutes), was used, was voided by its fifth wrong try, or was never asked for. Call pickUp(email) for a new one.

too-many-attempts

Thirty wrong codes for the address this hour, or too many requests from this network. retryAfter says how many seconds to wait, when KeyGrant gives it.

pickup-disabled

The product's Key pickup is off.

try-again

KeyGrant could not take the request just now. Try again.

network

KeyGrant did not respond, or returned something that is not an answer.

no-key

From restoreByEmail(): the address has no live key of the product. Its other errors are upgrade and outdated (the address has keys, none for this build) and activate()'s own.

Deactivation

deactivate() resolves { released }:

releasedWhen
trueKeyGrant confirmed the release.
trueNothing was stored.
trueThe stored license is held to another machine's ID (for example a copied or restored profile). The seat stays with the other machine, and KeyGrant is not contacted.
falseKeyGrant could not confirm the release, because it did not respond or returned an error. Tell the customer the seat may still be in use.
  • A copied license that is not held to a machine ID is released like any other.
  • In every case the key, the activation and the lease are removed from this device. The record of any device trial is kept, and so is a stored offline license file (removeLicenseFile() drops that).
  • deactivate() rejects when an earlier call is stuck. It also rejects when the license file cannot be read or saved, and the key then stays on the device.

The status

status(), refresh() and startTrial() resolve the same status object. It does not include the license's expiry date or grace period. A trial status includes the trial's end.

state

One of six states, listed below.

reason

Why, on an expired, invalid or unknown status.

key

The stored license key.

  • Absent on trial, unlicensed and unknown, on expired with reason trial, and on invalid with reason tampered.
  • On a status from an offline license file (source, below): the key the file records, on licensed and on invalid with reason revoked-file, upgrade or outdated; absent on any other.
trialEndsAt

On trial: when it ends, in Unix milliseconds.

trialDaysLeft

On trial: whole days left, rounded up.

test

true on a licensed status from a test license. Absent, not false, otherwise.

entitlements

On licensed and trial, when the lease grants at least one: a frozen object of flags, numbers and text values. See Editions & entitlements.

meters

On licensed, from a key's lease that names meters: the key's meters by code, each { left, total, offline, unsynced, resets } (below). Answered offline. Absent on every other status.

Since 0.8.0

source

'file' when the status rests on a stored offline license file (below), licensed or not. Absent for a key's lease, a trial and no license. A status from a file has no seat to deactivate.

Since 0.6.0

licensedTo

Who the stored offline license file is licensed to, as it signs it: { name?, email? }, to show as "Licensed to Ada Lovelace".

  • On a status from a file that is licensed, or invalid with reason revoked-file, upgrade or outdated; absent otherwise.

Since 0.6.0

stalled

true when the status was read from storage without changes, because an earlier call has not finished. Restarting the app clears it.

States and reasons

statereasonMeans
licensedA valid lease for this key, this device and this build.
trialA valid trial lease.
expiredofflineThe lease has run out and has not been renewed, because KeyGrant did not respond, returned a response that neither renews nor refuses the key (a server error or a rate limit, for example), or has not been contacted again yet.
expiredtrialThe trial has ended, or its lease no longer verifies here.
invalidrevokedKeyGrant refused the key because it was revoked, it is past its expiry and grace period, or this device's seat was released.
invalidupgradeThis build is a newer major version than the key covers.
invalidoutdatedThis build is an older major version than the key covers.
invaliddeviceThe license was activated on another machine.
invalidtamperedThe lease does not verify, or is for another product, key or activation.
invalidunknown-keyThe lease was signed by a key this build does not carry, such as one the product switched to after this build was made. See Rotating the signing key.
invalidrevoked-fileThe stored offline license file is one this build lists as revoked (revokedLicenseFiles). See License files.
unlicensedNo key and no trial is stored.
unknownstorageThe license file could not be read. Keep running as before and check again shortly. Do not show the activation screen for this status.

status() never rejects because of a storage error: an unreadable file gives unknown, and if a save fails, the status still reflects KeyGrant's response. What to tell the customer for each reason is in Offline validation & grace.

License files

A customer can be sent a .kglicense file: a key file, which activates its key, or an offline license file, which licenses the app with no network. How each behaves is in How every SDK behaves, and what each costs under Delivered as in License models. Since 0.6.0.

Warning

Keep every key that signed a file in publicJwks. A build without it reads such a file as invalid with reason unknown-key.

  • LICENSE_FILE_EXTENSION is .kglicense, for an open dialog's filter or a drop handler's test.
  • importLicenseFile takes the file's content, never its path, so your app reads the file and hands over its bytes.
  • List the ids of leaked files in revokedLicenseFiles (Blocking a leaked file).
  • These are not the file the SDK stores its own state in (below).
Electron
// main processimport { app, dialog, ipcMain } from 'electron'import { readFile } from 'node:fs/promises'import { License, LICENSE_FILE_EXTENSION } from '@keygrant/electron'
const license = new License({ product: 'sluice', apiBaseUrl: 'https://api.keygrant.dev', publicJwks: KEYS.keys, major: 3, revokedLicenseFiles: [], // ids of files that leaked: an update adds them here})
// The renderer hands over a dropped file's bytes.ipcMain.handle('license:import', (_event, bytes: Uint8Array) => license.importLicenseFile(bytes))
// "Open license file..." in a menu.async function openLicenseFile() { const { filePaths } = await dialog.showOpenDialog({ filters: [{ name: 'License file', extensions: [LICENSE_FILE_EXTENSION.slice(1)] }], }) if (filePaths[0]) return license.importLicenseFile(await readFile(filePaths[0]))}
// macOS: a .kglicense the app is registered for, double-clicked in Finder.app.on('open-file', async (event, path) => { event.preventDefault() try { const result = await license.importLicenseFile(await readFile(path)) if (!result.ok) showError(result.error) } catch (error) { showError(error) // unreadable, not saved, or a license call still running: try again }})

A dropped file reaches the main process from the renderer through the preload script:

Electron
// preloadimport { contextBridge, ipcRenderer } from 'electron'contextBridge.exposeInMainWorld('licensing', {  importFile: (bytes: Uint8Array) => ipcRenderer.invoke('license:import', bytes),})
// rendererwindow.addEventListener('dragover', (event) => event.preventDefault())window.addEventListener('drop', async (event) => { event.preventDefault() const file = event.dataTransfer?.files[0] if (!file) return const result = await window.licensing.importFile(new Uint8Array(await file.arrayBuffer())) if (result.ok) showThanks(result.licensedTo.name ?? result.licensedTo.email) else showError(result.error)})

What importLicenseFile answers

It verifies the file offline (Importing a file) and resolves { ok: true, kind: 'key' | 'offline', licensedTo }, or { ok: false, error } with nothing stored. error is one of these, or, for a key file whose key did not activate, whatever activate() answers (above).

malformed

Not a license file's text, or its token or claims are broken.

not-a-license-file

A signed token, but not a license file (a lease, say).

bad-signature

Not signed by the key it names.

unknown-key

Signed by a key this build does not carry (publicJwks): a newer one, after the product rotated its key, which an update of the app carries. A key file's notes print its key, which can be typed meanwhile.

wrong-product

Another product's file.

revoked-file

The file's id is listed in revokedLicenseFiles.

upgrade

An offline file that does not cover this build: this build is newer than the file covers.

outdated

An offline file that does not cover this build: this build is older than the file covers.

  • It rejects when an earlier call on the License has not finished, when an offline file cannot be saved, and, for a key file, wherever activate() rejects.
  • A key file of a locked key takes the same { email, code } as activate() (above), and an offline file ignores it.

The status of a stored file

A stored offline file is checked again on every status(), which then resolves { state: 'licensed', source: 'file', key, licensedTo, ... } with the file's entitlements, and never asks KeyGrant, refresh() included.

  • Show "Licensed to Ada Lovelace" from licensedTo.
  • A status with source: 'file' has no seat to deactivate: offer removeLicenseFile() instead. deactivate() leaves the file alone.
  • Which wins when the device also holds a key, and the reasons a file's status can be invalid, are in The status of a stored file.

Metered uses

A license model can count uses of what it sells (see Metered uses). The app spends them with use(meter, n), before the work they count, and does the work only when the spend is ok. Since 0.8.0.

App
const spent = await license.use('exports') // use('exports', 40) spends forty at once, whole or noneif (spent.ok) {  runExport() // spent.left: what is left now, net of this device's unsynced spends} else if (spent.error === 'used-up') {  offerMoreExports(spent.left) // KeyGrant's word: fewer left than asked (0 when none)} else if (spent.error === 'offline-limit') {  askToConnect() // KeyGrant could not be reached, and this is past what this device may spend without it} else {  showMeterProblem(spent.error) // 'no-meter', 'not-licensed', 'revoked', 'device', 'upgrade', 'outdated', 'storage', 'invalid-amount'}
const { meters } = await license.status() // offline, from the leasemeters?.exports // { left: 950, total: 1000, offline: 100, unsynced: 0, resets: false }
  • use() resolves { ok: true, left }, with offline: true when the device spent from its offline allowance, or { ok: false, error }. It never rejects.
  • A request waits at most 5 seconds for KeyGrant, less when httpTimeoutMs is shorter.
  • deactivate() sends the unsynced spends with the release.
  • How a spend works online and offline, and why a meter never takes the license away, is in How every SDK behaves.

When use() does not spend

used-up

The meter has fewer uses left than asked. left says how many are left (0 when none), so the app can offer fewer. It is KeyGrant's own word, not a guess.

offline-limit

KeyGrant could not be reached, and the spend is past this device's offline allowance for the meter or past what it last heard is left. left says what the device may still spend. Ask the customer to connect.

no-meter

The key has no meter with that code.

not-licensed

No key's lease licenses this build on this device, so nothing was asked. A device trial and an offline license file have no meters.

revoked, upgrade, outdated, device

KeyGrant's verdict on the key, this build or this device, as status() says it.

storage

The license file could not be read just now. Try again shortly.

invalid-amount

n is not a whole number from 1 to 1,000,000,000. This is a bug in the app: nothing was asked.

status().meters

By meter code, on a licensed status from a key's lease that names meters. It is a plain object: to test a code that is also an Object.prototype member, use Object.hasOwn(status.meters ?? {}, code).

left

What is left: the uses the lease said were left, less what this device has spent since and not reported. Never below 0.

total

The uses the key holds, for good or for this billing period, with any added on top.

offline

How many uses this device may spend while KeyGrant cannot be reached.

unsynced

Uses this device spent that KeyGrant has not heard of yet.

resets

true for a meter that comes back to its full uses each billing period.

Adapters

Pass any of them in adapters; the SDK uses the default for each one you leave out. The default fingerprinter has 2 seconds to read the machine ID during a status check, and 10 seconds during an activation or a trial.

Adapter interfaces
interface StorageAdapter {  // readOnly: write nothing, not even a repair.  read(opts?: { readOnly?: boolean }): Promise<StoredState | null>;  write(state: StoredState): Promise<void>;  // Optional. When present, every save goes through it.  update?(merge: (latest: StoredState) => StoredState): Promise<void>;}
interface RegistryStash { readTrialStart(): Promise<number | null>; writeTrialStart(ms: number): Promise<void>;}
interface Clock { now(): number; // Unix milliseconds}
interface Fingerprinter { get(): Promise<string>; // Used when present. Only kind "machine" holds a license to its device. identify?(opts: { timeoutMs: number }): Promise<DeviceIdentity>;}
interface DeviceIdentity { fingerprint: string | null; kind?: "machine" | "hostname"; alternates: string[];}
interface HttpAdapter { // Must resolve, not throw, on a 4xx. post( url: string, body: unknown, opts?: { timeoutMs?: number }, ): Promise<{ status: number; json: unknown }>;}
  • fingerprint: a custom fingerprinter holds a license to its machine only if it implements identify and reports kind: "machine". If it implements only get(), licenses are never held to the device.
  • registry: the default stores nothing. A registry stash you supply is read when a trial starts, and the earliest start date it holds is sent with the request. The start date KeyGrant returns is then written back to it.
  • http: if your adapter throws, the SDK treats it as no response.

Where the license is stored

The default storage is one JSON file, <product>.json, in the per-user folder every SDK shares. The folders, and how a save works, are in Where the license is stored. defaultStoragePath(product) gives its path, and createFileStorage(path) keeps it somewhere else (below).

Other exports

createFileStorage(path, options?)

The default storage, at a path you choose. options.migrateFrom is an older path: whenever the file at the new path is missing, the state at the older path is copied to it. The older file is left in place.

defaultStoragePath(product)

Where the default storage keeps <product>.json (below).

fetchHttp(options?)

The default HTTP adapter, on the global fetch. options.timeoutMs is its fallback timeout.

nodeFingerprinter()

The default fingerprinter: the operating system's machine ID, hashed. See Device management.

hostnameFingerprint()

The host name fingerprinter: the host name, platform and architecture, hashed.

hashFingerprint(raw)

The SHA-256, in hex, of keygrant| followed by raw. The default fingerprint is this hash of machine-id| and the machine ID.

noopRegistry()

The default registry stash, which stores nothing.

systemClock()

The default clock, Date.now().

DEFAULT_HTTP_TIMEOUT_MS

15000, the default httpTimeoutMs.

LICENSE_FILE_EXTENSION

.kglicense, the extension of a license file, for an open dialog's filter.

Since 0.6.0

grants(entitlements, code)

Whether a status's entitlements grant code, by the rule has(code) uses, for a status already in hand.

MissingEntitlementError

What require(code) rejects with. Its entitlement is the code.

The package also exports these types:

  • LicenseConfig, LicenseStatus, LicenseState, LicenseReason, LicenseError, ActivateResult, DeactivateResult, Adapters, StorageAdapter, StoredState, RegistryStash, Clock, Fingerprinter, DeviceIdentity, HttpAdapter, Entitlement and Entitlements.
  • In 0.6.0: ImportLicenseFileResult, LicenseFileError, LicenseFileKind and LicensedTo.
  • In 0.7.0: ActivateOptions.
  • In 0.8.0: MeterStatus, UseResult, UseError, MeterFigure and SpendEntry.
  • In 0.9.0: PickedKey, PickupError, PickUpSentResult, PickUpKeysResult and RestoreResult.