Initial Commit
This commit is contained in:
52
openspec/archive/2026-05-17-enterprise-license/proposal.md
Normal file
52
openspec/archive/2026-05-17-enterprise-license/proposal.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Add @suaveplan/license — port-main verbatim + enterprise stack
|
||||
|
||||
**Package**: `@suaveplan/license`
|
||||
**Status**: PROPOSED
|
||||
**Type**: migration (port-main wire-format verbatim) + enterprise enhancements (additive)
|
||||
|
||||
- **Spec delta**: ./specs/license/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
Software license-key issuance and validation. Ports `../suaveplan/packages/security/license/` verbatim at the wire-format level (72-bit bitpacked payload, secp256k1 signature truncated to 20 bytes by default, Crockford Base32 with signature-derived obfuscation) so genesis-generated keys interoperate byte-for-byte with upstream-generated keys. Layers the genesis enterprise stack on top: `SuaveplanError`-typed errors with a 12-code registry, Zod boundary validation, OpenTelemetry counters/histogram, `AbortSignal`, injectable `Clock`/`Rng`, sibling `.md` docs ≥200 words each, mitata benchmarks, fast-check property tests, dep-graph test, spec-conformance test, 100/100/100/100 coverage. Adds a pluggable `RevocationStore` with an in-memory implementation as an opt-in offline blocklist — does not change the wire format.
|
||||
|
||||
Unblocks `@suaveplan/server-keys` (in flight; `enterprise-server-server-keys`).
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package at version `0.1.0`. Public surface (every symbol from upstream `index.ts`/`index.types.ts`/`bitpack.ts`/`obfuscation.ts`, plus enterprise additions):
|
||||
|
||||
- Codec: `base32Encode` / `base32Decode`, `SimpleBitPacker` / `SimpleBitUnpacker`, `packPayload` / `unpackPayload`, `obfuscateLicenseKey` / `deobfuscateLicenseKey` / `extractSignatureFromKey`
|
||||
- Time: `getDayNumber`, `dayNumberToDate`, `checkExpiration` — `Clock` injectable
|
||||
- Flags: `hasFlag`, `combineFlags`, `hasFeature`, `combineFeatures`, `getEnabledFeatures`
|
||||
- Generation: `generateLicense`, `createPayload` (Rng injectable), `parseLicense`
|
||||
- Validation: `validateLicense`, `isValidLicense` — full + truncated signature paths
|
||||
- Manager: `LicenseManager` class with `addPublicKey` / `setSigningKey` / `setRevocationStore` / `generate` / `generateTrial` / `validate` / `isValid` / `parse` / `revoke` / `isRevoked`
|
||||
- Revocation: `RevocationStore` interface, `createMemoryRevocationStore` (LRU), `fingerprintLicense`, `LicenseValidationError.REVOKED` (additive enum variant; the other 6 variants are preserved verbatim)
|
||||
- Errors: `LicenseError extends SuaveplanError<LicenseErrorCode>` with 12 codes (`LICENSE.{INVALID_INPUT, INVALID_ENCODING, MALFORMED, PAYLOAD_OUT_OF_RANGE, UNSUPPORTED_VERSION, UNKNOWN_SIGNING_KEY, INVALID_SIGNATURE, EXPIRED, REVOKED, NO_SIGNING_KEY, FEATURE_INDEX_OUT_OF_RANGE, ABORTED}`)
|
||||
- Schemas: `LicensePayloadSchema`, `LicenseGenerationOptionsSchema`, `LicenseValidationOptionsSchema`, `QuickLicenseOptionsSchema` (Zod)
|
||||
- OTel: counter `license.generate_total`, counter `license.validate_total{license.valid}`, histogram `license.validate_duration_ms`
|
||||
- Enums + types preserved verbatim from upstream: `TimeEncodingType`, `LicenseFlags`, `PackageTier`, `LicenseValidationError`, `LicensePayload`, `LicenseDetails`, `LicenseGenerationOptions`, `LicenseGenerationResult`, `LicenseValidationOptions`, `LicenseValidationSuccess`, `LicenseValidationFailure`, `LicenseValidationResult`, `LicenseParsedResult`, `LicenseManagerConfig`, `QuickLicenseOptions`, `KeyRegistry`
|
||||
- Constants verbatim from upstream: `LICENSE_VERSION` (=2), `FULL_SIGNATURE_BYTES` (=64), `SIGNATURE_BYTES` (=20), `MIN_SIGNATURE_BYTES` (=16), `BASE32_ALPHABET`, `LICENSE_SCHEMA`, plus genesis-added `PAYLOAD_BYTES` (=9)
|
||||
- Subpath exports per feature module
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Network-based revocation (HTTP CRL/OCSP).** Follow-up package; the shipped `RevocationStore` interface accepts async implementations so the follow-up is non-breaking.
|
||||
- **Hardware-fingerprint enforcement.** `MACHINE_LOCKED` enum value is preserved (wire format); enforcement is application policy.
|
||||
- **License rotation workflows.** Business logic, owned by callers.
|
||||
- **Schema-registry integration.** Wire format is self-describing via the 3-bit `version` field.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Capability | Upstream source | Notes |
|
||||
|---|---|---|
|
||||
| Wire format (72-bit payload, sig modes, Base32, obfuscation) | `../suaveplan/packages/security/license/src/{index,index.types,bitpack,obfuscation}.ts` | verbatim |
|
||||
| Enterprise audit requirements (no React/Node deps, Zod, fast-check, deterministic Clock/Rng, OTel, structured errors, AbortSignal, i18n-ready, 100% coverage, 200-word `.md`) | `../suaveplan/enterprise/license.md` L37-L63 | all addressed |
|
||||
| `LicenseError` + `LICENSE_*` code registry, OTel instruments, `RevocationStore`, `Clock`/`Rng` injection, `AbortSignal` plumbing, dep-graph + spec-conformance tests, mitata benchmarks | genesis-original | genesis-wide enterprise policy (`CLAUDE.md`, `openspec/AGENTS.md`) — upstream throws plain Errors and has no OTel/Zod/revocation |
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
None today — net-new on genesis. First consumer will be `@suaveplan/server-keys` (separate change `enterprise-server-server-keys`).
|
||||
@@ -0,0 +1,216 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: license (enterprise-license)
|
||||
|
||||
**Package**: `@suaveplan/license`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Crockford Base32 codec
|
||||
|
||||
The package SHALL provide `base32Encode(bytes): string` and `base32Decode(str): Uint8Array` using `BASE32_ALPHABET` (omits I/L/O/U), encoder inserts `-` every 5 chars, decoder is dash-tolerant and case-insensitive. Non-alphabet characters SHALL raise `LicenseError(LICENSE_INVALID_ENCODING)`.
|
||||
|
||||
#### Scenario: round-trip
|
||||
|
||||
- **WHEN** `base32Decode(base32Encode(b))` is evaluated
|
||||
- **THEN** the result equals `b`
|
||||
|
||||
**Verified by:** `src/codec/base32/base32.test.ts` (`describe("base32Encode / base32Decode")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Bit-precise pack/unpack primitives
|
||||
|
||||
The package SHALL provide `SimpleBitPacker.packUint(value, bits)` / `getBytes()` / `getBitCount()` and `SimpleBitUnpacker.unpackUint(bits)` / `getBitPosition()`. Out-of-range values SHALL throw; reads past end SHALL throw a buffer-underflow error.
|
||||
|
||||
#### Scenario: round-trip
|
||||
|
||||
- **WHEN** values are packed in order and then unpacked with the same widths
|
||||
- **THEN** the unpacked values equal the originals
|
||||
|
||||
**Verified by:** `src/codec/bitpack/bitpack.test.ts` (`describe("SimpleBitPacker / SimpleBitUnpacker")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 72-bit payload pack and unpack
|
||||
|
||||
`packPayload` SHALL return exactly 9 bytes; `unpackPayload` SHALL require 9 bytes. Field widths SHALL match `LICENSE_SCHEMA`. Out-of-range fields SHALL raise `LicenseError(LICENSE_PAYLOAD_OUT_OF_RANGE)`; wrong-length input SHALL raise `LicenseError(LICENSE_MALFORMED)`.
|
||||
|
||||
#### Scenario: every field round-trips
|
||||
|
||||
- **WHEN** `unpackPayload(packPayload(p))` is evaluated
|
||||
- **THEN** every field equals the original
|
||||
|
||||
**Verified by:** `src/codec/payload/payload.test.ts` (`describe("packPayload / unpackPayload")`) AND `src/license.spec-conformance.test.ts::"LICENSE.PAYLOAD_OUT_OF_RANGE is thrown from packPayload"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Signature-derived obfuscation pass
|
||||
|
||||
`obfuscateLicenseKey` / `deobfuscateLicenseKey` SHALL rotate the first 15 non-dash chars within `BASE32_ALPHABET` by per-char offsets from the supplied seed. `extractSignatureFromKey` SHALL derive the seed from alphabet indices of unobfuscated chars 16-30 so the seed is recoverable from the obfuscated key. Keys with fewer than 15 non-dash chars SHALL pass through unchanged.
|
||||
|
||||
#### Scenario: round-trip
|
||||
|
||||
- **WHEN** `deobfuscateLicenseKey(obfuscateLicenseKey(k, s), s)` is evaluated
|
||||
- **THEN** the result equals `k`
|
||||
|
||||
**Verified by:** `src/codec/obfuscation/obfuscation.test.ts` (`describe("obfuscation")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Four time-encoding strategies with injectable Clock
|
||||
|
||||
`checkExpiration(type, value, refDate?, creationDate?)` SHALL implement `TimeEncodingType.{NONE, ABSOLUTE_DAYS, RELATIVE_DAYS, ABSOLUTE_TIMESTAMP}`. `getDayNumber(date?, clock?)` and other time helpers SHALL accept an optional `Clock` from `@suaveplan/types`, defaulting to wall time.
|
||||
|
||||
#### Scenario: each strategy's boundary
|
||||
|
||||
- **WHEN** `checkExpiration` is invoked for each strategy with reference dates straddling the expiration boundary
|
||||
- **THEN** `isExpired` flips at the boundary; `NONE` is always false; `RELATIVE_DAYS` uses `creationDate` as the baseline
|
||||
|
||||
**Verified by:** `src/time/time.test.ts` (`describe("time helpers")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Flags and feature-mask helpers
|
||||
|
||||
`hasFlag`, `combineFlags`, `hasFeature`, `combineFeatures`, `getEnabledFeatures` SHALL operate on the 5-bit flag and 19-bit feature mask. `hasFeature` / `combineFeatures` SHALL raise `LicenseError(LICENSE_FEATURE_INDEX_OUT_OF_RANGE)` for indices outside `[0, 18]`.
|
||||
|
||||
#### Scenario: out-of-range index throws
|
||||
|
||||
- **WHEN** `hasFeature(0, 19)` or `combineFeatures(0, 19)` is called
|
||||
- **THEN** `LicenseError(LICENSE_FEATURE_INDEX_OUT_OF_RANGE)` is thrown
|
||||
|
||||
**Verified by:** `src/flags/flags.test.ts` (`describe("flag and feature helpers")`) AND `src/license.spec-conformance.test.ts::"LICENSE.FEATURE_INDEX_OUT_OF_RANGE is thrown from hasFeature/combineFeatures"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: License generation with full and truncated signatures
|
||||
|
||||
`generateLicense(payload, options)` SHALL pack payload, sign with secp256k1 over SHA-256 digest via `@suaveplan/crypto-curve` + `@suaveplan/crypto-hash`, optionally embed a truncated (default-20-byte) hash-prefix verifier, Base32-encode, obfuscate. `createPayload(options?, injection?)` SHALL fill `LicensePayload` defaults from `QuickLicenseOptions` with an injectable `Rng` for nonce and `Clock` for absolute-day expirations. `parseLicense(key)` SHALL decode without verification.
|
||||
|
||||
#### Scenario: generated key parses back to its payload
|
||||
|
||||
- **WHEN** `parseLicense(generateLicense(p, { signingKey }).key).payload` is evaluated
|
||||
- **THEN** the result equals `p` (after `signingKeyIdOverride` applied)
|
||||
|
||||
**Verified by:** `src/generate/generate.test.ts` (`describe("generateLicense")`, `describe("createPayload")`, `describe("parseLicense")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: License validation discriminated-union result
|
||||
|
||||
`validateLicense(key, options)` SHALL return `{ success: true, details }` or `{ success: false, error, message, partialDetails? }` covering seven failure modes: `MALFORMED`, `INVALID_ENCODING`, `INVALID_SIGNATURE`, `UNKNOWN_SIGNING_KEY`, `EXPIRED`, `UNSUPPORTED_VERSION`, `REVOKED`. Full 64-byte sigs SHALL verify via standard secp256k1; truncated sigs SHALL verify via the hash-prefix scheme (SHA-256 over `payloadBytes || publicKey.p`, compare first N bytes). Accepts `publicKeys` as `Map<number, PublicKey>` or `Record<number, PublicKey>`. `isValidLicense` SHALL be a boolean convenience wrapper.
|
||||
|
||||
#### Scenario: tampered payload yields INVALID_SIGNATURE
|
||||
|
||||
- **WHEN** any byte of a valid key's payload portion is flipped and the key is validated
|
||||
- **THEN** the result has `error === LicenseValidationError.INVALID_SIGNATURE`
|
||||
|
||||
**Verified by:** `src/validate/validate.test.ts` (`describe("validateLicense")`, `describe("isValidLicense")`) AND `src/license.spec-conformance.test.ts` (`describe("spec-conformance (f) — LicenseValidationError literal-string enum")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: LicenseManager class
|
||||
|
||||
`LicenseManager` SHALL accept `LicenseManagerConfig` (publicKeys / signingKey / defaultSigningKeyId / defaultProductId / clock / rng / revocationStore) and expose `addPublicKey`, `setSigningKey`, `setRevocationStore`, `generate`, `generateTrial`, `validate`, `isValid`, `parse`, `revoke`, `isRevoked`. Calling `generate` without a configured signing key SHALL raise `LicenseError(LICENSE_NO_SIGNING_KEY)`.
|
||||
|
||||
#### Scenario: roundtrip via manager
|
||||
|
||||
- **WHEN** a manager configured with `signingKey` + matching `publicKey` generates a key and validates it
|
||||
- **THEN** `validate` returns `{ success: true }`
|
||||
|
||||
**Verified by:** `src/manager/manager.test.ts` (`describe("LicenseManager")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Pluggable RevocationStore with in-memory implementation
|
||||
|
||||
The package SHALL ship `RevocationStore` (interface: `add` / `has` / `list`, each `void|Promise<void>` / `boolean|Promise<boolean>` / `readonly string[]|Promise<readonly string[]>`), `createMemoryRevocationStore({ maxEntries? })` with LRU eviction, and `fingerprintLicense(key)` returning SHA-256 hex. A manager configured with a revocation store SHALL return `LicenseValidationFailure{error: LicenseValidationError.REVOKED}` for any key whose fingerprint is in the store. Async stores SHALL be awaited.
|
||||
|
||||
#### Scenario: revoked key validates as REVOKED
|
||||
|
||||
- **WHEN** `manager.revoke(fingerprintLicense(key))` is called and then `manager.validate(key)` is awaited
|
||||
- **THEN** the result has `error === LicenseValidationError.REVOKED`
|
||||
|
||||
**Verified by:** `src/revocation/revocation-store/revocation-store.test.ts` (`describe("createMemoryRevocationStore")`, `describe("fingerprintLicense")`) AND `src/license.spec-conformance.test.ts::"LICENSE.REVOKED surfaced on manager.validate when fingerprint is in store"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Zod schemas guard every public input
|
||||
|
||||
`LicensePayloadSchema`, `LicenseGenerationOptionsSchema`, `LicenseValidationOptionsSchema`, `QuickLicenseOptionsSchema` SHALL validate at boundary. Invalid input SHALL raise `LicenseError(LICENSE_INVALID_INPUT)` with the `ZodError` as `cause`.
|
||||
|
||||
#### Scenario: out-of-range field rejected
|
||||
|
||||
- **WHEN** `generateLicense({ ...valid, productId: 64 } as never, { signingKey })` is awaited
|
||||
- **THEN** the promise rejects with `LicenseError(LICENSE_INVALID_INPUT)` whose `cause` is a `ZodError`
|
||||
|
||||
**Verified by:** `src/schemas/schemas.test.ts` (`describe("zod schemas")`) AND `src/license.spec-conformance.test.ts` (`describe("spec-conformance (b) — every Zod boundary is wired")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OpenTelemetry counters, histogram, and attributes
|
||||
|
||||
The package SHALL register `license.generate_total` (counter), `license.validate_total` (counter, attribute `license.valid: boolean`), and `license.validate_duration_ms` (histogram, unit `ms`) via `@opentelemetry/api`. Validation paths SHALL emit attributes regardless of success/failure.
|
||||
|
||||
#### Scenario: instruments fire from production paths
|
||||
|
||||
- **WHEN** generation and validation paths run against a real in-memory `MeterProvider`
|
||||
- **THEN** both counters increment and the histogram has observations
|
||||
|
||||
**Verified by:** `src/otel/instruments.test.ts` (`describe("OTel instruments")`) AND `src/license.spec-conformance.test.ts` (`describe("spec-conformance (c) — OTel instruments fire from production paths")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AbortSignal propagates to async operations
|
||||
|
||||
`generateLicense`, `validateLicense`, `LicenseManager.generate`, `LicenseManager.validate` SHALL accept `signal?: AbortSignal`. Pre-aborted or in-flight aborts SHALL reject with `LicenseError(LICENSE_ABORTED)`.
|
||||
|
||||
#### Scenario: aborted signal rejects
|
||||
|
||||
- **WHEN** `generateLicense(payload, { signingKey, signal: AbortSignal.abort() })` is awaited
|
||||
- **THEN** the promise rejects with `LicenseError(LICENSE_ABORTED)`
|
||||
|
||||
**Verified by:** `src/license.spec-conformance.test.ts` (`describe("spec-conformance (d) — AbortSignal propagates")`) AND `src/license.spec-conformance.test.ts::"LICENSE.ABORTED is thrown from generateLicense / validateLicense"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Every LICENSE_* code is thrown from a production path
|
||||
|
||||
`LICENSE_CODES` SHALL define exactly 12 codes (`INVALID_INPUT`, `INVALID_ENCODING`, `MALFORMED`, `PAYLOAD_OUT_OF_RANGE`, `UNSUPPORTED_VERSION`, `UNKNOWN_SIGNING_KEY`, `INVALID_SIGNATURE`, `EXPIRED`, `REVOKED`, `NO_SIGNING_KEY`, `FEATURE_INDEX_OUT_OF_RANGE`, `ABORTED`). Each SHALL be raised by at least one production call site or surfaced via a `LicenseValidationFailure.error` enum mapping.
|
||||
|
||||
#### Scenario: every code is exercised
|
||||
|
||||
- **WHEN** the spec-conformance test exercises each code's production path
|
||||
- **THEN** every `LICENSE_*` code appears in the test assertions
|
||||
|
||||
**Verified by:** `src/license.spec-conformance.test.ts` (`describe("spec-conformance (a) — every LICENSE_* code is thrown")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Universal-tier purity, subpath exports, sibling docs
|
||||
|
||||
The package SHALL run unchanged in Bun, Node, and the browser. Its `package.json` SHALL declare a subpath export per feature module plus the `.` barrel. Every implementation `.ts` (excluding tests, types, barrels, benchmarks) SHALL have a sibling `.md` of ≥200 words with Purpose / Features / Usage: Basic / Usage: Advanced / API Reference / Implementation Notes. No `export *` in any feature barrel. All test files SHALL import runner primitives from `@suaveplan/testing/runner`.
|
||||
|
||||
#### Scenario: package shape is clean
|
||||
|
||||
- **WHEN** dep-graph and conformance tests inspect file tree, package.json, and barrels
|
||||
- **THEN** every assertion holds
|
||||
|
||||
**Verified by:** `tests/dep-graph.test.ts` (`describe("dep-graph and subpath exports")`) AND `src/license.spec-conformance.test.ts` (`describe("spec-conformance — sibling .md presence (sentinel)")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 100% coverage + property-based round-trip and tamper-detection
|
||||
|
||||
The package SHALL enforce 100/100/100/100 statements/branches/functions/lines via Istanbul. `tests/property.test.ts` SHALL use fast-check to assert `unpackPayload(packPayload(p))` equals `p` for arbitrary valid payloads AND any 1-byte mutation surfaces as `INVALID_SIGNATURE` or `MALFORMED`.
|
||||
|
||||
#### Scenario: coverage gate
|
||||
|
||||
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/license` runs
|
||||
- **THEN** all four thresholds are 100
|
||||
|
||||
**Verified by:** `tests/property.test.ts` (`describe("property tests")`)
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
52
openspec/archive/2026-05-17-enterprise-license/tasks.md
Normal file
52
openspec/archive/2026-05-17-enterprise-license/tasks.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Tasks — enterprise-license
|
||||
|
||||
> All checkboxes ticked reflect work done in this change. Package was implemented in one session; spec authored after impl per "collapse to one change" decision (matches `§13` reconciliation pattern).
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/license.md` end-to-end
|
||||
- [x] 1.2 Read upstream `index.ts` / `index.types.ts` / `bitpack.ts` / `obfuscation.ts`
|
||||
- [x] 1.3 Draft proposal + spec delta (one terse `### Requirement` per feature module, every Verified-by line points at a real `it(...)` or `describe(...)` title)
|
||||
|
||||
## 2. Implementation (port-main verbatim + enterprise stack)
|
||||
|
||||
- [x] 2.1 Scaffold: `package.json` v0.1.0, 16 subpath exports, tsconfig + tsconfig.lib, vite.config, vitest.config (`pool: forks`, `provider: istanbul`, 100/100/100/100), tests/setup/polyfills
|
||||
- [x] 2.2 `src/error-codes.ts` + `.md` — 12-code `LICENSE.*` registry + `LicenseError extends SuaveplanError<LicenseErrorCode>`
|
||||
- [x] 2.3 `src/constants/constants.ts` + `.md` — `LICENSE_VERSION`, `FULL_SIGNATURE_BYTES`, `SIGNATURE_BYTES`, `MIN_SIGNATURE_BYTES`, `BASE32_ALPHABET`, `LICENSE_SCHEMA`, `FIELD_BITS`, `PAYLOAD_BYTES`
|
||||
- [x] 2.4 `src/types/license.ts` + `.md` — every enum/interface/type from upstream `index.types.ts` verbatim
|
||||
- [x] 2.5 `src/codec/base32/base32.{ts,test.ts,md}`
|
||||
- [x] 2.6 `src/codec/bitpack/bitpack.{ts,test.ts,md}`
|
||||
- [x] 2.7 `src/codec/payload/payload.{ts,test.ts,md}`
|
||||
- [x] 2.8 `src/codec/obfuscation/obfuscation.{ts,test.ts,md}`
|
||||
- [x] 2.9 `src/time/time.{ts,test.ts,md}` — `Clock` injection
|
||||
- [x] 2.10 `src/flags/flags.{ts,test.ts,md}`
|
||||
- [x] 2.11 `src/schemas/schemas.{ts,test.ts,md}` — Zod boundary schemas
|
||||
- [x] 2.12 `src/otel/instruments.{ts,test.ts,md}` — counters + histogram + attributes
|
||||
- [x] 2.13 `src/generate/generate.{ts,test.ts,md}` — `generateLicense`, `createPayload`, `parseLicense`; OTel, Zod, AbortSignal, Rng/Clock injection
|
||||
- [x] 2.14 `src/validate/validate.{ts,test.ts,md}` — full + truncated sig paths; OTel; AbortSignal
|
||||
- [x] 2.15 `src/manager/manager.{ts,test.ts,md}` — full `LicenseManager` surface incl. `revoke` / `isRevoked`
|
||||
- [x] 2.16 `src/revocation/revocation-store/revocation-store.{ts,test.ts,md}` — `RevocationStore` interface + `createMemoryRevocationStore` (LRU) + `fingerprintLicense`
|
||||
- [x] 2.17 `src/index.ts` — named re-exports only
|
||||
- [x] 2.18 `README.md`
|
||||
- [x] 2.19 `src/__benchmarks__/license.bench.ts` (mitata)
|
||||
- [x] 2.20 `tests/property.test.ts` (fast-check), `tests/dep-graph.test.ts`, `src/license.spec-conformance.test.ts`
|
||||
|
||||
## 3. Gates
|
||||
|
||||
- [x] 3.1 `bunx turbo lint typecheck test --filter=@suaveplan/license` — 11/11 green, 15 test files, 158 tests pass
|
||||
- [x] 3.2 `bunx turbo test:coverage --filter=@suaveplan/license` — 100/100/100/100 across every module
|
||||
- [x] 3.3 `bunx turbo build --filter=@suaveplan/license` — green
|
||||
- [x] 3.4 `bun scripts/openspec-conformance.ts @suaveplan/license` — PASS (0 errors, 3 informational warnings about schemas-invoked-via-wrapper false positive)
|
||||
- [x] 3.5 Banned-pattern grep — clean (3 hits are the word "any" in JSDoc prose, not type annotations)
|
||||
- [x] 3.6 No `export *` in any feature barrel
|
||||
- [x] 3.7 No direct `vitest`/`bun:test` imports
|
||||
- [x] 3.8 Every implementation `.ts` has a sibling `.md` (≥200 words)
|
||||
|
||||
> Note: `bun run check` (monorepo-wide Biome) reports 1619 errors / 14 warnings but all are pre-existing in other packages; `bunx turbo lint --filter=@suaveplan/license` is clean.
|
||||
|
||||
## 4. Archive
|
||||
|
||||
- [ ] 4.1 Fold `specs/license/spec.md` → `openspec/specs/license/spec.md`
|
||||
- [ ] 4.2 Move `openspec/changes/enterprise-license/` → `openspec/archive/2026-05-17-enterprise-license/`
|
||||
- [ ] 4.3 Refresh `MANIFEST.md` (`bun scripts/folder-reorg/generate-manifest.ts`)
|
||||
- [ ] 4.4 Update root `STATUS.md` — `@suaveplan/license` `–` → `✅ enterprise`
|
||||
Reference in New Issue
Block a user