/** * Local OAuth callback server + flow base class. * * Pure-TS / Bun.serve reimplementation of gjc's * `packages/ai/src/utils/oauth/callback-server.ts`. Handles: * - preferred-port binding with random fallback (unless a fixed redirectUri is required) * - CSRF state validation * - browser callback OR manual paste of the redirect URL / code (whichever lands first) */ import type { OAuthController, OAuthCredentials } from "./types"; import { generateState } from "./pkce"; import { closeAuthTab } from "./browser-tab"; const DEFAULT_TIMEOUT_MS = 300_000; // Loopback callback host. `localhost` (not the 127.0.0.1 IP literal) is the intentional // default — providers register their redirect URIs against `localhost`, so the dynamic // loopback flows (Anthropic / Google / Antigravity) must match. Keep this as `localhost`. const DEFAULT_HOSTNAME = "localhost"; const DEFAULT_CALLBACK_PATH = "/callback"; export interface OAuthCallbackFlowOptions { preferredPort: number; callbackPath?: string; callbackHostname?: string; /** Exact redirect URI advertised to the provider; disables port fallback when set. */ redirectUri?: string; } export type CallbackResult = { code: string; state: string }; /** Thrown when the fixed callback port cannot be bound and the provider forbids a * port fallback (fixed redirect URI). Typed so callers (e.g. the OpenAI flow) can * detect the bind failure and fall back to the device-code flow instead of * string-matching the error message. */ export class CallbackPortUnavailableError extends Error { readonly port: number; constructor(port: number) { super(`OAuth callback port ${port} unavailable and this provider requires a fixed redirect URI.`); this.name = "CallbackPortUnavailableError"; this.port = port; } } // "jeo" wordmark — bold forged monospace lettering on a neon blue→violet→pink // gradient (the mascot's synthwave palette), generated via god-tibo-imagen and // matching jeo-pi's bold-wordmark typographic treatment. Embedded as a data URI // (not a served static file) so the callback page renders standalone even from // a bundled/compiled `jeo` binary with no static-asset pipeline. const JEO_WORDMARK_DATA_URI = "data:image/webp;base64,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"; const PAGE_HTML = `jeo — status
jeo

__TITLE__

__MSG__

Closing automatically in __SECONDS__s…

This browser refuses to close a tab it opened itself — press \u2318W (Ctrl+W) to close it.

