first commit
This commit is contained in:
129
docs/security-stack/security/license/src/generate/generate.md
Normal file
129
docs/security-stack/security/license/src/generate/generate.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# `@suaveplan/license` — generate
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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<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.
|
||||
Reference in New Issue
Block a user