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, andAbortSignalpropagation.createPayload— quick-builder with injectableRng/Clockand 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.