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

4.6 KiB

@suaveplan/license — generate

(no submodules)

Purpose

Top-level license-key generation, payload construction, and parse-only inspection. This is the place that brings together every lower module: it packs the payload, signs the packed bytes with secp256k1 (via @suaveplan/crypto-curve) over a SHA-256 digest, optionally truncates the signature, Base32-encodes payload || signature, applies the signature-derived obfuscation pass, and emits OpenTelemetry counters and span attributes — all behind a single async function.

createPayload is the quick-builder: callers supply a partial QuickLicenseOptions bag, and the function fills sensible defaults (version, signingKeyId, tier, etc.) and computes the time-encoding fields from expirationDays / useAbsoluteTime. The Rng for the nonce and the Clock for absolute-day expirations are both injectable so tests run deterministically.

parseLicense is the inverse of generation up to (but not including) signature verification — it deobfuscates, Base32-decodes, and unpacks the payload, returning the three byte slices plus the decoded record. It does NOT verify the signature or check expiration; for that, use validateLicense.

Features

  • generateLicense — full generation pipeline with Zod boundary validation, OTel emission, and AbortSignal propagation.
  • createPayload — quick-builder with injectable Rng / Clock and Zod-validated input.
  • parseLicense — pure decode-and-unpack, returning the byte slices plus the decoded payload.
  • Wire-format bit-identical to upstream @suaveplan/license.

Usage: Basic

import { secp256k1 } from "@suaveplan/crypto-curve";
import {
    createPayload,
    generateLicense,
} from "@suaveplan/license/generate/generate";

const s = secp256k1.randomPrivateKey();
const result = await generateLicense(
    createPayload({ productId: 5, expirationDays: 365 }),
    { signingKey: { s }, useFullSignature: false }
);
console.log(result.key); // "XYZ12-ABCDE-..."

Usage: Advanced

Deterministic generation with Rng injection, AbortSignal, and truncated signature for a compact key:

import { secp256k1 } from "@suaveplan/crypto-curve";
import type { Rng } from "@suaveplan/types";
import { LicenseError } from "@suaveplan/license";
import {
    createPayload,
    generateLicense,
    parseLicense,
} from "@suaveplan/license/generate/generate";

const rng: Rng = { nextUint32: () => 0x123, nextFloat: () => 0 };
const payload = createPayload(
    { productId: 7, expirationDays: 90 },
    { rng }
);

const ctrl = new AbortController();
try {
    const r = await generateLicense(payload, {
        signingKey: { s: secp256k1.randomPrivateKey() },
        useFullSignature: false,
        signal: ctrl.signal,
    });
    const parsed = parseLicense(r.key);
    console.log("payload bytes:", parsed.payloadBytes.length); // 9
    console.log("signature bytes:", parsed.signatureBytes.length); // 20
} catch (e) {
    if (e instanceof LicenseError && e.code === "LICENSE.ABORTED") {
        console.log("cancelled");
    }
}

API Reference

generateLicense(payload, options): Promise<LicenseGenerationResult>

Pack + sign + encode + obfuscate. Options: signingKey (required), signingKeyIdOverride?, useFullSignature? (default true at the function level; LicenseManager overrides to false), signal?, rng? (forwarded to createPayload when this function is invoked from the manager). Throws LicenseError(LICENSE.INVALID_INPUT) on schema failure, LicenseError(LICENSE.ABORTED) on a pre-aborted signal.

createPayload(options?, injection?): LicensePayload

Quick-builder; defaults: productId=1, tier=PackageTier.BASIC, expirationDays=0 (perpetual), flags=0, features=0, useAbsoluteTime=false. injection.rng drives the 10-bit nonce; injection.clock drives getDayNumber when useAbsoluteTime.

parseLicense(licenseKey): LicenseParsedResult

Deobfuscate + Base32-decode + slice + unpack. Throws LicenseError(LICENSE.MALFORMED) when the decoded byte length is less than PAYLOAD_BYTES + 1, or when the Base32 decode surfaces an INVALID_ENCODING.

Implementation Notes

The signature scheme is upstream-compatible: SHA-256 over the 9-byte payload, then secp256k1 sign over the digest. The truncated path slices the first 20 bytes of the resulting compact signature; the full path keeps all 64. This means a key generated by upstream verifies under suaveplan and vice versa as long as the same SignKey/PublicKey byte material is used. The Rng default uses crypto.getRandomValues to build a 32-bit unsigned integer, which is then masked to the 10-bit nonce by the caller.