Files
stack/packages/mosaic/src/commands/gateway/login.ts
jason.woltje 119ff0eb1b
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
fix(mosaic): gateway token recovery review remediations (#414)
2026-04-05 06:13:29 +00:00

88 lines
2.8 KiB
TypeScript

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.
*/
export async function runLogin(opts: {
gatewayUrl: string;
email?: string;
password?: string;
}): Promise<void> {
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);
console.log(`Signed in as ${auth.email} (${opts.gatewayUrl})`);
}
/**
* Derive the gateway base URL from meta.json with a fallback.
*/
export function getGatewayUrl(overrideUrl?: string): string {
if (overrideUrl) return overrideUrl;
const meta = readMeta();
if (meta) return `http://${meta.host}:${meta.port.toString()}`;
return 'http://localhost:14242';
}