Files
2026-06-09 21:06:27 +01:00
..
2026-06-09 21:06:27 +01:00

@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.

Documentation tree

  • base32 — Crockford-style Base32 codec used to serialise the packed payload and signature into a printable license-key string. Th…
    • base32 — Crockford-style Base32 codec used to serialise the packed payload and signature into a printable license-key string. Th…
  • bitpack — Bit-precise pack/unpack primitives used to serialise the 72-bit license payload into 9 bytes. Plain DataView / typed-…
    • bitpack — Bit-precise pack/unpack primitives used to serialise the 72-bit license payload into 9 bytes. Plain DataView / typed-…
  • obfuscation — Per-key character obfuscation pass run after Base32 encoding. The first 15 non-dash characters of the encoded key (whic…
    • obfuscation — Per-key character obfuscation pass run after Base32 encoding. The first 15 non-dash characters of the encoded key (whic…
  • payload — packPayload and unpackPayload are the 72-bit payload codec on top of the bitpack primitives. They emit exactly 9 by…
    • payload — packPayload and unpackPayload are the 72-bit payload codec on top of the bitpack primitives. They emit exactly 9 by…
  • constants — Wire-format constants for the license package. Every value here is part of the binary contract: the per-field b…
    • constants — Wire-format constants for the license package. Every value here is part of the binary contract: the per-field b…
  • flags — Bitmask helpers for the 5-bit flags field (LicenseFlags) and the 19-bit featureMask field. The flags field stores…
    • flags — Bitmask helpers for the 5-bit flags field (LicenseFlags) and the 19-bit featureMask field. The flags field stores…
  • generate — Top-level license-key generation, payload construction, and parse-only inspection. This is the place that brings togeth…
    • generate — Top-level license-key generation, payload construction, and parse-only inspection. This is the place that brings togeth…
  • manager — High-level LicenseManager class that bundles a public-key registry, an optional signing key, default signingKeyId /…
    • manager — High-level LicenseManager class that bundles a public-key registry, an optional signing key, default signingKeyId /…
  • otel — OpenTelemetry instrumentation for the license package. Registers a single @opentelemetry/api meter (`@suaveplan/licen…
    • instruments — OpenTelemetry instrumentation for the license package. Registers a single @opentelemetry/api meter (`@suaveplan/licen…
  • revocation-store — Pluggable revocation blocklist for the license package. The RevocationStore interface defines three methods (add, `…
    • revocation-store — Pluggable revocation blocklist for the license package. The RevocationStore interface defines three methods (add, `…
  • schemas — Zod schemas for every public input boundary in the license package. generateLicense, validateLicense, `createPayloa…
    • schemas — Zod schemas for every public input boundary in the license package. generateLicense, validateLicense, `createPayloa…
  • time — Time-encoding helpers for the four TimeEncodingType variants. The 72-bit payload encodes time in 18 bits with an addi…
    • time — Time-encoding helpers for the four TimeEncodingType variants. The 72-bit payload encodes time in 18 bits with an addi…
  • types — Public type surface for the license package, ported verbatim from the upstream index.types.ts at the wire-format leve…
    • license — Public type surface for the license package, ported verbatim from the upstream index.types.ts at the wire-format leve…
  • validate — License-key validation: parse, verify signature, check version, check expiration, surface every failure as a typed enum…
    • validate — License-key validation: parse, verify signature, check version, check expiration, surface every failure as a typed enum…
  • error-codes — This module owns the stable error-code registry for @suaveplan/license. It exposes LICENSE_CODES (a frozen `defineC…

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

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
Constants ./constants/constants src/constants/constants.md
Types + enums ./types/license src/types/license.md
Base32 codec ./codec/base32/base32 src/codec/base32/base32.md
Bitpack ./codec/bitpack/bitpack src/codec/bitpack/bitpack.md
Payload codec ./codec/payload/payload src/codec/payload/payload.md
Obfuscation ./codec/obfuscation/obfuscation src/codec/obfuscation/obfuscation.md
Time helpers ./time/time src/time/time.md
Flag helpers ./flags/flags src/flags/flags.md
Zod schemas ./schemas/schemas src/schemas/schemas.md
OTel instruments ./otel/instruments src/otel/instruments.md
Generate ./generate/generate src/generate/generate.md
Validate ./validate/validate src/validate/validate.md
Manager ./manager/manager src/manager/manager.md
Revocation ./revocation/revocation-store/revocation-store 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.

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:

| 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.