openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
329 lines (328 loc) • 14.2 kB
JavaScript
import { n as withOpenAIOAuthResponse, t as createOpenAIAuthorizationCodeForm } from "./openai-oauth-http.runtime-CYYBufQ-.mjs";
import { asOptionalRecord, isRecord, normalizeBoundedOptionalString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { createOAuthLoginCancelledError, generatePKCE, oauthErrorHtml, oauthSuccessHtml, parseOAuthAuthorizationInput, resolveOAuthTokenExpiresAt, resolveOAuthTokenLifetimeMs, resolveOpenAICodexAuthIdentity, throwIfOAuthLoginAborted, withOAuthLoginAbort } from "openclaw/plugin-sdk/provider-oauth-runtime";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
import { sleepWithAbort } from "openclaw/plugin-sdk/retry-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
//#region extensions/openai/openai-chatgpt-oauth-authorization.runtime.ts
const CLIENT_ID$1 = "app_EMoamEEZ73f0CkXaXp7hrann";
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
const CALLBACK_PORT = 1455;
const CALLBACK_PATH = "/auth/callback";
const DEFAULT_CALLBACK_HOST = "localhost";
const LOOPBACK_CALLBACK_HOSTS = /* @__PURE__ */ new Set([
"localhost",
"127.0.0.1",
"::1"
]);
const SCOPE = "openid profile email offline_access";
function resolveOpenAICallbackHost(env = process.env) {
const host = env.OPENCLAW_OAUTH_CALLBACK_HOST?.trim() || DEFAULT_CALLBACK_HOST;
if (!LOOPBACK_CALLBACK_HOSTS.has(host)) throw new Error("OpenAI Codex OAuth callback host must be localhost, 127.0.0.1, or ::1");
return host;
}
function resolveOpenAIRedirectUri(host) {
const url = new URL(`http://${host === "::1" ? "[::1]" : host}:${CALLBACK_PORT}`);
url.pathname = CALLBACK_PATH;
return url.toString();
}
async function createOpenAIAuthorizationFlow(originator, redirectUri) {
if (typeof process === "undefined" || !process.versions?.node && !process.versions?.bun) throw new Error("OpenAI Codex OAuth is only available in Node.js environments");
const [{ verifier, challenge }, { randomBytes }] = await Promise.all([generatePKCE(), import("node:crypto")]);
const state = randomBytes(16).toString("hex");
const url = new URL(AUTHORIZE_URL);
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", CLIENT_ID$1);
url.searchParams.set("redirect_uri", redirectUri);
url.searchParams.set("scope", SCOPE);
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("state", state);
url.searchParams.set("id_token_add_organizations", "true");
url.searchParams.set("codex_cli_simplified_flow", "true");
url.searchParams.set("originator", originator);
return {
verifier,
redirectUri,
state,
url: url.toString()
};
}
//#endregion
//#region extensions/openai/openai-chatgpt-oauth-token.runtime.ts
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
const TOKEN_URL = "https://auth.openai.com/oauth/token";
const OAUTH_TOKEN_SSRF_POLICY = {
allowRfc2544BenchmarkRange: true,
allowIpv6UniqueLocalRange: true,
hostnameAllowlist: ["auth.openai.com"]
};
const TOKEN_REQUEST_TIMEOUT_MS = 3e4;
const OAUTH_TOKEN_RESPONSE_BODY_LIMIT_BYTES = 1048576;
const OAUTH_TOKEN_ERROR_SUMMARY_MAX_CHARS = 500;
const TOKEN_FAILURE_REASON_BY_CODE = {
invalid_grant: "invalid_grant",
invalid_refresh_token: "invalid_refresh_token",
refresh_token_expired: "expired",
refresh_token_invalidated: "token_invalidated",
refresh_token_reused: "refresh_token_reused"
};
const TOKEN_FAILURE_CODES = new Set(Object.keys(TOKEN_FAILURE_REASON_BY_CODE));
function normalizeTokenErrorSummary(value) {
const normalized = normalizeBoundedOptionalString(value, OAUTH_TOKEN_ERROR_SUMMARY_MAX_CHARS * 4)?.replace(/\s+/gu, " ").trim();
return normalized ? normalizeBoundedOptionalString(redactSensitiveText(normalized, { mode: "tools" }), OAUTH_TOKEN_ERROR_SUMMARY_MAX_CHARS) : void 0;
}
function normalizeTokenErrorFact(value, allowlist) {
const normalized = normalizeOptionalString(value)?.toLowerCase();
return normalized && allowlist.has(normalized) ? normalized : void 0;
}
function buildTokenResponseFailure(params) {
let body;
try {
body = asOptionalRecord(JSON.parse(params.text));
} catch {}
const error = asOptionalRecord(body?.error);
const code = normalizeTokenErrorFact(error?.code ?? (typeof body?.error === "string" ? body.error : body?.code), TOKEN_FAILURE_CODES);
const rawType = normalizeOptionalString(error?.type ?? body?.type)?.toLowerCase();
const errorType = rawType === "invalid_grant" || rawType === "invalid_request_error" ? rawType : void 0;
const reason = code ? TOKEN_FAILURE_REASON_BY_CODE[code] : void 0;
const summary = normalizeTokenErrorSummary(error?.message ?? body?.error_description ?? body?.message) ?? `OpenAI Codex token ${params.operation} failed (HTTP ${params.response.status}).`;
return {
type: "failed",
operation: params.operation,
summary,
status: params.response.status,
...code ? { code } : {},
...errorType ? { errorType } : {},
...reason ? { reason } : {}
};
}
function formatMissingTokenResponseFields(json, existingRefreshToken) {
const missing = [];
if (!json.access_token) missing.push("access_token");
if (!json.refresh_token && !existingRefreshToken) missing.push("refresh_token");
if (resolveOAuthTokenLifetimeMs(json.expires_in) === void 0) missing.push("expires_in");
return missing.join(", ");
}
function formatTokenRequestError(operation, error, timeoutMs, signal) {
if (signal?.aborted) return "Login cancelled";
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) return `OpenAI Codex token ${operation} timed out after ${timeoutMs}ms`;
const detail = normalizeTokenErrorSummary(error instanceof Error ? error.message : String(error));
return normalizeTokenErrorSummary(`OpenAI Codex token ${operation} error${detail ? `: ${detail}` : ""}`) ?? `OpenAI Codex token ${operation} error`;
}
async function requestOpenAIToken(body, operation, options = {}, existingRefreshToken) {
const timeoutMs = options.timeoutMs ?? TOKEN_REQUEST_TIMEOUT_MS;
try {
throwIfOAuthLoginAborted(options.signal);
const { response: tokenResponse, text } = await withOpenAIOAuthResponse({
url: TOKEN_URL,
mode: "trusted_env_proxy",
policy: OAUTH_TOKEN_SSRF_POLICY,
init: {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body
},
timeoutMs,
signal: options.signal,
beforeRequest: options.assertCurrent,
auditContext: "openai-chatgpt-oauth-token"
}, async (response) => {
const bytes = await readResponseWithLimit(response, OAUTH_TOKEN_RESPONSE_BODY_LIMIT_BYTES, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`OpenAI Codex OAuth token response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
return {
response,
text: new TextDecoder().decode(bytes)
};
});
return readOpenAITokenResponse(tokenResponse, text, operation, existingRefreshToken);
} catch (error) {
return {
type: "failed",
operation,
...options.signal?.aborted ? { cancelled: true } : {},
summary: formatTokenRequestError(operation, error, timeoutMs, options.signal)
};
}
}
function readOpenAITokenResponse(response, text, operation, existingRefreshToken) {
if (!response.ok) return buildTokenResponseFailure({
response,
operation,
text
});
let json;
try {
json = JSON.parse(text);
} catch {
return {
type: "failed",
operation,
summary: `OpenAI Codex token ${operation} failed: response is not valid JSON`
};
}
if (!isRecord(json)) return {
type: "failed",
operation,
summary: `OpenAI Codex token ${operation} failed: expected JSON object response`
};
const expires = resolveOAuthTokenExpiresAt(json.expires_in);
const refreshToken = json.refresh_token || existingRefreshToken;
if (!json.access_token || !refreshToken || expires === void 0) return {
type: "failed",
operation,
summary: `OpenAI Codex token ${operation} response missing fields: ${formatMissingTokenResponseFields(json, existingRefreshToken)}`
};
return {
type: "success",
access: json.access_token,
refresh: refreshToken,
expires
};
}
async function exchangeOpenAIAuthorizationCode(code, verifier, redirectUri, options = {}) {
return await requestOpenAIToken(createOpenAIAuthorizationCodeForm({
clientId: CLIENT_ID,
code,
verifier,
redirectUri
}), "exchange", options);
}
async function refreshOpenAIAccessToken(refreshToken, options = {}) {
return await requestOpenAIToken(new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLIENT_ID
}), "refresh", options, refreshToken);
}
//#endregion
//#region extensions/openai/openai-chatgpt-oauth-flow.runtime.ts
const CALLBACK_HOST = resolveOpenAICallbackHost();
const REDIRECT_URI = resolveOpenAIRedirectUri(CALLBACK_HOST);
const MANUAL_PROMPT_FALLBACK_MS = 15e3;
const loadOAuthCallbackServer = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/provider-auth-runtime").then(({ startProviderOAuthLoopbackCallbackServer }) => startProviderOAuthLoopbackCallbackServer));
function waitForManualPromptFallback(signal) {
return sleepWithAbort(MANUAL_PROMPT_FALLBACK_MS, signal, { ref: false }).then(() => null, () => {
throw createOAuthLoginCancelledError();
});
}
function parseAuthorizationCode(input, state) {
const parsed = parseOAuthAuthorizationInput(input);
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
return parsed.code;
}
async function promptForAuthorizationCode(onPrompt, state) {
return parseAuthorizationCode(await onPrompt({ message: "Paste the authorization code (or full redirect URL):" }), state);
}
function resolveOpenAICredentials(result) {
if (result.type !== "success") {
if (result.cancelled) throw createOAuthLoginCancelledError();
const facts = [
result.status ? `HTTP ${result.status}` : void 0,
result.code ? `code=${result.code}` : void 0,
result.errorType ? `type=${result.errorType}` : void 0
].filter((value) => Boolean(value));
const diagnostic = facts.length > 0 ? `OpenAI Codex token ${result.operation} failed (${facts.join("; ")}).` : void 0;
throw Object.assign(new Error([result.summary, diagnostic].filter(Boolean).join("\n\n")), { oauthRefreshFailure: {
summary: result.summary,
...result.errorType ? { errorType: result.errorType } : {},
...result.reason ? { reason: result.reason } : {},
...result.status ? { status: result.status } : {}
} });
}
const accountId = resolveOpenAICodexAuthIdentity({ access: result.access }).accountId;
if (!accountId) throw new Error("Failed to extract accountId from token");
return {
access: result.access,
refresh: result.refresh,
expires: result.expires,
accountId
};
}
async function loginOpenAICodex(options) {
options.assertCurrent?.();
throwIfOAuthLoginAborted(options.signal);
const { verifier, redirectUri, state, url } = await createOpenAIAuthorizationFlow(options.originator ?? "openclaw", REDIRECT_URI);
const startCallbackServer = await loadOAuthCallbackServer();
options.assertCurrent?.();
throwIfOAuthLoginAborted(options.signal);
let server;
try {
server = await startCallbackServer({
redirectUrl: REDIRECT_URI,
expectedState: state,
bindOnlyHostname: CALLBACK_HOST,
signal: options.signal,
renderSuccess: () => ({
body: oauthSuccessHtml("OpenAI authentication completed. You can close this window."),
contentType: "text/html; charset=utf-8"
}),
renderError: (message) => ({
body: oauthErrorHtml(message),
contentType: "text/html; charset=utf-8"
})
});
} catch {
options.assertCurrent?.();
throwIfOAuthLoginAborted(options.signal);
}
let cancelWait;
const cancelledWait = new Promise((resolve) => {
cancelWait = () => resolve(null);
});
const waitForLogin = (promise) => withOAuthLoginAbort(promise, options.signal, cancelWait);
let code;
try {
options.assertCurrent?.();
throwIfOAuthLoginAborted(options.signal);
await waitForLogin(Promise.resolve(options.onAuth({
url,
instructions: "A browser window should open. Complete login to finish."
})));
throwIfOAuthLoginAborted(options.signal);
const callbackPromise = Promise.race([server ? server.waitForCallback().then((result) => {
if (result.type === "oauth_error") throw new Error("OpenAI authorization was not completed.");
return { code: result.code };
}) : Promise.resolve(null), cancelledWait]);
callbackPromise.catch(() => void 0);
if (options.onManualCodeInput) {
let manualCode;
let manualError;
const manualPromise = options.onManualCodeInput().then((input) => {
manualCode = input;
cancelWait();
}).catch((err) => {
manualError = err instanceof Error ? err : new Error(String(err));
cancelWait();
});
const result = await waitForLogin(callbackPromise);
if (!result?.code && !manualCode && !manualError) await waitForLogin(manualPromise);
if (manualError) throw manualError;
if (result?.code) code = result.code;
else if (manualCode) code = parseAuthorizationCode(manualCode, state);
} else {
const result = await waitForLogin(Promise.race([callbackPromise, waitForManualPromptFallback(options.signal)]));
if (result?.code) code = result.code;
else {
const promptCodePromise = promptForAuthorizationCode(options.onPrompt, state).then((promptCode) => {
cancelWait();
return promptCode;
});
code = await waitForLogin(Promise.race([callbackPromise.then((callback) => callback?.code), promptCodePromise]));
}
}
if (!code) code = await waitForLogin(promptForAuthorizationCode(options.onPrompt, state));
if (!code) throw new Error("Missing authorization code");
return resolveOpenAICredentials(await exchangeOpenAIAuthorizationCode(code, verifier, redirectUri, {
signal: options.signal,
assertCurrent: options.assertCurrent
}));
} finally {
await server?.close();
}
}
async function refreshOpenAICodexToken(refreshToken) {
return resolveOpenAICredentials(await refreshOpenAIAccessToken(refreshToken));
}
//#endregion
export { refreshOpenAICodexToken as n, loginOpenAICodex as t };