first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
# `@suaveplan/license` — manager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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:
```ts
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.