UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

467 lines (466 loc) 18 kB
import { l as toErrorObject } from "./error-coercion-D_-xJ90S.js"; import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js"; import "./utils-P__uGsPB.js"; import "./errors-Db3Ymjlb.js"; import "./http-body-D3IMwTJJ.js"; import { i as readResponseWithLimit } from "./http-response-body-CwT_cCNz.js"; import { t as startOAuthLoopbackCallbackServer } from "./oauth-loopback-callback-BTvKedhW.js"; import { a as oauthErrorHtml, c as resolveOAuthTokenExpiresAt, d as withOAuthLoginAbort, i as generatePKCE, n as createOAuthLoginCancelledError, o as oauthSuccessHtml, r as generateBase64UrlOAuthState, s as parseOAuthAuthorizationInput, t as buildOAuthRequestSignal, u as throwIfOAuthLoginAborted } from "./provider-oauth-runtime-CduuU4e2.js"; import { n as loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime-BjmVS_9G.js"; import { s as resolveSharedAuthStorePath } from "./path-resolve-oRkRBkQd.js"; import { o as inspectPersistedAuthProfileStoreRaw } from "./sqlite-MN_7y26V.js"; import path from "node:path"; //#region src/llm/utils/oauth/anthropic.ts /** * Anthropic OAuth flow (Claude Pro/Max) * * NOTE: This module uses Node.js http.createServer for the OAuth callback server. * It is only intended for CLI use, not browser environments. */ const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; const AUTHORIZE_URL = "https://claude.ai/oauth/authorize"; const TOKEN_URL = "https://platform.claude.com/v1/oauth/token"; const DEFAULT_CALLBACK_HOST = "127.0.0.1"; const LOOPBACK_CALLBACK_HOSTS = /* @__PURE__ */ new Set([ "localhost", "127.0.0.1", "::1" ]); const REDIRECT_URI = `http://localhost:53692/callback`; const CALLBACK_TIMEOUT_MS = 3e5; function resolveCallbackHost(env = process.env) { const host = env.OPENCLAW_OAUTH_CALLBACK_HOST?.trim() || DEFAULT_CALLBACK_HOST; if (!LOOPBACK_CALLBACK_HOSTS.has(host)) throw new Error("Anthropic OAuth callback host must be localhost, 127.0.0.1, or ::1"); return host; } const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"; /** Max response body bytes for Anthropic OAuth token endpoint (16 MiB). */ const OAUTH_RESPONSE_MAX_BYTES = 16777216; function formatErrorDetails(error) { if (error instanceof Error) { const details = [`${error.name}: ${error.message}`]; const errorWithCode = error; if (errorWithCode.code) details.push(`code=${errorWithCode.code}`); if (errorWithCode.errno !== void 0) details.push(`errno=${String(errorWithCode.errno)}`); if (error.cause !== void 0) details.push(`cause=${formatErrorDetails(error.cause)}`); if (error.stack) details.push(`stack=${error.stack}`); return details.join("; "); } return String(error); } function formatTokenResponseParseContext(responseBody) { return `bodyBytes=${Buffer.byteLength(responseBody, "utf8")}`; } function parseTokenCredentials(responseBody, options) { let data; try { data = JSON.parse(responseBody); } catch (error) { throw new Error(`${options.invalidJsonMessage} url=${TOKEN_URL}; ${formatTokenResponseParseContext(responseBody)}; details=${formatErrorDetails(error)}`, { cause: error }); } if (!data || typeof data !== "object") throw new Error(`${options.invalidFieldsMessage} url=${TOKEN_URL}; ${formatTokenResponseParseContext(responseBody)}`); const record = data; const expires = resolveOAuthTokenExpiresAt(record.expires_in, { refreshSkewMs: 3e5 }); if (typeof record.access_token !== "string" || !record.access_token || typeof record.refresh_token !== "string" || !record.refresh_token || expires === void 0) throw new Error(`${options.invalidFieldsMessage} url=${TOKEN_URL}; ${formatTokenResponseParseContext(responseBody)}`); return { refresh: record.refresh_token, access: record.access_token, expires }; } async function startCallbackServer(expectedState) { if (typeof process === "undefined" || !process.versions?.node && !process.versions?.bun) throw new Error("Anthropic OAuth is only available in Node.js environments"); const callback = await startOAuthLoopbackCallbackServer({ redirectUrl: REDIRECT_URI, expectedState, timeoutMs: CALLBACK_TIMEOUT_MS, bindHostname: resolveCallbackHost(), renderSuccess: () => ({ body: oauthSuccessHtml("Authorization received; return to the terminal while OpenClaw finishes."), contentType: "text/html; charset=utf-8" }), renderError: (message) => ({ body: oauthErrorHtml(message), contentType: "text/html; charset=utf-8" }) }); return { cancelWait: () => void callback.close(), waitForCode: async () => { try { const result = await callback.waitForCallback(); if (result.type === "oauth_error") throw new Error(`Anthropic OAuth error: ${result.error}`); return { code: result.code, state: result.state }; } catch (error) { if (error instanceof Error && (error.message === "OAuth callback timeout" || error.message === "OAuth callback cancelled")) return null; throw error; } }, close: callback.close }; } async function postJson(url, body, options = {}) { const timeoutMs = options.timeoutMs ?? 3e4; throwIfOAuthLoginAborted(options.signal); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(body), signal: buildOAuthRequestSignal({ signal: options.signal, timeoutMs }) }); const buffer = await readResponseWithLimit(response, OAUTH_RESPONSE_MAX_BYTES, { onOverflow: ({ size }) => /* @__PURE__ */ new Error(`Anthropic OAuth response too large: ${size} bytes`) }); const responseBody = new TextDecoder().decode(buffer); if (!response.ok) throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`); return responseBody; } async function exchangeAuthorizationCode(code, state, verifier, redirectUri, signal) { let responseBody; try { responseBody = await postJson(TOKEN_URL, { grant_type: "authorization_code", client_id: CLIENT_ID, code, state, redirect_uri: redirectUri, code_verifier: verifier }, { signal }); } catch (error) { if (signal?.aborted) throw createOAuthLoginCancelledError(); throw new Error(`Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; response_type=authorization_code; details=${formatErrorDetails(error)}`, { cause: error }); } return parseTokenCredentials(responseBody, { invalidJsonMessage: "Token exchange returned invalid JSON.", invalidFieldsMessage: "Token exchange returned invalid token fields." }); } /** * Login with Anthropic OAuth (authorization code + PKCE) */ async function loginAnthropic(options) { throwIfOAuthLoginAborted(options.signal); const { verifier, challenge } = await generatePKCE(); const expectedState = generateBase64UrlOAuthState(); const server = await startCallbackServer(expectedState); let code; let state; try { throwIfOAuthLoginAborted(options.signal); const authParams = new URLSearchParams({ code: "true", client_id: CLIENT_ID, response_type: "code", redirect_uri: REDIRECT_URI, scope: SCOPES, code_challenge: challenge, code_challenge_method: "S256", state: expectedState }); options.onAuth({ url: `${AUTHORIZE_URL}?${authParams.toString()}`, instructions: "Complete login in your browser. If the browser is on another machine, paste the final redirect URL here." }); throwIfOAuthLoginAborted(options.signal); if (options.onManualCodeInput) { let manualInput; let manualError; const manualPromise = options.onManualCodeInput().then((input) => { manualInput = input; server.cancelWait(); }).catch((err) => { manualError = err instanceof Error ? err : new Error(String(err)); server.cancelWait(); }); const result = await withOAuthLoginAbort(server.waitForCode(), options.signal, server.cancelWait); if (manualError) throw manualError; if (result?.code) { code = result.code; state = result.state; } else if (manualInput) { const parsed = parseOAuthAuthorizationInput(manualInput); if (parsed.state && parsed.state !== expectedState) throw new Error("OAuth state mismatch"); code = parsed.code; state = parsed.state ?? expectedState; } if (!code) { await withOAuthLoginAbort(manualPromise, options.signal, server.cancelWait); if (manualError) throw toErrorObject(manualError, "Non-Error thrown"); if (manualInput) { const parsed = parseOAuthAuthorizationInput(manualInput); if (parsed.state && parsed.state !== expectedState) throw new Error("OAuth state mismatch"); code = parsed.code; state = parsed.state ?? expectedState; } } } else { const result = await withOAuthLoginAbort(server.waitForCode(), options.signal, server.cancelWait); if (result?.code) { code = result.code; state = result.state; } } if (!code) { const input = await withOAuthLoginAbort(options.onPrompt({ message: "Paste the authorization code or full redirect URL:", placeholder: REDIRECT_URI }), options.signal, server.cancelWait); const parsed = parseOAuthAuthorizationInput(input); if (parsed.state && parsed.state !== expectedState) throw new Error("OAuth state mismatch"); code = parsed.code; state = parsed.state ?? expectedState; } if (!code) throw new Error("Missing authorization code"); if (!state) throw new Error("Missing OAuth state"); options.onProgress?.("Exchanging authorization code for tokens..."); return exchangeAuthorizationCode(code, state, verifier, REDIRECT_URI, options.signal); } finally { await server.close(); } } /** * Refresh Anthropic OAuth token */ async function refreshAnthropicToken(refreshToken) { let responseBody; try { responseBody = await postJson(TOKEN_URL, { grant_type: "refresh_token", client_id: CLIENT_ID, refresh_token: refreshToken }); } catch (error) { throw new Error(`Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}`, { cause: error }); } return parseTokenCredentials(responseBody, { invalidJsonMessage: "Anthropic token refresh returned invalid JSON.", invalidFieldsMessage: "Anthropic token refresh returned invalid token fields." }); } const anthropicOAuthProvider = { id: "anthropic", name: "Anthropic (Claude Pro/Max)", usesCallbackServer: true, async login(callbacks) { return loginAnthropic({ onAuth: callbacks.onAuth, onPrompt: callbacks.onPrompt, onProgress: callbacks.onProgress, onManualCodeInput: callbacks.onManualCodeInput, signal: callbacks.signal }); }, async refreshToken(credentials) { return refreshAnthropicToken(credentials.refresh); }, getApiKey(credentials) { return credentials.access; } }; //#endregion //#region src/llm/utils/oauth/openai-chatgpt.ts const OPENAI_CODEX_PROVIDER_ID = "openai"; function loadOpenAICodexOAuthFacade() { return loadActivatedBundledPluginPublicSurfaceModuleSync({ dirName: "openai", artifactBasename: "api.js" }); } function createLegacyRuntime(callbacks) { return { log: (message) => callbacks.onProgress?.(String(message)), error: (message) => callbacks.onProgress?.(String(message)), exit: (code) => { throw new Error(`exit:${code}`); } }; } function createLegacyPrompter(callbacks) { const progress = { update: (message) => callbacks.onProgress?.(message), stop: (message) => { if (message) callbacks.onProgress?.(message); } }; return { intro: async () => {}, outro: async () => {}, note: async (message) => callbacks.onProgress?.(message), select: async (params) => params.options[0]?.value, multiselect: async (params) => params.initialValues ?? [], text: async (prompt) => { const input = callbacks.onPrompt({ message: prompt.message, placeholder: prompt.placeholder }); return await withOAuthLoginAbort(input, callbacks.signal); }, confirm: async () => false, progress: () => progress }; } async function refreshViaProviderRuntime(refreshToken) { const { refreshProviderOAuthCredentialWithPlugin } = await import("./plugins/provider-runtime.runtime.js"); const refreshed = await refreshProviderOAuthCredentialWithPlugin({ provider: OPENAI_CODEX_PROVIDER_ID, context: { type: "oauth", provider: OPENAI_CODEX_PROVIDER_ID, access: "", refresh: refreshToken, expires: 0 } }); if (!refreshed) return await loadOpenAICodexOAuthFacade().refreshOpenAICodexToken(refreshToken); const credentials = { ...refreshed }; delete credentials.type; delete credentials.provider; return credentials; } /** Runs the ChatGPT/Codex OAuth login flow and returns normalized credentials. */ async function loginOpenAICodex(callbacks) { throwIfOAuthLoginAborted(callbacks.signal); const { loginOpenAICodexOAuth } = await import("./provider-openai-chatgpt-oauth-C-ztLoba.js"); const manualCodeInput = callbacks.onManualCodeInput; const onManualCodeInput = manualCodeInput ? async () => await withOAuthLoginAbort(manualCodeInput(), callbacks.signal) : void 0; const credentials = await withOAuthLoginAbort(loginOpenAICodexOAuth({ prompter: createLegacyPrompter(callbacks), runtime: createLegacyRuntime(callbacks), isRemote: false, signal: callbacks.signal, onManualCodeInput, openUrl: async (url) => { throwIfOAuthLoginAborted(callbacks.signal); await callbacks.onAuth({ url }); } }), callbacks.signal); if (!credentials) throw new Error("OpenAI Codex OAuth login did not return credentials."); return credentials; } /** Refreshes a ChatGPT/Codex OAuth token through the provider runtime or bundled facade. */ async function refreshOpenAICodexToken(refreshToken) { return await refreshViaProviderRuntime(refreshToken); } //#endregion //#region src/llm/utils/oauth/index.ts /** * OAuth credential management for AI providers. * * This module handles login, token refresh, and credential storage * for OAuth-based providers: * - Anthropic (Claude Pro/Max) * - provider plugins through their runtime auth hooks */ const BUILT_IN_OAUTH_PROVIDERS = [anthropicOAuthProvider, { id: OPENAI_CODEX_PROVIDER_ID, name: "ChatGPT Plus/Pro (Codex Subscription)", usesCallbackServer: true, async login(callbacks) { return await loginOpenAICodex(callbacks); }, async refreshToken(credentials) { return await refreshOpenAICodexToken(credentials.refresh); }, getApiKey(credentials) { return credentials.access; } }]; async function resolveOAuthApiKey(provider, credentials) { let creds = credentials[provider.id]; if (!creds) return null; if (Date.now() >= creds.expires) try { creds = await provider.refreshToken(creds); } catch (error) { throw new Error(`Failed to refresh OAuth token for ${provider.id}`, { cause: error }); } return { newCredentials: creds, apiKey: provider.getApiKey(creds) }; } /** Mutable OAuth provider registrations owned by one auth/session runtime. */ var OAuthProviderRegistry = class { constructor() { this.providers = /* @__PURE__ */ new Map(); this.reset(); } get(id) { return this.providers.get(id); } register(provider) { this.providers.set(provider.id, provider); } reset() { this.providers.clear(); for (const provider of BUILT_IN_OAUTH_PROVIDERS) this.providers.set(provider.id, provider); } getAll() { return Array.from(this.providers.values()); } async getApiKey(providerId, credentials) { const provider = this.get(providerId); if (!provider) throw new Error(`Unknown OAuth provider: ${providerId}`); return resolveOAuthApiKey(provider, credentials); } }; /** * Get a built-in OAuth provider by ID. */ function getOAuthProvider(id) { return BUILT_IN_OAUTH_PROVIDERS.find((provider) => provider.id === id); } /** * Get all built-in OAuth providers. */ function getOAuthProviders() { return [...BUILT_IN_OAUTH_PROVIDERS]; } /** * Get API key for a provider from OAuth credentials. * Automatically refreshes expired tokens. * * @returns API key string and updated credentials, or null if no credentials * @throws Error if refresh fails */ async function getOAuthApiKey(providerId, credentials) { const provider = getOAuthProvider(providerId); if (!provider) throw new Error(`Unknown OAuth provider: ${providerId}`); return resolveOAuthApiKey(provider, credentials); } //#endregion //#region src/plugins/provider-runtime.errors.ts const OAUTH_PROVIDER_CONFIGURED_UNAVAILABLE = "OAUTH_PROVIDER_CONFIGURED_UNAVAILABLE"; /** A known OAuth provider could not load its owning plugin or required auth hooks. */ var OAuthProviderConfiguredUnavailableError = class extends Error { constructor(providerId) { super(`OAuth provider "${providerId}" is configured but unavailable. Install or enable its owning plugin, then retry; run openclaw doctor for diagnostics.`); this.code = OAUTH_PROVIDER_CONFIGURED_UNAVAILABLE; this.state = "configured-unavailable"; this.name = "OAuthProviderConfiguredUnavailableError"; this.providerId = providerId; } }; //#endregion //#region src/agents/auth-profiles/paths.ts /** * Public path barrel for auth-profile stores. * Import through this file for canonical SQLite display and lock paths. */ /** Resolve the user-facing path for the database selected by the auth store loader. */ function resolveAuthStorePathForDisplay(agentDir) { const pathname = agentDir && inspectPersistedAuthProfileStoreRaw(agentDir).status !== "missing" ? path.join(resolveUserPath(agentDir), "openclaw-agent.sqlite") : resolveSharedAuthStorePath(); return pathname.startsWith("~") ? pathname : resolveUserPath(pathname); } /** Retained name for callers that present auth runtime state from the same selected store. */ function resolveAuthStatePathForDisplay(agentDir) { return resolveAuthStorePathForDisplay(agentDir); } //#endregion export { getOAuthApiKey as a, OAuthProviderRegistry as i, resolveAuthStorePathForDisplay as n, getOAuthProviders as o, OAuthProviderConfiguredUnavailableError as r, resolveAuthStatePathForDisplay as t };