/** * Kimchi browser login — same flow as getkimchi/kimchi src/cli-auth. * Opens https://app.kimchi.dev/cli-auth?callback=...&state=... */ import { randomBytes } from "node:crypto"; import { type IncomingMessage, type Server, type ServerResponse, createServer, } from "node:http"; import open from "open"; const CALLBACK_PATH = "/callback"; const CALLBACK_TIMEOUT_MS = 300_000; const DEFAULT_WEB_APP = "https://app.kimchi.dev"; export function generateState(): string { return randomBytes(32).toString("hex"); } function successHtml(message: string): string { return `

Authentication successful

${message}

`; } function errorHtml(title: string, detail?: string): string { const d = detail ? `

${detail}

` : ""; return `

${title}

${d}`; } export function startCallbackServer(expectedState: string): Promise<{ url: string; result: Promise<{ token?: string; error?: string }>; close: () => void; }> { return new Promise((resolveStart, rejectStart) => { let server: Server | undefined; let resolved = false; let resolveResult: ((r: { token?: string; error?: string }) => void) | undefined; const finish = (result: { token?: string; error?: string }) => { if (resolved) return; resolved = true; resolveResult?.(result); setTimeout(() => { try { server?.closeAllConnections?.(); server?.close(); } catch { /* ignore */ } server = undefined; }, 100); }; const onRequest = (req: IncomingMessage, res: ServerResponse) => { try { const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); const remote = req.socket.remoteAddress ?? ""; if (!(remote.startsWith("127.") || remote === "::1" || remote === "::ffff:127.0.0.1")) { res.writeHead(403, { "Content-Type": "text/html" }); res.end(errorHtml("Forbidden")); return; } if (url.pathname !== CALLBACK_PATH) { res.writeHead(404); res.end("Not found"); return; } const state = url.searchParams.get("state"); if (!state || state !== expectedState) { const msg = "Invalid state. Try logging in again from the terminal."; res.writeHead(400, { "Content-Type": "text/html" }); res.end(errorHtml("Login error", msg)); finish({ error: msg }); return; } const err = url.searchParams.get("error"); const errDesc = url.searchParams.get("error_description"); if (err) { const msg = errDesc || err; res.writeHead(200, { "Content-Type": "text/html" }); res.end(errorHtml("Authentication failed", msg)); finish({ error: msg }); return; } const token = url.searchParams.get("token"); if (!token) { const msg = "No token returned"; res.writeHead(400, { "Content-Type": "text/html" }); res.end(errorHtml("Missing token", msg)); finish({ error: msg }); return; } res.writeHead(200, { "Content-Type": "text/html" }); res.end(successHtml("Your CLI is connected. You can close this window.")); finish({ token }); } catch (e) { const message = e instanceof Error ? e.message : String(e); res.writeHead(500); res.end("Internal error"); finish({ error: message }); } }; server = createServer(onRequest); server.listen(0, "127.0.0.1", () => { const addr = server?.address(); if (!addr || typeof addr === "string") { rejectStart(new Error("Could not bind callback server")); return; } const callbackUrl = `http://127.0.0.1:${addr.port}${CALLBACK_PATH}`; const resultPromise = new Promise<{ token?: string; error?: string }>((resolve) => { resolveResult = resolve; }); setTimeout(() => finish({ error: "Browser login timed out" }), CALLBACK_TIMEOUT_MS); resolveStart({ url: callbackUrl, result: resultPromise, close: () => finish({ error: "Login cancelled" }), }); }); server.on("error", (err) => rejectStart(err)); }); } export async function authenticateViaBrowser(options: { webAppUrl?: string; onBrowserUrl?: (url: string) => void; openBrowser?: boolean; signal?: AbortSignal; }): Promise { const webAppUrl = options.webAppUrl ?? process.env.KIMCHI_WEB_APP_URL ?? DEFAULT_WEB_APP; const state = generateState(); const callbackServer = await startCallbackServer(state); const onAbort = () => callbackServer.close(); if (options.signal?.aborted) { callbackServer.close(); throw new Error("Login cancelled"); } options.signal?.addEventListener("abort", onAbort, { once: true }); try { const callbackUrl = encodeURIComponent(callbackServer.url); const browserUrl = `${webAppUrl}/cli-auth?callback=${callbackUrl}&state=${encodeURIComponent(state)}`; options.onBrowserUrl?.(browserUrl); if (options.openBrowser !== false) { try { await open(browserUrl); } catch { /* user can open URL manually */ } } const result = await callbackServer.result; if (result.error) throw new Error(result.error); if (!result.token) throw new Error("No token received"); return result.token; } finally { options.signal?.removeEventListener("abort", onAbort); callbackServer.close(); } }