fix: harden Chutes OAuth flow (#726) (thanks @FrieSei)
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
### Changes
|
### Changes
|
||||||
- Models/Moonshot: add Kimi K2 0905 + turbo/thinking variants to the preset + docs. (#818 — thanks @mickahouan)
|
- Models/Moonshot: add Kimi K2 0905 + turbo/thinking variants to the preset + docs. (#818 — thanks @mickahouan)
|
||||||
- Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm)
|
- Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm)
|
||||||
|
- Auth: add Chutes OAuth (PKCE + refresh + onboarding choice). (#726 — thanks @FrieSei)
|
||||||
- Agents: make workspace bootstrap truncation configurable (default 20k) and warn when files are truncated.
|
- Agents: make workspace bootstrap truncation configurable (default 20k) and warn when files are truncated.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
|
|||||||
import { createSubsystemLogger } from "../logging.js";
|
import { createSubsystemLogger } from "../logging.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
||||||
import { type ChutesStoredOAuth, refreshChutesTokens } from "./chutes-oauth.js";
|
import { refreshChutesTokens } from "./chutes-oauth.js";
|
||||||
import {
|
import {
|
||||||
readClaudeCliCredentialsCached,
|
readClaudeCliCredentialsCached,
|
||||||
readCodexCliCredentialsCached,
|
readCodexCliCredentialsCached,
|
||||||
@@ -68,7 +68,8 @@ export type TokenCredential = {
|
|||||||
|
|
||||||
export type OAuthCredential = OAuthCredentials & {
|
export type OAuthCredential = OAuthCredentials & {
|
||||||
type: "oauth";
|
type: "oauth";
|
||||||
provider: OAuthProvider;
|
provider: string;
|
||||||
|
clientId?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ async function updateAuthProfileStoreWithLock(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildOAuthApiKey(
|
function buildOAuthApiKey(
|
||||||
provider: OAuthProvider,
|
provider: string,
|
||||||
credentials: OAuthCredentials,
|
credentials: OAuthCredentials,
|
||||||
): string {
|
): string {
|
||||||
const needsProjectId =
|
const needsProjectId =
|
||||||
@@ -187,7 +188,6 @@ function buildOAuthApiKey(
|
|||||||
|
|
||||||
async function refreshOAuthTokenWithLock(params: {
|
async function refreshOAuthTokenWithLock(params: {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
provider: OAuthProvider;
|
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
}): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {
|
}): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {
|
||||||
const authPath = resolveAuthStorePath(params.agentDir);
|
const authPath = resolveAuthStorePath(params.agentDir);
|
||||||
@@ -218,11 +218,11 @@ async function refreshOAuthTokenWithLock(params: {
|
|||||||
String(cred.provider) === "chutes"
|
String(cred.provider) === "chutes"
|
||||||
? await (async () => {
|
? await (async () => {
|
||||||
const newCredentials = await refreshChutesTokens({
|
const newCredentials = await refreshChutesTokens({
|
||||||
credential: cred as unknown as ChutesStoredOAuth,
|
credential: cred,
|
||||||
});
|
});
|
||||||
return { apiKey: newCredentials.access, newCredentials };
|
return { apiKey: newCredentials.access, newCredentials };
|
||||||
})()
|
})()
|
||||||
: await getOAuthApiKey(cred.provider, oauthCreds);
|
: await getOAuthApiKey(cred.provider as OAuthProvider, oauthCreds);
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
store.profiles[params.profileId] = {
|
store.profiles[params.profileId] = {
|
||||||
...cred,
|
...cred,
|
||||||
@@ -269,7 +269,7 @@ function coerceLegacyStore(raw: unknown): LegacyAuthStore | null {
|
|||||||
}
|
}
|
||||||
entries[key] = {
|
entries[key] = {
|
||||||
...typed,
|
...typed,
|
||||||
provider: typed.provider ?? (key as OAuthProvider),
|
provider: String(typed.provider ?? key),
|
||||||
} as AuthProfileCredential;
|
} as AuthProfileCredential;
|
||||||
}
|
}
|
||||||
return Object.keys(entries).length > 0 ? entries : null;
|
return Object.keys(entries).length > 0 ? entries : null;
|
||||||
@@ -336,7 +336,7 @@ function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
|
|||||||
if (store.profiles[profileId]) continue;
|
if (store.profiles[profileId]) continue;
|
||||||
store.profiles[profileId] = {
|
store.profiles[profileId] = {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
provider: provider as OAuthProvider,
|
provider,
|
||||||
...creds,
|
...creds,
|
||||||
};
|
};
|
||||||
mutated = true;
|
mutated = true;
|
||||||
@@ -478,7 +478,7 @@ function syncExternalCliCredentials(
|
|||||||
const existingCodex = store.profiles[CODEX_CLI_PROFILE_ID];
|
const existingCodex = store.profiles[CODEX_CLI_PROFILE_ID];
|
||||||
const shouldSyncCodex =
|
const shouldSyncCodex =
|
||||||
!existingCodex ||
|
!existingCodex ||
|
||||||
existingCodex.provider !== ("openai-codex" as OAuthProvider) ||
|
existingCodex.provider !== "openai-codex" ||
|
||||||
!isExternalProfileFresh(existingCodex, now);
|
!isExternalProfileFresh(existingCodex, now);
|
||||||
const codexCreds = shouldSyncCodex
|
const codexCreds = shouldSyncCodex
|
||||||
? readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS })
|
? readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS })
|
||||||
@@ -490,7 +490,7 @@ function syncExternalCliCredentials(
|
|||||||
// Codex creds don't carry expiry; use file mtime heuristic for freshness.
|
// Codex creds don't carry expiry; use file mtime heuristic for freshness.
|
||||||
const shouldUpdate =
|
const shouldUpdate =
|
||||||
!existingOAuth ||
|
!existingOAuth ||
|
||||||
existingOAuth.provider !== ("openai-codex" as unknown as OAuthProvider) ||
|
existingOAuth.provider !== "openai-codex" ||
|
||||||
existingOAuth.expires <= now ||
|
existingOAuth.expires <= now ||
|
||||||
codexCreds.expires > existingOAuth.expires;
|
codexCreds.expires > existingOAuth.expires;
|
||||||
|
|
||||||
@@ -535,14 +535,14 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
|||||||
if (cred.type === "api_key") {
|
if (cred.type === "api_key") {
|
||||||
store.profiles[profileId] = {
|
store.profiles[profileId] = {
|
||||||
type: "api_key",
|
type: "api_key",
|
||||||
provider: cred.provider ?? (provider as OAuthProvider),
|
provider: String(cred.provider ?? provider),
|
||||||
key: cred.key,
|
key: cred.key,
|
||||||
...(cred.email ? { email: cred.email } : {}),
|
...(cred.email ? { email: cred.email } : {}),
|
||||||
};
|
};
|
||||||
} else if (cred.type === "token") {
|
} else if (cred.type === "token") {
|
||||||
store.profiles[profileId] = {
|
store.profiles[profileId] = {
|
||||||
type: "token",
|
type: "token",
|
||||||
provider: cred.provider ?? (provider as OAuthProvider),
|
provider: String(cred.provider ?? provider),
|
||||||
token: cred.token,
|
token: cred.token,
|
||||||
...(typeof cred.expires === "number"
|
...(typeof cred.expires === "number"
|
||||||
? { expires: cred.expires }
|
? { expires: cred.expires }
|
||||||
@@ -552,7 +552,7 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
|||||||
} else {
|
} else {
|
||||||
store.profiles[profileId] = {
|
store.profiles[profileId] = {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
provider: cred.provider ?? (provider as OAuthProvider),
|
provider: String(cred.provider ?? provider),
|
||||||
access: cred.access,
|
access: cred.access,
|
||||||
refresh: cred.refresh,
|
refresh: cred.refresh,
|
||||||
expires: cred.expires,
|
expires: cred.expires,
|
||||||
@@ -600,14 +600,14 @@ export function ensureAuthProfileStore(
|
|||||||
if (cred.type === "api_key") {
|
if (cred.type === "api_key") {
|
||||||
store.profiles[profileId] = {
|
store.profiles[profileId] = {
|
||||||
type: "api_key",
|
type: "api_key",
|
||||||
provider: cred.provider ?? (provider as OAuthProvider),
|
provider: String(cred.provider ?? provider),
|
||||||
key: cred.key,
|
key: cred.key,
|
||||||
...(cred.email ? { email: cred.email } : {}),
|
...(cred.email ? { email: cred.email } : {}),
|
||||||
};
|
};
|
||||||
} else if (cred.type === "token") {
|
} else if (cred.type === "token") {
|
||||||
store.profiles[profileId] = {
|
store.profiles[profileId] = {
|
||||||
type: "token",
|
type: "token",
|
||||||
provider: cred.provider ?? (provider as OAuthProvider),
|
provider: String(cred.provider ?? provider),
|
||||||
token: cred.token,
|
token: cred.token,
|
||||||
...(typeof cred.expires === "number"
|
...(typeof cred.expires === "number"
|
||||||
? { expires: cred.expires }
|
? { expires: cred.expires }
|
||||||
@@ -617,7 +617,7 @@ export function ensureAuthProfileStore(
|
|||||||
} else {
|
} else {
|
||||||
store.profiles[profileId] = {
|
store.profiles[profileId] = {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
provider: cred.provider ?? (provider as OAuthProvider),
|
provider: String(cred.provider ?? provider),
|
||||||
access: cred.access,
|
access: cred.access,
|
||||||
refresh: cred.refresh,
|
refresh: cred.refresh,
|
||||||
expires: cred.expires,
|
expires: cred.expires,
|
||||||
@@ -1231,7 +1231,6 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
try {
|
try {
|
||||||
const result = await refreshOAuthTokenWithLock({
|
const result = await refreshOAuthTokenWithLock({
|
||||||
profileId,
|
profileId,
|
||||||
provider: cred.provider,
|
|
||||||
agentDir: params.agentDir,
|
agentDir: params.agentDir,
|
||||||
});
|
});
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
@@ -1351,7 +1350,6 @@ async function tryResolveOAuthProfile(params: {
|
|||||||
|
|
||||||
const refreshed = await refreshOAuthTokenWithLock({
|
const refreshed = await refreshOAuthTokenWithLock({
|
||||||
profileId,
|
profileId,
|
||||||
provider: cred.provider,
|
|
||||||
agentDir: params.agentDir,
|
agentDir: params.agentDir,
|
||||||
});
|
});
|
||||||
if (!refreshed) return null;
|
if (!refreshed) return null;
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export type ChutesOAuthAppConfig = {
|
|||||||
|
|
||||||
export type ChutesStoredOAuth = OAuthCredentials & {
|
export type ChutesStoredOAuth = OAuthCredentials & {
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
clientSecret?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function generateChutesPkce(): ChutesPkce {
|
export function generateChutesPkce(): ChutesPkce {
|
||||||
@@ -45,7 +44,7 @@ export function parseOAuthCallbackInput(
|
|||||||
try {
|
try {
|
||||||
const url = new URL(trimmed);
|
const url = new URL(trimmed);
|
||||||
const code = url.searchParams.get("code");
|
const code = url.searchParams.get("code");
|
||||||
const state = url.searchParams.get("state") ?? expectedState;
|
const state = url.searchParams.get("state");
|
||||||
if (!code) return { error: "Missing 'code' parameter in URL" };
|
if (!code) return { error: "Missing 'code' parameter in URL" };
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return { error: "Missing 'state' parameter. Paste the full URL." };
|
return { error: "Missing 'state' parameter. Paste the full URL." };
|
||||||
@@ -138,7 +137,6 @@ export async function exchangeChutesCodeForTokens(params: {
|
|||||||
email: info?.username,
|
email: info?.username,
|
||||||
accountId: info?.sub,
|
accountId: info?.sub,
|
||||||
clientId: params.app.clientId,
|
clientId: params.app.clientId,
|
||||||
clientSecret: params.app.clientSecret,
|
|
||||||
} as unknown as ChutesStoredOAuth;
|
} as unknown as ChutesStoredOAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,10 +160,7 @@ export async function refreshChutesTokens(params: {
|
|||||||
"Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth).",
|
"Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth).",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const clientSecret =
|
const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined;
|
||||||
params.credential.clientSecret?.trim() ??
|
|
||||||
process.env.CHUTES_CLIENT_SECRET?.trim() ??
|
|
||||||
undefined;
|
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
@@ -201,6 +196,5 @@ export async function refreshChutesTokens(params: {
|
|||||||
refresh: newRefresh || refreshToken,
|
refresh: newRefresh || refreshToken,
|
||||||
expires: coerceExpiresAt(expiresIn, now),
|
expires: coerceExpiresAt(expiresIn, now),
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
|
||||||
} as unknown as ChutesStoredOAuth;
|
} as unknown as ChutesStoredOAuth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ describe("buildBootstrapContextFiles", () => {
|
|||||||
);
|
);
|
||||||
expect(result?.content.length).toBeLessThan(long.length);
|
expect(result?.content.length).toBeLessThan(long.length);
|
||||||
expect(result?.content.startsWith(long.slice(0, 120))).toBe(true);
|
expect(result?.content.startsWith(long.slice(0, 120))).toBe(true);
|
||||||
expect(result?.content.endsWith(long.slice(-expectedTailChars))).toBe(
|
expect(result?.content.endsWith(long.slice(-expectedTailChars))).toBe(true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(warnings).toHaveLength(1);
|
expect(warnings).toHaveLength(1);
|
||||||
expect(warnings[0]).toContain("TOOLS.md");
|
expect(warnings[0]).toContain("TOOLS.md");
|
||||||
expect(warnings[0]).toContain("limit 200");
|
expect(warnings[0]).toContain("limit 200");
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { loginOpenAICodex, type OAuthCredentials } from "@mariozechner/pi-ai";
|
||||||
loginOpenAICodex,
|
|
||||||
type OAuthCredentials,
|
|
||||||
type OAuthProvider,
|
|
||||||
} from "@mariozechner/pi-ai";
|
|
||||||
import { resolveAgentConfig } from "../agents/agent-scope.js";
|
import { resolveAgentConfig } from "../agents/agent-scope.js";
|
||||||
import {
|
import {
|
||||||
CLAUDE_CLI_PROFILE_ID,
|
CLAUDE_CLI_PROFILE_ID,
|
||||||
@@ -52,7 +48,6 @@ import {
|
|||||||
applySyntheticConfig,
|
applySyntheticConfig,
|
||||||
applySyntheticProviderConfig,
|
applySyntheticProviderConfig,
|
||||||
applyZaiConfig,
|
applyZaiConfig,
|
||||||
MINIMAX_HOSTED_MODEL_REF,
|
|
||||||
MOONSHOT_DEFAULT_MODEL_REF,
|
MOONSHOT_DEFAULT_MODEL_REF,
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||||
@@ -105,6 +100,56 @@ function normalizeApiKeyInput(raw: string): string {
|
|||||||
const validateApiKeyInput = (value: unknown) =>
|
const validateApiKeyInput = (value: unknown) =>
|
||||||
normalizeApiKeyInput(String(value ?? "")).length > 0 ? undefined : "Required";
|
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(
|
function formatApiKeyPreview(
|
||||||
raw: string,
|
raw: string,
|
||||||
opts: { head?: number; tail?: number } = {},
|
opts: { head?: number; tail?: number } = {},
|
||||||
@@ -574,43 +619,25 @@ export async function applyAuthChoice(params: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const spin = params.prompter.progress("Starting OAuth flow…");
|
const spin = params.prompter.progress("Starting OAuth flow…");
|
||||||
let manualCodePromise: Promise<string> | undefined;
|
|
||||||
try {
|
try {
|
||||||
|
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
|
||||||
|
isRemote,
|
||||||
|
prompter: params.prompter,
|
||||||
|
runtime: params.runtime,
|
||||||
|
spin,
|
||||||
|
localBrowserMessage: "Complete sign-in in browser…",
|
||||||
|
});
|
||||||
|
|
||||||
const creds = await loginChutes({
|
const creds = await loginChutes({
|
||||||
app: {
|
app: {
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
redirectUri,
|
redirectUri,
|
||||||
scopes: scopes.split(/\\s+/).filter(Boolean),
|
scopes: scopes.split(/\s+/).filter(Boolean),
|
||||||
},
|
},
|
||||||
manual: isRemote,
|
manual: isRemote,
|
||||||
onAuth: async ({ url }) => {
|
onAuth,
|
||||||
if (isRemote) {
|
onPrompt,
|
||||||
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);
|
|
||||||
},
|
|
||||||
onProgress: (msg) => spin.update(msg),
|
onProgress: (msg) => spin.update(msg),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -618,11 +645,7 @@ export async function applyAuthChoice(params: {
|
|||||||
const email = creds.email?.trim() || "default";
|
const email = creds.email?.trim() || "default";
|
||||||
const profileId = `chutes:${email}`;
|
const profileId = `chutes:${email}`;
|
||||||
|
|
||||||
await writeOAuthCredentials(
|
await writeOAuthCredentials("chutes", creds, params.agentDir);
|
||||||
"chutes" as unknown as OAuthProvider,
|
|
||||||
creds,
|
|
||||||
params.agentDir,
|
|
||||||
);
|
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
profileId,
|
profileId,
|
||||||
provider: "chutes",
|
provider: "chutes",
|
||||||
@@ -637,7 +660,7 @@ export async function applyAuthChoice(params: {
|
|||||||
"Verify CHUTES_CLIENT_ID (and CHUTES_CLIENT_SECRET if required).",
|
"Verify CHUTES_CLIENT_ID (and CHUTES_CLIENT_SECRET if required).",
|
||||||
`Verify the OAuth app redirect URI includes: ${redirectUri}`,
|
`Verify the OAuth app redirect URI includes: ${redirectUri}`,
|
||||||
"Chutes docs: https://chutes.ai/docs/sign-in-with-chutes/overview",
|
"Chutes docs: https://chutes.ai/docs/sign-in-with-chutes/overview",
|
||||||
].join("\\n"),
|
].join("\n"),
|
||||||
"OAuth help",
|
"OAuth help",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -658,47 +681,23 @@ export async function applyAuthChoice(params: {
|
|||||||
"OpenAI Codex OAuth",
|
"OpenAI Codex OAuth",
|
||||||
);
|
);
|
||||||
const spin = params.prompter.progress("Starting OAuth flow…");
|
const spin = params.prompter.progress("Starting OAuth flow…");
|
||||||
let manualCodePromise: Promise<string> | undefined;
|
|
||||||
try {
|
try {
|
||||||
const creds = await loginOpenAICodex({
|
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
|
||||||
onAuth: async ({ url }) => {
|
isRemote,
|
||||||
if (isRemote) {
|
prompter: params.prompter,
|
||||||
spin.stop("OAuth URL ready");
|
runtime: params.runtime,
|
||||||
params.runtime.log(
|
spin,
|
||||||
`\nOpen this URL in your LOCAL browser:\n\n${url}\n`,
|
localBrowserMessage: "Complete sign-in in browser…",
|
||||||
);
|
|
||||||
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);
|
|
||||||
},
|
const creds = await loginOpenAICodex({
|
||||||
|
onAuth,
|
||||||
|
onPrompt,
|
||||||
onProgress: (msg) => spin.update(msg),
|
onProgress: (msg) => spin.update(msg),
|
||||||
});
|
});
|
||||||
spin.stop("OpenAI OAuth complete");
|
spin.stop("OpenAI OAuth complete");
|
||||||
if (creds) {
|
if (creds) {
|
||||||
await writeOAuthCredentials(
|
await writeOAuthCredentials("openai-codex", creds, params.agentDir);
|
||||||
"openai-codex" as unknown as OAuthProvider,
|
|
||||||
creds,
|
|
||||||
params.agentDir,
|
|
||||||
);
|
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
profileId: "openai-codex:default",
|
profileId: "openai-codex:default",
|
||||||
provider: "openai-codex",
|
provider: "openai-codex",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { randomBytes } from "node:crypto";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
|
|
||||||
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
import type { ChutesOAuthAppConfig } from "../agents/chutes-oauth.js";
|
import type { ChutesOAuthAppConfig } from "../agents/chutes-oauth.js";
|
||||||
import {
|
import {
|
||||||
|
CHUTES_AUTHORIZE_ENDPOINT,
|
||||||
exchangeChutesCodeForTokens,
|
exchangeChutesCodeForTokens,
|
||||||
generateChutesPkce,
|
generateChutesPkce,
|
||||||
parseOAuthCallbackInput,
|
parseOAuthCallbackInput,
|
||||||
@@ -30,7 +32,7 @@ function buildAuthorizeUrl(params: {
|
|||||||
code_challenge: params.challenge,
|
code_challenge: params.challenge,
|
||||||
code_challenge_method: "S256",
|
code_challenge_method: "S256",
|
||||||
});
|
});
|
||||||
return `https://api.chutes.ai/idp/authorize?${qs.toString()}`;
|
return `${CHUTES_AUTHORIZE_ENDPOINT}?${qs.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForLocalCallback(params: {
|
async function waitForLocalCallback(params: {
|
||||||
@@ -129,7 +131,7 @@ export async function loginChutes(params: {
|
|||||||
fetchFn?: typeof fetch;
|
fetchFn?: typeof fetch;
|
||||||
}): Promise<OAuthCredentials> {
|
}): Promise<OAuthCredentials> {
|
||||||
const { verifier, challenge } = generateChutesPkce();
|
const { verifier, challenge } = generateChutesPkce();
|
||||||
const state = verifier;
|
const state = randomBytes(16).toString("hex");
|
||||||
const timeoutMs = params.timeoutMs ?? 3 * 60 * 1000;
|
const timeoutMs = params.timeoutMs ?? 3 * 60 * 1000;
|
||||||
|
|
||||||
const url = buildAuthorizeUrl({
|
const url = buildAuthorizeUrl({
|
||||||
|
|||||||
@@ -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 { resolveDefaultAgentDir } from "../agents/agent-scope.js";
|
||||||
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||||
import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js";
|
import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js";
|
||||||
@@ -103,7 +103,7 @@ function buildMoonshotModelDefinition(): ModelDefinitionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function writeOAuthCredentials(
|
export async function writeOAuthCredentials(
|
||||||
provider: OAuthProvider,
|
provider: string,
|
||||||
creds: OAuthCredentials,
|
creds: OAuthCredentials,
|
||||||
agentDir?: string,
|
agentDir?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user