Files
2026-05-27 23:47:53 +01:00

4.7 KiB

commands/license/license

(no submodules)

Purpose

Wraps @suaveplan/license LicenseManager into the five sub-commands the operator-facing license bin exposes: keygen, generate, validate, parse, revoke, plus an exportPublicKeyFile helper that derives a public-keys JSON from a freshly generated keypair file. Each sub-command translates a LicenseError into ServerKeysError(SERVER_KEYS_LICENSE_FAILED) with the original error preserved as cause, so the top-level runFunction wrapper still prints a stable code on failure. Input boundary errors raise SERVER_KEYS_INVALID_INPUT, and missing key files raise SERVER_KEYS_FILE_NOT_FOUND — all four error paths are exercised in the spec-conformance tests.

The on-disk JSON envelopes (LicenseKeypairFile, LicensePublicKeyFile) are deliberately minimal: hex-encoded secp256k1 secret + public, a numeric key id, and an ISO-8601 creation timestamp. Operators can edit them by hand without a tool. The revocation store is a JSON array of hex license fingerprints — created on first revoke, append-only.

Features

  • keygen(outputPath, keyId, options?) — fresh secp256k1 keypair via crypto-curve's secp256k1.randomPrivateKey() + getPublicKey().
  • generate(opts) — LicenseManager.generate wrapper with hex-encoded signing-key loading.
  • validate(licenseKey, opts) — public-keys JSON loader, supports either an object or an array layout.
  • parse(licenseKey, options?) — synchronous, no signature check.
  • revoke(licenseKey, opts) — appends to a JSON revocation-store file; creates the file if absent.
  • exportPublicKeyFile(keypairFile, outputPath) — derive the public- keys file from a freshly generated keypair (used by the bin's --export-pub <path> flag and by tests).

Usage: Basic

import {
    keygen,
    generate,
    validate,
    parse,
    revoke,
    exportPublicKeyFile,
} from "@suaveplan/server-keys/commands/license/license";
import { PackageTier } from "@suaveplan/license";

const kp = await keygen("/tmp/lic/keys.json", 1);
await exportPublicKeyFile(kp, "/tmp/lic/public-keys.json");

const lic = await generate({
    signingKeyPath: "/tmp/lic/keys.json",
    productId: 42,
    tier: PackageTier.PRO,
    expirationDays: 365,
});

const valid = await validate(lic.key, {
    publicKeysPath: "/tmp/lic/public-keys.json",
});
expect(valid.success).toBe(true);

const parsed = parse(lic.key);
expect(parsed.payload.productId).toBe(42);

await revoke(lic.key, { revocationStorePath: "/tmp/lic/revocations.json" });
const after = await validate(lic.key, {
    publicKeysPath: "/tmp/lic/public-keys.json",
});
// `revoke` modifies the in-memory store only inside the same Manager;
// the file-backed store is observed by the next caller that constructs
// a Manager with that store path (see bin/license.ts for the wiring).

Usage: Advanced

Flag a license as a TRIAL with a feature mask, then validate against a fixed reference date for deterministic expiration windows:

import { generate, validate } from "@suaveplan/server-keys/commands/license/license";
import { LicenseFlags, PackageTier } from "@suaveplan/license";

const lic = await generate({
    signingKeyPath: "/tmp/lic/keys.json",
    productId: 1,
    tier: PackageTier.BASIC,
    expirationDays: 30,
    flags: LicenseFlags.TRIAL,
    featureMask: 0b1011, // features 0, 1, 3
    useAbsoluteTime: false,
    useFullSignature: true,
});

const checked = await validate(lic.key, {
    publicKeysPath: "/tmp/lic/public-keys.json",
    referenceDate: new Date("2026-01-01T00:00:00Z"),
});

API Reference

  • keygen(outputPath, keyId, options?) => Promise<LicenseKeypairFile>
  • generate(opts: GenerateLicenseOptions) => Promise<LicenseGenerationResult>
  • validate(licenseKey, opts: ValidateLicenseOptions) => Promise<LicenseValidationResult>
  • parse(licenseKey, options?: LicenseCommandOptions) => LicenseParsedResult
  • revoke(licenseKey, opts: RevokeLicenseOptions) => Promise<string>
  • exportPublicKeyFile(keypairFile, outputPath) => Promise<LicensePublicKeyFile>

All option bags accept { logger?, signal? } for injection.

Implementation Notes

generate constructs a LicenseManager per call (instead of caching one across sub-commands) because each call's signing key may be different — the bin invocation is the granularity that owns a key. The revocation store loader tolerates ENOENT (no file yet) but treats any other I/O error as a LICENSE_FAILED failure so operators don't silently revoke into a misconfigured path. The optional signal parameter is checked at the boundary of each sub-command and at the start of generate/validate so an aborted run never burns CPU on the LicenseManager's hash + signature work.