Spaces:
Sleeping
Sleeping
| import { | |
| getOAuthApiKey, | |
| getOAuthProviders, | |
| type OAuthCredentials, | |
| type OAuthProvider, | |
| } from "@mariozechner/pi-ai"; | |
| import lockfile from "proper-lockfile"; | |
| import type { OpenClawConfig } from "../../config/config.js"; | |
| import type { AuthProfileStore } from "./types.js"; | |
| import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js"; | |
| import { refreshChutesTokens } from "../chutes-oauth.js"; | |
| import { AUTH_STORE_LOCK_OPTIONS, log } 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"; | |
| const OAUTH_PROVIDER_IDS = new Set<OAuthProvider>( | |
| getOAuthProviders().map((provider) => provider.id), | |
| ); | |
| function isOAuthProvider(provider: string): provider is OAuthProvider { | |
| // biome-ignore lint/suspicious/noExplicitAny: type guard needs runtime check | |
| return OAUTH_PROVIDER_IDS.has(provider as any); | |
| } | |
| const resolveOAuthProvider = (provider: string): OAuthProvider | null => | |
| isOAuthProvider(provider) ? provider : null; | |
| 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<void>) | 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<string, OAuthCredentials> = { | |
| [cred.provider]: cred, | |
| }; | |
| const result = | |
| String(cred.provider) === "chutes" | |
| ? await (async () => { | |
| const newCredentials = await refreshChutesTokens({ | |
| credential: cred, | |
| }); | |
| return { apiKey: newCredentials.access, newCredentials }; | |
| })() | |
| : String(cred.provider) === "qwen-portal" | |
| ? await (async () => { | |
| const newCredentials = await refreshQwenPortalCredentials(cred); | |
| return { apiKey: newCredentials.access, newCredentials }; | |
| })() | |
| : await (async () => { | |
| const oauthProvider = resolveOAuthProvider(cred.provider); | |
| if (!oauthProvider) { | |
| return null; | |
| } | |
| return await getOAuthApiKey(oauthProvider, oauthCreds); | |
| })(); | |
| if (!result) { | |
| return null; | |
| } | |
| store.profiles[params.profileId] = { | |
| ...cred, | |
| ...result.newCredentials, | |
| type: "oauth", | |
| }; | |
| saveAuthProfileStore(store, params.agentDir); | |
| return result; | |
| } finally { | |
| if (release) { | |
| try { | |
| await release(); | |
| } catch { | |
| // ignore unlock errors | |
| } | |
| } | |
| } | |
| } | |
| async function tryResolveOAuthProfile(params: { | |
| cfg?: OpenClawConfig; | |
| 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?: OpenClawConfig; | |
| 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 | |
| } | |
| } | |
| // Fallback: if this is a secondary agent, try using the main agent's credentials | |
| if (params.agentDir) { | |
| try { | |
| const mainStore = ensureAuthProfileStore(undefined); // main agent (no agentDir) | |
| const mainCred = mainStore.profiles[profileId]; | |
| if (mainCred?.type === "oauth" && Date.now() < mainCred.expires) { | |
| // Main agent has fresh credentials - copy them to this agent and use them | |
| refreshedStore.profiles[profileId] = { ...mainCred }; | |
| saveAuthProfileStore(refreshedStore, params.agentDir); | |
| log.info("inherited fresh OAuth credentials from main agent", { | |
| profileId, | |
| agentDir: params.agentDir, | |
| expires: new Date(mainCred.expires).toISOString(), | |
| }); | |
| return { | |
| apiKey: buildOAuthApiKey(mainCred.provider, mainCred), | |
| provider: mainCred.provider, | |
| email: mainCred.email, | |
| }; | |
| } | |
| } catch { | |
| // keep original error if main agent fallback also fails | |
| } | |
| } | |
| 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}` : ""), | |
| { cause: error }, | |
| ); | |
| } | |
| } | |