/**
* 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();
}
}