# `@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 ```ts 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: ```ts 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` 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.