fix(mosaic): gateway token recovery review remediations (#414)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #414.
This commit is contained in:
2026-04-05 06:13:29 +00:00
parent 3abd63ea5c
commit 119ff0eb1b
5 changed files with 90 additions and 30 deletions

View File

@@ -2,6 +2,62 @@ import { createInterface } from 'node:readline';
import { signIn, saveSession } from '../../auth.js';
import { readMeta } from './daemon.js';
/**
* Prompt for a single line of input (with echo).
*/
export function promptLine(question: string): Promise<string> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
/**
* Prompt for a secret value without echoing the typed characters to the terminal.
* Uses TTY raw mode when available so that passwords do not appear in terminal
* recordings, scrollback, or shared screen sessions.
*/
export function promptSecret(question: string): Promise<string> {
return new Promise((resolve) => {
process.stdout.write(question);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding('utf-8');
let secret = '';
const onData = (char: string): void => {
if (char === '\n' || char === '\r' || char === '\u0004') {
process.stdout.write('\n');
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
process.stdin.removeListener('data', onData);
resolve(secret);
} else if (char === '\u0003') {
// ^C
process.stdout.write('\n');
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
process.stdin.removeListener('data', onData);
process.exit(130);
} else if (char === '\u007f' || char === '\b') {
secret = secret.slice(0, -1);
} else {
secret += char;
}
};
process.stdin.on('data', onData);
});
}
/**
* Shared login helper used by both `mosaic login` and `mosaic gateway login`.
* Prompts for email/password if not supplied, signs in, and persists the session.
@@ -11,17 +67,9 @@ export async function runLogin(opts: {
email?: string;
password?: string;
}): Promise<void> {
let email = opts.email;
let password = opts.password;
if (!email || !password) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
const ask = (q: string): Promise<string> => new Promise((resolve) => rl.question(q, resolve));
if (!email) email = await ask('Email: ');
if (!password) password = await ask('Password: ');
rl.close();
}
const email = opts.email ?? (await promptLine('Email: '));
// Do not trim password — it may intentionally contain leading/trailing whitespace
const password = opts.password ?? (await promptSecret('Password: '));
const auth = await signIn(opts.gatewayUrl, email, password);
saveSession(opts.gatewayUrl, auth);