openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
943 lines (942 loc) • 40.1 kB
JavaScript
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 };