`; // Seconds before the tab tries to close itself — applies to BOTH the success and // the failure page (each is a final page that should not linger). Pressing // "Close this tab" runs the same attempt immediately, without waiting out the // countdown, and stays clickable afterwards. const AUTO_CLOSE_SECONDS = 3; function renderHtml(ok: boolean, msg: string): string { // Closing contract, in order of what actually works: // 1. `window.open("","_self")` before `window.close()` — the legacy self-adopt // trick that still lets Firefox (and older engines) close an OS-opened tab. // 2. `window.close()` — succeeds only where the tab counts as script-opened. // 3. If the tab is still here a moment later, the browser refused: tell the // user how to close it and KEEP the button working. // // The old code branched on `window.close()`'s return value — that call returns // `undefined` ALWAYS, so the failure branch fired unconditionally and hid the // Close button on the first tick. That left the tab open with its only manual // control gone (the "인증 브라우저가 종료되지 않는다" report). Success is now // detected by observing that the document is actually going away // (`document.hidden` / the `pagehide` unload path), never by a return value. // // The CLI side additionally closes the tab through the OS on macOS once the // callback lands (`closeAuthTab` in ./browser-tab.ts), which is the only path // that works when the browser refuses the scripted close outright. const script = `(function(){ var n=${AUTO_CLOSE_SECONDS}, el=document.getElementById("jeo-countdown"), countdownMsg=document.getElementById("jeo-countdown-msg"), btn=document.getElementById("jeo-close"), closedMsg=document.getElementById("jeo-closed-msg"), t,gone=false; window.addEventListener("pagehide",function(){gone=true;}); function showManual(){ if(gone||document.hidden)return; if(countdownMsg)countdownMsg.style.display="none"; if(closedMsg)closedMsg.classList.remove("closed"); } function attemptClose(){ if(t){clearInterval(t);t=null;} try{window.open("","_self");}catch(e){} try{window.close();}catch(e){} setTimeout(showManual,250); } t=setInterval(function(){n-=1;if(el)el.textContent=String(Math.max(n,0));if(n<=0)attemptClose();},1000); if(btn)btn.addEventListener("click",attemptClose); })();`; return PAGE_HTML .replace("__STATUS__", ok ? "ok" : "fail") .replace("__TITLE__", ok ? "Login complete \u2713" : "Login failed") .replace("__MSG__", msg) .replace("__SECONDS__", String(AUTO_CLOSE_SECONDS)) .replace("__SCRIPT__", script); } export abstract class OAuthCallbackFlow { protected ctrl: OAuthController; protected preferredPort: number; protected callbackPath: string; protected callbackHostname: string; protected fixedRedirectUri?: string; #resolve?: (r: CallbackResult) => void; #reject?: (e: Error) => void; /** Set for the duration of one `login()`; fired the moment the callback PAGE is * served, which is the only proof a browser tab of ours exists to close. */ #onPageServed?: () => void; constructor(ctrl: OAuthController, opts: number | OAuthCallbackFlowOptions, callbackPath = DEFAULT_CALLBACK_PATH) { this.ctrl = ctrl; if (typeof opts === "number") { this.preferredPort = opts; this.callbackPath = callbackPath; this.callbackHostname = DEFAULT_HOSTNAME; return; } this.preferredPort = opts.preferredPort; this.callbackPath = opts.callbackPath ?? DEFAULT_CALLBACK_PATH; this.callbackHostname = opts.callbackHostname ?? DEFAULT_HOSTNAME; this.fixedRedirectUri = opts.redirectUri; } abstract generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }>; abstract exchangeToken(code: string, state: string, redirectUri: string): Promise; async login(): Promise { const state = generateState(); const { server, redirectUri } = this.#startServer(state); // Whether the browser actually reached the callback page. A manual paste (or a // cancelled/timed-out flow) never rendered a tab of ours, so there is nothing to // close and no reason to pay for an osascript spawn. let servedCallbackPage = false; this.#onPageServed = () => { servedCallbackPage = true; }; try { const { url, instructions } = await this.generateAuthUrl(state, redirectUri); this.ctrl.onAuth?.({ url, instructions }); this.ctrl.onProgress?.("Waiting for browser authentication..."); const { code, state: returnedState } = await this.#waitForCallback(state); this.ctrl.onProgress?.("Exchanging authorization code for tokens..."); return await this.exchangeToken(code, returnedState || state, redirectUri); } finally { server.stop(); this.#onPageServed = undefined; // Close the leftover callback tab from the OUTSIDE (macOS only; see // ./browser-tab.ts). Awaited — a one-shot `jeo auth login` exits as soon as // this returns, so a fire-and-forget spawn would be killed before it ran — // but hard-capped, silent, and never able to fail the login it follows. if (servedCallbackPage) { try { await this.closeBrowserTab(redirectUri); } catch { /* best effort only */ } } } } /** OS-level close of the callback tab (see ./browser-tab.ts). Overridable so tests * can observe the attempt without driving a real browser. */ protected closeBrowserTab(redirectUri: string): Promise { return closeAuthTab(redirectUri); } #startServer(expectedState: string): { server: Bun.Server; redirectUri: string } { const serve = (port: number) => Bun.serve({ hostname: this.callbackHostname, port, reusePort: false, // gjc parity: never share the port — a busy port must FAIL, not silently split traffic fetch: req => this.#handle(req, expectedState), }); try { const server = serve(this.preferredPort); const redirectUri = this.fixedRedirectUri ?? `http://${this.callbackHostname}:${server.port}${this.callbackPath}`; return { server, redirectUri }; } catch { if (this.fixedRedirectUri) { throw new CallbackPortUnavailableError(this.preferredPort); } const server = serve(0); const redirectUri = `http://${this.callbackHostname}:${server.port}${this.callbackPath}`; this.ctrl.onProgress?.(`Port ${this.preferredPort} busy; using ${server.port}.`); return { server, redirectUri }; } } #handle(req: Request, expectedState: string): Response { const url = new URL(req.url); if (url.pathname !== this.callbackPath) return new Response("Not Found", { status: 404 }); const code = url.searchParams.get("code"); const state = url.searchParams.get("state") || ""; const error = url.searchParams.get("error"); const errorDesc = url.searchParams.get("error_description") || error; let ok = false; let message: string; if (error) message = `Authorization failed: ${errorDesc}`; else if (!code) message = "Missing authorization code"; else if (expectedState && state !== expectedState) message = "State mismatch — possible CSRF attack"; else { ok = true; message = "Authentication succeeded. Return to the terminal — jeo is ready."; } this.#onPageServed?.(); // a real browser tab now holds this page — it can be closed const resolve = this.#resolve; const reject = this.#reject; queueMicrotask(() => { if (ok && code) resolve?.({ code, state }); else reject?.(new Error(message)); }); return new Response(renderHtml(ok, message), { status: ok ? 200 : 500, // gjc parity: provider/CSRF failures are server-side errors (500), not 400 headers: { "content-type": "text/html" }, }); } #waitForCallback(expectedState: string): Promise { const timeout = AbortSignal.timeout(DEFAULT_TIMEOUT_MS); const signal = this.ctrl.signal ? AbortSignal.any([this.ctrl.signal, timeout]) : timeout; const callbackPromise = new Promise((resolve, reject) => { this.#resolve = resolve; this.#reject = reject; signal.addEventListener("abort", () => { this.#resolve = undefined; this.#reject = undefined; reject(new Error(`OAuth callback cancelled: ${signal.reason}`)); }); }); if (this.ctrl.onManualCodeInput) { const ask = this.ctrl.onManualCodeInput; const manualPromise = (async (): Promise => { while (true) { // Cooperative cancellation: once the controller signal aborts (the // caller finished or failed the login), STOP re-prompting. Without // this guard an aborted `ask()` rejects instantly, the catch maps it // to null, and the loop spins re-asking forever. if (signal.aborted) return callbackPromise; const result = await Promise.race([ callbackPromise, ask() .then(input => { const parsed = parseCallbackInput(input); if (!parsed.code) return null; // gjc parity: a bare pasted code WITHOUT a state is accepted (the provider // may strip it); only an explicitly MISMATCHED state is rejected. if (expectedState && parsed.state && parsed.state !== expectedState) return null; return { code: parsed.code, state: parsed.state ?? "" } as CallbackResult; }) .catch(() => null), ]); if (result) return result; } })(); return Promise.race([callbackPromise, manualPromise]); } return callbackPromise; } } /** Parse a pasted redirect URL or bare `code#state` into its parts. */ export function parseCallbackInput(input: string): { code?: string; state?: string } { const value = input.trim(); if (!value) return {}; try { const url = new URL(value); return { code: url.searchParams.get("code") ?? undefined, state: url.searchParams.get("state") ?? undefined, }; } catch { /* not a URL */ } if (value.includes("code=")) { const params = new URLSearchParams(value.replace(/^[?#]/, "")); return { code: params.get("code") ?? undefined, state: params.get("state") ?? undefined }; } const [code, state] = value.split("#", 2); return { code, state }; }