# 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 --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 ` (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 ` (default `./keys/root.vault`) + `--output ` (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 ` + `--output `, 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.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 ` --output --server-vault `, 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` 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"`