File size: 3,012 Bytes
67d18ac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | /**
* Shared plumbing for the headless "paste" login (auth-code flow): the
* terminal instructions and the readline-with-timeout that collects the
* pasted callback URL. Pure presentation/IO — the URL parsing and CSRF state
* check live in `parsePastedCallbackUrl` (auth/oauth.ts).
*
* Used by the CLI (`auth login bigmodel --paste`) and the TUI (`L` key),
* which suspend their own rendering before calling into these.
*/
import { createInterface } from "node:readline";
/** Bold only when attached to a terminal — keeps piped output ANSI-free. */
export function boldIfTTY(text: string): string {
return process.stdout.isTTY ? `\x1b[1m${text}\x1b[0m` : text;
}
/**
* Multi-line instructions shown after the flow started, before reading the
* pasted URL. Printed to the REAL terminal by both entries (the CLI's
* console is never intercepted; the TUI passes its captured stdout writer).
* Deliberately loud: the user must understand that the browser "error" page
* is the expected hand-off, and what the redirected URL looks like.
*/
export function pasteLoginInstructions(
authorizeUrl: string,
callbackUrl: string,
timeoutMs: number,
): string {
const bar = "=".repeat(72);
return [
"",
bar,
boldIfTTY(" PASTE LOGIN — the callback page is expected NOT to load"),
bar,
"",
"Open this URL to authorize (any machine with a browser):",
"",
` ${authorizeUrl}`,
"",
"After you authorize, the browser redirects to a localhost URL shaped like:",
"",
` (${callbackUrl}?authCode=xxxxxxxx&state=xxxxxxxx)`,
"",
"That page will NOT load (connection refused) — that is NORMAL on a",
"headless machine. Copy the FULL redirected URL from the browser's",
"address bar, paste it below, and press Enter.",
`(timeout: ${Math.round(timeoutMs / 1000)}s)`,
bar,
].join("\n");
}
/**
* Read one line of stdin (the pasted callback URL) with an overall timeout.
* Callers put the terminal into cooked mode first (the TUI leaves raw mode
* before this), so the tty line discipline echoes the paste. Rejects with
* the standard login-timeout message, or when stdin closes without a line
* (e.g. piped/empty input) so the flow can never hang forever.
*/
export function readPastedLine(
timeoutMs: number,
input: NodeJS.ReadableStream = process.stdin,
): Promise<string> {
return new Promise<string>((resolve, reject) => {
const rl = createInterface({ input, crlfDelay: Infinity });
let settled = false;
const settle = (fn: () => void): void => {
if (settled) return;
settled = true;
clearTimeout(timer);
rl.close();
fn();
};
const timer = setTimeout(() => {
settle(() => reject(new Error("Authorization timed out. Please retry login.")));
}, timeoutMs);
rl.on("line", (line: string) => settle(() => resolve(line)));
rl.on("close", () => {
settle(() => reject(new Error("stdin closed before a callback URL was pasted.")));
});
});
}
|