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:
committed by
decolua
co-authored by
Cursor
parent
0654d7bb35
commit
d976f4cc87
@@ -5,6 +5,7 @@
|
||||
|
||||
// Ensure outbound fetch respects HTTP(S)_PROXY/ALL_PROXY in Node runtime
|
||||
import "open-sse/index.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
import { generatePKCE, generateState } from "./utils/pkce";
|
||||
import {
|
||||
@@ -25,6 +26,11 @@ import {
|
||||
CODEBUDDY_CONFIG,
|
||||
getOAuthClientMetadata,
|
||||
} from "./constants/oauth";
|
||||
import { XAI_CONFIG, XAI_PKCE_VERIFIER_BYTES } from "./constants/xai";
|
||||
import {
|
||||
decodeIdTokenEmail as decodeXaiIdTokenEmail,
|
||||
discoverEndpoints as discoverXaiEndpoints,
|
||||
} from "./services/xai";
|
||||
|
||||
const BASE64_BLOCK_SIZE = 4;
|
||||
|
||||
@@ -186,6 +192,77 @@ const PROVIDERS = {
|
||||
},
|
||||
},
|
||||
|
||||
xai: {
|
||||
config: XAI_CONFIG,
|
||||
flowType: "authorization_code_pkce",
|
||||
fixedPort: XAI_CONFIG.loopbackPort,
|
||||
callbackPath: XAI_CONFIG.callbackPath,
|
||||
pkceVerifierBytes: XAI_PKCE_VERIFIER_BYTES,
|
||||
prepareConfig: async (config) => {
|
||||
const endpoints = await discoverXaiEndpoints();
|
||||
return {
|
||||
...config,
|
||||
authorizeUrl: endpoints.authorizeUrl,
|
||||
tokenUrl: endpoints.tokenUrl,
|
||||
};
|
||||
},
|
||||
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
|
||||
// Mirror CLIProxyAPI BuildAuthorizeURL: includes nonce, plan, referrer
|
||||
const nonce = crypto.randomBytes(16).toString("hex");
|
||||
const params = {
|
||||
response_type: "code",
|
||||
client_id: config.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope: config.scope,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: config.codeChallengeMethod,
|
||||
state,
|
||||
nonce,
|
||||
plan: "generic",
|
||||
referrer: "cli-proxy-api",
|
||||
};
|
||||
const qs = Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
||||
.join("&");
|
||||
return `${config.authorizeUrl}?${qs}`;
|
||||
},
|
||||
exchangeToken: async (config, code, redirectUri, codeVerifier) => {
|
||||
const response = await fetch(config.tokenUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
client_id: config.clientId,
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
code_verifier: codeVerifier,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`xAI token exchange failed: ${error}`);
|
||||
}
|
||||
return await response.json();
|
||||
},
|
||||
mapTokens: (tokens) => {
|
||||
const mapped = {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
expiresIn: tokens.expires_in,
|
||||
scope: tokens.scope,
|
||||
};
|
||||
const email = decodeXaiIdTokenEmail(tokens.id_token);
|
||||
if (email) mapped.email = email;
|
||||
if (tokens.id_token) {
|
||||
mapped.providerSpecificData = { idToken: tokens.id_token };
|
||||
}
|
||||
return mapped;
|
||||
},
|
||||
},
|
||||
|
||||
"gemini-cli": {
|
||||
config: GEMINI_CONFIG,
|
||||
flowType: "authorization_code",
|
||||
@@ -1183,18 +1260,21 @@ export function getProviderNames() {
|
||||
* Generate auth data for a provider
|
||||
* @param {object} [meta] - Provider-specific metadata (e.g. gitlab clientId/baseUrl)
|
||||
*/
|
||||
export function generateAuthData(providerName, redirectUri, meta) {
|
||||
export async function generateAuthData(providerName, redirectUri, meta) {
|
||||
const provider = getProvider(providerName);
|
||||
const { codeVerifier, codeChallenge, state } = generatePKCE();
|
||||
const config = provider.prepareConfig
|
||||
? await provider.prepareConfig(provider.config, meta || {})
|
||||
: provider.config;
|
||||
const { codeVerifier, codeChallenge, state } = generatePKCE(provider.pkceVerifierBytes);
|
||||
|
||||
let authUrl;
|
||||
if (provider.flowType === "device_code") {
|
||||
// Device code flow doesn't have auth URL upfront
|
||||
authUrl = null;
|
||||
} else if (provider.flowType === "authorization_code_pkce") {
|
||||
authUrl = provider.buildAuthUrl(provider.config, redirectUri, state, codeChallenge, meta || {});
|
||||
authUrl = provider.buildAuthUrl(config, redirectUri, state, codeChallenge, meta || {});
|
||||
} else {
|
||||
authUrl = provider.buildAuthUrl(provider.config, redirectUri, state, undefined, meta || {});
|
||||
authUrl = provider.buildAuthUrl(config, redirectUri, state, undefined, meta || {});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1215,8 +1295,11 @@ export function generateAuthData(providerName, redirectUri, meta) {
|
||||
*/
|
||||
export async function exchangeTokens(providerName, code, redirectUri, codeVerifier, state, meta) {
|
||||
const provider = getProvider(providerName);
|
||||
const config = provider.prepareConfig
|
||||
? await provider.prepareConfig(provider.config, meta || {})
|
||||
: provider.config;
|
||||
|
||||
const tokens = await provider.exchangeToken(provider.config, code, redirectUri, codeVerifier, state, meta || {});
|
||||
const tokens = await provider.exchangeToken(config, code, redirectUri, codeVerifier, state, meta || {});
|
||||
|
||||
let extra = null;
|
||||
if (provider.postExchange) {
|
||||
|
||||
Reference in New Issue
Block a user