first commit
This commit is contained in:
108
docs/security-stack/security/server-keys/src/prompt/prompt.md
Normal file
108
docs/security-stack/security/server-keys/src/prompt/prompt.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# `prompt`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Thin, test-injectable wrappers around `@inquirer/prompts`' password prompt
|
||||
for the two interactive flows every server-keys bin needs: a single
|
||||
silent password entry (`promptForPassword`) and a confirm-twice new
|
||||
password entry (`promptForNewPassword`). The wrappers add three things
|
||||
upstream lacks: a structured `ServerKeysError(SERVER_KEYS_PASSWORD_MISMATCH)`
|
||||
on mismatch (instead of a recursive re-prompt that bypasses the bin's
|
||||
top-level handler), a typed dependency-injection seam for the prompt
|
||||
function (so tests can run without touching stdin/stdout), and explicit
|
||||
threading of `input/output/signal` into inquirer's `context` so the
|
||||
prompts compose with the bin's `AbortController` and with non-TTY test
|
||||
runners.
|
||||
|
||||
The minimum password length is exported as `MIN_PASSWORD_LENGTH` so callers
|
||||
and tests share the constant.
|
||||
|
||||
## Features
|
||||
|
||||
- `mask: "*"` non-echo entry — passwords never appear in the buffered
|
||||
terminal.
|
||||
- `validate: (input) => ...` rejects empty input and (on new-password)
|
||||
enforces a minimum length, surfacing the message through inquirer's
|
||||
re-prompt loop.
|
||||
- `SERVER_KEYS.PASSWORD_MISMATCH` raised on confirmation mismatch — the
|
||||
caller's `runFunction` wrapper translates it into a process-exit
|
||||
banner.
|
||||
- Injectable `passwordPrompt` for fully deterministic tests; injectable
|
||||
`inputStream`, `outputStream`, and `signal` for partial stubs that
|
||||
still drive the real inquirer prompt under a non-TTY harness.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
promptForPassword,
|
||||
promptForNewPassword,
|
||||
} from "@suaveplan/server-keys/prompt/prompt";
|
||||
|
||||
const existing = await promptForPassword("Enter root vault password");
|
||||
const fresh = await promptForNewPassword("Enter password for the new vault");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a fake prompt for tests, then exercise both happy and mismatch
|
||||
paths without any TTY:
|
||||
|
||||
```ts
|
||||
import { PassThrough } from "node:stream";
|
||||
import {
|
||||
promptForNewPassword,
|
||||
type PasswordPrompt,
|
||||
} from "@suaveplan/server-keys/prompt/prompt";
|
||||
|
||||
const calls: string[] = [];
|
||||
const responses = ["correcthorse", "correcthorse"];
|
||||
const fake: PasswordPrompt = async (cfg) => {
|
||||
calls.push(cfg.message);
|
||||
return responses.shift() as string;
|
||||
};
|
||||
const pw = await promptForNewPassword("Enter password", {
|
||||
passwordPrompt: fake,
|
||||
});
|
||||
expect(pw).toBe("correcthorse");
|
||||
expect(calls).toEqual(["Enter password", "Re-enter password to confirm"]);
|
||||
|
||||
// Mismatch raises a typed error:
|
||||
await expect(
|
||||
promptForNewPassword("x", {
|
||||
passwordPrompt: async () =>
|
||||
(["abcdefgh", "ABCDEFGH"].shift() ?? "") as string,
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "SERVER_KEYS.PASSWORD_MISMATCH" });
|
||||
|
||||
// Production with a SIGINT-bound AbortController:
|
||||
const ac = new AbortController();
|
||||
process.once("SIGINT", () => ac.abort());
|
||||
const pw2 = await promptForPassword("Enter vault password", {
|
||||
signal: ac.signal,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `MIN_PASSWORD_LENGTH = 8` — new-password length floor.
|
||||
- `PasswordPrompt` — function type matching `@inquirer/prompts.password`.
|
||||
- `PromptOptions = { passwordPrompt?, inputStream?, outputStream?, signal? }`.
|
||||
- `promptForPassword(message?, options?) => Promise<string>`.
|
||||
- `promptForNewPassword(message?, options?) => Promise<string>` — throws
|
||||
`ServerKeysError(SERVER_KEYS_PASSWORD_MISMATCH)` on mismatch.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The validators are kept in-prompt (instead of after the await) so
|
||||
inquirer's re-prompt UX still works for empty / too-short input. The
|
||||
mismatch check lives outside the prompt so the failure can be caught by
|
||||
the top-level handler instead of trapping the user in an infinite re-ask
|
||||
loop. `buildContext` only assigns keys that were actually provided so
|
||||
inquirer's defaults apply for the rest, and returns `undefined` for the
|
||||
empty case so the prompt can resolve its own stdin / stdout / signal
|
||||
defaults.
|
||||
Reference in New Issue
Block a user