import { getOAuthApiKey, type OAuthCredentials, type OAuthProvider } from "@mariozechner/pi-ai"; import lockfile from "proper-lockfile"; import type { ClawdbotConfig } from "../../config/config.js"; import { refreshChutesTokens } from "../chutes-oauth.js"; import { writeClaudeCliCredentials } from "../cli-credentials.js"; import { AUTH_STORE_LOCK_OPTIONS, CLAUDE_CLI_PROFILE_ID } from "./constants.js"; import { formatAuthDoctorHint } from "./doctor.js"; import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js"; import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js"; import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js"; import type { AuthProfileStore } from "./types.js"; function buildOAuthApiKey(provider: string, credentials: OAuthCredentials): string { const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity"; return needsProjectId ? JSON.stringify({ token: credentials.access, projectId: credentials.projectId, }) : credentials.access; } async function refreshOAuthTokenWithLock(params: { profileId: string; agentDir?: string; }): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> { const authPath = resolveAuthStorePath(params.agentDir); ensureAuthStoreFile(authPath); let release: (() => Promise) | undefined; try { release = await lockfile.lock(authPath, { ...AUTH_STORE_LOCK_OPTIONS, }); const store = ensureAuthProfileStore(params.agentDir); const cred = store.profiles[params.profileId]; if (!cred || cred.type !== "oauth") return null; if (Date.now() < cred.expires) { return { apiKey: buildOAuthApiKey(cred.provider, cred), newCredentials: cred, }; } const oauthCreds: Record = { [cred.provider]: cred, }; const result = String(cred.provider) === "chutes" ? await (async () => { const newCredentials = await refreshChutesTokens({ credential: cred, }); return { apiKey: newCredentials.access, newCredentials }; })() : await getOAuthApiKey(cred.provider as OAuthProvider, oauthCreds); if (!result) return null; store.profiles[params.profileId] = { ...cred, ...result.newCredentials, type: "oauth", }; saveAuthProfileStore(store, params.agentDir); // Sync refreshed credentials back to Claude CLI if this is the claude-cli profile // This ensures Claude Code continues to work after ClawdBot refreshes the token if (params.profileId === CLAUDE_CLI_PROFILE_ID && cred.provider === "anthropic") { writeClaudeCliCredentials(result.newCredentials); } return result; } finally { if (release) { try { await release(); } catch { // ignore unlock errors } } } } async function tryResolveOAuthProfile(params: { cfg?: ClawdbotConfig; store: AuthProfileStore; profileId: string; agentDir?: string; }): Promise<{ apiKey: string; provider: string; email?: string } | null> { const { cfg, store, profileId } = params; const cred = store.profiles[profileId]; if (!cred || cred.type !== "oauth") return null; const profileConfig = cfg?.auth?.profiles?.[profileId]; if (profileConfig && profileConfig.provider !== cred.provider) return null; if (profileConfig && profileConfig.mode !== cred.type) return null; if (Date.now() < cred.expires) { return { apiKey: buildOAuthApiKey(cred.provider, cred), provider: cred.provider, email: cred.email, }; } const refreshed = await refreshOAuthTokenWithLock({ profileId, agentDir: params.agentDir, }); if (!refreshed) return null; return { apiKey: refreshed.apiKey, provider: cred.provider, email: cred.email, }; } export async function resolveApiKeyForProfile(params: { cfg?: ClawdbotConfig; store: AuthProfileStore; profileId: string; agentDir?: string; }): Promise<{ apiKey: string; provider: string; email?: string } | null> { const { cfg, store, profileId } = params; const cred = store.profiles[profileId]; if (!cred) return null; const profileConfig = cfg?.auth?.profiles?.[profileId]; if (profileConfig && profileConfig.provider !== cred.provider) return null; if (profileConfig && profileConfig.mode !== cred.type) { // Compatibility: treat "oauth" config as compatible with stored token profiles. if (!(profileConfig.mode === "oauth" && cred.type === "token")) return null; } if (cred.type === "api_key") { return { apiKey: cred.key, provider: cred.provider, email: cred.email }; } if (cred.type === "token") { const token = cred.token?.trim(); if (!token) return null; if ( typeof cred.expires === "number" && Number.isFinite(cred.expires) && cred.expires > 0 && Date.now() >= cred.expires ) { return null; } return { apiKey: token, provider: cred.provider, email: cred.email }; } if (Date.now() < cred.expires) { return { apiKey: buildOAuthApiKey(cred.provider, cred), provider: cred.provider, email: cred.email, }; } try { const result = await refreshOAuthTokenWithLock({ profileId, agentDir: params.agentDir, }); if (!result) return null; return { apiKey: result.apiKey, provider: cred.provider, email: cred.email, }; } catch (error) { const refreshedStore = ensureAuthProfileStore(params.agentDir); const refreshed = refreshedStore.profiles[profileId]; if (refreshed?.type === "oauth" && Date.now() < refreshed.expires) { return { apiKey: buildOAuthApiKey(refreshed.provider, refreshed), provider: refreshed.provider, email: refreshed.email ?? cred.email, }; } const fallbackProfileId = suggestOAuthProfileIdForLegacyDefault({ cfg, store: refreshedStore, provider: cred.provider, legacyProfileId: profileId, }); if (fallbackProfileId && fallbackProfileId !== profileId) { try { const fallbackResolved = await tryResolveOAuthProfile({ cfg, store: refreshedStore, profileId: fallbackProfileId, agentDir: params.agentDir, }); if (fallbackResolved) return fallbackResolved; } catch { // keep original error } } const message = error instanceof Error ? error.message : String(error); const hint = formatAuthDoctorHint({ cfg, store: refreshedStore, provider: cred.provider, profileId, }); throw new Error( `OAuth token refresh failed for ${cred.provider}: ${message}. ` + "Please try again or re-authenticate." + (hint ? `\n\n${hint}` : ""), ); } }