UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

943 lines (942 loc) 40.1 kB
import { o as asDateTimestampMs } from "./number-coercion-CLj0HTDM.js"; import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js"; import { p as redactSensitiveText } from "./redact-BtvPPfTi.js"; import { r as normalizeProviderId } from "./provider-id-DMd-TDFp.js"; import { h as normalizeSecretInputString, s as coerceSecretRef } from "./types.secrets-kC0nOetj.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { f as secretRefKey } from "./ref-contract-D92DqQ-r.js"; import { n as resolveProviderIdForAuth } from "./provider-auth-aliases-DhA9c2am.js"; import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js"; import { s as withFileLock } from "./file-lock-B0wiaenm.js"; import "./file-lock-DPooFrLa.js"; import "./config-Cs0XXL3x.js"; import { a as getOAuthApiKey, o as getOAuthProviders, r as OAuthProviderConfiguredUnavailableError } from "./paths-DLuhw03H.js"; import { n as buildProviderAuthDoctorHintWithPlugin, r as formatProviderAuthProfileApiKeyWithPlugin, s as resolveProviderOAuthCredentialWithPlugin } from "./provider-runtime.runtime.js"; import { a as resolveOAuthRefreshLockPath, s as resolveSharedAuthStorePath } from "./path-resolve-oRkRBkQd.js"; import { h as resolveAuthProfileDatabasePath } from "./sqlite-MN_7y26V.js"; import { t as resolveAuthProfileSecretOwnerId } from "./runtime-auth-profile-owner-DA1NOJfL.js"; import { c as findActiveDegradedSecretOwner, n as SecretSurfaceUnavailableError } from "./runtime-degraded-state-D5EZZ925.js"; import { i as isUserModelAuthProfileId } from "./profile-usage-stats-dkZh2v8y.js"; import { t as normalizeOptionalSecretInput } from "./normalize-secret-input-Df_qhWv_.js"; import { C as isSafeToAdoptBootstrapOAuthIdentity, E as shouldBootstrapFromExternalCliCredential, I as OAUTH_REFRESH_CALL_TIMEOUT_MS, L as OAUTH_REFRESH_LOCK_OPTIONS, M as CLAUDE_CLI_PROFILE_ID, O as shouldReplaceStoredOAuthCredential, S as hasMatchingOAuthIdentity, j as shouldMirrorRefreshedOAuthCredential, w as isSafeToAdoptMainStoreOAuthIdentity, x as areOAuthCredentialsEquivalent, z as authProfilesLog } from "./persisted-B_qhhBlh.js"; import { a as resolveTokenExpiryState, n as evaluateStoredCredentialEligibility, r as hasUsableOAuthCredential } from "./credential-state-N1MIGw99.js"; import { o as readExternalCliBootstrapCredential } from "./external-auth-D5zyqyNH.js"; import { c as readProviderOAuthRefreshFailure, t as OAuthRefreshFailureError } from "./oauth-refresh-failure-DP-bO7C0.js"; import { c as getRuntimeAuthProfileStoreSnapshotCore, p as hasRuntimeAuthProfileStoreSnapshot, w as updateRuntimeAuthProfileStoreSnapshot } from "./runtime-snapshots-CHAErv1O.js"; import { S as updateAuthProfileStoreWithLock, a as ensureAuthProfileStoreWithoutExternalProfiles, g as resolvePersistedAuthProfileOwnerAgentDir, m as loadAuthProfileStoreWithoutExternalProfiles, o as findPersistedAuthProfileCredential, p as loadAuthProfileStoreForSecretsRuntime } from "./store-F1B2duCT.js"; import { t as assertNoOAuthSecretRefPolicyViolations } from "./policy-DzU6nQxS.js"; import { t as clearLastGoodProfileWithLock } from "./profiles-DOTqXcYA.js"; import { n as suggestOAuthProfileIdForLegacyDefault } from "./repair-DXThsgJF.js"; import { isDeepStrictEqual } from "node:util"; //#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 Qwen API key: openclaw onboard --auth-choice qwen-api-key."; function hasLegacyQwenPortalOAuthProfile(store, profileId) { return (profileId ? [store.profiles[profileId]] : Object.values(store.profiles)).some((profile) => profile?.type === "oauth" && normalizeProviderId(profile.provider) === "qwen-portal"); } async function formatAuthDoctorHintWithPluginBuilder(params, buildPluginHint) { const normalizedProvider = normalizeProviderId(params.provider); if (normalizedProvider === "qwen-portal" && hasLegacyQwenPortalOAuthProfile(params.store, params.profileId)) return QWEN_PORTAL_OAUTH_MIGRATION_HINT; const pluginHint = await buildPluginHint({ provider: normalizedProvider, context: { config: params.cfg, store: params.store, provider: normalizedProvider, profileId: params.profileId } }); if (typeof pluginHint === "string" && pluginHint.trim()) return pluginHint; return ""; } /** Formats provider-specific auth doctor guidance for a profile/store. */ async function formatAuthDoctorHint(params) { return await formatAuthDoctorHintWithPluginBuilder(params, buildProviderAuthDoctorHintWithPlugin); } //#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 surfacedCause = structuredCause?.code === "refresh_contention" && params.cause instanceof Error ? new Error(params.cause.message) : params.cause; const storedCredential = params.refreshedStore.profiles[params.profileId]; const secrets = collectOAuthCredentialSecrets(params.credential, ...params.attemptedCredentials ?? [], storedCredential?.type === "oauth" ? storedCredential : void 0); const presentation = readProviderOAuthRefreshFailure(params.cause); const causeMessage = formatRedactedOAuthRefreshError(surfacedCause, secrets); super({ provider: params.credential.provider, profileId: params.profileId, message: `OAuth token refresh failed for ${params.credential.provider}: ${causeMessage}`, cause: createRedactedOAuthRefreshCause(surfacedCause, secrets), errorType: presentation?.errorType, reason: presentation?.reason, status: presentation?.status, summary: presentation?.summary ? formatRedactedOAuthRefreshError(presentation.summary, secrets) : void 0 }); 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 = /* @__PURE__ */ 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, profileId) { return loadAuthProfileStoreWithoutExternalProfiles(agentDir, { allowKeychainPrompt: true, profileId }); } async function loadFreshStoredOAuthCredential(params) { const reloaded = loadStoredOAuthRefreshStore(params.agentDir, params.profileId).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 resolveEffectiveOAuthCredentialCore(params) { if (isUserModelAuthProfileId(params.profileId)) return params.credential; const imported = params.readBootstrapCredential({ store: params.store, profileId: params.profileId, credential: params.credential }); if (!imported) return params.credential; if (hasUsableOAuthCredential(params.credential)) { authProfilesLog.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)) { authProfilesLog.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 })) { authProfilesLog.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 || isUserModelAuthProfileId(params.profileId)) 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 }; authProfilesLog.info("adopted newer OAuth credentials from main agent", { profileId: params.profileId, agentDir: params.agentDir, expires: new Date(mainCred.expires).toISOString() }); return mainCred; } } catch (err) { authProfilesLog.debug("adoptNewerMainOAuthCredential failed", { profileId: params.profileId, error: formatErrorMessage(err) }); } return null; } let refreshQueue = new KeyedAsyncQueue(); 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") authProfilesLog.warn("refused to mirror OAuth credential: identity mismatch or regression", { profileId: params.profileId }); return false; } store.profiles[params.profileId] = { ...params.refreshed }; authProfilesLog.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) { authProfilesLog.debug("mirrorRefreshedCredentialIntoMainStore failed", { profileId: params.profileId, error: formatErrorMessage(err) }); } } async function saveOAuthCredentialWithStoreLock(params) { let saved = false; return await updateAuthProfileStoreWithLock({ agentDir: params.agentDir, profileId: params.profileId, 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))) { authProfilesLog.debug("skipped OAuth credential write because stored profile changed", { profileId: params.profileId }); return false; } if (!isSafeToAdoptBootstrapOAuthIdentity(existing, params.credential) || !shouldReplaceStoredOAuthCredential(existing, params.credential)) { authProfilesLog.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 resolveOAuthCredentialAfterPersistMiss(params) { let adopted = null; return await updateAuthProfileStoreWithLock({ agentDir: params.agentDir, profileId: params.profileId, updater: (store) => { const existing = store.profiles[params.profileId]; if (existing?.type !== "oauth" || existing.provider !== params.refreshed.provider) return false; if (!isUserModelAuthProfileId(params.profileId) && hasMatchingOAuthIdentity(existing, params.refreshed)) { store.profiles[params.profileId] = { ...params.refreshed }; adopted = params.refreshed; return true; } adopted = hasUsableOAuthCredential(existing) ? existing : null; return false; } }) === null ? null : adopted; } async function doRefreshOAuthTokenWithLock(params) { const personalProfile = isUserModelAuthProfileId(params.profileId); const ownerAgentDir = personalProfile ? void 0 : resolvePersistedAuthProfileOwnerAgentDir(params); const authPath = ownerAgentDir ? resolveAuthProfileDatabasePath(ownerAgentDir) : resolveSharedAuthStorePath(); const globalRefreshLockPath = resolveOAuthRefreshLockPath(params.provider, params.profileId); try { return await withFileLock(globalRefreshLockPath, OAUTH_REFRESH_LOCK_OPTIONS, async () => { const store = loadStoredOAuthRefreshStore(ownerAgentDir, params.profileId); 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 && !personalProfile) 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 }; authProfilesLog.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)) authProfilesLog.warn("refused to adopt fresh main-store OAuth credential: identity mismatch", { profileId: params.profileId, agentDir: params.agentDir }); } catch (err) { authProfilesLog.debug("inside-lock main-store adoption failed; proceeding to refresh", { profileId: params.profileId, error: formatErrorMessage(err) }); } const externallyManaged = personalProfile ? null : adapter.readBootstrapCredential({ store, profileId: params.profileId, credential: cred }); if (externallyManaged) { if (externallyManaged.provider !== cred.provider) authProfilesLog.warn("refused external oauth bootstrap credential: provider mismatch", { profileId: params.profileId, provider: cred.provider }); else if (!isSafeToAdoptBootstrapOAuthIdentity(cred, externallyManaged)) authProfilesLog.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, { cfg: params.cfg, agentDir: params.agentDir }); 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 })) { const recovered = await resolveOAuthCredentialAfterPersistMiss({ agentDir: ownerAgentDir, profileId: params.profileId, refreshed: refreshedCredentials }); if (!recovered) throw new Error("Failed to persist refreshed OAuth credential"); if (recovered !== refreshedCredentials) return { apiKey: await adapter.buildApiKey(recovered.provider, recovered, { cfg: params.cfg, agentDir: params.agentDir }), credential: recovered }; } if (ownerAgentDir) { if (resolveSharedAuthStorePath() !== 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); return await refreshQueue.enqueue(key, () => doRefreshOAuthTokenWithLock(params)); } async function resolveOAuthAccess(params) { const personalProfile = isUserModelAuthProfileId(params.profileId); let credential = params.credential; if (personalProfile) { const owned = loadStoredOAuthRefreshStore(params.agentDir, params.profileId).profiles[params.profileId]; if (owned?.type !== "oauth") return null; credential = owned; } const adoptedCredential = adoptNewerMainOAuthCredential({ store: params.store, profileId: params.profileId, agentDir: params.agentDir, credential }) ?? credential; const effectiveCredential = resolveEffectiveOAuthCredentialCore({ store: params.store, 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: credential.provider, agentDir: params.agentDir, cfg: params.cfg, forceRefresh: params.forceRefresh, attemptedCredentials }); } catch (error) { const refreshedStore = loadStoredOAuthRefreshStore(params.agentDir, params.profileId); 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 === credential.provider && hasOAuthCredentialChanged(credential, refreshed)) { const recovered = await loadFreshStoredOAuthCredential({ profileId: params.profileId, agentDir: params.agentDir, provider: 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: credential.provider, agentDir: params.agentDir, cfg: params.cfg, forceRefresh: params.forceRefresh, attemptedCredentials }); if (retried) return retried; } catch {} } if (params.agentDir && !personalProfile) try { const mainCred = ensureAuthProfileStoreWithoutExternalProfiles(void 0, { allowKeychainPrompt: false }).profiles[params.profileId]; if (mainCred?.type === "oauth" && mainCred.provider === credential.provider && hasUsableOAuthCredential(mainCred) && canReuseOAuthCredentialAfterRefreshFailure({ forceRefresh: params.forceRefresh, attempted: effectiveCredential, candidate: mainCred }) && isSafeToAdoptMainStoreOAuthIdentity(credential, mainCred)) { refreshedStore.profiles[params.profileId] = { ...mainCred }; authProfilesLog.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 {} throw new OAuthManagerRefreshError({ credential, attemptedCredentials: [effectiveCredential, ...attemptedCredentials], profileId: params.profileId, refreshedStore, cause: error }); } } function resetRefreshQueuesForTest() { refreshQueue = new KeyedAsyncQueue(); } 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 = /* @__PURE__ */ 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 }, credential: { value: params.credential, 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, context = {}) { const pluginResult = await resolveProviderOAuthCredentialWithPlugin({ provider: credential.provider, config: context.cfg, credential, refresh: true }); if (pluginResult.status === "available") return pluginResult.credential; if (pluginResult.status === "configured-unavailable") throw new OAuthProviderConfiguredUnavailableError(credential.provider); 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, { cfg: params.cfg }); return refreshed ? { ...params.credential, ...refreshed, type: "oauth" } : null; } const oauthManager = createOAuthManager({ buildApiKey: buildOAuthApiKey, refreshCredential: refreshOAuthCredential, readBootstrapCredential: ({ store, profileId, credential }) => readExternalCliBootstrapCredential({ store, profileId, credential }), isRefreshTokenReusedError }); /** Clear in-process OAuth refresh queues between isolated tests. */ function resetOAuthRefreshQueuesForTest() { oauthManager.resetRefreshQueuesForTest(); } if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.oauthTestApi")] = { isRefreshTokenReusedError, resetOAuthRefreshQueuesForTest }; async function tryResolveOAuthProfile(params) { const { cfg, store, profileId } = params; if (isRetiredOAuthProfileId(profileId)) return null; 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, credential: resolved.credential }); } function isRetiredOAuthProfileId(profileId) { return profileId === CLAUDE_CLI_PROFILE_ID; } function authProfileSecretRefKey(profile, defaults) { const ref = profile.type === "api_key" ? coerceSecretRef(profile.keyRef, defaults) ?? coerceSecretRef(profile.key, defaults) : profile.type === "token" ? coerceSecretRef(profile.tokenRef, defaults) ?? coerceSecretRef(profile.token, defaults) : null; return ref ? secretRefKey(ref) : void 0; } function resolveRuntimeAuthProfile(params) { const runtimeProfile = getRuntimeAuthProfileStoreSnapshotCore(params.agentDir)?.profiles[params.profileId]; const inputRefKey = authProfileSecretRefKey(params.profile, params.defaults); const runtimeRefKey = runtimeProfile ? authProfileSecretRefKey(runtimeProfile, params.defaults) : void 0; const published = Boolean(runtimeProfile && (isDeepStrictEqual(runtimeProfile, params.profile) || inputRefKey && runtimeRefKey === inputRefKey && runtimeProfile.type === params.profile.type && runtimeProfile.provider === params.profile.provider)); let profile = params.profile; if (published && runtimeProfile?.type === "api_key" && params.profile.type === "api_key") { const value = runtimeProfile.key; profile = { ...params.profile, key: value }; } else if (published && runtimeProfile?.type === "token" && params.profile.type === "token") { const value = runtimeProfile.token; profile = { ...params.profile, token: value }; } return { profile, published }; } function assertRuntimeAuthProfileSecretOwnerAvailable(params) { const degraded = findActiveDegradedSecretOwner("account", resolveAuthProfileSecretOwnerId(params)); if (degraded && params.published) throw new SecretSurfaceUnavailableError(degraded); } function throwUnmaterializedAuthProfileSecretRef(params) { throw new SecretSurfaceUnavailableError({ ownerKind: "account", ownerId: resolveAuthProfileSecretOwnerId(params), state: "unavailable", paths: [`auth-profiles.${params.profileId}.${params.pathSuffix}`], refKeys: [secretRefKey(params.ref)], reason: "secret reference was not materialized by the active runtime" }); } /** Resolve a selected auth profile into the provider API key string. */ async function resolveApiKeyForProfile(params) { const { cfg, store, profileId } = params; const storedProfile = isUserModelAuthProfileId(profileId) ? findPersistedAuthProfileCredential({ agentDir: params.agentDir, profileId }) : store.profiles[profileId]; if (!storedProfile) return null; if (isRetiredOAuthProfileId(profileId)) return null; const configForRefResolution = cfg ?? getRuntimeConfig(); const refDefaults = configForRefResolution.secrets?.defaults; const runtimeProfile = resolveRuntimeAuthProfile({ agentDir: params.agentDir, profileId, profile: storedProfile, defaults: refDefaults }); const cred = runtimeProfile.profile; if (!isProfileConfigCompatible({ cfg, profileId, provider: cred.provider, mode: cred.type, allowOAuthTokenCompatibility: true })) return null; assertNoOAuthSecretRefPolicyViolations({ store, cfg: configForRefResolution, profileIds: [profileId], context: `auth profile ${profileId}` }); if (cred.type === "api_key") { if (!evaluateStoredCredentialEligibility({ credential: cred }).eligible) return null; assertRuntimeAuthProfileSecretOwnerAvailable({ agentDir: params.agentDir, profileId, published: runtimeProfile.published }); const keyRef = coerceSecretRef(cred.keyRef, refDefaults) ?? coerceSecretRef(cred.key, refDefaults); const key = normalizeOptionalSecretInput(cred.key); if (keyRef && (!runtimeProfile.published || !key)) throwUnmaterializedAuthProfileSecretRef({ agentDir: params.agentDir, profileId, pathSuffix: "key", ref: keyRef }); 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; assertRuntimeAuthProfileSecretOwnerAvailable({ agentDir: params.agentDir, profileId, published: runtimeProfile.published }); const tokenRef = coerceSecretRef(cred.tokenRef, refDefaults) ?? coerceSecretRef(cred.token, refDefaults); const token = normalizeOptionalSecretInput(cred.token); if (tokenRef && (!runtimeProfile.published || !token)) throwUnmaterializedAuthProfileSecretRef({ agentDir: params.agentDir, profileId, pathSuffix: "token", ref: tokenRef }); 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, credential: resolved.credential }); } catch (error) { let refreshedStore = error instanceof OAuthManagerRefreshError ? error.getRefreshedStore() : loadAuthProfileStoreForSecretsRuntime(params.agentDir, { profileId }); const surfacedCause = error instanceof OAuthManagerRefreshError && error.cause ? error.cause : error; if (isRefreshTokenReusedError(surfacedCause)) { const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir({ agentDir: params.agentDir, profileId }); let clearedLastGood = false; try { await clearLastGoodProfileWithLock({ provider: cred.provider, profileId, agentDir: ownerAgentDir }); clearedLastGood = true; } catch (cleanupError) { authProfilesLog.warn("failed to clear stale OAuth last-good state after refresh failure", { error: formatErrorMessage(cleanupError) }); } if (params.agentDir !== ownerAgentDir && hasRuntimeAuthProfileStoreSnapshot(params.agentDir)) { const snapshot = getRuntimeAuthProfileStoreSnapshotCore(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; updateRuntimeAuthProfileStoreSnapshot(snapshot, params.agentDir); } } if (clearedLastGood) refreshedStore = loadAuthProfileStoreForSecretsRuntime(params.agentDir, { profileId }); } const fallbackProfileId = params.allowProfileFallback === false ? null : 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(surfacedCause); const hint = await formatAuthDoctorHint({ cfg, store: refreshedStore, provider: cred.provider, profileId }); throw new OAuthRefreshFailureError({ provider: cred.provider, profileId, 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, resolveEffectiveOAuthCredentialCore as r, refreshOAuthCredentialForRuntime as t };