Files
skills/docs/security-stack/security/license/src/manager/manager.md
2026-05-29 08:20:56 +01:00

4.9 KiB

@suaveplan/license — manager

(no submodules)

Purpose

High-level LicenseManager class that bundles a public-key registry, an optional signing key, default signingKeyId / productId, injectable Clock / Rng, and an optional RevocationStore into a single object. Most callers will use only the manager — the lower-level generateLicense / validateLicense functions are exposed for the rare case where the caller wants stateless calls.

LicenseManager.generate defaults useFullSignature: false (compact keys) to match upstream's caller-side default; passing { useFullSignature: true } opts into 64-byte signatures. revoke and isRevoked reach into the configured RevocationStore (lazily allocating an in-memory store on first revoke call if none was configured).

Features

  • 11 methods covering the full lifecycle: addPublicKey, setSigningKey, setRevocationStore, generate, generateTrial, validate, isValid, parse, revoke, isRevoked (+ constructor).
  • Quick-trial helper generateTrial({ expirationDays, productId?, tier?, features? }) that sets the TRIAL flag and uses RELATIVE_DAYS time encoding.
  • Injectable Clock is used both as the default referenceDate for validate and as the time base for createPayload absolute-day computations.
  • Injectable Rng flows through to createPayload for deterministic nonces.
  • Optional RevocationStore consulted before signature verification.
  • Throws LicenseError(LICENSE.NO_SIGNING_KEY) when generate is called without a configured signing key.

Usage: Basic

import { secp256k1 } from "@suaveplan/crypto-curve";
import { LicenseManager, PackageTier } from "@suaveplan/license";

const s = secp256k1.randomPrivateKey();
const p = secp256k1.getPublicKey(s);

const m = new LicenseManager({
    signingKey: { s, p },
    publicKeys: { 0: { p } },
});

const r = await m.generate({ productId: 5, tier: PackageTier.PRO });
const v = await m.validate(r.key); // { success: true, details: { ... } }

Usage: Advanced

A manager with deterministic test injection, a revocation store, and a multi-key registry:

import { secp256k1 } from "@suaveplan/crypto-curve";
import type { Clock, Rng } from "@suaveplan/types";
import {
    createMemoryRevocationStore,
    fingerprintLicense,
    LicenseManager,
} from "@suaveplan/license";

const kp1 = (() => {
    const s = secp256k1.randomPrivateKey();
    return { s, p: secp256k1.getPublicKey(s) };
})();
const kp2 = (() => {
    const s = secp256k1.randomPrivateKey();
    return { s, p: secp256k1.getPublicKey(s) };
})();

const clock: Clock = () => Date.UTC(2026, 5, 17, 12, 0, 0);
const rng: Rng = { nextUint32: () => 0xdead, nextFloat: () => 0 };
const store = createMemoryRevocationStore({ maxEntries: 10_000 });

const m = new LicenseManager({
    signingKey: { s: kp1.s },
    publicKeys: { 0: { p: kp1.p }, 1: { p: kp2.p } },
    defaultSigningKeyId: 0,
    defaultProductId: 5,
    clock,
    rng,
    revocationStore: store,
});

const trial = await m.generateTrial({ expirationDays: 14 });
await m.revoke(fingerprintLicense(trial.key));
const v = await m.validate(trial.key);
// → { success: false, error: 'REVOKED', message: 'License has been revoked' }

API Reference

class LicenseManager

Method Returns Notes
constructor(config?) LicenseManager All config fields optional
addPublicKey(keyId, publicKey) void Idempotent overwrite
setSigningKey(signingKey, keyId?) void Updates default signing key id when supplied
setRevocationStore(store) void Replaces an existing store
generate(options, genOptions?) Promise<LicenseGenerationResult> Throws NO_SIGNING_KEY if unconfigured
generateTrial(opts) Promise<LicenseGenerationResult> Sets TRIAL flag, RELATIVE_DAYS encoding
validate(licenseKey, options?) Promise<LicenseValidationResult> Consults revocation store first
isValid(licenseKey) Promise<boolean> Wraps validate
parse(licenseKey) LicenseParsedResult No verification
revoke(fingerprint) Promise<void> Lazily allocates store if none
isRevoked(fingerprint) Promise<boolean> Returns false if no store

interface GenerateTrialOptions

{ expirationDays: number, productId?: number, tier?: PackageTier, features?: number }.

Implementation Notes

The lazy-allocation behaviour of revoke (auto-creates an in-memory store on first call) means simple consumers can use revocation without constructing a store explicitly, while still letting power users plug in a custom backend. The manager's clock injection drives BOTH the generation-time getDayNumber (for absolute-day expirations) AND the validation-time referenceDate (when none is supplied) — keeping the two sides on the same simulated time line for deterministic tests.