openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
221 lines (220 loc) • 11.4 kB
JavaScript
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { c as isRecord } from "./utils-CCC-BEJH.js";
import "./agent-scope-MrLta7Pq.js";
import { a as resolveAgentDir, c as resolveDefaultAgentId, n as listAgentIds, s as resolveDefaultAgentDir } from "./agent-scope-config-CgCYpZfK.js";
import { n as ensureAuthProfileStore } from "./store-C8spD0DG.js";
import { n as hasLocalAuthProfileStoreSource, t as hasAnyAuthProfileStoreSource } from "./source-check-DlvZuh4h.js";
import "./auth-profiles-84rzaGag.js";
import { i as formatAuthDoctorHint, n as resolveApiKeyForProfile } from "./oauth-goXQ01JL.js";
import { n as buildOAuthRefreshFailureLoginCommand, r as classifyOAuthRefreshFailure } from "./oauth-refresh-failure-C54bUOW9.js";
import { o as resolveProfileUnusableUntilForDisplay } from "./usage-K7-k2Oys.js";
import { t as buildProviderAuthRecoveryHint } from "./provider-auth-recovery-hint-Cyy5KXVJ.js";
import { t as note } from "./note-BRSJp0UF.js";
import { n as buildAuthHealthSummary, r as formatRemainingShort, t as DEFAULT_OAUTH_WARN_MS } from "./auth-health-4xis_1sG.js";
import path from "node:path";
//#region src/commands/doctor-auth.ts
/** Doctor notes for auth profile health, OAuth refresh failures, and legacy Codex config. */
const OPENAI_PROVIDER_ID = "openai";
const LEGACY_CODEX_PROVIDER_ID = "openai-codex";
const CODEX_OAUTH_WARNING_TITLE = "Codex OAuth";
const OPENAI_BASE_URL = "https://api.openai.com/v1";
const LEGACY_CODEX_APIS = new Set(["openai-responses", "openai-completions"]);
const DOCTOR_REAUTH_PROVIDER_ALIASES = { [LEGACY_CODEX_PROVIDER_ID]: OPENAI_PROVIDER_ID };
function hasConfiguredCodexOAuthProfile(cfg) {
return Object.values(cfg.auth?.profiles ?? {}).some((profile) => (profile.provider === OPENAI_PROVIDER_ID || profile.provider === LEGACY_CODEX_PROVIDER_ID) && profile.mode === "oauth");
}
function hasStoredCodexOAuthProfile() {
const store = ensureAuthProfileStore(void 0, { allowKeychainPrompt: false });
return Object.values(store.profiles).some((profile) => (profile.provider === OPENAI_PROVIDER_ID || profile.provider === LEGACY_CODEX_PROVIDER_ID) && profile.type === "oauth");
}
function normalizeCodexOverrideBaseUrl(baseUrl) {
if (typeof baseUrl !== "string") return;
return baseUrl.trim().replace(/\/+$/, "");
}
function isLegacyCodexTransportShape(value, inheritedBaseUrl) {
if (!isRecord(value)) return false;
const api = typeof value.api === "string" ? value.api : void 0;
if (!api || !LEGACY_CODEX_APIS.has(api)) return false;
const baseUrl = normalizeCodexOverrideBaseUrl(value.baseUrl ?? inheritedBaseUrl);
return !baseUrl || baseUrl === OPENAI_BASE_URL;
}
function hasLegacyCodexTransportOverride(providerOverride) {
if (!isRecord(providerOverride)) return false;
if (isLegacyCodexTransportShape(providerOverride)) return true;
const models = providerOverride.models;
if (!Array.isArray(models)) return false;
return models.some((model) => isLegacyCodexTransportShape(model, providerOverride.baseUrl));
}
function buildCodexProviderOverrideWarning(providerOverride) {
const lines = [`- models.providers.${LEGACY_CODEX_PROVIDER_ID} contains a legacy transport override while Codex OAuth is configured.`, "- Older OpenAI transport settings can shadow the built-in Codex OAuth provider path."];
if (isRecord(providerOverride)) {
const record = providerOverride;
if (typeof record.api === "string") lines.push(`- models.providers.${LEGACY_CODEX_PROVIDER_ID}.api=${record.api}`);
if (typeof record.baseUrl === "string") lines.push(`- models.providers.${LEGACY_CODEX_PROVIDER_ID}.baseUrl=${record.baseUrl}`);
}
lines.push(`- Remove or rewrite the legacy transport override to restore the built-in Codex OAuth provider path after recent fixes.`);
lines.push("- Custom proxies and header-only overrides can stay; this warning only targets old OpenAI transport settings.");
return lines.join("\n");
}
/** Emits a warning when legacy Codex transport overrides can shadow configured Codex OAuth. */
function noteLegacyCodexProviderOverride(cfg) {
const providerOverride = cfg.models?.providers?.[LEGACY_CODEX_PROVIDER_ID];
if (!providerOverride) return;
if (!hasLegacyCodexTransportOverride(providerOverride)) return;
if (!hasConfiguredCodexOAuthProfile(cfg) && !hasStoredCodexOAuthProfile()) return;
note(buildCodexProviderOverrideWarning(providerOverride), CODEX_OAUTH_WARNING_TITLE);
}
function formatAgentNoteTitle(title, agentId, labelAgents) {
return labelAgents ? `${title} (agent: ${agentId})` : title;
}
function listAuthProfileHealthTargets(cfg) {
const defaultAgentId = resolveDefaultAgentId(cfg);
const targets = /* @__PURE__ */ new Map();
const addTarget = (agentId, agentDir, isDefault) => {
const key = path.resolve(agentDir);
const existing = targets.get(key);
if (!existing || isDefault) targets.set(key, {
agentId,
agentDir,
isDefault: isDefault || existing?.isDefault === true
});
};
addTarget(defaultAgentId, resolveDefaultAgentDir(cfg), true);
for (const agentId of listAgentIds(cfg)) {
if (agentId === defaultAgentId) continue;
const agentDir = resolveAgentDir(cfg, agentId);
if (hasLocalAuthProfileStoreSource(agentDir)) addTarget(agentId, agentDir, false);
}
return [...targets.values()];
}
/** Returns the short doctor hint for disabled or cooldown auth profiles. */
function resolveUnusableProfileHint(params) {
if (params.kind === "disabled") {
if (params.reason === "billing") return "Top up credits (provider billing) or switch provider.";
if (params.reason === "auth_permanent" || params.reason === "auth") return "Refresh or replace credentials, then retry.";
}
return "Wait for cooldown or switch provider.";
}
function formatOAuthRefreshFailureReason(reason) {
switch (reason) {
case "refresh_token_reused": return "refresh_token_reused";
case "invalid_grant": return "invalid_grant";
case "sign_in_again": return "sign in again";
case "invalid_refresh_token": return "invalid refresh token";
case "revoked": return "revoked";
default: return "refresh failed";
}
}
/** Formats provider OAuth refresh failures as actionable doctor note lines. */
function formatOAuthRefreshFailureDoctorLine(params) {
const classified = classifyOAuthRefreshFailure(params.message);
if (!classified) return null;
const rawProvider = classified.provider ?? params.provider;
const command = buildOAuthRefreshFailureLoginCommand(rawProvider ? DOCTOR_REAUTH_PROVIDER_ALIASES[rawProvider] ?? rawProvider : null);
if (classified.reason) return `- ${params.profileId}: re-auth required [${formatOAuthRefreshFailureReason(classified.reason)}] — Run \`${command}\`.`;
return `- ${params.profileId}: OAuth refresh failed — Try again; if this persists, run \`${command}\`.`;
}
async function resolveAuthIssueHint(issue, cfg, store) {
if (issue.reasonCode === "invalid_expires") return "Invalid token expires metadata. Set a future Unix ms timestamp or remove expires.";
const providerHint = await formatAuthDoctorHint({
cfg,
store,
provider: issue.provider,
profileId: issue.profileId
});
if (providerHint.trim()) return providerHint;
return buildProviderAuthRecoveryHint({ provider: issue.provider }).replace(/^Run /, "Re-auth via ");
}
async function formatAuthIssueLine(issue, cfg, store) {
const remaining = issue.remainingMs !== void 0 ? ` (${formatRemainingShort(issue.remainingMs)})` : "";
const hint = await resolveAuthIssueHint(issue, cfg, store);
const reason = issue.reasonCode ? ` [${issue.reasonCode}]` : "";
return `- ${issue.profileId}: ${issue.status}${reason}${remaining}${hint ? ` — ${hint}` : ""}`;
}
async function noteAuthProfileHealthForTarget(params) {
const store = ensureAuthProfileStore(params.target.agentDir, { allowKeychainPrompt: params.allowKeychainPrompt });
const noteTitle = (title) => formatAgentNoteTitle(title, params.target.agentId, params.labelAgents);
const unusable = (() => {
const now = Date.now();
const out = [];
for (const profileId of Object.keys(store.usageStats ?? {})) {
const until = resolveProfileUnusableUntilForDisplay(store, profileId);
if (!until || now >= until) continue;
const stats = store.usageStats?.[profileId];
const remaining = formatRemainingShort(until - now);
const disabledActive = typeof stats?.disabledUntil === "number" && now < stats.disabledUntil;
const kind = disabledActive ? `disabled${stats.disabledReason ? `:${stats.disabledReason}` : ""}` : "cooldown";
const hint = resolveUnusableProfileHint({
kind: disabledActive ? "disabled" : "cooldown",
reason: stats?.disabledReason
});
out.push(`- ${profileId}: ${kind} (${remaining})${hint ? ` — ${hint}` : ""}`);
}
return out;
})();
if (unusable.length > 0) note(unusable.join("\n"), noteTitle("Auth profile cooldowns"));
let summary = buildAuthHealthSummary({
store,
cfg: params.cfg,
warnAfterMs: DEFAULT_OAUTH_WARN_MS,
allowKeychainPrompt: params.allowKeychainPrompt
});
const findIssues = () => summary.profiles.filter((profile) => (profile.type === "oauth" || profile.type === "token") && (profile.status === "expired" || profile.status === "expiring" || profile.status === "missing"));
let issues = findIssues();
if (issues.length === 0) return;
if (await params.prompter.confirmAutoFix({
message: "Refresh expiring OAuth tokens now? (static tokens need re-auth)",
initialValue: true
})) {
const refreshTargets = issues.filter((issue) => issue.type === "oauth" && [
"expired",
"expiring",
"missing"
].includes(issue.status));
const errors = [];
for (const profile of refreshTargets) try {
await resolveApiKeyForProfile({
cfg: params.cfg,
store,
profileId: profile.profileId,
agentDir: params.target.agentDir
});
} catch (err) {
const message = formatErrorMessage(err);
errors.push(formatOAuthRefreshFailureDoctorLine({
profileId: profile.profileId,
provider: profile.provider,
message
}) ?? `- ${profile.profileId}: ${message}`);
}
if (errors.length > 0) note(errors.join("\n"), noteTitle("OAuth refresh errors"));
summary = buildAuthHealthSummary({
store: ensureAuthProfileStore(params.target.agentDir, { allowKeychainPrompt: false }),
cfg: params.cfg,
warnAfterMs: DEFAULT_OAUTH_WARN_MS,
allowKeychainPrompt: false
});
issues = findIssues();
}
if (issues.length > 0) note((await Promise.all(issues.map((issue) => formatAuthIssueLine({
profileId: issue.profileId,
provider: issue.provider,
status: issue.status,
reasonCode: issue.reasonCode,
remainingMs: issue.remainingMs
}, params.cfg, store)))).join("\n"), noteTitle("Model auth"));
}
/** Checks configured agent auth stores and emits doctor notes for stale or unusable profiles. */
async function noteAuthProfileHealth(params) {
const configuredProfiles = Object.keys(params.cfg.auth?.profiles ?? {}).length > 0;
const activeTargets = listAuthProfileHealthTargets(params.cfg).filter((target) => target.isDefault ? hasAnyAuthProfileStoreSource(target.agentDir) || configuredProfiles : hasLocalAuthProfileStoreSource(target.agentDir));
if (activeTargets.length === 0) return;
const labelAgents = activeTargets.length > 1;
for (const target of activeTargets) await noteAuthProfileHealthForTarget({
...params,
target,
labelAgents
});
}
//#endregion
export { noteAuthProfileHealth, noteLegacyCodexProviderOverride };