app/cli/commands/login.ts
zzmq.dev@gmail.com/zz / app / cli / commands / login.ts
@ 0b64dedd
Download
Ladder Summary
- file The file implements the `zz login` command: it starts an OAuth login flow by requesting a one‑time nonce and URL from the server, tries to open that URL in the user's browser, then repeatedly polls the server until the login is completed and a token is returned, finally writing the credentials to disk. It exports a single async function `login` (no classes are exported).
// `zz login`: opens a browser to the same OAuth flow the web app
// itself uses, then polls a single-use nonce until the callback mints
// a token and hands it back -- the same shape `gh auth login`/
// `wrangler login` use, built on our own OAuth flow instead of
// embedding a Google/Cloudflare client secret in a distributed CLI
// (which shouldn't happen -- those secrets belong server-side only).
import { exec } from "node:child_process";
import { apiBase, writeCredentials } from "../lib/credentials.js";
interface LoginStartResponse {
nonce: string;
url: string;
}
interface LoginPollResponse {
status: "pending" | "ready";
token?: string;
agent?: { username: string };
}
function tryOpenBrowser(url: string): void {
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
exec(`${cmd} "${url}"`, () => {
// Best-effort only -- the URL is printed either way, nothing fails if this doesn't work.
});
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function login(opts: { apiBase?: string; timeoutMs?: number; quiet?: boolean } = {}): Promise<string> {
const base = opts.apiBase ?? apiBase();
const startResp = await fetch(`${base}/api/login/start`, { method: "POST" });
if (!startResp.ok) throw new Error(`could not start login: ${startResp.status} ${await startResp.text()}`);
const { nonce, url } = (await startResp.json()) as LoginStartResponse;
// Suppressed for a caller rendering an alt-screen TUI (zz top's own
// login hotkey) -- a stray console.log writes into the alt-screen
// buffer at whatever the cursor's own position happens to be,
// visually corrupting the rest of the frame instead of appearing as
// a normal terminal line the way it does for `zz login` on its own.
if (!opts.quiet) {
console.log(`Opening ${url}`);
console.log("(if it doesn't open automatically, paste that URL into a browser)");
}
tryOpenBrowser(url);
const deadline = Date.now() + (opts.timeoutMs ?? 5 * 60 * 1000);
while (Date.now() < deadline) {
await sleep(1500);
const pollResp = await fetch(`${base}/api/login/poll?nonce=${encodeURIComponent(nonce)}`);
if (!pollResp.ok) continue;
const poll = (await pollResp.json()) as LoginPollResponse;
if (poll.status === "ready" && poll.token && poll.agent) {
writeCredentials({ apiBase: base, token: poll.token, username: poll.agent.username });
if (!opts.quiet) console.log(`Logged in as ${poll.agent.username}.`);
return poll.agent.username;
}
}
throw new Error("timed out waiting for login to complete -- run `zz login` again");
}