scripts/codex-oauth-login.ts

#!/usr/bin/env bun /**

  • One-time script to obtain OpenAI OAuth tokens for clanky.
  • Canonical entrypoint: bun scripts/openai-oauth-login.ts
    1. Starts a temporary localhost callback server
    1. Opens the OpenAI authorization URL in your browser
    1. Completes ChatGPT account login
    1. Saves tokens to data/openai-oauth-tokens.json */

import { buildAuthorizeUrl, codexOAuthConstants, exchangeCodeForTokens } from "../src/llm/codexOAuth.ts";

const redirectUri = codexOAuthConstants.defaultRedirectUri; const { url, verifier, state } = buildAuthorizeUrl({ redirectUri });

console.log(" --- OpenAI OAuth Login --- "); console.log("1. Open this URL in your browser: "); console.log( ${url}); console.log("2. Log in with a supported ChatGPT account and authorize. "); console.log("3. This script will capture the callback automatically. ");

let resolveCode: ((value: string) => void) | null = null; let rejectCode: ((error: Error) => void) | null = null;

const codePromise = new Promise((resolve, reject) => { resolveCode = resolve; rejectCode = reject; });

const server = Bun.serve({ port: codexOAuthConstants.defaultCallbackPort, fetch(request) { const requestUrl = new URL(request.url);

if (requestUrl.pathname === "/auth/callback") {
  const code = String(requestUrl.searchParams.get("code") || "").trim();
  const returnedState = String(requestUrl.searchParams.get("state") || "").trim();
  const error = String(requestUrl.searchParams.get("error") || "").trim();
  const errorDescription = String(requestUrl.searchParams.get("error_description") || "").trim();

  if (error) {
    rejectCode?.(new Error(errorDescription || error));
    return new Response(
      `<html><body><h1>Authorization failed</h1><p>${errorDescription || error}</p></body></html>`,
      {
        status: 400,
        headers: { "Content-Type": "text/html" }
      }
    );
  }

  if (!code) {
    rejectCode?.(new Error("Missing authorization code"));
    return new Response(
      "<html><body><h1>Authorization failed</h1><p>Missing authorization code.</p></body></html>",
      {
        status: 400,
        headers: { "Content-Type": "text/html" }
      }
    );
  }

  if (returnedState !== state) {
    rejectCode?.(new Error("OAuth state mismatch"));
    return new Response(
      "<html><body><h1>Authorization failed</h1><p>OAuth state mismatch.</p></body></html>",
      {
        status: 400,
        headers: { "Content-Type": "text/html" }
      }
    );
  }

  resolveCode?.(code);
  return new Response(
    "<html><body><h1>Authorization successful</h1><p>You can close this window.</p><script>setTimeout(() => window.close(), 1500)</script></body></html>",
    {
      headers: { "Content-Type": "text/html" }
    }
  );
}

if (requestUrl.pathname === "/cancel") {
  rejectCode?.(new Error("Login cancelled"));
  return new Response("Login cancelled");
}

return new Response("Not found", { status: 404 });

} });

const timeout = setTimeout(() => { rejectCode?.(new Error("OAuth callback timeout - authorization took too long")); }, 5 * 60 * 1000);

try { try { const proc = Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" }); await proc.exited; } catch { // ignore - user can open manually }

const code = await codePromise; const tokens = await exchangeCodeForTokens({ code, redirectUri, verifier });

console.log(" OpenAI OAuth tokens saved to data/openai-oauth-tokens.json"); console.log(Refresh token: ${tokens.refreshToken.slice(0, 12)}...); console.log(Account id: ${tokens.accountId || "(not returned)"}); console.log(Access token expires: ${new Date(tokens.expiresAt).toISOString()}); console.log(" You can now use OpenAI OAuth in the app via provider: openai-oauth."); console.log("Or set DEFAULT_PROVIDER=openai-oauth in your .env "); } catch (error) { console.error(" Failed to complete OpenAI OAuth login:", (error as Error).message); process.exitCode = 1; } finally { clearTimeout(timeout); server.stop(); }