"use client"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; /** * OAuth Callback Page * * Reads URL params via window.location.search (not useSearchParams) to avoid * the Next.js Suspense boundary requirement, which can delay hydration in popup * windows that navigate back from a cross-origin OAuth page (e.g. Google). * Sends the callback data back via three methods in order of reliability: * 1. postMessage to window.opener (may be null after COOP cross-origin nav) * 2. BroadcastChannel (same-origin, works across browsing context groups) * 3. localStorage storage event (works across browsing context groups) */ export default function CallbackPage() { const [status, setStatus] = useState<"processing" | "success" | "done" | "manual">("processing"); const [currentUrl] = useState(() => typeof window === "undefined" ? "" : window.location.href ); const t = useTranslations("auth"); useEffect(() => { const params = new URLSearchParams(window.location.search); const code = params.get("code"); const state = params.get("state"); const error = params.get("error"); const errorDescription = params.get("error_description"); const callbackData = { code, state, error, errorDescription, fullUrl: window.location.href, }; let sent = false; let openerSameOrigin = false; const queueStatusUpdate = (nextStatus: "processing" | "success" | "done" | "manual") => { queueMicrotask(() => setStatus(nextStatus)); }; if (window.opener) { try { openerSameOrigin = window.opener.location.origin === window.location.origin; } catch { openerSameOrigin = false; } } // Method 1: postMessage to opener (popup mode). // May be null when Google OAuth's COOP header severs the opener reference. // // Only relay {code, state} to a known-trusted target origin. A wildcard "*" // here would leak the OAuth code/state to a hostile opener — e.g. a page // that opened this callback URL in a popup to phish the code. The browser // delivers postMessage only when the opener's origin matches `targetOrigin`, // so iterating over an allowlist lets the same-origin parent and Codex's // fixed loopback helper receive it while silently dropping it for any other // origin. Methods 2 (BroadcastChannel) and 3 (localStorage) cover the // same-origin fallback when the opener was severed by COOP. const trustedTargetOrigins = [ window.location.origin, // Same origin (dashboard popup mode). "http://localhost:1455", // Codex helper (fixed loopback port). "http://127.0.0.1:1455", // Same Codex helper, IPv4 literal form. ]; if (window.opener) { for (const origin of trustedTargetOrigins) { try { window.opener.postMessage( { type: "oauth_callback", data: callbackData }, origin ); sent = true; } catch (e) { console.log("postMessage failed:", e); } } } // Method 2: BroadcastChannel — works across browsing context groups for same origin. try { const channel = new BroadcastChannel("oauth_callback"); channel.postMessage(callbackData); channel.close(); sent = true; } catch (e) { console.log("BroadcastChannel failed:", e); } // Method 3: localStorage — triggers storage event in all same-origin windows, // regardless of browsing context group isolation from COOP. try { localStorage.setItem( "oauth_callback", JSON.stringify({ ...callbackData, timestamp: Date.now() }) ); sent = true; } catch (e) { console.log("localStorage failed:", e); } if (sent && (code || error)) { if (window.opener && openerSameOrigin) { queueStatusUpdate("success"); setTimeout(() => { window.close(); // If close is prevented (browser policy), fall through to manual close prompt. setTimeout(() => setStatus("done"), 500); }, 1500); } else { // Opened as a tab, opener severed by COOP, or remote dashboard using a // loopback/tunnel callback. Keep the full URL visible as a manual fallback // in case the opener cannot receive the cross-origin postMessage. queueStatusUpdate("manual"); } } else { // No code/error in URL or all send methods failed — show URL for manual copy. // Batch the URL and status update so they render together (React 18 auto-batching). queueStatusUpdate("manual"); } }, []); return (
{t("pleaseWait")}
> )} {(status === "success" || status === "done") && ( <>{status === "success" ? t("windowWillClose") : t("closeTabNow")}
> )} {status === "manual" && ( <>{t("copyUrlManual")}
{currentUrl}