13 KiB
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> --helpis 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.vaultandoutput/root.pubexist, vault is decryptable withpassword, 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 successfulcreate-root-keys - THEN
server.vaultis created, decryptable withserverPassword, 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 currentserver.v1.vault - THEN
server.v2.vaultis created, decryptable, and its keychain'sversion === 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: trueanddepth === 2
Scenario: tampered chain fails verification
- WHEN any byte of the chain file is flipped and
verifyKeyChainCommandis invoked - THEN the result has
valid: falsewith a populatederrorsarray
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 andserveFileCommand(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 andlicenseCommand("validate", key, ...)is invoked - THEN the validation prints
success: trueand 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_CODESand 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
--outputpointing 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
commandattribute
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.tsinspects 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-keysruns - 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"