179 lines
11 KiB
Markdown
179 lines
11 KiB
Markdown
# @suaveplan/license - Compact, offline-verifiable, secp256k1-signed software-license-key format: 72-bit bitpacked payload (version + signing-key id + product id + tier + flags + time encoding + time value + feature mask + nonce) plus full or truncated (20-byte) signature, Crockford-Base32 encoded with signature-derived obfuscation. suaveplan-enterprise additions: SuaveplanError-typed errors, OTel counters/histograms/spans, Zod boundary validation, injectable Clock/Rng, AbortSignal propagation, pluggable RevocationStore. Universal.
|
|
|
|
Compact, offline-verifiable, secp256k1-signed software-license-key
|
|
format. Bit-for-bit wire-format compatible with upstream
|
|
`@suaveplan/license`. runs unchanged in Bun, Node, and the browser.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [base32](./src/codec/base32/base32.md) — Crockford-style Base32 codec used to serialise the packed payload and signature into a printable license-key string. Th…
|
|
- [base32](./src/codec/base32/base32.md) — Crockford-style Base32 codec used to serialise the packed payload and signature into a printable license-key string. Th…
|
|
- [bitpack](./src/codec/bitpack/bitpack.md) — Bit-precise pack/unpack primitives used to serialise the 72-bit license payload into 9 bytes. Plain `DataView` / typed-…
|
|
- [bitpack](./src/codec/bitpack/bitpack.md) — Bit-precise pack/unpack primitives used to serialise the 72-bit license payload into 9 bytes. Plain `DataView` / typed-…
|
|
- [obfuscation](./src/codec/obfuscation/obfuscation.md) — Per-key character obfuscation pass run after Base32 encoding. The first 15 non-dash characters of the encoded key (whic…
|
|
- [obfuscation](./src/codec/obfuscation/obfuscation.md) — Per-key character obfuscation pass run after Base32 encoding. The first 15 non-dash characters of the encoded key (whic…
|
|
- [payload](./src/codec/payload/payload.md) — `packPayload` and `unpackPayload` are the 72-bit payload codec on top of the bitpack primitives. They emit exactly 9 by…
|
|
- [payload](./src/codec/payload/payload.md) — `packPayload` and `unpackPayload` are the 72-bit payload codec on top of the bitpack primitives. They emit exactly 9 by…
|
|
- [constants](./src/constants/constants.md) — Wire-format constants for the license package. Every value here is part of the binary contract: the per-field b…
|
|
- [constants](./src/constants/constants.md) — Wire-format constants for the license package. Every value here is part of the binary contract: the per-field b…
|
|
- [flags](./src/flags/flags.md) — Bitmask helpers for the 5-bit `flags` field (`LicenseFlags`) and the 19-bit `featureMask` field. The flags field stores…
|
|
- [flags](./src/flags/flags.md) — Bitmask helpers for the 5-bit `flags` field (`LicenseFlags`) and the 19-bit `featureMask` field. The flags field stores…
|
|
- [generate](./src/generate/generate.md) — Top-level license-key generation, payload construction, and parse-only inspection. This is the place that brings togeth…
|
|
- [generate](./src/generate/generate.md) — Top-level license-key generation, payload construction, and parse-only inspection. This is the place that brings togeth…
|
|
- [manager](./src/manager/manager.md) — High-level `LicenseManager` class that bundles a public-key registry, an optional signing key, default `signingKeyId` /…
|
|
- [manager](./src/manager/manager.md) — High-level `LicenseManager` class that bundles a public-key registry, an optional signing key, default `signingKeyId` /…
|
|
- [otel](./src/otel/instruments.md) — OpenTelemetry instrumentation for the license package. Registers a single `@opentelemetry/api` meter (`@suaveplan/licen…
|
|
- [instruments](./src/otel/instruments.md) — OpenTelemetry instrumentation for the license package. Registers a single `@opentelemetry/api` meter (`@suaveplan/licen…
|
|
- [revocation-store](./src/revocation/revocation-store/revocation-store.md) — Pluggable revocation blocklist for the license package. The `RevocationStore` interface defines three methods (`add`, `…
|
|
- [revocation-store](./src/revocation/revocation-store/revocation-store.md) — Pluggable revocation blocklist for the license package. The `RevocationStore` interface defines three methods (`add`, `…
|
|
- [schemas](./src/schemas/schemas.md) — Zod schemas for every public input boundary in the license package. `generateLicense`, `validateLicense`, `createPayloa…
|
|
- [schemas](./src/schemas/schemas.md) — Zod schemas for every public input boundary in the license package. `generateLicense`, `validateLicense`, `createPayloa…
|
|
- [time](./src/time/time.md) — Time-encoding helpers for the four `TimeEncodingType` variants. The 72-bit payload encodes time in 18 bits with an addi…
|
|
- [time](./src/time/time.md) — Time-encoding helpers for the four `TimeEncodingType` variants. The 72-bit payload encodes time in 18 bits with an addi…
|
|
- [types](./src/types/license.md) — Public type surface for the license package, ported verbatim from the upstream `index.types.ts` at the wire-format leve…
|
|
- [license](./src/types/license.md) — Public type surface for the license package, ported verbatim from the upstream `index.types.ts` at the wire-format leve…
|
|
- [validate](./src/validate/validate.md) — License-key validation: parse, verify signature, check version, check expiration, surface every failure as a typed enum…
|
|
- [validate](./src/validate/validate.md) — License-key validation: parse, verify signature, check version, check expiration, surface every failure as a typed enum…
|
|
- [error-codes](./src/error-codes.md) — This module owns the stable error-code registry for `@suaveplan/license`. It exposes `LICENSE_CODES` (a frozen `defineC…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Highlights
|
|
|
|
- **72-bit bitpacked payload** (version + signing-key id + product id
|
|
- tier + flags + time encoding + time value + feature mask + nonce)
|
|
encoded as 9 bytes.
|
|
- **Truncated 20-byte signature by default** keeps keys ~50-60
|
|
characters; opt into 64-byte full signatures with
|
|
`useFullSignature: true`.
|
|
- **Crockford-style Base32 alphabet** (no `I`/`L`/`O`/`U` — paste-
|
|
friendly).
|
|
- **Signature-derived obfuscation pass** rotates the first 15 payload
|
|
characters by per-license offsets drawn from the signature.
|
|
- **Four time-encoding strategies**: `NONE`, `ABSOLUTE_DAYS`,
|
|
`RELATIVE_DAYS`, `ABSOLUTE_TIMESTAMP`.
|
|
- **19-bit feature mask** + 5-bit flag bitfield (TRIAL, UPGRADE,
|
|
MACHINE_LOCKED, SUBSCRIPTION, EDUCATIONAL).
|
|
- **Enterprise extras**: typed `LicenseError` registry, OTel
|
|
counters/histograms, Zod boundary validation, injectable `Clock` /
|
|
`Rng`, `AbortSignal` propagation, pluggable `RevocationStore`.
|
|
|
|
## Quick-start
|
|
|
|
```ts
|
|
import { secp256k1 } from "@suaveplan/crypto-curve";
|
|
import { LicenseManager, PackageTier } from "@suaveplan/license";
|
|
|
|
// 1. Generate a signing key (typically done once, off-line)
|
|
const secret = secp256k1.randomPrivateKey();
|
|
const publicKey = { p: secp256k1.getPublicKey(secret) };
|
|
|
|
// 2. Bind the manager to that key
|
|
const m = new LicenseManager({
|
|
signingKey: { s: secret, p: publicKey.p },
|
|
publicKeys: { 0: publicKey },
|
|
});
|
|
|
|
// 3. Mint a license key
|
|
const { key } = await m.generate({
|
|
productId: 5,
|
|
tier: PackageTier.PRO,
|
|
expirationDays: 365,
|
|
});
|
|
|
|
// 4. Hand the key to the user, validate it on incoming requests
|
|
const v = await m.validate(key);
|
|
if (v.success) {
|
|
console.log(`Tier ${v.details.packageTier}, expires ${v.details.expirationDate}`);
|
|
}
|
|
```
|
|
|
|
## Modules
|
|
|
|
Every feature module is a tree-shakeable subpath export:
|
|
|
|
| Module | Subpath | Docs |
|
|
|---|---|---|
|
|
| Error codes | `./error-codes/error-codes` | [src/error-codes.md](src/error-codes.md) |
|
|
| Constants | `./constants/constants` | [src/constants/constants.md](src/constants/constants.md) |
|
|
| Types + enums | `./types/license` | [src/types/license.md](src/types/license.md) |
|
|
| Base32 codec | `./codec/base32/base32` | [src/codec/base32/base32.md](src/codec/base32/base32.md) |
|
|
| Bitpack | `./codec/bitpack/bitpack` | [src/codec/bitpack/bitpack.md](src/codec/bitpack/bitpack.md) |
|
|
| Payload codec | `./codec/payload/payload` | [src/codec/payload/payload.md](src/codec/payload/payload.md) |
|
|
| Obfuscation | `./codec/obfuscation/obfuscation` | [src/codec/obfuscation/obfuscation.md](src/codec/obfuscation/obfuscation.md) |
|
|
| Time helpers | `./time/time` | [src/time/time.md](src/time/time.md) |
|
|
| Flag helpers | `./flags/flags` | [src/flags/flags.md](src/flags/flags.md) |
|
|
| Zod schemas | `./schemas/schemas` | [src/schemas/schemas.md](src/schemas/schemas.md) |
|
|
| OTel instruments | `./otel/instruments` | [src/otel/instruments.md](src/otel/instruments.md) |
|
|
| Generate | `./generate/generate` | [src/generate/generate.md](src/generate/generate.md) |
|
|
| Validate | `./validate/validate` | [src/validate/validate.md](src/validate/validate.md) |
|
|
| Manager | `./manager/manager` | [src/manager/manager.md](src/manager/manager.md) |
|
|
| Revocation | `./revocation/revocation-store/revocation-store` | [src/revocation/revocation-store/revocation-store.md](src/revocation/revocation-store/revocation-store.md) |
|
|
|
|
The package root (`.`) re-exports every public symbol from each module
|
|
for ergonomic single-import usage.
|
|
|
|
## Benchmarks
|
|
|
|
Captured with `mitata` on Intel(R) Xeon(R) CPU E5-2696 v4 @ 2.20GHz,
|
|
Bun 1.3.14 (Linux x64). Run yourself with
|
|
`bun src/__benchmarks__/license.bench.ts`.
|
|
|
|
| Benchmark | Mean | p75 | p99 |
|
|
|---|---|---|---|
|
|
| `base32Encode` (73 bytes) | 38.09 µs | 42.82 µs | 134.18 µs |
|
|
| `base32Decode` (73 bytes) | 65.06 µs | 70.70 µs | 189.76 µs |
|
|
| `packPayload` | 733.58 ns | 744.46 ns | 1.37 µs |
|
|
| `unpackPayload` | 1.16 µs | 1.18 µs | 1.75 µs |
|
|
| `generateLicense` (truncated) | 67.64 µs | 73.84 µs | 242.36 µs |
|
|
| `validateLicense` (truncated) | 40.52 µs | 42.73 µs | 116.04 µs |
|
|
|
|
A full generate + validate round-trip completes in ~108 µs on this
|
|
hardware, comfortably below the 1 ms p99 budget targeted by the
|
|
upstream enterprise audit.
|
|
|
|
## Revocation
|
|
|
|
Revocation is an opt-in pluggable layer. Wire any store implementing
|
|
the `RevocationStore` interface (sync or async); the package ships a
|
|
synchronous in-memory store with optional LRU eviction.
|
|
|
|
```ts
|
|
import {
|
|
createMemoryRevocationStore,
|
|
fingerprintLicense,
|
|
LicenseManager,
|
|
} from "@suaveplan/license";
|
|
|
|
const store = createMemoryRevocationStore({ maxEntries: 100_000 });
|
|
const m = new LicenseManager({ signingKey, publicKeys, revocationStore: store });
|
|
|
|
// Block a compromised key
|
|
await m.revoke(fingerprintLicense(compromisedKey));
|
|
|
|
// Subsequent validates of that key return { error: 'REVOKED' }
|
|
```
|
|
|
|
## Wire format
|
|
|
|
The on-the-wire layout is unchanged from upstream `@suaveplan/license`.
|
|
Each 9-byte payload packs nine fields MSB-first:
|
|
|
|
```text
|
|
| v(3) | kid(5) | pid(6) | tier(4) | flg(5) | te(2) | timeValue(18) | featureMask(19) | nonce(10) |
|
|
total = 72 bits
|
|
```
|
|
|
|
A complete key encodes `payload || signature` (29 bytes with truncated
|
|
signature, 73 bytes with full signature) as Base32, dashes inserted
|
|
every 5 chars for legibility, then runs the signature-derived
|
|
obfuscation pass over the first 15 payload chars. The
|
|
seed-from-key-region trick (chars 16-30 carry the obfuscation seed)
|
|
breaks the circular dependency between "decode needs the seed" and
|
|
"the seed comes from a decoded signature".
|
|
|
|
## License
|
|
|
|
MIT.
|
|
|