openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
450 lines (449 loc) • 19.6 kB
JavaScript
import { A as resolveExpiresAtMsFromEpochSeconds, T as positiveSecondsToSafeMilliseconds, k as resolveExpiresAtMsFromDurationSeconds } from "../../number-coercion-CLj0HTDM.js";
import { a as asOptionalRecord } from "../../record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "../../string-coerce-CIXf7egm.js";
import { t as sleep } from "../../sleep-D7nua6TP.js";
import { t as formatErrorMessage } from "../../errors-Db3Ymjlb.js";
import { i as readResponseWithLimit } from "../../http-response-body-CwT_cCNz.js";
import { a as matchesNoProxy, o as resolveEnvHttpProxyAgentOptions } from "../../proxy-env-BepksPz2.js";
import { i as fetchWithSsrFGuard } from "../../fetch-guard-BMdGQhbb.js";
import { o as toFormUrlEncoded } from "../../provider-auth-BeZ7NZUU.js";
import { t as buildOauthProviderAuthResult } from "../../provider-auth-result-BJQq6asp.js";
import "../../error-runtime-Bz9Tw57Z.js";
import "../../response-limit-runtime-BV0RL9tn.js";
import "../../runtime-env-0Q-gCyUb.js";
import "../../fetch-runtime-Dww9PPyp.js";
import "../../number-runtime-Cy4drVnh.js";
import "../../string-coerce-runtime-GQa0ehRA.js";
import "../../ssrf-runtime-Bum5C6NN.js";
import { i as applyXaiOAuthConfig, n as XAI_OAUTH_DEFAULT_MODEL_REF } from "../../onboard-YJAncfKe.js";
import { t as xaiUserAgent } from "../../xai-user-agent-DducGhLp.js";
//#region extensions/xai/xai-oauth.ts
const PROVIDER_ID = "xai";
const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
const XAI_OAUTH_ISSUER = "https://auth.x.ai";
const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
const XAI_LEGACY_OAUTH_TOKEN_ENDPOINT = `${XAI_OAUTH_ISSUER}/oauth/token`;
const XAI_OAUTH_TIMEOUT_MS = 3e5;
const XAI_OAUTH_FETCH_TIMEOUT_MS = 3e4;
const XAI_OAUTH_RESPONSE_MAX_BYTES = 16777216;
const XAI_OAUTH_REFRESH_MAX_ATTEMPTS = 3;
const XAI_OAUTH_REFRESH_RETRY_DELAY_MS = 250;
const XAI_DEVICE_CODE_DEFAULT_INTERVAL_MS = 5e3;
const XAI_DEVICE_CODE_MIN_INTERVAL_MS = 1e3;
const XAI_DEVICE_CODE_SLOW_DOWN_INCREMENT_MS = 5e3;
const XAI_DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
function fetchXaiOAuth(url, options, body) {
return fetchWithSsrFGuard({
url,
fetchImpl: options.fetchImpl,
beforeRequest: options.assertCurrent,
mode: "trusted_explicit_proxy",
resolveDispatcherPolicy: (target) => {
const proxies = resolveEnvHttpProxyAgentOptions();
const proxyUrl = target.protocol === "https:" ? proxies?.httpsProxy : proxies?.httpProxy;
return proxyUrl && !matchesNoProxy(target.toString()) ? {
mode: "explicit-proxy",
proxyUrl,
allowPrivateProxy: true
} : void 0;
},
signal: options.signal,
timeoutMs: XAI_OAUTH_FETCH_TIMEOUT_MS,
requireHttps: true,
auditContext: "xai-oauth",
init: {
headers: {
Accept: "application/json",
"User-Agent": xaiUserAgent(),
...body ? { "Content-Type": "application/x-www-form-urlencoded" } : {}
},
...body ? {
method: "POST",
body: toFormUrlEncoded(body)
} : {}
}
});
}
function isTrustedXaiOAuthEndpoint(endpoint) {
try {
const url = new URL(endpoint);
if (url.protocol !== "https:") return false;
return url.hostname === "x.ai" || url.hostname.endsWith(".x.ai");
} catch {
return false;
}
}
function requireTrustedXaiOAuthEndpoint(endpoint, label) {
if (!isTrustedXaiOAuthEndpoint(endpoint)) throw new Error(`xAI OAuth discovery returned untrusted ${label}`);
return endpoint;
}
async function readResponseBody({ response, release }) {
try {
const buffer = await readResponseWithLimit(response, XAI_OAUTH_RESPONSE_MAX_BYTES, { onOverflow: ({ maxBytes }) => /* @__PURE__ */ new Error(`xAI OAuth response exceeds ${maxBytes} bytes`) });
const text = new TextDecoder().decode(buffer);
let json;
try {
json = JSON.parse(text);
} catch {
json = null;
}
return {
json,
text
};
} finally {
await release();
}
}
async function readJsonResponse(result, context) {
const { response } = result;
const body = await readResponseBody(result);
if (!response.ok) {
const json = asOptionalRecord(body.json);
const errorText = json?.error_description ?? json?.error;
throw new Error(`${context} failed (${response.status})${typeof errorText === "string" ? `: ${errorText}` : ""}`);
}
return body.json;
}
async function fetchXaiOAuthDiscoveryDocument(options = {}) {
const response = await fetchXaiOAuth(XAI_OAUTH_DISCOVERY_URL, options);
return asOptionalRecord(await readJsonResponse(response, "xAI OAuth discovery")) ?? {};
}
async function fetchXaiOAuthDiscovery(options = {}) {
const tokenEndpoint = (await fetchXaiOAuthDiscoveryDocument(options)).token_endpoint;
if (typeof tokenEndpoint !== "string") throw new Error("xAI OAuth discovery response is missing the token endpoint");
return { tokenEndpoint: requireTrustedXaiOAuthEndpoint(tokenEndpoint, "token endpoint") };
}
async function fetchXaiDeviceCodeDiscovery(options = {}) {
const json = await fetchXaiOAuthDiscoveryDocument(options);
const deviceAuthorizationEndpoint = json.device_authorization_endpoint;
const tokenEndpoint = json.token_endpoint;
if (typeof deviceAuthorizationEndpoint !== "string" || typeof tokenEndpoint !== "string") throw new Error("xAI OAuth discovery response is missing device code endpoints");
return {
deviceAuthorizationEndpoint: requireTrustedXaiOAuthEndpoint(deviceAuthorizationEndpoint, "device authorization endpoint"),
tokenEndpoint: requireTrustedXaiOAuthEndpoint(tokenEndpoint, "token endpoint")
};
}
function normalizeExpires(value, now) {
return resolveExpiresAtMsFromDurationSeconds(value, { nowMs: now() });
}
function parseXaiOAuthTokenResponse(value, now, options = {}) {
const json = asOptionalRecord(value) ?? {};
const accessToken = json.access_token;
if (typeof accessToken !== "string" || accessToken.trim().length === 0) throw new Error("xAI OAuth token response is missing access_token");
const refreshToken = typeof json.refresh_token === "string" && json.refresh_token.trim().length > 0 ? json.refresh_token : void 0;
if (options.requireRefreshToken && !refreshToken) throw new Error("xAI OAuth token response is missing refresh_token. Re-run the login; if the issue persists, the OAuth client is not configured to issue refresh tokens (commonly because the offline_access scope was rejected).");
const idToken = typeof json.id_token === "string" && json.id_token.trim().length > 0 ? json.id_token : void 0;
const expires = normalizeExpires(json.expires_in, now) ?? deriveExpiresFromJwt(accessToken);
return {
accessToken,
...refreshToken ? { refreshToken } : {},
...idToken ? { idToken } : {},
...expires ? { expires } : {}
};
}
function deriveExpiresFromJwt(token) {
if (!token) return;
const exp = decodeJwtPayload(token).exp;
return resolveExpiresAtMsFromEpochSeconds(exp);
}
function parseXaiOAuthErrorResponse(value) {
const json = asOptionalRecord(value) ?? {};
const error = typeof json.error === "string" ? json.error : void 0;
const errorDescription = typeof json.error_description === "string" ? json.error_description : void 0;
return {
...error ? { error } : {},
...errorDescription ? { errorDescription } : {}
};
}
function formatXaiOAuthError(params) {
const error = parseXaiOAuthErrorResponse(params.body);
if (error.error && error.errorDescription) return `${params.context} failed (${params.status}): ${error.error} (${error.errorDescription})`;
if (error.error) return `${params.context} failed (${params.status}): ${error.error}`;
return `${params.context} failed (${params.status})`;
}
function isLikelyXaiCloudflareChallenge(params) {
const contentType = params.response.headers.get("content-type") ?? "";
return params.response.headers.get("cf-mitigated") === "challenge" || /text\/html/i.test(contentType) || / 0 ? requireTrustedXaiOAuthEndpoint(verificationUriComplete, "complete device verification URI") : void 0;
return {
deviceCode,
userCode,
verificationUri: trustedVerificationUri,
...trustedVerificationUriComplete ? { verificationUriComplete: trustedVerificationUriComplete } : {},
expiresInMs: positiveSecondsToSafeMilliseconds(json.expires_in) ?? XAI_OAUTH_TIMEOUT_MS,
intervalMs: positiveSecondsToSafeMilliseconds(json.interval) ?? XAI_DEVICE_CODE_DEFAULT_INTERVAL_MS
};
}
function resolveNextXaiDeviceCodePollDelayMs(intervalMs, deadlineMs) {
const remainingMs = Math.max(0, deadlineMs - Date.now());
return Math.min(Math.max(intervalMs, XAI_DEVICE_CODE_MIN_INTERVAL_MS), remainingMs);
}
async function pollXaiDeviceCodeToken(params) {
const deadlineMs = Date.now() + params.expiresInMs;
let intervalMs = params.intervalMs;
while (Date.now() < deadlineMs) {
const result = await fetchXaiOAuth(requireTrustedXaiOAuthEndpoint(params.tokenEndpoint, "token endpoint"), params, {
grant_type: XAI_DEVICE_CODE_GRANT_TYPE,
client_id: XAI_OAUTH_CLIENT_ID,
device_code: params.deviceCode
});
const { response } = result;
let body;
try {
body = (await readResponseBody(result)).json;
} catch {
body = null;
}
if (response.ok) return parseXaiOAuthTokenResponse(body, params.now ?? Date.now, { requireRefreshToken: true });
const error = parseXaiOAuthErrorResponse(body).error;
if (error === "authorization_pending") {
await waitForXaiDeviceCodePoll(resolveNextXaiDeviceCodePollDelayMs(intervalMs, deadlineMs), params.signal);
continue;
}
if (error === "slow_down") {
intervalMs += XAI_DEVICE_CODE_SLOW_DOWN_INCREMENT_MS;
await waitForXaiDeviceCodePoll(resolveNextXaiDeviceCodePollDelayMs(intervalMs, deadlineMs), params.signal);
continue;
}
if (error === "access_denied" || error === "authorization_denied") throw new Error("xAI device authorization was denied");
if (error === "expired_token") throw new Error("xAI device code expired. Re-run the login.");
throw new Error(formatXaiOAuthError({
context: "xAI device token exchange",
status: response.status,
body
}));
}
throw new Error("xAI device authorization timed out");
}
async function waitForXaiDeviceCodePoll(delayMs, signal) {
if (!signal) {
await new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
return;
}
await new Promise((resolve, reject) => {
const onAbort = () => {
clearTimeout(timeout);
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("xAI login cancelled"));
};
const timeout = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve();
}, delayMs);
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) onAbort();
});
}
function decodeJwtPayload(token) {
if (!token) return {};
const part = token.split(".")[1];
if (!part) return {};
try {
return asOptionalRecord(JSON.parse(Buffer.from(part, "base64url").toString("utf8"))) ?? {};
} catch {
return {};
}
}
function resolveXaiOAuthIdentity(tokens) {
const payload = decodeJwtPayload(tokens.idToken ?? tokens.accessToken);
const email = typeof payload.email === "string" ? payload.email : void 0;
const name = typeof payload.name === "string" ? payload.name : void 0;
const sub = typeof payload.sub === "string" ? payload.sub : void 0;
return {
...email ? { email } : {},
...name ? { displayName: name } : {},
...sub ? { accountId: sub } : {}
};
}
function isLegacyXaiOAuthTokenEndpoint(endpoint) {
try {
const url = new URL(endpoint);
return `${url.origin}${url.pathname}` === XAI_LEGACY_OAUTH_TOKEN_ENDPOINT;
} catch {
return false;
}
}
async function resolveXaiOAuthRefreshTokenEndpoint(credential, options) {
const cachedEndpoint = normalizeOptionalString(credential.tokenEndpoint);
if (!cachedEndpoint || isLegacyXaiOAuthTokenEndpoint(cachedEndpoint)) return (await fetchXaiOAuthDiscovery(options)).tokenEndpoint;
return cachedEndpoint;
}
async function noteXaiDeviceCode(ctx, deviceCode) {
const expiresInMinutes = Math.max(1, Math.round(deviceCode.expiresInMs / 6e4));
if (ctx.prompter.deviceCode) {
await ctx.prompter.deviceCode({
title: "xAI OAuth",
code: deviceCode.userCode,
expiresInMinutes,
message: "Enter this one-time code on the xAI sign-in page."
});
return;
}
await ctx.prompter.note([
ctx.isRemote ? "Open this URL in your LOCAL browser and enter the code below." : "Open this URL in your browser and enter the code below.",
`URL: ${deviceCode.verificationUriComplete ?? deviceCode.verificationUri}`,
`Code: ${deviceCode.userCode}`,
`Code expires in ${expiresInMinutes} minutes. Never share it.`
].join("\n"), "xAI OAuth");
}
async function loginXaiDeviceCode(ctx) {
const progress = ctx.prompter.progress("Starting xAI OAuth...");
const requestOptions = {
signal: ctx.signal,
assertCurrent: ctx.assertCurrent
};
try {
const discovery = await fetchXaiDeviceCodeDiscovery(requestOptions);
progress.update("Requesting xAI OAuth device code...");
const deviceCode = await requestXaiDeviceCode({
deviceAuthorizationEndpoint: discovery.deviceAuthorizationEndpoint,
...requestOptions
});
const browserUrl = deviceCode.verificationUriComplete ?? deviceCode.verificationUri;
let openedBrowser = false;
try {
await ctx.openUrl(browserUrl);
openedBrowser = true;
} catch {
ctx.runtime.log(`Open manually: ${deviceCode.verificationUri}`);
}
await noteXaiDeviceCode(ctx, deviceCode);
const logUrl = deviceCode.verificationUri;
if (ctx.isRemote) ctx.runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${logUrl}\n`);
else if (openedBrowser) ctx.runtime.log(`Open: ${logUrl}`);
progress.update("Waiting for xAI device authorization...");
const tokens = await pollXaiDeviceCodeToken({
tokenEndpoint: discovery.tokenEndpoint,
deviceCode: deviceCode.deviceCode,
expiresInMs: deviceCode.expiresInMs,
intervalMs: deviceCode.intervalMs,
...requestOptions
});
const identity = resolveXaiOAuthIdentity(tokens);
progress.stop("xAI OAuth complete");
return buildOauthProviderAuthResult({
providerId: PROVIDER_ID,
defaultModel: XAI_OAUTH_DEFAULT_MODEL_REF,
access: tokens.accessToken,
refresh: tokens.refreshToken,
expires: tokens.expires,
email: identity.email,
displayName: identity.displayName,
profileName: identity.email ?? identity.accountId,
configPatch: applyXaiOAuthConfig(ctx.config),
credentialExtra: {
tokenEndpoint: discovery.tokenEndpoint,
deviceAuthorizationEndpoint: discovery.deviceAuthorizationEndpoint,
issuer: XAI_OAUTH_ISSUER,
authFlow: "device-code",
...tokens.idToken ? { idToken: tokens.idToken } : {},
...identity.accountId ? { accountId: identity.accountId } : {}
},
notes: ["xAI OAuth uses device-code verification without requiring a localhost callback.", "xAI may label the consent app as Grok Build because OpenClaw uses xAI's shared OAuth client."]
});
} catch (err) {
progress.stop("xAI OAuth failed");
throw new Error(`xAI OAuth failed: ${formatErrorMessage(err)}`, { cause: err });
}
}
async function refreshXaiOAuthCredential(credential, options = {}) {
const refreshToken = credential.refresh;
if (!refreshToken) throw new Error("xAI OAuth credential is missing refresh token");
const tokenEndpoint = await resolveXaiOAuthRefreshTokenEndpoint(credential, options);
const tokens = await exchangeXaiOAuthToken({
...options,
tokenEndpoint,
context: "xAI OAuth refresh",
body: {
grant_type: "refresh_token",
client_id: XAI_OAUTH_CLIENT_ID,
refresh_token: refreshToken
}
});
const identity = resolveXaiOAuthIdentity(tokens);
return {
...credential,
type: "oauth",
provider: PROVIDER_ID,
access: tokens.accessToken,
refresh: tokens.refreshToken ?? refreshToken,
...tokens.expires ? { expires: tokens.expires } : {},
...tokens.idToken ? { idToken: tokens.idToken } : {},
...identity.email ? { email: identity.email } : {},
...identity.displayName ? { displayName: identity.displayName } : {},
...identity.accountId ? { accountId: identity.accountId } : {},
tokenEndpoint,
issuer: XAI_OAUTH_ISSUER
};
}
//#endregion
export { loginXaiDeviceCode, refreshXaiOAuthCredential };