3.8 KiB
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_MISMATCHraised on confirmation mismatch — the caller'srunFunctionwrapper translates it into a process-exit banner.- Injectable
passwordPromptfor fully deterministic tests; injectableinputStream,outputStream, andsignalfor partial stubs that still drive the real inquirer prompt under a non-TTY harness.
Usage: Basic
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:
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>— throwsServerKeysError(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.