# `prompt` _(no submodules)_ ## 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`. - `promptForNewPassword(message?, options?) => Promise` — 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.