Initial Commit
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: server-keys (enterprise-server-keys)
|
||||
|
||||
**Package**: `@suaveplan/server-keys`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Top-level commander dispatcher with 17 sub-commands
|
||||
|
||||
The package SHALL ship a `suaveplan` bin that uses `commander` to route to all 17 sub-commands (create/roll/verify key + chain bins, viewer bins, file transport bins, license bin). Each sub-command SHALL be lazy-imported. Calling `suaveplan` with no arguments SHALL print help and exit 0.
|
||||
|
||||
#### Scenario: every sub-command is reachable from the dispatcher
|
||||
|
||||
- **WHEN** `suaveplan <subcommand> --help` is invoked for each of the 17 sub-commands
|
||||
- **THEN** each prints its own help block and exits 0
|
||||
|
||||
**Verified by:** `src/bin/index.test.ts` (`describe("suaveplan dispatcher")`)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Root-key creation bin produces encrypted vault + signed public-key file
|
||||
|
||||
`create-root-keys` SHALL accept `--output <dir>` (default `./keys`), prompt for a new password (confirm prompt), generate a root keychain via `@suaveplan/crypto`, encrypt it into a vault file via `@suaveplan/crypto-vault`, and write a signed public-key file alongside. Failures SHALL exit with code 1 and a `ServerKeysError` message.
|
||||
|
||||
#### Scenario: produces vault + public-key file at the configured output path
|
||||
|
||||
- **WHEN** `createRootKeysCommand(password, { output })` is called
|
||||
- **THEN** `output/root.vault` and `output/root.pub` exist, vault is decryptable with `password`, public-key file is signature-verified by the embedded chain
|
||||
|
||||
**Verified by:** `src/commands/keys/keys.test.ts::"createRootKeysCommand writes encrypted vault and signed public-key file"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Server-key derivation bin from root vault
|
||||
|
||||
`create-server-keys` SHALL accept `--root-vault <path>` (default `./keys/root.vault`) + `--output <dir>` (default `./keys`), prompt for the root password and a new server password, derive a server keychain from the root master key, encrypt it into `server.vault`, and write a signed `server.pub`.
|
||||
|
||||
#### Scenario: derives server keychain from a root vault
|
||||
|
||||
- **WHEN** `createServerKeysCommand(rootVaultPath, rootPassword, serverPassword, { output })` is called after a successful `create-root-keys`
|
||||
- **THEN** `server.vault` is created, decryptable with `serverPassword`, and its public key is signed by the root keychain
|
||||
|
||||
**Verified by:** `src/commands/keys/keys.test.ts::"createServerKeysCommand derives from root vault and writes server vault + public-key file"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Server-key rotation bin bumps keychain version
|
||||
|
||||
`roll-server-keys` SHALL accept `--server-vault <path>` + `--output <dir>`, prompt for both passwords (root + current server), generate a new server keychain at `version = current + 1` deterministically derived from the root master key, write `server.v<N>.vault`, and emit a rotation manifest.
|
||||
|
||||
#### Scenario: rotation increments keychain version
|
||||
|
||||
- **WHEN** `rollServerKeysCommand(...)` is called with a current `server.v1.vault`
|
||||
- **THEN** `server.v2.vault` is created, decryptable, and its keychain's `version === 2`
|
||||
|
||||
**Verified by:** `src/commands/keys/keys.test.ts::"rollServerKeysCommand bumps keychain version"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Hierarchical chained-key bins (create-root/server, roll-server with chain)
|
||||
|
||||
`create-root-keys-chain`, `create-server-keys-chain`, `roll-server-keys-chain`, and `verify-key-chain` SHALL use `@suaveplan/crypto-keys` `ChainedKeySigner` to produce / verify chained public-key bundles. Root chains are self-signed; server chains are root-signed; rolled-server chains are signed by the current root. `verify-key-chain` SHALL accept a chain file + an explicit trusted root and report `valid: boolean` + walked depth + per-level error details.
|
||||
|
||||
#### Scenario: end-to-end chain — create root, derive server, verify
|
||||
|
||||
- **WHEN** the three create commands run in sequence then `verifyKeyChainCommand(chainFile, trustedRoot)` is invoked
|
||||
- **THEN** the result has `valid: true` and `depth === 2`
|
||||
|
||||
#### Scenario: tampered chain fails verification
|
||||
|
||||
- **WHEN** any byte of the chain file is flipped and `verifyKeyChainCommand` is invoked
|
||||
- **THEN** the result has `valid: false` with a populated `errors` array
|
||||
|
||||
**Verified by:** `src/commands/keys/keys.test.ts::"end-to-end chain create + verify"` AND `src/commands/keys/keys.test.ts::"tampered chain fails verifyKeyChainCommand"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Viewer bins decode keystore artifacts without mutating state
|
||||
|
||||
`view-keys`, `view-vault`, `view-public-key-file`, `view-chain-public` SHALL be read-only inspectors. `view-keys` prompts for a password and prints decrypted keychain summary; `view-vault` prints vault file metadata without decryption; `view-public-key-file` prints the public-key file's claimed identity and signature status; `view-chain-public` walks a chain file and prints each level's public key + signer + optional verification result.
|
||||
|
||||
#### Scenario: view-vault works without password
|
||||
|
||||
- **WHEN** `viewVaultCommand(vaultPath)` is called on a valid vault
|
||||
- **THEN** stdout contains the vault's version, algorithm, and creation timestamp; no decryption is attempted
|
||||
|
||||
**Verified by:** `src/commands/keys/keys.test.ts::"viewVaultCommand prints metadata without decryption"` AND `src/commands/keys/keys.test.ts::"viewKeysCommand requires password and prints keychain summary"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: File transport bins (prepare / serve / validate / stats / validate-packet)
|
||||
|
||||
`prepare-file` SHALL accept `<input-file> --output <dir> --server-vault <path>`, prompt for the server password, pack the file into signed packets via `@suaveplan/packer`, and write them under `output/`. `serve-file` SHALL read packets from a directory and reconstruct the original file. `validate-file` SHALL walk every packet and verify integrity + signature against the embedded signer public key. `get-file-stats` SHALL print packet count + estimated reconstructed size + manifest summary without verification. `validate-packet` SHALL validate a single packet in isolation. Every command SHALL accept `--signal: AbortSignal` (programmatic API) and SHALL exit cleanly on `SIGINT`.
|
||||
|
||||
#### Scenario: prepare then serve reconstructs the input
|
||||
|
||||
- **WHEN** `prepareFileCommand(input, opts)` produces packet files and `serveFileCommand(packetDir, output)` then reconstructs
|
||||
- **THEN** the reconstructed file is byte-identical to `input`
|
||||
|
||||
#### Scenario: validate-file detects a tampered packet
|
||||
|
||||
- **WHEN** any byte of any packet body is flipped and `validateFileCommand(packetDir)` is invoked
|
||||
- **THEN** the command reports the tampered packet's path and exits non-zero
|
||||
|
||||
**Verified by:** `src/commands/files/files.test.ts::"prepareFile + serveFile round-trip a binary input"` AND `src/commands/files/files.test.ts::"validateFile detects tampered packet"` AND `src/commands/files/files.test.ts::"getFileStats prints manifest without verification"` AND `src/commands/files/files.test.ts::"validatePacket reports a single-packet failure"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: License bin wraps @suaveplan/license LicenseManager
|
||||
|
||||
`license` SHALL expose sub-commands `generate`, `validate`, `parse`, `revoke` that delegate to `@suaveplan/license` `LicenseManager`. `license generate` accepts a server-vault path + product/tier/expiry/feature flags; signs with the server vault's signing key; prints the resulting license key. `license validate` accepts a key + a public-key file (or vault); prints success/failure + decoded payload. `license parse` decodes without verification. `license revoke` writes the fingerprint to a configured `RevocationStore` (default: file-backed in `./keys/revocations.json`).
|
||||
|
||||
#### Scenario: generate then validate round-trip
|
||||
|
||||
- **WHEN** `licenseCommand("generate", ...)` produces a key and `licenseCommand("validate", key, ...)` is invoked
|
||||
- **THEN** the validation prints `success: true` and exits 0
|
||||
|
||||
**Verified by:** `src/commands/license/license.test.ts::"license generate + validate round-trip"` AND `src/commands/license/license.test.ts::"license parse decodes without verification"` AND `src/commands/license/license.test.ts::"license revoke marks key as REVOKED on subsequent validate"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Password prompts via @inquirer/prompts, no echo
|
||||
|
||||
`promptForPassword(message)` SHALL read a single password from stdin with no echo. `promptForNewPassword(message)` SHALL prompt twice and assert equality; mismatches SHALL raise `ServerKeysError(SERVER_KEYS_PASSWORD_MISMATCH)`. Both SHALL respect a `--no-tty` test mode where the password is read from an `inputStream` injected via `PromptOptions`.
|
||||
|
||||
#### Scenario: confirm prompt mismatch raises typed error
|
||||
|
||||
- **WHEN** `promptForNewPassword("test", { inputStream: stream(["one", "two"]) })` is awaited
|
||||
- **THEN** the promise rejects with `ServerKeysError(SERVER_KEYS_PASSWORD_MISMATCH)`
|
||||
|
||||
**Verified by:** `src/prompt/prompt.test.ts::"promptForPassword reads stdin with no echo"` AND `src/prompt/prompt.test.ts::"promptForNewPassword rejects mismatched confirmation"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ServerKeysError extends SuaveplanError with SERVER_KEYS_* code registry
|
||||
|
||||
The package SHALL define `ServerKeysError extends SuaveplanError<ServerKeysErrorCode>` with codes registered via `defineCodes("SERVER_KEYS", [...])`. Codes SHALL include `SERVER_KEYS_VAULT_NOT_FOUND`, `SERVER_KEYS_VAULT_DECRYPT_FAILED`, `SERVER_KEYS_PASSWORD_MISMATCH`, `SERVER_KEYS_CHAIN_VERIFY_FAILED`, `SERVER_KEYS_PACKET_INTEGRITY_FAILED`, `SERVER_KEYS_FILE_NOT_FOUND`, `SERVER_KEYS_INVALID_INPUT`, `SERVER_KEYS_LICENSE_FAILED`, `SERVER_KEYS_UNKNOWN_COMMAND`, `SERVER_KEYS_ABORTED`. Every registered code SHALL be raised by at least one production call site.
|
||||
|
||||
#### Scenario: every SERVER_KEYS_* code is exercised
|
||||
|
||||
- **WHEN** the spec-conformance test enumerates `SERVER_KEYS_CODES` and exercises each code's production path
|
||||
- **THEN** every code appears in the assertions
|
||||
|
||||
**Verified by:** `src/server-keys.spec-conformance.test.ts::"every SERVER_KEYS_* code is thrown from a production path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Zod boundary validation for every CLI option bag
|
||||
|
||||
The package SHALL Zod-validate every `commander` action's options bag (output paths, vault paths, signal/abort flags, file paths, license-bin sub-options). Invalid input SHALL raise `ServerKeysError(SERVER_KEYS_INVALID_INPUT)` with the `ZodError` as `cause`. CLI invocation SHALL exit non-zero on validation failure with a one-line human-readable summary printed to stderr.
|
||||
|
||||
#### Scenario: invalid --output path rejected
|
||||
|
||||
- **WHEN** a command receives `--output` pointing at a non-writable path
|
||||
- **THEN** the command exits with code 1 and stderr contains `SERVER_KEYS_INVALID_INPUT`
|
||||
|
||||
**Verified by:** `src/server-keys.spec-conformance.test.ts::"Zod validation rejects unwritable --output path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OpenTelemetry counter + histogram per command
|
||||
|
||||
The package SHALL register counter `server_keys.command_total{command}` and histogram `server_keys.command_duration_ms{command}` via `@opentelemetry/api`. Every command (`runFunction` wrapper) SHALL increment and record on completion regardless of success/failure (attribute `command.outcome: "success"|"error"`).
|
||||
|
||||
#### Scenario: counter and histogram emit per command
|
||||
|
||||
- **WHEN** any command runs against a real in-memory `MeterProvider`
|
||||
- **THEN** both instruments have at least one observation with the correct `command` attribute
|
||||
|
||||
**Verified by:** `src/server-keys.spec-conformance.test.ts::"OTel command counter + histogram emit per run"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Universal-tier discipline — sibling docs, subpath exports, no banned patterns
|
||||
|
||||
The package SHALL ship a sibling `.md` (≥200 words, Purpose/Features/Usage:Basic/Usage:Advanced/API Reference/Implementation Notes) for every implementation `.ts`. `package.json` SHALL declare subpath exports for every command + prompt module. Feature barrels SHALL use named re-exports only (no `export *`). Test files SHALL import runner primitives from `@suaveplan/testing/runner`. The `src/` SHALL contain no `@deprecated` / `TODO` / `FIXME` / `ts-ignore` / `.skip` / `console.log` / bare `any`.
|
||||
|
||||
#### Scenario: package shape is clean
|
||||
|
||||
- **WHEN** `tests/dep-graph.test.ts` inspects file tree, package.json, and barrels
|
||||
- **THEN** every assertion holds
|
||||
|
||||
**Verified by:** `tests/dep-graph.test.ts::"every implementation .ts has a sibling .md"` AND `tests/dep-graph.test.ts::"subpath exports cover every command module"` AND `tests/dep-graph.test.ts::"no export * in any feature barrel"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 100% coverage with integration tests against the real filesystem
|
||||
|
||||
The package SHALL enforce 100/100/100/100 statements/branches/functions/lines via Istanbul. Integration tests SHALL exercise the full prepare-file → serve-file round-trip against a real temp directory and a real `@suaveplan/storage-file` instance — no mocks of crypto, packer, or storage. Property tests SHALL cover keychain round-trips and packet manifest round-trips via fast-check.
|
||||
|
||||
#### Scenario: coverage gate
|
||||
|
||||
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/server-keys` runs
|
||||
- **THEN** all four thresholds are 100
|
||||
|
||||
**Verified by:** `tests/integration/prepare-serve-roundtrip.test.ts::"prepares and reconstructs a binary file against a real temp dir"` AND `tests/property.test.ts::"chain encode/decode round-trips for arbitrary depth"` AND `tests/property.test.ts::"packet manifest round-trips arbitrary payloads"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user