feat(xai): add xAI Grok provider with OAuth + API key auth + image

Adapted from PR #1286 (mugnimaestra/feat/xai-grok-provider) to match
existing app architecture. Includes:

- OAuth 2.0 with PKCE on loopback port 56121 (Grok Build)
- API key auth path (console.x.ai)
- Token refresh wiring (open-sse + sse tokenRefresh)
- Dashboard OAuth modal with fixed-port flow + manual code fallback
- Provider registry entries (OAuth + API key)
- xAI image generation via OpenAI-compatible adapter
  (grok-2-image-1212 model, no size/quality/style params)

Excludes (intentionally, to match app patterns):
- Custom xAI Responses executor (DefaultExecutor handles /chat/completions)
- xAI-specific translators (app uses OpenAI as intermediate format)
- Image edits (not supported by current imageGenerationCore)
- Video endpoints (app has no video subsystem yet)
- CLI xai-login command

Refs decolua#1286

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Muhammad Mugni Hadi
2026-05-21 11:33:18 +07:00
committed by decolua
co-authored by Cursor
parent 0654d7bb35
commit d976f4cc87
21 changed files with 1058 additions and 72 deletions
+86 -13
View File
@@ -66,6 +66,25 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
}
}, [authData, provider, onSuccess]);
const completeXaiManualCode = useCallback(async (code) => {
if (!authData?.state) return;
try {
const res = await fetch("/api/oauth/xai/manual-code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, state: authData.state }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
setStep("success");
onSuccess?.();
} catch (err) {
setError(err.message);
setStep("error");
}
}, [authData, onSuccess]);
// Poll for device code token
const startPolling = useCallback(async (deviceCode, codeVerifier, interval, extraData) => {
pollingAbortRef.current = false;
@@ -175,6 +194,8 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
let redirectUri;
if (provider === "codex") {
redirectUri = "http://localhost:1455/auth/callback";
} else if (provider === "xai") {
redirectUri = "http://127.0.0.1:56121/callback";
} else {
redirectUri = `http://localhost:${appPort}/callback`;
}
@@ -208,7 +229,30 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
}
}
setAuthData({ ...data, redirectUri, codexServerSide });
// xAI: same fixed-port server-side proxy pattern as codex (port 56121)
let xaiProxyActive = false;
let xaiServerSide = false;
if (provider === "xai") {
try {
const proxyUrl = new URL(`/api/oauth/xai/start-proxy`, window.location.origin);
proxyUrl.searchParams.set("app_port", appPort);
proxyUrl.searchParams.set("state", data.state);
proxyUrl.searchParams.set("code_verifier", data.codeVerifier);
proxyUrl.searchParams.set("redirect_uri", redirectUri);
const proxyRes = await fetch(proxyUrl.toString());
const proxyData = await proxyRes.json();
xaiProxyActive = proxyData.success;
xaiServerSide = !!proxyData.serverSide;
if (!xaiProxyActive && proxyData.reason === "port_busy") {
throw new Error("Port 56121 in use; close the conflicting process and retry");
}
} catch (e) {
if (e?.message) throw e;
xaiProxyActive = false;
}
}
setAuthData({ ...data, redirectUri, codexServerSide, xaiServerSide });
if (provider === "codex" && codexProxyActive) {
// Proxy active: callback will be handled server-side (auto-exchange) or via channels (fallback)
@@ -217,12 +261,18 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
if (!popupRef.current) {
setStep("input");
}
} else if (!isLocalhost || provider === "codex") {
} else if (provider === "xai" && xaiProxyActive) {
setStep("waiting");
popupRef.current = window.open(data.authUrl, "oauth_popup", "width=600,height=700");
if (!popupRef.current) {
setStep("input");
}
} else if (!isLocalhost || provider === "codex" || provider === "xai") {
// Non-localhost or proxy failed: manual input mode
setStep("input");
window.open(data.authUrl, "_blank");
} else {
// Localhost (non-Codex): Open popup and wait for message
// Localhost (non-Codex/xAI): Open popup and wait for message
setStep("waiting");
popupRef.current = window.open(data.authUrl, "oauth_popup", "width=600,height=700");
if (!popupRef.current) {
@@ -251,13 +301,16 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
pollingAbortRef.current = true;
if (provider === "codex") {
fetch("/api/oauth/codex/stop-proxy").catch(() => {});
} else if (provider === "xai") {
fetch("/api/oauth/xai/stop-proxy").catch(() => {});
}
}
}, [isOpen, provider, startOAuthFlow]);
// Codex server-side mode: poll status (proxy auto-exchanges + saves DB)
// Fixed-port server-side mode: poll status (proxy auto-exchanges + saves DB)
useEffect(() => {
if (!authData?.codexServerSide || !authData?.state) return;
const pollProvider = authData?.codexServerSide ? "codex" : authData?.xaiServerSide ? "xai" : null;
if (!pollProvider || !authData?.state) return;
if (callbackProcessedRef.current) return;
let cancelled = false;
const POLL_INTERVAL_MS = 1500;
@@ -268,7 +321,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
if (cancelled || callbackProcessedRef.current) return;
attempts += 1;
try {
const res = await fetch(`/api/oauth/codex/poll-status?state=${encodeURIComponent(authData.state)}`);
const res = await fetch(`/api/oauth/${pollProvider}/poll-status?state=${encodeURIComponent(authData.state)}`);
const data = await res.json();
if (cancelled || callbackProcessedRef.current) return;
if (data.status === "done") {
@@ -392,6 +445,11 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
return;
}
if (provider === "xai" && input && !input.includes("://") && !input.includes("?") && !input.includes("code=")) {
await completeXaiManualCode(input);
return;
}
const url = new URL(input);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
@@ -402,7 +460,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
}
if (!code) {
throw new Error("No authorization code found in URL");
throw new Error(provider === "xai" ? "Paste the callback URL or copied xAI code" : "No authorization code found in URL");
}
await exchangeTokens(code, state);
@@ -416,15 +474,22 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
const handleClose = useCallback(() => {
if (provider === "codex") {
fetch("/api/oauth/codex/stop-proxy").catch(() => {});
} else if (provider === "xai") {
fetch("/api/oauth/xai/stop-proxy").catch(() => {});
}
onClose();
}, [onClose, provider]);
if (!provider || !providerInfo) return null;
const isXaiProvider = provider === "xai";
const deviceLoginUrl = deviceData?.verification_uri_complete || deviceData?.verification_uri || "";
const modalTitle = isXaiProvider ? "Connect Grok Build OAuth" : `Connect ${providerInfo.name}`;
const manualPlaceholder = isXaiProvider
? "http://127.0.0.1:56121/callback?code=... or copied code"
: placeholderUrl;
return (
<Modal isOpen={isOpen} title={`Connect ${providerInfo.name}`} onClose={handleClose} size="lg">
<Modal isOpen={isOpen} title={modalTitle} onClose={handleClose} size="lg">
<div className="flex flex-col gap-4">
{/* Waiting + Manual Input combined (non-device-code) */}
{(step === "waiting" || step === "input") && !isDeviceCode && (
@@ -434,7 +499,9 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
<span className="material-symbols-outlined text-base text-primary animate-spin">
progress_activity
</span>
<span className="text-sm">Waiting for popup authorization</span>
<span className="text-sm">
{isXaiProvider ? "Waiting for Grok Build OAuth…" : "Waiting for popup authorization…"}
</span>
</div>
{/* Divider */}
@@ -447,7 +514,9 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
{/* Option B: Manual paste */}
<div className="space-y-4">
<div>
<p className="text-sm font-medium mb-2">Step 1: Open this URL in your browser</p>
<p className="text-sm font-medium mb-2">
Step 1: Open this {isXaiProvider ? "Grok Build OAuth URL" : "URL"} in your browser
</p>
<div className="flex gap-2">
<Input value={authData?.authUrl || ""} readOnly className="flex-1 font-mono text-xs" />
<Button variant="secondary" icon={copied === "auth_url" ? "check" : "content_copy"} onClick={() => copy(authData?.authUrl, "auth_url")} disabled={!authData?.authUrl}>
@@ -457,14 +526,18 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
</div>
<div>
<p className="text-sm font-medium mb-2">Step 2: Paste the callback URL here</p>
<p className="text-sm font-medium mb-2">
Step 2: Paste the {provider === "xai" ? "callback URL or copied code" : "callback URL"} here
</p>
<p className="text-xs text-text-muted mb-2">
After authorization, copy the full URL from your browser.
{provider === "xai"
? "If xAI shows a code instead of redirecting, paste that code here."
: "After authorization, copy the full URL from your browser."}
</p>
<Input
value={callbackUrl}
onChange={(e) => setCallbackUrl(e.target.value)}
placeholder={placeholderUrl}
placeholder={manualPlaceholder}
className="font-mono text-xs"
/>
</div>