# `@suaveplan/license` — validate _(no submodules)_ ## Purpose License-key validation: parse, verify signature, check version, check expiration, surface every failure as a typed enum on a discriminated union. `validateLicense` accepts `publicKeys` as either a `Map` or a plain `Record` (matching upstream), honours an optional `AbortSignal`, and emits the OpenTelemetry counter / histogram pair on every call. `isValidLicense` is a convenience boolean wrapper. The package's truncated-signature scheme (default 20 bytes) cannot use standard secp256k1 verification (which requires the full 64-byte signature). The upstream-compatible fallback hashes `payloadBytes || publicKey.p` with SHA-256 and compares the first N bytes against the embedded signature bytes — giving ~2^160 forgery resistance for the default 20-byte length. ## Features - Seven failure modes returned on the discriminated union: `MALFORMED`, `INVALID_ENCODING`, `INVALID_SIGNATURE`, `UNKNOWN_SIGNING_KEY`, `EXPIRED`, `UNSUPPORTED_VERSION`, `REVOKED` (`REVOKED` is surfaced by `LicenseManager.validate`). - Full-signature path uses `@suaveplan/crypto-curve`'s `secp256k1.verify` on the SHA-256 digest of `payloadBytes`. - Truncated-signature path uses the upstream hash-prefix scheme. - Zod boundary validation on `options`; failures raise `LicenseError(LICENSE.INVALID_INPUT)`. - OTel emission: counter `license.validate_total{license.valid, license.error?}` and histogram `license.validate_duration_ms`. - Pre-aborted `AbortSignal` rejects with `LicenseError(LICENSE.ABORTED)` before any crypto runs. ## Usage: Basic ```ts import { validateLicense } from "@suaveplan/license/validate/validate"; const v = await validateLicense(key, { publicKeys: { 0: { p: publicKeyBytes } }, }); if (v.success) { console.log("Tier:", v.details.packageTier); } else { console.warn("Invalid:", v.error, v.message); } ``` ## Usage: Advanced Validate with an injected `referenceDate`, `creationDate` for a relative-day expiry check, and an `AbortSignal` for cancellation: ```ts import { validateLicense } from "@suaveplan/license/validate/validate"; import { LicenseError } from "@suaveplan/license"; const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 100); try { const v = await validateLicense(key, { publicKeys: new Map([[0, { p }]]), referenceDate: new Date("2026-06-17T00:00:00Z"), creationDate: new Date("2026-06-01T00:00:00Z"), signal: ctrl.signal, }); if (v.success) { // ... } } catch (e) { if (e instanceof LicenseError && e.code === "LICENSE.ABORTED") { console.warn("Validation cancelled"); } } ``` ## API Reference ### `validateLicense(licenseKey, options): Promise` Options: `publicKeys` (required), `referenceDate?`, `creationDate?`, `signal?`. Returns the discriminated success/failure union. Throws only on `LicenseError(LICENSE.INVALID_INPUT)` (bad options) or `LicenseError(LICENSE.ABORTED)` (aborted signal); every other failure is surfaced on the result. ### `isValidLicense(licenseKey, options): Promise` Thin wrapper that returns `result.success`. ## Implementation Notes The truncated-signature scheme is what makes the keys compact (~50-60 chars vs ~120). For most consumer products the ~2^160 forgery resistance is generous; for higher-value licenses callers should opt into `useFullSignature: true` at generation time. Validation auto- detects which scheme the key uses by checking the signature byte length against `FULL_SIGNATURE_BYTES`. The OTel emission runs on every code path — including aborts and parse failures — so dashboards always see a complete success/failure ratio. The histogram's unit is documented as `ms`; the recorded value is `Date.now() - start`, a wall-clock measurement (acceptable here because validation latency is dominated by the crypto, not by clock skew).