UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

144 lines (143 loc) 5.8 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { T as resolveExpiresAtMsFromDurationSeconds } from "./number-coercion-CJQ8TR--.js"; import "./parse-finite-number-Z7n6tXLk.js"; import { createHash, randomBytes } from "node:crypto"; //#region src/agents/chutes-oauth.ts /** * Implements Chutes OAuth PKCE, callback parsing, token exchange, and refresh * for agent model authentication. */ const CHUTES_OAUTH_ISSUER = "https://api.chutes.ai"; const CHUTES_AUTHORIZE_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/authorize`; const CHUTES_TOKEN_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/token`; const CHUTES_USERINFO_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/userinfo`; const DEFAULT_EXPIRES_BUFFER_MS = 300 * 1e3; /** Generates a PKCE verifier/challenge pair for Chutes login. */ function generateChutesPkce() { const verifier = randomBytes(32).toString("hex"); return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") }; } /** Parses pasted Chutes redirect input and enforces the expected OAuth state. */ function parseOAuthCallbackInput(input, expectedState) { const trimmed = input.trim(); if (!trimmed) return { error: "No input provided" }; let url; try { url = new URL(trimmed); } catch { if (!/\s/.test(trimmed) && !trimmed.includes("://") && !trimmed.includes("?") && !trimmed.includes("=")) return { error: "Paste the full redirect URL (must include code + state)." }; const qs = trimmed.startsWith("?") ? trimmed : `?${trimmed}`; try { url = new URL(`http://localhost/${qs}`); } catch { return { error: "Paste the full redirect URL (must include code + state)." }; } } const code = normalizeOptionalString(url.searchParams.get("code")); const state = normalizeOptionalString(url.searchParams.get("state")); if (!code) return { error: "Missing 'code' parameter in URL" }; if (!state) return { error: "Missing 'state' parameter. Paste the full redirect URL." }; if (state !== expectedState) return { error: "OAuth state mismatch - possible CSRF attack. Please retry login." }; return { code, state }; } function resolveChutesExpiresAt(value, now) { return resolveExpiresAtMsFromDurationSeconds(value, { nowMs: now, bufferMs: DEFAULT_EXPIRES_BUFFER_MS, minRemainingMs: 3e4 }); } async function fetchChutesUserInfo(params) { const response = await (params.fetchFn ?? fetch)(CHUTES_USERINFO_ENDPOINT, { headers: { Authorization: `Bearer ${params.accessToken}` } }); if (!response.ok) return null; const data = await response.json(); if (!data || typeof data !== "object") return null; return data; } /** Exchanges an authorization code for stored Chutes OAuth credentials. */ async function exchangeChutesCodeForTokens(params) { const fetchFn = params.fetchFn ?? fetch; const now = params.now ?? Date.now(); const body = new URLSearchParams({ grant_type: "authorization_code", client_id: params.app.clientId, code: params.code, redirect_uri: params.app.redirectUri, code_verifier: params.codeVerifier }); if (params.app.clientSecret) body.set("client_secret", params.app.clientSecret); const response = await fetchFn(CHUTES_TOKEN_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body }); if (!response.ok) { const text = await response.text(); throw new Error(`Chutes token exchange failed: ${text}`); } const data = await response.json(); const access = data.access_token?.trim(); const refresh = data.refresh_token?.trim(); const expires = resolveChutesExpiresAt(data.expires_in, now); if (!access) throw new Error("Chutes token exchange returned no access_token"); if (!refresh) throw new Error("Chutes token exchange returned no refresh_token"); if (expires === void 0) throw new Error("Chutes token exchange returned invalid expires_in"); const info = await fetchChutesUserInfo({ accessToken: access, fetchFn }); return { access, refresh, expires, email: info?.username, accountId: info?.sub, clientId: params.app.clientId }; } /** Refreshes stored Chutes OAuth credentials, preserving refresh tokens when absent. */ async function refreshChutesTokens(params) { const fetchFn = params.fetchFn ?? fetch; const now = params.now ?? Date.now(); const refreshToken = params.credential.refresh?.trim(); if (!refreshToken) throw new Error("Chutes OAuth credential is missing refresh token"); const clientId = params.credential.clientId?.trim() ?? process.env.CHUTES_CLIENT_ID?.trim(); if (!clientId) throw new Error("Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth)."); const clientSecret = normalizeOptionalString(process.env.CHUTES_CLIENT_SECRET); const body = new URLSearchParams({ grant_type: "refresh_token", client_id: clientId, refresh_token: refreshToken }); if (clientSecret) body.set("client_secret", clientSecret); const response = await fetchFn(CHUTES_TOKEN_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body }); if (!response.ok) { const text = await response.text(); throw new Error(`Chutes token refresh failed: ${text}`); } const data = await response.json(); const access = data.access_token?.trim(); const newRefresh = data.refresh_token?.trim(); const expires = resolveChutesExpiresAt(data.expires_in, now); if (!access) throw new Error("Chutes token refresh returned no access_token"); if (expires === void 0) throw new Error("Chutes token refresh returned invalid expires_in"); return { ...params.credential, access, refresh: newRefresh || refreshToken, expires, clientId }; } //#endregion export { refreshChutesTokens as a, parseOAuthCallbackInput as i, exchangeChutesCodeForTokens as n, generateChutesPkce as r, CHUTES_AUTHORIZE_ENDPOINT as t };