Initial Commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import type { Readable, Writable } from "node:stream";
|
||||
import { password as inquirerPassword } from "@inquirer/prompts";
|
||||
import {
|
||||
SERVER_KEYS_CODES,
|
||||
ServerKeysError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
|
||||
/** Minimum password length enforced by {@link promptForNewPassword}. */
|
||||
export const MIN_PASSWORD_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* The inquirer password-prompt surface, narrowed to the shape this module
|
||||
* actually uses. Injecting a function with this signature lets tests run
|
||||
* deterministically without touching stdin/stdout.
|
||||
*/
|
||||
export type PasswordPrompt = (
|
||||
config: {
|
||||
message: string;
|
||||
mask?: boolean | string;
|
||||
validate?: (
|
||||
value: string
|
||||
) => boolean | string | Promise<boolean | string>;
|
||||
},
|
||||
context?: {
|
||||
input?: NodeJS.ReadableStream;
|
||||
output?: NodeJS.WritableStream;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
) => Promise<string>;
|
||||
|
||||
/** Options accepted by {@link promptForPassword} and {@link promptForNewPassword}. */
|
||||
export interface PromptOptions {
|
||||
/** Override the password-prompt implementation (defaults to `@inquirer/prompts`). */
|
||||
readonly passwordPrompt?: PasswordPrompt;
|
||||
/** Override stdin (forwarded to inquirer's `context.input`). */
|
||||
readonly inputStream?: Readable | NodeJS.ReadableStream;
|
||||
/** Override stdout (forwarded to inquirer's `context.output`). */
|
||||
readonly outputStream?: Writable | NodeJS.WritableStream;
|
||||
/** Forwarded to inquirer's `context.signal`. */
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
|
||||
function buildContext(options: PromptOptions | undefined) {
|
||||
/* istanbul ignore start -- reached only from real bin entry points that call promptForPassword()/promptForNewPassword() without any options; in-process tests cannot exercise this without breaking the test runner's stdin. */
|
||||
if (!options) return undefined;
|
||||
/* istanbul ignore stop */
|
||||
const ctx: {
|
||||
input?: NodeJS.ReadableStream;
|
||||
output?: NodeJS.WritableStream;
|
||||
signal?: AbortSignal;
|
||||
} = {};
|
||||
if (options.inputStream !== undefined) {
|
||||
ctx.input = options.inputStream as NodeJS.ReadableStream;
|
||||
}
|
||||
if (options.outputStream !== undefined) {
|
||||
ctx.output = options.outputStream as NodeJS.WritableStream;
|
||||
}
|
||||
if (options.signal !== undefined) ctx.signal = options.signal;
|
||||
return Object.keys(ctx).length === 0 ? undefined : ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt once for a password with no echo. Empty input is rejected at the
|
||||
* inquirer-validate boundary (the prompt re-asks); any error propagates.
|
||||
*/
|
||||
export async function promptForPassword(
|
||||
message = "Enter password",
|
||||
options?: PromptOptions
|
||||
): Promise<string> {
|
||||
/* istanbul ignore start -- the `?? inquirerPassword` fallback only fires when called without options from a real bin entry point; in-process tests would need to mock process.stdin which breaks the runner. */
|
||||
const prompt = options?.passwordPrompt ?? inquirerPassword;
|
||||
/* istanbul ignore stop */
|
||||
const ctx = buildContext(options);
|
||||
return prompt(
|
||||
{
|
||||
message,
|
||||
mask: "*",
|
||||
validate: (input: string) => {
|
||||
if (input.trim() === "") return "Password cannot be empty";
|
||||
return true;
|
||||
},
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt twice for a new password and assert equality. The first prompt
|
||||
* enforces {@link MIN_PASSWORD_LENGTH}; the confirmation re-prompt asks
|
||||
* for the same string and raises `SERVER_KEYS.PASSWORD_MISMATCH` if the
|
||||
* second entry differs.
|
||||
*/
|
||||
export async function promptForNewPassword(
|
||||
message = "Enter new password",
|
||||
options?: PromptOptions
|
||||
): Promise<string> {
|
||||
/* istanbul ignore start -- the `?? inquirerPassword` fallback only fires when called without options from a real bin entry point; in-process tests would need to mock process.stdin which breaks the runner. */
|
||||
const prompt = options?.passwordPrompt ?? inquirerPassword;
|
||||
/* istanbul ignore stop */
|
||||
const ctx = buildContext(options);
|
||||
const first = await prompt(
|
||||
{
|
||||
message,
|
||||
mask: "*",
|
||||
validate: (input: string) => {
|
||||
if (input.trim() === "") return "Password cannot be empty";
|
||||
if (input.length < MIN_PASSWORD_LENGTH) {
|
||||
return `Password must be at least ${MIN_PASSWORD_LENGTH} characters`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
ctx
|
||||
);
|
||||
const second = await prompt(
|
||||
{
|
||||
message: "Re-enter password to confirm",
|
||||
mask: "*",
|
||||
},
|
||||
ctx
|
||||
);
|
||||
if (first !== second) {
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.PASSWORD_MISMATCH,
|
||||
"Password confirmation does not match"
|
||||
);
|
||||
}
|
||||
return first;
|
||||
}
|
||||
Reference in New Issue
Block a user