Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
# Capability Spec Delta: crypto-cli (restore-crypto-keychain-packer)
**Package**: `@suaveplan/crypto-cli`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: `keychain` binary surface
The package SHALL ship a `keychain` binary (declared in `package.json` `bin`) implementing eight subcommands: `root init`, `server init`, `server roll`, `server revoke`, `inspect`, `verify`, `export-public`, `restore`. The CLI is built on `@suaveplan/cli` (argument parser, command router, prompts, output formatters) and `@suaveplan/readline` (TTY input). All cryptographic work goes through `@suaveplan/crypto`; the CLI never imports `@suaveplan/crypto-core` directly.
Every subcommand supports `--output json|binary|text` (default `text`), reads/writes from stdin/stdout when paths are `-`, and emits OTel spans for the operation.
#### Scenario: every documented subcommand exists and is invokable
- **WHEN** `keychain --help` is run
- **THEN** the output lists exactly the eight subcommands above with one-line descriptions
#### Scenario: stdin/stdout streaming via `-`
- **WHEN** `keychain inspect -` is run with a packed public bundle on stdin
- **THEN** the inspector reads the bundle from stdin and writes the parsed report to stdout, exit code 0
**Verified by:** `tests/cli-surface.test.ts::"help lists all eight subcommands"` AND `tests/cli-surface.test.ts::"inspect reads from stdin via -"` AND `src/commands/<command>.test.ts::"<command> emits an OTel span"` (one per subcommand)
---
### Requirement: TTY-only password input
The package SHALL accept passwords only through (a) the `cli`-package `password` prompt (TTY echo `*`), or (b) the `KEYCHAIN_PASSWORD` environment variable. A `--password` flag MUST be rejected at argument-parse time with `CryptoError` code `CLI.PASSWORD_FLAG_FORBIDDEN` and an exit code of 2 (usage error). The error message points the user at the two supported input paths.
This is a credential-handling rule: shell history is a documented credential leak path.
#### Scenario: `--password` flag is rejected
- **WHEN** `keychain root init --password=hunter2` is invoked
- **THEN** the binary exits with code 2 and stderr names the forbidden flag and lists the two allowed alternatives
#### Scenario: env-var input works
- **WHEN** `KEYCHAIN_PASSWORD=hunter2 keychain root init --output json` is invoked in a non-interactive shell
- **THEN** the binary uses the env-var value, exit code 0
#### Scenario: TTY prompt works
- **WHEN** the CLI is driven via a fake `InputReader` (per `cli` package) and a password is supplied at the prompt
- **THEN** the binary uses the supplied password, exit code 0; the password is never echoed (the test asserts no plaintext appears in the captured TTY transcript)
**Verified by:** `tests/no-password-flag.test.ts::"--password flag exits with code 2 and security pointer"` AND `tests/tty-mock.test.ts::"env-var KEYCHAIN_PASSWORD is consumed"` AND `tests/tty-mock.test.ts::"TTY password prompt does not echo plaintext"`
---
### Requirement: Distinct exit codes per failure class
The package SHALL exit with distinct, documented exit codes per failure class so operators can branch on them in scripts:
| Code | Meaning |
|---|---|
| 0 | success |
| 2 | usage error (bad flag, missing argument, `--password` rejected) |
| 10 | bad password (`VAULT.BAD_PASSWORD`) |
| 11 | tampered vault (`VAULT.TAMPERED`) |
| 12 | expired key (`KEYCHAIN.EXPIRED`) |
| 13 | revoked key (`AUTHORITY.REVOKED` / CRL hit) |
| 14 | missing trust anchor (`TRUST_ANCHOR.MISSING`) |
| 15 | audit-log mismatch (`AUDIT.TAMPERED`) |
| 16 | replay rejected (`PACKER.REPLAY_TOO_OLD` / `PACKER.REPLAY_NONCE_SEEN`) |
Every other failure exits with code 1.
#### Scenario: each documented exit code is reachable
- **WHEN** the CLI is driven into each documented failure class
- **THEN** the exit code matches the table above
**Verified by:** `tests/exit-codes.test.ts::"each documented exit code is produced by its failure class"`
---
### Requirement: Idempotency on state-changing commands
The package SHALL accept `--idempotency-key <key>` on `server roll` and `server revoke`. The CLI maintains a local idempotency ledger at `$XDG_STATE_HOME/suaveplan/keychain/idempotency.json` (or platform equivalent). A repeated call with the same key returns the cached result without re-issuing or re-revoking; the cached result includes the original exit code.
The ledger entries expire after 24 hours; expired keys are treated as fresh.
#### Scenario: repeated roll with same idempotency key is a no-op
- **WHEN** `keychain server roll --idempotency-key abc` is invoked twice in succession
- **THEN** the first call succeeds and produces a new server identity; the second call exits 0 with the same output and does not append a second `roll` entry to the audit log
**Verified by:** `src/commands/server-roll.test.ts::"idempotency key replays the cached result"`
---
### Requirement: Per-command behaviour
Each subcommand SHALL have documented inputs, outputs, and side effects. The full table MUST be captured in `src/commands/COMMANDS.md` and asserted by per-command tests. Every subcommand MUST be invokable with the same flags documented here without runtime drift.
| Subcommand | Inputs | Output | Side effects |
|---|---|---|---|
| `root init` | `--user-id`, password (TTY/env), `--mnemonic` (optional) | encrypted vault + public bundle | writes vault to `--vault-path`, public bundle to `--public-path`; appends audit log if `--audit-log` is supplied |
| `server init` | root vault path, root password, `--user-id`, server password | server vault + signed public bundle | writes server vault, server public bundle, appends audit log, no CRL change |
| `server roll` | root vault, root password, server vault, server password, `--idempotency-key` | new server vault + new public bundle (version+1) | writes new files, appends CRL entry for prior version, appends audit log |
| `server revoke` | root vault, root password, server identity ref, `--reason`, `--idempotency-key` | CRL entry + audit-log entry | appends CRL, appends audit log; no new server identity |
| `inspect` | packed public bundle (path or `-`) | parsed report (text/json) | none (read-only) |
| `verify` | envelope (path or `-`), `--trust-anchor`, `--crl` | OK / failure with code | none (read-only) |
| `export-public` | vault path + password | public-only bundle | writes public bundle |
| `restore` | mnemonic (TTY/env), `--user-id`, `--version`, password | restored vault | writes restored vault |
#### Scenario: each subcommand round-trips with itself
- **WHEN** `root init` produces a vault, then `inspect` reads its public bundle, then `verify` checks an envelope produced under that root, then `export-public` re-emits the public bundle
- **THEN** all four steps succeed end-to-end and the public bundles produced by `root init` and `export-public` are byte-equal
**Verified by:** `tests/end-to-end.test.ts::"root-init → inspect → verify → export-public round-trips"` AND `src/commands/<command>.test.ts::"<command> happy path"` AND `src/commands/<command>.test.ts::"<command> failure modes"` (one per command)