Skip to main content
Introducing packages.sweber.dev
Documentation menuRevocation, devices and trials

Revocation, devices and trials

Revoke offline licenses, bind licenses to one device and show trial or expiry status.

Revoke licenses that are checked offline

An offline license stays valid until its exp. That becomes a problem after a refund or a leaked key, and lifetime licenses have no exp at all. A signed revocation list fixes this: you sign the revoked ids with the same private key, your app downloads the list now and then, and verifyLicense rejects every listed license.

import { signRevocationList } from "@sweberdev/integral";

// On your server, whenever a license is revoked:
const token = await signRevocationList({ ids: ["lic_123"], product: "my-app" }, privateKey);
// Serve `token` as text, e.g. from https://example.com/revocations
import { verifyLicense, verifyRevocationList } from "@sweberdev/integral";

const list = await verifyRevocationList(await (await fetch(url)).text(), {
  publicKey,
  product: "my-app",
});
const result = await verifyLicense(licenseKey, { publicKey, revocations: list });
// result.reason === "revoked" for listed licenses

The list is signed, so it can come from any host or be bundled with your app. Keep the last list you received and use it while offline. Plain ids you already trust (new Set(["lic_123"])) work too. From the terminal: npx integral revoke lic_123 lic_456 --product my-app > revoked.txt.

Bind a license to one device

machineId() hashes values that identify a device into an anonymous id. Only the hash is sent to you. Put it into the license, and verifyLicense rejects the license on every other device:

import os from "node:os";
import { machineId, signLicense, verifyLicense } from "@sweberdev/integral";

const machine = await machineId(os.hostname(), os.userInfo().username);
// Your server issues a license with { machine } after activation:
await signLicense({ product: "my-app", plan: "pro", machine }, privateKey);
// In the app:
await verifyLicense(licenseKey, { publicKey, machine }); // reason "wrong_machine" elsewhere

Choose values that survive updates but differ between devices. In desktop apps, an OS machine id (for example from node-machine-id) works well.

Trials and expiry warnings

Mark a trial with trial: true and give it an exp. licenseStatus() turns a verified license into what your UI needs:

import { licenseStatus } from "@sweberdev/integral";

const status = licenseStatus(result.license, { warnDays: 14 });
// { state: "active" | "expiring" | "expired" | "not_yet_valid",
//   trial, daysLeft, updatesEnded, updatesDaysLeft }
if (status.trial && status.state === "expiring") showBanner(`${status.daysLeft} days left`);