UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

927 lines (926 loc) 37.6 kB
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js"; import { c as redactSensitiveText } from "./redact-DduLliKq.js"; import { o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import "./number-coercion-Z7n6tXLk.js"; import { d as normalizeSecretInputString, o as coerceSecretRef } from "./types.secrets-_0JOMGE5.js"; import { i as normalizeProviderId } from "./provider-id-Dq06Bcx6.js"; import { i as resolveSecretRefString } from "./resolve-DPFnV1p0.js"; import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js"; import { r as resolveProviderIdForAuth } from "./provider-auth-aliases-BNZrcvHv.js"; import { o as withFileLock } from "./file-lock-BOaqUSu6.js"; import "./file-lock-DQM8YrNh.js"; import "./config-C9RxTsn1.js"; import { f as resolveOAuthRefreshLockPath, i as hasRuntimeAuthProfileStoreSnapshot, l as resolveAuthStorePath, n as getRuntimeAuthProfileStoreSnapshot, o as setRuntimeAuthProfileStoreSnapshot } from "./runtime-snapshots-CGKcj2Tz.js"; import { A as shouldReplaceStoredOAuthCredential, B as OAUTH_REFRESH_CALL_TIMEOUT_MS, C as readManagedExternalCliCredential, D as isSafeToAdoptBootstrapOAuthIdentity, E as hasUsableOAuthCredential, H as log, O as isSafeToAdoptMainStoreOAuthIdentity, P as resolveTokenExpiryState, S as readExternalCliFallbackCredential, T as hasMatchingOAuthIdentity, V as OAUTH_REFRESH_LOCK_OPTIONS, f as resolvePersistedAuthProfileOwnerAgentDir, i as ensureAuthProfileStoreWithoutExternalProfiles, k as shouldBootstrapFromExternalCliCredential, l as loadAuthProfileStoreForSecretsRuntime, m as updateAuthProfileStoreWithLock, u as loadAuthProfileStoreWithoutExternalProfiles, w as areOAuthCredentialsEquivalent } from "./store-C8spD0DG.js"; import { i as formatProviderAuthProfileApiKeyWithPlugin, n as buildProviderAuthDoctorHintWithPlugin, o as refreshProviderOAuthCredentialWithPlugin } from "./provider-runtime.runtime.js"; import { r as getOAuthProviders, t as getOAuthApiKey } from "./oauth-DS42R_dy.js"; import { t as normalizeOptionalSecretInput } from "./normalize-secret-input-OwRUfByL.js"; import { a as refreshChutesTokens } from "./chutes-oauth-DeD73yqX.js"; import { t as OAuthRefreshFailureError } from "./oauth-refresh-failure-C54bUOW9.js"; import { t as assertNoOAuthSecretRefPolicyViolations } from "./policy-CpwggtQ0.js"; import { t as clearLastGoodProfileWithLock } from "./profiles-BMWtmBgj.js"; import { n as suggestOAuthProfileIdForLegacyDefault } from "./repair-ELDRMhgP.js"; //#region src/agents/auth-profiles/doctor.ts /** * Provider-specific auth doctor hints. * Adds local migration guidance for known legacy profiles before falling back * to provider plugin doctor copy. */ const QWEN_PORTAL_OAUTH_MIGRATION_HINT = "Legacy Qwen Portal OAuth profiles are not refreshable. Re-authenticate with a current portal token: openclaw onboard --auth-choice qwen-oauth."; function hasLegacyQwenPortalOAuthProfile(store, profileId) { return (profileId ? [store.profiles[profileId]] : Object.values(store.profiles)).some((profile) => profile?.type === "oauth" && normalizeProviderId(profile.provider) === "qwen-portal"); } /** Formats provider-specific auth doctor guidance for a profile/store. */ async function formatAuthDoctorHint(params) { const normalizedProvider = normalizeProviderId(params.provider); if (normalizedProvider === "qwen-portal" && hasLegacyQwenPortalOAuthProfile(params.store, params.profileId)) return QWEN_PORTAL_OAUTH_MIGRATION_HINT; const pluginHint = await buildProviderAuthDoctorHintWithPlugin({ provider: normalizedProvider, context: { config: params.cfg, store: params.store, provider: normalizedProvider, profileId: params.profileId } }); if (typeof pluginHint === "string" && pluginHint.trim()) return pluginHint; return ""; } //#endregion //#region src/agents/auth-profiles/oauth-identity.ts /** * OAuth identity comparison and mirroring decisions. * Guards cross-agent credential copy/adoption so refreshed credentials cannot * overwrite a different account's local auth state. */ /** Normalize account-id style identity tokens for exact comparison. */ function normalizeAuthIdentityToken(value) { const trimmed = value?.trim(); return trimmed ? trimmed : void 0; } /** Normalize email identity tokens for case-insensitive comparison. */ function normalizeAuthEmailToken(value) { return normalizeAuthIdentityToken(value)?.toLowerCase(); } /** * One-sided copy gate for both directions: * - mirror: sub-agent refresh -> main-agent store * - adopt: main-agent store -> sub-agent store */ function isSafeToCopyOAuthIdentity(existing, incoming) { const aAcct = normalizeAuthIdentityToken(existing.accountId); const bAcct = normalizeAuthIdentityToken(incoming.accountId); const aEmail = normalizeAuthEmailToken(existing.email); const bEmail = normalizeAuthEmailToken(incoming.email); if (aAcct !== void 0 && bAcct !== void 0) return aAcct === bAcct; if (aEmail !== void 0 && bEmail !== void 0) return aEmail === bEmail; if (aAcct !== void 0 || aEmail !== void 0) return false; return true; } /** Decide whether a refreshed OAuth credential should mirror into another store. */ function shouldMirrorRefreshedOAuthCredential(params) { const { existing, refreshed } = params; if (!existing) return { shouldMirror: true, reason: "no-existing-credential" }; if (existing.type !== "oauth") return { shouldMirror: false, reason: "non-oauth-existing-credential" }; if (existing.provider !== refreshed.provider) return { shouldMirror: false, reason: "provider-mismatch" }; if (!isSafeToCopyOAuthIdentity(existing, refreshed)) return { shouldMirror: false, reason: "identity-mismatch-or-regression" }; const refreshedExpires = asDateTimestampMs(refreshed.expires); if (refreshedExpires === void 0) return { shouldMirror: false, reason: "incoming-not-fresher" }; const existingExpires = asDateTimestampMs(existing.expires); if (existingExpires !== void 0 && existingExpires >= refreshedExpires) return { shouldMirror: false, reason: "incoming-not-fresher" }; return { shouldMirror: true, reason: "incoming-fresher" }; } //#endregion //#region src/agents/auth-profiles/oauth-refresh-lock-errors.ts /** * OAuth refresh lock error helpers. * Distinguishes global refresh-lock contention from auth-store lock timeouts * and builds the user-facing contention error. */ /** Returns true when an error came from the global OAuth refresh lock. */ function isGlobalRefreshLockTimeoutError(error, lockPath) { const candidate = typeof error === "object" && error !== null ? error : void 0; return candidate?.code === "file_lock_timeout" && candidate.lockPath === `${lockPath}.lock`; } /** Builds the user-facing OAuth refresh contention error. */ function buildRefreshContentionError(params) { return Object.assign(new Error(`OAuth refresh failed (refresh_contention): another process is already refreshing ${params.provider} for ${params.profileId}. Please wait for the in-flight refresh to finish and retry.`, { cause: params.cause }), { code: "refresh_contention", cause: params.cause }); } //#endregion //#region src/agents/auth-profiles/oauth-manager.ts /** Refresh failure that preserves a redacted refreshed store and credential. */ var OAuthManagerRefreshError = class extends OAuthRefreshFailureError { #refreshedStore; #credential; constructor(params) { const structuredCause = typeof params.cause === "object" && params.cause !== null ? params.cause : void 0; const delegatedCause = structuredCause?.code === "refresh_contention" && structuredCause.cause ? structuredCause.cause : params.cause; const storedCredential = params.refreshedStore.profiles[params.profileId]; const secrets = collectOAuthCredentialSecrets(params.credential, ...params.attemptedCredentials ?? [], storedCredential?.type === "oauth" ? storedCredential : void 0); const causeMessage = formatRedactedOAuthRefreshError(params.cause, secrets); super({ provider: params.credential.provider, message: `OAuth token refresh failed for ${params.credential.provider}: ${causeMessage}`, cause: createRedactedOAuthRefreshCause(delegatedCause, secrets) }); this.name = "OAuthManagerRefreshError"; this.#credential = params.credential; this.profileId = params.profileId; this.#refreshedStore = params.refreshedStore; if (structuredCause) { this.code = typeof structuredCause.code === "string" ? structuredCause.code : void 0; if (typeof structuredCause.lockPath === "string") this.lockPath = structuredCause.lockPath; else if (typeof structuredCause.cause === "object" && structuredCause.cause !== null && "lockPath" in structuredCause.cause && typeof structuredCause.cause.lockPath === "string") this.lockPath = structuredCause.cause.lockPath; } } getRefreshedStore() { return this.#refreshedStore; } getCredential() { return this.#credential; } toJSON() { return { name: this.name, message: this.message, profileId: this.profileId, provider: this.provider }; } }; function hasOAuthCredentialChanged(previous, current) { return previous.access !== current.access || previous.refresh !== current.refresh || previous.expires !== current.expires; } function canReuseOAuthCredentialAfterRefreshFailure(params) { return !params.forceRefresh || hasOAuthCredentialChanged(params.attempted, params.candidate); } function collectOAuthCredentialSecrets(...credentials) { const secrets = /* @__PURE__ */ new Set(); for (const credential of credentials) for (const secret of [ credential?.access, credential?.refresh, credential?.idToken ]) if (secret) secrets.add(secret); return Array.from(secrets).toSorted((a, b) => b.length - a.length); } function redactOAuthCredentialSecrets(message, secrets) { let redacted = message; for (const secret of secrets) redacted = redacted.split(secret).join("[redacted]"); return redacted; } function formatRawErrorMessage(error) { if (error instanceof Error) { let formatted = error.message || error.name || "Error"; let cause = error.cause; const seen = new Set([error]); while (cause && !seen.has(cause)) { seen.add(cause); if (cause instanceof Error) { if (cause.message) formatted += ` | ${cause.message}`; cause = cause.cause; } else if (typeof cause === "string") { formatted += ` | ${cause}`; break; } else break; } return formatted; } if (typeof error === "string" || typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return String(error); try { return JSON.stringify(error) ?? String(error); } catch { return Object.prototype.toString.call(error); } } function formatRedactedOAuthRefreshError(error, secrets) { return redactSensitiveText(redactOAuthCredentialSecrets(formatRawErrorMessage(error), secrets)); } function createRedactedOAuthRefreshCause(cause, secrets) { const redacted = formatRedactedOAuthRefreshError(cause, secrets); const sanitized = new Error(redacted); if (cause instanceof Error && cause.name) sanitized.name = cause.name; return sanitized; } function loadStoredOAuthRefreshStore(agentDir) { return loadAuthProfileStoreWithoutExternalProfiles(agentDir, { allowKeychainPrompt: true }); } async function loadFreshStoredOAuthCredential(params) { const reloaded = loadStoredOAuthRefreshStore(params.agentDir).profiles[params.profileId]; if (reloaded?.type !== "oauth" || reloaded.provider !== params.provider || !hasUsableOAuthCredential(reloaded)) return null; if (params.requireChange && params.previous && !hasOAuthCredentialChanged(params.previous, reloaded)) return null; return reloaded; } /** Select local OAuth unless a safe external bootstrap credential should win. */ function resolveEffectiveOAuthCredential(params) { const imported = params.readBootstrapCredential({ profileId: params.profileId, credential: params.credential }); if (!imported) return params.credential; if (hasUsableOAuthCredential(params.credential)) { log.debug("resolved oauth credential from canonical local store", { profileId: params.profileId, provider: params.credential.provider, localExpires: params.credential.expires, externalExpires: imported.expires }); return params.credential; } if (!isSafeToAdoptBootstrapOAuthIdentity(params.credential, imported)) { log.warn("refused external oauth bootstrap credential: identity mismatch or missing binding", { profileId: params.profileId, provider: params.credential.provider }); return params.credential; } if (shouldBootstrapFromExternalCliCredential({ existing: params.credential, imported })) { log.debug("resolved oauth credential from external cli bootstrap", { profileId: params.profileId, provider: imported.provider, localExpires: params.credential.expires, externalExpires: imported.expires }); return imported; } return params.credential; } /** Create an OAuth manager bound to provider-specific build/refresh adapters. */ function createOAuthManager(adapter) { function adoptNewerMainOAuthCredential(params) { if (!params.agentDir) return null; try { const mainCred = ensureAuthProfileStoreWithoutExternalProfiles(void 0, { allowKeychainPrompt: false }).profiles[params.profileId]; if (mainCred?.type !== "oauth") return null; const mainExpires = asDateTimestampMs(mainCred.expires); const localExpires = asDateTimestampMs(params.credential.expires); if (mainCred.provider === params.credential.provider && hasUsableOAuthCredential(mainCred) && mainExpires !== void 0 && (localExpires === void 0 || mainExpires > localExpires) && isSafeToAdoptMainStoreOAuthIdentity(params.credential, mainCred)) { params.store.profiles[params.profileId] = { ...mainCred }; log.info("adopted newer OAuth credentials from main agent", { profileId: params.profileId, agentDir: params.agentDir, expires: new Date(mainCred.expires).toISOString() }); return mainCred; } } catch (err) { log.debug("adoptNewerMainOAuthCredential failed", { profileId: params.profileId, error: formatErrorMessage(err) }); } return null; } const refreshQueues = /* @__PURE__ */ new Map(); function refreshQueueKey(provider, profileId) { return `${provider}\u0000${profileId}`; } async function withRefreshCallTimeout(label, timeoutMs, fn) { let timeoutHandle; try { return await new Promise((resolve, reject) => { timeoutHandle = setTimeout(() => { reject(/* @__PURE__ */ new Error(`OAuth refresh call "${label}" exceeded hard timeout (${timeoutMs}ms)`)); }, timeoutMs); fn().then(resolve, reject); }); } finally { if (timeoutHandle) clearTimeout(timeoutHandle); } } async function mirrorRefreshedCredentialIntoMainStore(params) { try { await updateAuthProfileStoreWithLock({ agentDir: void 0, updater: (store) => { const existing = store.profiles[params.profileId]; const decision = shouldMirrorRefreshedOAuthCredential({ existing, refreshed: params.refreshed }); if (!decision.shouldMirror) { if (decision.reason === "identity-mismatch-or-regression") log.warn("refused to mirror OAuth credential: identity mismatch or regression", { profileId: params.profileId }); return false; } store.profiles[params.profileId] = { ...params.refreshed }; log.debug("mirrored refreshed OAuth credential to main agent store", { profileId: params.profileId, expires: Number.isFinite(params.refreshed.expires) ? new Date(params.refreshed.expires).toISOString() : void 0 }); return true; } }); } catch (err) { log.debug("mirrorRefreshedCredentialIntoMainStore failed", { profileId: params.profileId, error: formatErrorMessage(err) }); } } async function saveOAuthCredentialWithStoreLock(params) { let saved = false; return await updateAuthProfileStoreWithLock({ agentDir: params.agentDir, updater: (store) => { const existing = store.profiles[params.profileId]; const expectedCredentials = Array.isArray(params.expected) ? params.expected : [params.expected]; if (existing?.type !== "oauth" || !expectedCredentials.some((expected) => areOAuthCredentialsEquivalent(existing, expected))) { log.debug("skipped OAuth credential write because stored profile changed", { profileId: params.profileId }); return false; } if (!isSafeToAdoptBootstrapOAuthIdentity(existing, params.credential) || !shouldReplaceStoredOAuthCredential(existing, params.credential)) { log.debug("skipped OAuth credential write because stored profile changed", { profileId: params.profileId }); return false; } store.profiles[params.profileId] = { ...params.credential }; saved = true; return true; } }) !== null && saved; } async function doRefreshOAuthTokenWithLock(params) { const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir(params); const authPath = resolveAuthStorePath(ownerAgentDir); const globalRefreshLockPath = resolveOAuthRefreshLockPath(params.provider, params.profileId); try { return await withFileLock(globalRefreshLockPath, OAUTH_REFRESH_LOCK_OPTIONS, async () => { const store = loadStoredOAuthRefreshStore(ownerAgentDir); const cred = store.profiles[params.profileId]; if (!cred || cred.type !== "oauth") return null; let credentialToRefresh = cred; if (!params.forceRefresh && hasUsableOAuthCredential(cred)) return { apiKey: await adapter.buildApiKey(cred.provider, cred, { cfg: params.cfg, agentDir: params.agentDir }), credential: cred }; if (params.agentDir) try { const mainCred = loadStoredOAuthRefreshStore(void 0).profiles[params.profileId]; if (mainCred?.type === "oauth" && mainCred.provider === cred.provider && hasUsableOAuthCredential(mainCred) && !params.forceRefresh && isSafeToAdoptMainStoreOAuthIdentity(cred, mainCred)) { store.profiles[params.profileId] = { ...mainCred }; log.info("adopted fresh OAuth credential from main store (under refresh lock)", { profileId: params.profileId, agentDir: params.agentDir, expires: new Date(mainCred.expires).toISOString() }); return { apiKey: await adapter.buildApiKey(mainCred.provider, mainCred, { cfg: params.cfg, agentDir: params.agentDir }), credential: mainCred }; } else if (mainCred?.type === "oauth" && mainCred.provider === cred.provider && hasUsableOAuthCredential(mainCred) && !isSafeToAdoptMainStoreOAuthIdentity(cred, mainCred)) log.warn("refused to adopt fresh main-store OAuth credential: identity mismatch", { profileId: params.profileId, agentDir: params.agentDir }); } catch (err) { log.debug("inside-lock main-store adoption failed; proceeding to refresh", { profileId: params.profileId, error: formatErrorMessage(err) }); } const externallyManaged = adapter.readBootstrapCredential({ profileId: params.profileId, credential: cred }); if (externallyManaged) if (externallyManaged.provider !== cred.provider) log.warn("refused external oauth bootstrap credential: provider mismatch", { profileId: params.profileId, provider: cred.provider }); else if (!isSafeToAdoptBootstrapOAuthIdentity(cred, externallyManaged)) log.warn("refused external oauth bootstrap credential: identity mismatch or missing binding", { profileId: params.profileId, provider: cred.provider }); else { if (shouldReplaceStoredOAuthCredential(cred, externallyManaged) && !areOAuthCredentialsEquivalent(cred, externallyManaged)) { store.profiles[params.profileId] = { ...externallyManaged }; await saveOAuthCredentialWithStoreLock({ agentDir: ownerAgentDir, profileId: params.profileId, expected: cred, credential: externallyManaged }); } credentialToRefresh = externallyManaged; if (!params.forceRefresh && hasUsableOAuthCredential(externallyManaged)) return { apiKey: await adapter.buildApiKey(externallyManaged.provider, externallyManaged, { cfg: params.cfg, agentDir: params.agentDir }), credential: externallyManaged }; } if (normalizeSecretInputString(credentialToRefresh.refresh) === void 0) return null; const refreshedCredentials = await withRefreshCallTimeout(`refreshOAuthCredential(${cred.provider})`, OAUTH_REFRESH_CALL_TIMEOUT_MS, async () => { params.attemptedCredentials?.push(credentialToRefresh); const refreshed = await adapter.refreshCredential(credentialToRefresh); return refreshed ? { ...credentialToRefresh, ...refreshed, type: "oauth" } : null; }); if (!refreshedCredentials) return null; store.profiles[params.profileId] = refreshedCredentials; if (!await saveOAuthCredentialWithStoreLock({ agentDir: ownerAgentDir, profileId: params.profileId, expected: credentialToRefresh === cred || areOAuthCredentialsEquivalent(credentialToRefresh, cred) ? credentialToRefresh : [credentialToRefresh, cred], credential: refreshedCredentials })) throw new Error("Failed to persist refreshed OAuth credential"); if (ownerAgentDir) { if (resolveAuthStorePath(void 0) !== authPath) await mirrorRefreshedCredentialIntoMainStore({ profileId: params.profileId, refreshed: refreshedCredentials }); } return { apiKey: await adapter.buildApiKey(cred.provider, refreshedCredentials, { cfg: params.cfg, agentDir: params.agentDir }), credential: refreshedCredentials }; }); } catch (error) { if (isGlobalRefreshLockTimeoutError(error, globalRefreshLockPath)) throw buildRefreshContentionError({ provider: params.provider, profileId: params.profileId, cause: error }); throw error; } } async function refreshOAuthTokenWithLock(params) { const key = refreshQueueKey(params.provider, params.profileId); const prev = refreshQueues.get(key) ?? Promise.resolve(); let release; const gate = new Promise((resolve) => { release = resolve; }); refreshQueues.set(key, gate); try { await prev; return await doRefreshOAuthTokenWithLock(params); } finally { release(); if (refreshQueues.get(key) === gate) refreshQueues.delete(key); } } async function resolveOAuthAccess(params) { const adoptedCredential = adoptNewerMainOAuthCredential({ store: params.store, profileId: params.profileId, agentDir: params.agentDir, credential: params.credential }) ?? params.credential; const effectiveCredential = resolveEffectiveOAuthCredential({ profileId: params.profileId, credential: adoptedCredential, readBootstrapCredential: adapter.readBootstrapCredential }); const attemptedCredentials = []; if (!params.forceRefresh && hasUsableOAuthCredential(effectiveCredential)) return { apiKey: await adapter.buildApiKey(effectiveCredential.provider, effectiveCredential, { cfg: params.cfg, agentDir: params.agentDir }), credential: effectiveCredential }; try { return await refreshOAuthTokenWithLock({ profileId: params.profileId, provider: params.credential.provider, agentDir: params.agentDir, cfg: params.cfg, forceRefresh: params.forceRefresh, attemptedCredentials }); } catch (error) { const refreshedStore = loadStoredOAuthRefreshStore(params.agentDir); const refreshed = refreshedStore.profiles[params.profileId]; if (refreshed?.type === "oauth" && hasUsableOAuthCredential(refreshed) && canReuseOAuthCredentialAfterRefreshFailure({ forceRefresh: params.forceRefresh, attempted: effectiveCredential, candidate: refreshed })) return { apiKey: await adapter.buildApiKey(refreshed.provider, refreshed, { cfg: params.cfg, agentDir: params.agentDir }), credential: refreshed }; if (adapter.isRefreshTokenReusedError(error) && refreshed?.type === "oauth" && refreshed.provider === params.credential.provider && hasOAuthCredentialChanged(params.credential, refreshed)) { const recovered = await loadFreshStoredOAuthCredential({ profileId: params.profileId, agentDir: params.agentDir, provider: params.credential.provider, previous: effectiveCredential, requireChange: true }); if (recovered) return { apiKey: await adapter.buildApiKey(recovered.provider, recovered, { cfg: params.cfg, agentDir: params.agentDir }), credential: recovered }; try { const retried = await refreshOAuthTokenWithLock({ profileId: params.profileId, provider: params.credential.provider, agentDir: params.agentDir, cfg: params.cfg, forceRefresh: params.forceRefresh, attemptedCredentials }); if (retried) return retried; } catch {} } if (params.agentDir) try { const mainCred = ensureAuthProfileStoreWithoutExternalProfiles(void 0, { allowKeychainPrompt: false }).profiles[params.profileId]; if (mainCred?.type === "oauth" && mainCred.provider === params.credential.provider && hasUsableOAuthCredential(mainCred) && canReuseOAuthCredentialAfterRefreshFailure({ forceRefresh: params.forceRefresh, attempted: effectiveCredential, candidate: mainCred }) && isSafeToAdoptMainStoreOAuthIdentity(params.credential, mainCred)) { refreshedStore.profiles[params.profileId] = { ...mainCred }; log.info("inherited fresh OAuth credentials from main agent", { profileId: params.profileId, agentDir: params.agentDir, expires: new Date(mainCred.expires).toISOString() }); return { apiKey: await adapter.buildApiKey(mainCred.provider, mainCred, { cfg: params.cfg, agentDir: params.agentDir }), credential: mainCred }; } } catch {} const fallback = adapter.readFallbackCredential?.({ profileId: params.profileId, credential: effectiveCredential }); if (fallback && fallback.provider === params.credential.provider && hasUsableOAuthCredential(fallback) && hasMatchingOAuthIdentity(params.credential, fallback) && canReuseOAuthCredentialAfterRefreshFailure({ forceRefresh: params.forceRefresh, attempted: effectiveCredential, candidate: fallback })) { log.info("using external OAuth credential after refresh failure", { profileId: params.profileId, provider: fallback.provider, expires: new Date(fallback.expires).toISOString() }); return { apiKey: await adapter.buildApiKey(fallback.provider, fallback, { cfg: params.cfg, agentDir: params.agentDir }), credential: fallback }; } throw new OAuthManagerRefreshError({ credential: params.credential, attemptedCredentials: [effectiveCredential, ...attemptedCredentials], profileId: params.profileId, refreshedStore, cause: error }); } } function resetRefreshQueuesForTest() { refreshQueues.clear(); } return { resolveOAuthAccess, resetRefreshQueuesForTest }; } //#endregion //#region src/agents/auth-profiles/oauth.ts /** * Auth profile API-key/OAuth runtime resolver. * Converts selected auth profiles into provider API keys, refreshes OAuth * credentials, resolves SecretRefs, and maintains runtime store snapshots. */ function listOAuthProviderIds() { if (typeof getOAuthProviders !== "function") return []; const providers = getOAuthProviders(); if (!Array.isArray(providers)) return []; return providers.map((provider) => provider && typeof provider === "object" && "id" in provider && typeof provider.id === "string" ? provider.id : void 0).filter((providerId) => typeof providerId === "string"); } const OAUTH_PROVIDER_IDS = new Set(listOAuthProviderIds()); const isOAuthProvider = (provider) => OAUTH_PROVIDER_IDS.has(provider); const resolveOAuthProvider = (provider) => isOAuthProvider(provider) ? provider : null; /** Bearer-token auth modes that are interchangeable (oauth tokens and raw tokens). */ const BEARER_AUTH_MODES = new Set(["oauth", "token"]); const isCompatibleModeType = (mode, type) => { if (!mode || !type) return false; if (mode === type) return true; return BEARER_AUTH_MODES.has(mode) && BEARER_AUTH_MODES.has(type); }; function isProfileConfigCompatible(params) { const profileConfig = params.cfg?.auth?.profiles?.[params.profileId]; if (profileConfig && profileConfig.provider !== params.provider) return false; if (profileConfig && !isCompatibleModeType(profileConfig.mode, params.mode)) return false; return true; } async function buildOAuthApiKey(provider, credentials, context) { const formatted = await formatProviderAuthProfileApiKeyWithPlugin({ provider, config: context.cfg, context: credentials }); return typeof formatted === "string" && formatted.length > 0 ? formatted : credentials.access; } function buildApiKeyProfileResult(params) { const result = { apiKey: params.apiKey, provider: params.provider, email: params.email }; Object.defineProperties(result, { profileId: { value: params.profileId, enumerable: false }, profileType: { value: params.profileType, enumerable: false } }); return result; } function extractErrorMessage(error) { return formatErrorMessage(error); } /** Detect provider errors caused by single-use OAuth refresh token races. */ function isRefreshTokenReusedError(error) { const message = normalizeLowercaseStringOrEmpty(extractErrorMessage(error)); return message.includes("refresh_token_reused") || message.includes("refresh token has already been used") || message.includes("already been used to generate a new access token"); } async function refreshOAuthCredential(credential) { const pluginRefreshed = await refreshProviderOAuthCredentialWithPlugin({ provider: credential.provider, context: credential }); if (pluginRefreshed) return pluginRefreshed; if (credential.provider === "chutes") return await refreshChutesTokens({ credential }); const oauthProvider = resolveOAuthProvider(credential.provider); if (!oauthProvider || typeof getOAuthApiKey !== "function") return null; return (await getOAuthApiKey(oauthProvider, { [credential.provider]: credential }))?.newCredentials ?? null; } /** Refresh one OAuth credential and merge provider-returned token fields. */ async function refreshOAuthCredentialForRuntime(params) { const refreshed = await refreshOAuthCredential(params.credential); return refreshed ? { ...params.credential, ...refreshed, type: "oauth" } : null; } const oauthManager = createOAuthManager({ buildApiKey: buildOAuthApiKey, refreshCredential: refreshOAuthCredential, readBootstrapCredential: ({ profileId, credential }) => readManagedExternalCliCredential({ profileId, credential }), readFallbackCredential: ({ profileId, credential }) => credential.provider === "openai" ? readExternalCliFallbackCredential({ profileId, credential, allowKeychainPrompt: false }) : null, isRefreshTokenReusedError }); async function tryResolveOAuthProfile(params) { const { cfg, store, profileId } = params; const cred = store.profiles[profileId]; if (!cred || cred.type !== "oauth") return null; if (!isProfileConfigCompatible({ cfg, profileId, provider: cred.provider, mode: cred.type })) return null; const resolved = await oauthManager.resolveOAuthAccess({ store, profileId, credential: cred, agentDir: params.agentDir, cfg, forceRefresh: params.forceRefresh }); if (!resolved) return null; return buildApiKeyProfileResult({ apiKey: resolved.apiKey, provider: resolved.credential.provider, email: resolved.credential.email ?? cred.email, profileId, profileType: cred.type }); } async function resolveProfileSecretString(params) { let resolvedValue = params.value?.trim(); if (resolvedValue) { const inlineRef = coerceSecretRef(resolvedValue, params.refDefaults); if (inlineRef) try { resolvedValue = await resolveSecretRefString(inlineRef, { config: params.configForRefResolution, env: process.env, cache: params.cache }); } catch (err) { log.debug(params.inlineFailureMessage, { profileId: params.profileId, provider: params.provider, error: formatErrorMessage(err) }); } } const explicitRef = coerceSecretRef(params.valueRef, params.refDefaults); if (!resolvedValue && explicitRef) try { resolvedValue = await resolveSecretRefString(explicitRef, { config: params.configForRefResolution, env: process.env, cache: params.cache }); } catch (err) { log.debug(params.refFailureMessage, { profileId: params.profileId, provider: params.provider, error: formatErrorMessage(err) }); } return normalizeOptionalSecretInput(resolvedValue); } /** Resolve a selected auth profile into the provider API key string. */ async function resolveApiKeyForProfile(params) { const { cfg, store, profileId } = params; const cred = store.profiles[profileId]; if (!cred) return null; if (!isProfileConfigCompatible({ cfg, profileId, provider: cred.provider, mode: cred.type, allowOAuthTokenCompatibility: true })) return null; const refResolveCache = {}; const configForRefResolution = cfg ?? getRuntimeConfig(); const refDefaults = configForRefResolution.secrets?.defaults; assertNoOAuthSecretRefPolicyViolations({ store, cfg: configForRefResolution, profileIds: [profileId], context: `auth profile ${profileId}` }); if (cred.type === "api_key") { const key = await resolveProfileSecretString({ profileId, provider: cred.provider, value: cred.key, valueRef: cred.keyRef, refDefaults, configForRefResolution, cache: refResolveCache, inlineFailureMessage: "failed to resolve inline auth profile api_key ref", refFailureMessage: "failed to resolve auth profile api_key ref" }); if (!key) return null; return buildApiKeyProfileResult({ apiKey: key, provider: cred.provider, email: cred.email, profileId, profileType: cred.type }); } if (cred.type === "token") { const expiryState = resolveTokenExpiryState(cred.expires); if (expiryState === "expired" || expiryState === "invalid_expires") return null; const token = await resolveProfileSecretString({ profileId, provider: cred.provider, value: cred.token, valueRef: cred.tokenRef, refDefaults, configForRefResolution, cache: refResolveCache, inlineFailureMessage: "failed to resolve inline auth profile token ref", refFailureMessage: "failed to resolve auth profile token ref" }); if (!token) return null; return buildApiKeyProfileResult({ apiKey: token, provider: cred.provider, email: cred.email, profileId, profileType: cred.type }); } try { const resolved = await oauthManager.resolveOAuthAccess({ store, agentDir: params.agentDir, profileId, credential: cred, cfg, forceRefresh: params.forceRefresh }); if (!resolved) return null; return buildApiKeyProfileResult({ apiKey: resolved.apiKey, provider: resolved.credential.provider, email: resolved.credential.email ?? cred.email, profileId, profileType: cred.type }); } catch (error) { let refreshedStore = error instanceof OAuthManagerRefreshError ? error.getRefreshedStore() : loadAuthProfileStoreForSecretsRuntime(params.agentDir); const surfacedCause = error instanceof OAuthManagerRefreshError && error.cause ? error.cause : error; const surfacedMessageError = error instanceof OAuthManagerRefreshError && error.code === "refresh_contention" ? error : surfacedCause; if (isRefreshTokenReusedError(surfacedCause)) { const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir({ agentDir: params.agentDir, profileId }); await clearLastGoodProfileWithLock({ provider: cred.provider, profileId, agentDir: ownerAgentDir }); if (params.agentDir !== ownerAgentDir && hasRuntimeAuthProfileStoreSnapshot(params.agentDir)) { const snapshot = getRuntimeAuthProfileStoreSnapshot(params.agentDir); const providerKey = resolveProviderIdForAuth(cred.provider); if (snapshot?.lastGood?.[providerKey] === profileId) { delete snapshot.lastGood[providerKey]; if (Object.keys(snapshot.lastGood).length === 0) snapshot.lastGood = void 0; setRuntimeAuthProfileStoreSnapshot(snapshot, params.agentDir); } } refreshedStore = loadAuthProfileStoreForSecretsRuntime(params.agentDir); } 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, forceRefresh: params.forceRefresh }); if (fallbackResolved) return fallbackResolved; } catch {} const message = extractErrorMessage(surfacedMessageError); const hint = await formatAuthDoctorHint({ cfg, store: refreshedStore, provider: cred.provider, profileId }); throw new OAuthRefreshFailureError({ provider: cred.provider, message: `OAuth token refresh failed for ${cred.provider}: ${message}. Please try again or re-authenticate.` + (hint ? `\n\n${hint}` : ""), cause: error }); } } //#endregion export { formatAuthDoctorHint as i, resolveApiKeyForProfile as n, resolveEffectiveOAuthCredential as r, refreshOAuthCredentialForRuntime as t };