Files
skills/docs/security-stack/security/server-keys/src/prompt/prompt.md
2026-05-29 08:20:56 +01:00

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_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

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> — 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.