openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
147 lines (146 loc) • 5.73 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { T as resolveExpiresAtMsFromDurationSeconds } from "./number-coercion-CJQ8TR--.js";
import "./number-runtime-DBLVDypr.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { l as generatePkceVerifierChallenge, u as toFormUrlEncoded } from "./provider-auth-DAOC_qI9.js";
import { i as parseOAuthCallbackInput, s as waitForLocalOAuthCallback } from "./provider-auth-runtime-D2xYnWtb.js";
import { randomBytes } from "node:crypto";
//#region extensions/chutes/oauth.ts
/**
* Chutes OAuth PKCE login flow.
*/
const CHUTES_AUTHORIZE_ENDPOINT = "https://api.chutes.ai/idp/authorize";
const CHUTES_TOKEN_ENDPOINT = "https://api.chutes.ai/idp/token";
const CHUTES_USERINFO_ENDPOINT = "https://api.chutes.ai/idp/userinfo";
function parseRedirectUri(redirectUri) {
const url = new URL(redirectUri);
if (url.protocol !== "http:") throw new Error(`Chutes OAuth redirect URI must be http:// (got ${redirectUri})`);
const hostname = url.hostname || "127.0.0.1";
if (hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "::1") throw new Error(`Chutes OAuth redirect hostname must be loopback (got ${hostname}). Use http://127.0.0.1:<port>/...`);
return {
hostname,
port: url.port ? Number.parseInt(url.port, 10) : 80,
pathname: url.pathname || "/"
};
}
function parseManualOAuthInput(input, expectedState) {
const parsed = parseOAuthCallbackInput(input, {
invalidInput: "Paste the full redirect URL (must include code + state).",
missingState: "Missing 'state' parameter. Paste the full redirect URL."
});
if ("error" in parsed) throw new Error(parsed.error);
if (parsed.state !== expectedState) throw new Error("OAuth state mismatch - possible CSRF attack. Please retry login.");
return parsed;
}
function buildAuthorizeUrl(params) {
return `${CHUTES_AUTHORIZE_ENDPOINT}?${new URLSearchParams({
client_id: params.clientId,
redirect_uri: params.redirectUri,
response_type: "code",
scope: params.scopes.join(" "),
state: params.state,
code_challenge: params.challenge,
code_challenge_method: "S256"
}).toString()}`;
}
function resolveChutesExpiresAt(value, now) {
return resolveExpiresAtMsFromDurationSeconds(value, {
nowMs: now,
bufferMs: 300 * 1e3,
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();
return data && typeof data === "object" ? data : null;
}
async function exchangeChutesCodeForTokens(params) {
const fetchFn = params.fetchFn ?? fetch;
const now = params.now ?? Date.now();
const body = new URLSearchParams(toFormUrlEncoded({
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) throw new Error(`Chutes token exchange failed: ${await response.text()}`);
const data = await response.json();
const access = normalizeOptionalString(data.access_token);
const refresh = normalizeOptionalString(data.refresh_token);
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
};
}
/** Runs Chutes OAuth and returns refreshable stored credentials. */
async function loginChutes(params) {
const { verifier, challenge } = generatePkceVerifierChallenge();
const state = params.createState?.() ?? randomBytes(16).toString("hex");
const timeoutMs = params.timeoutMs ?? 180 * 1e3;
const url = buildAuthorizeUrl({
clientId: params.app.clientId,
redirectUri: params.app.redirectUri,
scopes: params.app.scopes,
state,
challenge
});
let codeAndState;
if (params.manual) {
await params.onAuth({ url });
params.onProgress?.("Waiting for redirect URL...");
codeAndState = parseManualOAuthInput(await params.onPrompt({
message: "Paste the redirect URL",
placeholder: `${params.app.redirectUri}?code=...&state=...`
}), state);
} else {
const redirect = parseRedirectUri(params.app.redirectUri);
const callback = waitForLocalOAuthCallback({
expectedState: state,
timeoutMs,
port: redirect.port,
callbackPath: redirect.pathname,
redirectUri: params.app.redirectUri,
successTitle: "Chutes OAuth complete",
hostname: redirect.hostname,
onProgress: params.onProgress
}).catch(async () => {
params.onProgress?.("OAuth callback not detected; paste redirect URL...");
return parseManualOAuthInput(await params.onPrompt({
message: "Paste the redirect URL",
placeholder: `${params.app.redirectUri}?code=...&state=...`
}), state);
});
await params.onAuth({ url });
codeAndState = await callback;
}
params.onProgress?.("Exchanging code for tokens...");
return await exchangeChutesCodeForTokens({
app: params.app,
code: codeAndState.code,
codeVerifier: verifier,
fetchFn: params.fetchFn
});
}
//#endregion
export { loginChutes as t };