"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import Button from "@/shared/components/Button"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { runCodexDeviceFlow, CodexDeviceFlowError, type CodexUserCode, } from "@/lib/oauth/codexDeviceFlow"; type Status = "validating" | "ready" | "starting" | "awaiting" | "saving" | "success" | "error"; /** * Drives the public Codex device flow entirely in the visitor's browser * (auth.openai.com blocks datacenter IPs but allows CORS), then posts the final * tokens back to the ticket-gated completion endpoint for persistence. */ export default function CodexConnectClient({ token }: { token: string }) { const [status, setStatus] = useState("validating"); const [error, setError] = useState(null); const [userCode, setUserCode] = useState(null); const { copied, copy } = useCopyToClipboard(); const abortRef = useRef(null); // Validate the link on load so we can show "ready" vs "expired" before starting. useEffect(() => { let cancelled = false; (async () => { try { const res = await fetch(`/api/codex/connect/${token}`); if (cancelled) return; if (res.ok) { setStatus("ready"); } else { const data = await res.json().catch(() => ({})); setError(data?.error || "This link is invalid or expired."); setStatus("error"); } } catch { if (!cancelled) { setError("Could not reach the server to validate this link."); setStatus("error"); } } })(); return () => { cancelled = true; }; }, [token]); // Abort any in-flight device flow if the visitor leaves. useEffect(() => () => abortRef.current?.abort(), []); const start = useCallback(async () => { setError(null); setUserCode(null); setStatus("starting"); const controller = new AbortController(); abortRef.current = controller; try { const tokens = await runCodexDeviceFlow({ signal: controller.signal, onUserCode: (uc) => { setUserCode(uc); setStatus("awaiting"); }, }); setStatus("saving"); const res = await fetch(`/api/codex/connect/${token}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(tokens), }); const data = await res.json().catch(() => ({})); if (res.ok && data?.success) { setStatus("success"); } else { setError(data?.error || "Could not save the connection. The link may have expired."); setStatus("error"); } } catch (err) { if (err instanceof CodexDeviceFlowError) { setError( err.code === "device_disabled" ? "Device code login is disabled for this OpenAI account. Enable it in ChatGPT security settings (or ask your workspace admin)." : err.code === "timeout" ? "Authorization timed out. Click Start again to retry." : err.code === "aborted" ? "Authentication was cancelled." : err.message ); } else { setError("Unexpected error during authentication. Please try again."); } setStatus("error"); } }, [token]); return (
key

Connect OpenAI Codex

Authorize a ChatGPT account to finish setting up this connection.

{status === "validating" && (

Validating link…

)} {status === "ready" && (

Click below to generate a one-time code, then sign in to OpenAI.

)} {status === "starting" && (

Requesting code from OpenAI…

)} {status === "awaiting" && userCode && (

1. Open the OpenAI verification page and 2. enter this code. This page updates automatically once you authorize.

Your code

{userCode.userCode}

Waiting for authorization…

)} {status === "saving" && (

Saving connection…

)} {status === "success" && (
check_circle

Connected!

The Codex account was registered. You can close this tab.

)} {status === "error" && (
error

{error}

)}
); }