UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

251 lines (250 loc) 10.6 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { g as readResponseTextLimited, m as readProviderJsonResponse } from "./provider-http-errors-U-nhuk_f.js"; import { a as generatePkceVerifierChallenge } from "./provider-auth-BeZ7NZUU.js"; import { n as buildApiKeyCredential } from "./provider-auth-helpers-CaYTVMoC.js"; import "./error-runtime-Bz9Tw57Z.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import { n as generateHexOAuthState, s as startProviderOAuthLoopbackCallbackServer } from "./provider-auth-runtime-BqyR7mkf.js"; import "./provider-http-k9RMI7iG.js"; import { n as applyOpenrouterConfig, t as OPENROUTER_DEFAULT_MODEL_REF } from "./onboard-BB-6NgIZ.js"; //#region extensions/openrouter/oauth.ts const PROVIDER_ID = "openrouter"; const OPENROUTER_OAUTH_METHOD_ID = "oauth"; const OPENROUTER_OAUTH_CHOICE_ID = "openrouter-oauth"; const OPENROUTER_OAUTH_AUTHORIZE_URL = "https://openrouter.ai/auth"; const OPENROUTER_OAUTH_TOKEN_URL = "https://openrouter.ai/api/v1/auth/keys"; const OPENROUTER_OAUTH_REDIRECT_URI = `http://localhost:3000/openrouter-oauth/callback`; const OPENROUTER_OAUTH_CODE_CHALLENGE_METHOD = "S256"; const OPENROUTER_OAUTH_TIMEOUT_MS = 3e5; const OPENROUTER_OAUTH_FETCH_TIMEOUT_MS = 3e4; const OPENROUTER_OAUTH_ERROR_BODY_LIMIT_BYTES = 8192; const OPENROUTER_OAUTH_PROFILE_ID = "openrouter:default"; function extractOpenRouterError(value) { if (typeof value === "string") return value.trim() || void 0; if (!isRecord(value)) return; const direct = normalizeOptionalString(value.message) ?? normalizeOptionalString(value.error_description); if (direct) return direct; const error = value.error; if (typeof error === "string") return error.trim() || void 0; if (isRecord(error)) return normalizeOptionalString(error.message) ?? normalizeOptionalString(error.code); } async function readResponseBody(response) { if (response.ok) return await readProviderJsonResponse(response, "OpenRouter OAuth key exchange"); const text = await readResponseTextLimited(response, OPENROUTER_OAUTH_ERROR_BODY_LIMIT_BYTES).catch(() => ""); if (!text.trim()) return null; try { return JSON.parse(text); } catch { return text; } } function parseOpenRouterKeyResponse(value) { if (!isRecord(value)) throw new Error("OpenRouter OAuth key exchange returned an unexpected response."); const key = normalizeOptionalString(value.key); if (!key) throw new Error("OpenRouter OAuth key exchange returned no API key."); const userId = normalizeOptionalString(value.user_id) ?? normalizeOptionalString(value.userId); return { key, ...userId ? { userId } : {} }; } function buildOpenRouterOAuthRedirectUri(params) { const url = new URL(OPENROUTER_OAUTH_REDIRECT_URI); url.searchParams.set("state", params.state); return url.toString(); } function buildOpenRouterOAuthAuthorizeUrl(params) { const url = new URL(OPENROUTER_OAUTH_AUTHORIZE_URL); url.searchParams.set("callback_url", buildOpenRouterOAuthRedirectUri({ state: params.state })); url.searchParams.set("code_challenge", params.codeChallenge); url.searchParams.set("code_challenge_method", OPENROUTER_OAUTH_CODE_CHALLENGE_METHOD); return url.toString(); } function requireOpenRouterOAuthState(state, expectedState) { if (!state) throw new Error("Missing OpenRouter OAuth state. Paste the full redirect URL."); if (state !== expectedState) throw new Error("OpenRouter OAuth state mismatch. Please retry login."); return state; } function parseOpenRouterOAuthCallbackInput(input, expectedState) { const trimmed = input.trim(); if (!trimmed) throw new Error("No input provided."); const parseParams = (params) => { const state = requireOpenRouterOAuthState(normalizeOptionalString(params.get("state")), expectedState); const error = normalizeOptionalString(params.get("error")); if (error) { const description = normalizeOptionalString(params.get("error_description")); throw new Error(`OpenRouter OAuth error: ${description ? `${error}: ${description}` : error}`); } const code = normalizeOptionalString(params.get("code")); if (!code) throw new Error("Missing 'code' parameter in redirect URL."); return { code, state }; }; try { return parseParams(new URL(trimmed).searchParams); } catch (err) { if (err instanceof TypeError) { if (trimmed.includes("code=") || trimmed.includes("error=")) return parseParams(new URLSearchParams(trimmed)); throw new Error("Paste the full OpenRouter redirect URL, not just the code.", { cause: err }); } throw err; } } async function exchangeOpenRouterOAuthCode(params) { const response = await (params.fetchImpl ?? fetch)(OPENROUTER_OAUTH_TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ code: params.code, code_verifier: params.codeVerifier, code_challenge_method: OPENROUTER_OAUTH_CODE_CHALLENGE_METHOD }), signal: params.signal ? AbortSignal.any([params.signal, AbortSignal.timeout(OPENROUTER_OAUTH_FETCH_TIMEOUT_MS)]) : AbortSignal.timeout(OPENROUTER_OAUTH_FETCH_TIMEOUT_MS) }); const body = await readResponseBody(response); if (!response.ok) { const message = extractOpenRouterError(body); throw new Error(`OpenRouter OAuth key exchange failed (${response.status})${message ? `: ${message}` : ""}`); } return parseOpenRouterKeyResponse(body); } async function promptForOpenRouterRedirect(ctx, expectedState) { return parseOpenRouterOAuthCallbackInput(await ctx.prompter.text({ message: "Paste the OpenRouter redirect URL", placeholder: `${OPENROUTER_OAUTH_REDIRECT_URI}?state=...&code=...`, validate: (value) => value.trim().length > 0 ? void 0 : "Required" }), expectedState).code; } async function resolveOpenRouterOAuthCode(ctx, params) { await ctx.prompter.note(ctx.isRemote ? [ "Open this URL in your LOCAL browser.", "After signing in, paste the redirect URL back here.", "", `Redirect URI: ${OPENROUTER_OAUTH_REDIRECT_URI}` ].join("\n") : [ "Browser will open for OpenRouter authentication.", "If the callback does not auto-complete, paste the redirect URL.", "", `Redirect URI: ${OPENROUTER_OAUTH_REDIRECT_URI}` ].join("\n"), "OpenRouter OAuth"); if (ctx.isRemote) { ctx.runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${params.authorizeUrl}\n`); await ctx.openUrl(params.authorizeUrl); await ctx.prompter.note(`Open this URL in your LOCAL browser:\n\n${params.authorizeUrl}`, "OpenRouter OAuth"); return await promptForOpenRouterRedirect(ctx, params.state); } let callback; try { callback = await params.startCallback({ redirectUrl: OPENROUTER_OAUTH_REDIRECT_URI, expectedState: params.state, timeoutMs: OPENROUTER_OAUTH_TIMEOUT_MS, ...ctx.signal ? { signal: ctx.signal } : {}, renderSuccess: () => ({ body: "<!doctype html><html><head><meta charset='utf-8'/></head><body><h2>OpenRouter OAuth complete</h2><p>You can close this window and return to OpenClaw.</p></body></html>", contentType: "text/html; charset=utf-8" }) }); params.onProgress(`Waiting for OpenRouter OAuth callback on ${OPENROUTER_OAUTH_REDIRECT_URI}...`); } catch (error) { if (ctx.signal?.aborted) throw error; params.onProgress("OAuth callback not detected; waiting for redirect URL..."); } try { await ctx.openUrl(params.authorizeUrl); ctx.runtime.log(`Open: ${params.authorizeUrl}`); } catch { ctx.runtime.log(`Open manually: ${params.authorizeUrl}`); } if (!callback) return await promptForOpenRouterRedirect(ctx, params.state); let result; try { try { result = await callback.waitForCallback(); } finally { await callback.close(); } } catch (error) { if (ctx.signal?.aborted) throw error; params.onProgress("OAuth callback not detected; waiting for redirect URL..."); return await promptForOpenRouterRedirect(ctx, params.state); } if (result.type === "oauth_error") { const detail = result.errorDescription ? `${result.error}: ${result.errorDescription}` : result.error; throw new Error(`OpenRouter OAuth error: ${detail}`); } return result.code; } async function loginOpenRouterOAuth(ctx, options = {}) { const progress = ctx.prompter.progress("Starting OpenRouter OAuth..."); try { const pkce = options.createPkce?.() ?? generatePkceVerifierChallenge(); const state = options.createState?.() ?? generateHexOAuthState(); const code = await resolveOpenRouterOAuthCode(ctx, { authorizeUrl: buildOpenRouterOAuthAuthorizeUrl({ codeChallenge: pkce.challenge, state }), state, startCallback: options.startCallback ?? startProviderOAuthLoopbackCallbackServer, onProgress: (message) => progress.update(message) }); progress.update("Exchanging OpenRouter OAuth code..."); const token = await exchangeOpenRouterOAuthCode({ code, codeVerifier: pkce.verifier, fetchImpl: options.fetchImpl, ...ctx.signal ? { signal: ctx.signal } : {} }); progress.stop("OpenRouter OAuth complete"); const metadata = { authFlow: "oauth-pkce", ...token.userId ? { userId: token.userId } : {} }; const credential = { ...buildApiKeyCredential(PROVIDER_ID, token.key, metadata), displayName: token.userId ? `OpenRouter ${token.userId}` : "OpenRouter OAuth" }; return { profiles: [{ profileId: OPENROUTER_OAUTH_PROFILE_ID, credential }], configPatch: applyOpenrouterConfig(ctx.config), defaultModel: OPENROUTER_DEFAULT_MODEL_REF, notes: ["OpenRouter OAuth issued an OpenRouter API key and stored it in the default OpenRouter auth profile.", "Re-run OpenRouter OAuth to rotate that key or use the API-key setup path for a key you manage manually."] }; } catch (err) { progress.stop("OpenRouter OAuth failed"); throw new Error(`OpenRouter OAuth failed: ${formatErrorMessage(err)}`, { cause: err }); } } function createOpenRouterOAuthAuthMethod(options = {}) { return { id: OPENROUTER_OAUTH_METHOD_ID, label: "OpenRouter OAuth", hint: "Browser sign-in", kind: "oauth", wizard: { choiceId: OPENROUTER_OAUTH_CHOICE_ID, choiceLabel: "OpenRouter OAuth", choiceHint: "Browser sign-in", groupId: PROVIDER_ID, groupLabel: "OpenRouter", groupHint: "OAuth or API key", methodId: OPENROUTER_OAUTH_METHOD_ID, onboardingScopes: ["text-inference", "music-generation"], onboardingFeatured: true }, run: async (ctx) => await loginOpenRouterOAuth(ctx, options) }; } //#endregion export { createOpenRouterOAuthAuthMethod as t };