openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
169 lines (168 loc) • 10.2 kB
JavaScript
import { t as sanitizeForLog } from "./ansi-DrXAcdMD.js";
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js";
import { i as normalizeBoundedOptionalString } from "./string-coerce-CIXf7egm.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { r as normalizeProviderId } from "./provider-id-DMd-TDFp.js";
import { n as formatInlineCodeSpan } from "./markdown-code-Buzx6wvi.js";
//#region src/agents/auth-profiles/oauth-refresh-failure.ts
/**
* OAuth refresh failure classification and operator hints.
* Parses provider/reason codes from refresh failures and formats safe login
* commands without trusting raw provider text.
*/
const OAUTH_REFRESH_FAILURE_ERROR_TYPE_MAX_CHARS = 100;
const OAUTH_REFRESH_FAILURE_SUMMARY_MAX_CHARS = 500;
function readProviderOAuthRefreshFailure(error) {
const presentation = asOptionalRecord(asOptionalRecord(error)?.oauthRefreshFailure);
if (!presentation) return null;
const summary = normalizeBoundedOptionalString(presentation.summary, OAUTH_REFRESH_FAILURE_SUMMARY_MAX_CHARS);
const errorType = normalizeBoundedOptionalString(presentation.errorType, OAUTH_REFRESH_FAILURE_ERROR_TYPE_MAX_CHARS);
const reason = typeof presentation.reason === "string" ? classifyOAuthRefreshFailureReason(presentation.reason) : null;
const status = typeof presentation.status === "number" && Number.isInteger(presentation.status) && presentation.status >= 100 && presentation.status <= 599 ? presentation.status : void 0;
if (!summary && !errorType && !reason && !status) return null;
return {
...errorType ? { errorType } : {},
...reason ? { reason } : {},
...status ? { status } : {},
...summary ? { summary } : {}
};
}
/** Error type that carries provider and classified OAuth refresh failure reason. */
var OAuthRefreshFailureError = class OAuthRefreshFailureError extends Error {
constructor(params) {
super(params.message, { cause: params.cause });
const inherited = params.cause instanceof OAuthRefreshFailureError ? params.cause : readProviderOAuthRefreshFailure(params.cause);
this.name = "OAuthRefreshFailureError";
this.errorType = normalizeBoundedOptionalString(params.errorType ?? inherited?.errorType, OAUTH_REFRESH_FAILURE_ERROR_TYPE_MAX_CHARS);
this.provider = params.provider;
this.profileId = params.profileId;
this.reason = params.reason !== void 0 ? params.reason : inherited?.reason ?? classifyOAuthRefreshFailureReason(params.message);
this.status = params.status ?? inherited?.status;
this.summary = normalizeBoundedOptionalString(params.summary ?? inherited?.summary, OAUTH_REFRESH_FAILURE_SUMMARY_MAX_CHARS);
}
};
const OAUTH_REFRESH_FAILURE_PROVIDER_RE = /OAuth token refresh failed for ([^:]+):/i;
const SAFE_PROVIDER_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
const CLAUDE_CLI_AUTH_FAILURE_RE = /\bclaude-cli\b.+?\b(failed to authenticate|401\s+invalid authentication credentials)\b/is;
function isClaudeCliExpiredOAuthMessage(message) {
return CLAUDE_CLI_AUTH_FAILURE_RE.test(message);
}
function readStructuredClaudeCliAuthFailure(err) {
if (!err || typeof err !== "object") return null;
const candidate = err;
if (candidate.name !== "FailoverError" || candidate.provider !== "claude-cli" || candidate.reason !== "auth" || candidate.status !== 401) return null;
return candidate;
}
function classifyStructuredClaudeCliOAuthFailureReason(err) {
const failure = readStructuredClaudeCliAuthFailure(err);
if (!failure) return null;
const rawError = typeof failure.rawError === "string" ? failure.rawError : "";
const combined = `${err instanceof Error ? err.message : ""}\n${rawError}`;
const lower = combined.toLowerCase();
if (/\bnot logged in\b\s*·\s*please run \/login\b/i.test(combined)) return "sign_in_again";
return lower.includes("failed to authenticate") || lower.includes("invalid authentication credentials") ? "revoked" : null;
}
function isOAuthRefreshFailureMessage(message) {
const lower = message.toLowerCase();
return lower.includes("oauth token refresh failed") || lower.includes("access token could not be refreshed") || lower.includes("authentication session could not be refreshed automatically") || isClaudeCliExpiredOAuthMessage(message);
}
function extractOAuthRefreshFailureProvider(message) {
if (isClaudeCliExpiredOAuthMessage(message)) return "claude-cli";
const provider = message.match(OAUTH_REFRESH_FAILURE_PROVIDER_RE)?.[1]?.trim();
return provider && provider.length > 0 ? provider : null;
}
function sanitizeOAuthRefreshFailureProvider(provider) {
const sanitized = provider ? sanitizeForLog(provider).replaceAll("`", "").trim() : "";
const normalized = normalizeProviderId(sanitized);
return normalized && SAFE_PROVIDER_ID_RE.test(normalized) ? normalized : null;
}
function sanitizeOAuthRefreshFailureProfileId(profileId) {
return (profileId ? sanitizeForLog(profileId).trim() : "") || null;
}
function quoteShellArg(value) {
return `'${process.platform === "win32" ? value.replaceAll("'", "''") : value.replaceAll("'", "'\\''")}'`;
}
/** Wrap a rendered login command in a Markdown code span that survives embedded backticks. */
function formatOAuthRefreshFailureLoginCommandMarkdown(command) {
return formatInlineCodeSpan(command);
}
/** Classify a raw OAuth refresh failure message into a stable reason code. */
function classifyOAuthRefreshFailureReason(message) {
const lower = message.toLowerCase();
if (lower.includes("refresh_token_reused")) return "refresh_token_reused";
if (lower.includes("refresh_token_expired")) return "expired";
if (lower.includes("invalid_grant")) return "invalid_grant";
if (lower.includes("token_invalidated")) return "token_invalidated";
if (lower.includes("sign_in_again") || lower.includes("signing in again") || lower.includes("sign in again") || lower.includes("log in again")) return "sign_in_again";
if (lower.includes("invalid_refresh_token") || lower.includes("invalid refresh token")) return "invalid_refresh_token";
if (lower.includes("expired or revoked") || lower.includes("revoked")) return "revoked";
if (isClaudeCliExpiredOAuthMessage(message)) return "revoked";
return null;
}
/** Classify provider/reason from a user-facing OAuth refresh failure message. */
function classifyOAuthRefreshFailure(message) {
if (!isOAuthRefreshFailureMessage(message)) return null;
return {
provider: sanitizeOAuthRefreshFailureProvider(extractOAuthRefreshFailureProvider(message)),
reason: classifyOAuthRefreshFailureReason(message)
};
}
/** Classify provider/reason from the structured OAuth refresh failure error. */
function classifyOAuthRefreshFailureError(err) {
const seen = /* @__PURE__ */ new Set();
let rawFallback = null;
let candidate = err;
while (candidate && typeof candidate === "object") {
const claudeCliReason = classifyStructuredClaudeCliOAuthFailureReason(candidate);
if (claudeCliReason) return {
provider: "claude-cli",
reason: claudeCliReason
};
if (candidate instanceof OAuthRefreshFailureError) {
const profileId = sanitizeOAuthRefreshFailureProfileId(candidate.profileId);
return {
...candidate.errorType ? { errorType: candidate.errorType } : {},
provider: sanitizeOAuthRefreshFailureProvider(candidate.provider),
...profileId ? { profileId } : {},
reason: candidate.reason,
...candidate.status ? { status: candidate.status } : {},
...candidate.summary ? { summary: candidate.summary } : {}
};
}
const record = asOptionalRecord(candidate);
const rawError = record?.rawError;
if (typeof rawError === "string") {
const classified = classifyOAuthRefreshFailure(rawError);
if (classified) {
const rawProfileId = record?.profileId;
const profileId = sanitizeOAuthRefreshFailureProfileId(typeof rawProfileId === "string" ? rawProfileId : void 0);
rawFallback ??= {
...classified,
...profileId ? { profileId } : {}
};
}
}
if (seen.has(candidate)) return rawFallback;
seen.add(candidate);
candidate = candidate.cause;
}
return rawFallback;
}
/** Build the login command operators should run after OAuth refresh failure. */
function buildOAuthRefreshFailureLoginCommand(provider, options) {
const sanitizedProvider = sanitizeOAuthRefreshFailureProvider(provider);
const sanitizedProfileId = sanitizeOAuthRefreshFailureProfileId(options?.profileId);
if (sanitizedProvider === "claude-cli") return `${formatCliCommand("claude auth login")} && ${formatCliCommand(sanitizedProfileId ? `openclaw models auth login --provider anthropic --method cli --profile-id ${quoteShellArg(sanitizedProfileId)}` : "openclaw models auth login --provider anthropic --method cli")}`;
return sanitizedProvider ? formatCliCommand(sanitizedProfileId ? `openclaw models auth login --provider ${sanitizedProvider} --profile-id ${quoteShellArg(sanitizedProfileId)}` : `openclaw models auth login --provider ${sanitizedProvider}`) : formatCliCommand("openclaw models auth login");
}
/** Build operator guidance for an active profile cooldown or disable window. */
function buildAuthProfileUnusableHint(params) {
if (params.reason === "auth" || params.reason === "auth_permanent" || params.reason === "session_expired") {
if (params.provider === "google-gemini-cli") return `Gemini CLI OAuth cannot be repaired by OpenClaw. Connect Google with an AI Studio API key using ${formatOAuthRefreshFailureLoginCommandMarkdown(formatCliCommand("openclaw models auth login --provider google"))}, then select that Google profile for the Gemini CLI runtime.`;
return `Re-authenticate with ${formatOAuthRefreshFailureLoginCommandMarkdown(buildOAuthRefreshFailureLoginCommand(params.provider, { profileId: params.profileId }))}.`;
}
if (params.kind === "disabled" && params.reason === "billing") return "Top up credits (provider billing) or switch provider.";
return "Wait for cooldown or switch provider.";
}
//#endregion
export { classifyOAuthRefreshFailureError as a, readProviderOAuthRefreshFailure as c, classifyOAuthRefreshFailure as i, buildAuthProfileUnusableHint as n, classifyOAuthRefreshFailureReason as o, buildOAuthRefreshFailureLoginCommand as r, formatOAuthRefreshFailureLoginCommandMarkdown as s, OAuthRefreshFailureError as t };