fix: harden Chutes OAuth flow (#726) (thanks @FrieSei)

This commit is contained in:
Peter Steinberger
2026-01-13 05:01:08 +00:00
parent 3271ff1d6e
commit f566e6451f
7 changed files with 101 additions and 109 deletions

View File

@@ -1,8 +1,4 @@
import {
loginOpenAICodex,
type OAuthCredentials,
type OAuthProvider,
} from "@mariozechner/pi-ai";
import { loginOpenAICodex, type OAuthCredentials } from "@mariozechner/pi-ai";
import { resolveAgentConfig } from "../agents/agent-scope.js";
import {
CLAUDE_CLI_PROFILE_ID,
@@ -52,7 +48,6 @@ import {
applySyntheticConfig,
applySyntheticProviderConfig,
applyZaiConfig,
MINIMAX_HOSTED_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_REF,
@@ -105,6 +100,56 @@ function normalizeApiKeyInput(raw: string): string {
const validateApiKeyInput = (value: unknown) =>
normalizeApiKeyInput(String(value ?? "")).length > 0 ? undefined : "Required";
const validateRequiredInput = (value: string) =>
value.trim().length > 0 ? undefined : "Required";
function createVpsAwareOAuthHandlers(params: {
isRemote: boolean;
prompter: WizardPrompter;
runtime: RuntimeEnv;
spin: ReturnType<WizardPrompter["progress"]>;
localBrowserMessage: string;
}): {
onAuth: (event: { url: string }) => Promise<void>;
onPrompt: (prompt: {
message: string;
placeholder?: string;
}) => Promise<string>;
} {
let manualCodePromise: Promise<string> | undefined;
return {
onAuth: async ({ url }) => {
if (params.isRemote) {
params.spin.stop("OAuth URL ready");
params.runtime.log(
`\nOpen this URL in your LOCAL browser:\n\n${url}\n`,
);
manualCodePromise = params.prompter
.text({
message: "Paste the redirect URL (or authorization code)",
validate: validateRequiredInput,
})
.then((value) => String(value));
return;
}
params.spin.update(params.localBrowserMessage);
await openUrl(url);
params.runtime.log(`Open: ${url}`);
},
onPrompt: async (prompt) => {
if (manualCodePromise) return manualCodePromise;
const code = await params.prompter.text({
message: prompt.message,
placeholder: prompt.placeholder,
validate: validateRequiredInput,
});
return String(code);
},
};
}
function formatApiKeyPreview(
raw: string,
opts: { head?: number; tail?: number } = {},
@@ -574,43 +619,25 @@ export async function applyAuthChoice(params: {
);
const spin = params.prompter.progress("Starting OAuth flow…");
let manualCodePromise: Promise<string> | undefined;
try {
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
isRemote,
prompter: params.prompter,
runtime: params.runtime,
spin,
localBrowserMessage: "Complete sign-in in browser…",
});
const creds = await loginChutes({
app: {
clientId,
clientSecret,
redirectUri,
scopes: scopes.split(/\\s+/).filter(Boolean),
scopes: scopes.split(/\s+/).filter(Boolean),
},
manual: isRemote,
onAuth: async ({ url }) => {
if (isRemote) {
spin.stop("OAuth URL ready");
params.runtime.log(
`\\nOpen this URL in your LOCAL browser:\\n\\n${url}\\n`,
);
manualCodePromise = params.prompter
.text({
message: "Paste the redirect URL (or authorization code)",
validate: (value) => (value?.trim() ? undefined : "Required"),
})
.then((value) => String(value));
} else {
spin.update("Complete sign-in in browser…");
await openUrl(url);
params.runtime.log(`Open: ${url}`);
}
},
onPrompt: async (prompt) => {
if (manualCodePromise) return manualCodePromise;
const code = await params.prompter.text({
message: prompt.message,
placeholder: prompt.placeholder,
validate: (value) => (value?.trim() ? undefined : "Required"),
});
return String(code);
},
onAuth,
onPrompt,
onProgress: (msg) => spin.update(msg),
});
@@ -618,11 +645,7 @@ export async function applyAuthChoice(params: {
const email = creds.email?.trim() || "default";
const profileId = `chutes:${email}`;
await writeOAuthCredentials(
"chutes" as unknown as OAuthProvider,
creds,
params.agentDir,
);
await writeOAuthCredentials("chutes", creds, params.agentDir);
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: "chutes",
@@ -637,7 +660,7 @@ export async function applyAuthChoice(params: {
"Verify CHUTES_CLIENT_ID (and CHUTES_CLIENT_SECRET if required).",
`Verify the OAuth app redirect URI includes: ${redirectUri}`,
"Chutes docs: https://chutes.ai/docs/sign-in-with-chutes/overview",
].join("\\n"),
].join("\n"),
"OAuth help",
);
}
@@ -658,47 +681,23 @@ export async function applyAuthChoice(params: {
"OpenAI Codex OAuth",
);
const spin = params.prompter.progress("Starting OAuth flow…");
let manualCodePromise: Promise<string> | undefined;
try {
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
isRemote,
prompter: params.prompter,
runtime: params.runtime,
spin,
localBrowserMessage: "Complete sign-in in browser…",
});
const creds = await loginOpenAICodex({
onAuth: async ({ url }) => {
if (isRemote) {
spin.stop("OAuth URL ready");
params.runtime.log(
`\nOpen this URL in your LOCAL browser:\n\n${url}\n`,
);
manualCodePromise = params.prompter
.text({
message: "Paste the redirect URL (or authorization code)",
validate: (value) => (value?.trim() ? undefined : "Required"),
})
.then((value) => String(value));
} else {
spin.update("Complete sign-in in browser…");
await openUrl(url);
params.runtime.log(`Open: ${url}`);
}
},
onPrompt: async (prompt) => {
if (manualCodePromise) {
return manualCodePromise;
}
const code = await params.prompter.text({
message: prompt.message,
placeholder: prompt.placeholder,
validate: (value) => (value?.trim() ? undefined : "Required"),
});
return String(code);
},
onAuth,
onPrompt,
onProgress: (msg) => spin.update(msg),
});
spin.stop("OpenAI OAuth complete");
if (creds) {
await writeOAuthCredentials(
"openai-codex" as unknown as OAuthProvider,
creds,
params.agentDir,
);
await writeOAuthCredentials("openai-codex", creds, params.agentDir);
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "openai-codex:default",
provider: "openai-codex",

View File

@@ -1,9 +1,11 @@
import { randomBytes } from "node:crypto";
import { createServer } from "node:http";
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import type { ChutesOAuthAppConfig } from "../agents/chutes-oauth.js";
import {
CHUTES_AUTHORIZE_ENDPOINT,
exchangeChutesCodeForTokens,
generateChutesPkce,
parseOAuthCallbackInput,
@@ -30,7 +32,7 @@ function buildAuthorizeUrl(params: {
code_challenge: params.challenge,
code_challenge_method: "S256",
});
return `https://api.chutes.ai/idp/authorize?${qs.toString()}`;
return `${CHUTES_AUTHORIZE_ENDPOINT}?${qs.toString()}`;
}
async function waitForLocalCallback(params: {
@@ -129,7 +131,7 @@ export async function loginChutes(params: {
fetchFn?: typeof fetch;
}): Promise<OAuthCredentials> {
const { verifier, challenge } = generateChutesPkce();
const state = verifier;
const state = randomBytes(16).toString("hex");
const timeoutMs = params.timeoutMs ?? 3 * 60 * 1000;
const url = buildAuthorizeUrl({

View File

@@ -1,4 +1,4 @@
import type { OAuthCredentials, OAuthProvider } from "@mariozechner/pi-ai";
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { resolveDefaultAgentDir } from "../agents/agent-scope.js";
import { upsertAuthProfile } from "../agents/auth-profiles.js";
import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js";
@@ -103,7 +103,7 @@ function buildMoonshotModelDefinition(): ModelDefinitionConfig {
}
export async function writeOAuthCredentials(
provider: OAuthProvider,
provider: string,
creds: OAuthCredentials,
agentDir?: string,
): Promise<void> {