openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
308 lines (307 loc) • 13.3 kB
JavaScript
import { o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import "./number-coercion-Z7n6tXLk.js";
import { c as isSecretRef } from "./types.secrets-_0JOMGE5.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { i as normalizeProviderId } from "./provider-id-Dq06Bcx6.js";
import "./agent-scope-MrLta7Pq.js";
import { s as resolveDefaultAgentDir } from "./agent-scope-config-CgCYpZfK.js";
import { r as resolveProviderIdForAuth } from "./provider-auth-aliases-BNZrcvHv.js";
import { f as resolvePersistedAuthProfileOwnerAgentDir, i as ensureAuthProfileStoreWithoutExternalProfiles, n as ensureAuthProfileStore, t as clearRuntimeAuthProfileStoreSnapshots } from "./store-C8spD0DG.js";
import "./auth-profiles-84rzaGag.js";
import { n as externalCliDiscoveryForConfigStatus } from "./external-cli-discovery-Cr-vJMRB.js";
import { n as listProfilesForProvider } from "./profile-list-DI8_o0Rw.js";
import { i as removeProviderAuthProfilesWithLock } from "./profiles-BMWtmBgj.js";
import { r as clearCurrentProviderAuthState } from "./model-provider-auth-state-DW_JYm-o.js";
import { a as warmCurrentProviderAuthStateOffMainThread } from "./model-provider-auth-DawbDRrd.js";
import { in as errorShape, rn as ErrorCodes } from "./schema-BwaBORnA.js";
import "./src-oj0IwW6K.js";
import { n as abortChatRunsForProvider } from "./chat-abort-Cu_ehtms.js";
import { t as formatForLog } from "./ws-log-DlzuStZb.js";
import { n as PROVIDER_LABELS, o as resolveUsageProviderId } from "./provider-usage.shared-DOxrEMuU.js";
import { t as loadProviderUsageSummary } from "./provider-usage.load-DRNSvlul.js";
import { n as buildAuthHealthSummary, r as formatRemainingShort } from "./auth-health-4xis_1sG.js";
import { s as refreshActiveSecretsRuntimeSnapshot } from "./runtime-CLmjiLBt.js";
//#region src/gateway/server-methods/models-auth-status.ts
const log = createSubsystemLogger("models-auth-status");
const apiKeyUsageStatusProviders = new Set(["deepseek"]);
const CACHE_TTL_MS = 6e4;
let cached = null;
/**
* Invalidate the in-memory cache. Reserved for future gateway-side auth
* mutation handlers (login, logout, token rotation) so the next read returns
* fresh data. Today those mutations happen via the CLI and the 60s TTL plus
* `{refresh: true}` param cover the stale-data window.
*/
function invalidateModelAuthStatusCache() {
cached = null;
clearCurrentProviderAuthState();
}
async function refreshModelAuthStatusRuntimeState() {
invalidateModelAuthStatusCache();
try {
if (await refreshActiveSecretsRuntimeSnapshot()) return;
} catch (err) {
log.warn(`runtime auth snapshot refresh before auth status failed: ${formatForLog(err)}`);
return;
}
clearRuntimeAuthProfileStoreSnapshots();
}
function readProviderParam(params) {
const raw = params.provider;
if (typeof raw !== "string") return null;
return normalizeProviderId(raw) || null;
}
function createAuthLogoutAbortOps(context) {
return {
chatAbortControllers: context.chatAbortControllers,
chatRunBuffers: context.chatRunBuffers,
chatAbortedRuns: context.chatAbortedRuns,
clearChatRunState: context.clearChatRunState,
removeChatRun: context.removeChatRun,
agentRunSeq: context.agentRunSeq,
broadcast: context.broadcast,
nodeSendToSession: context.nodeSendToSession
};
}
async function removeProviderAuthProfilesAcrossOwnerStores(params) {
const ownerAgentDirs = new Set([params.agentDir]);
for (const profileId of params.profileIds) ownerAgentDirs.add(resolvePersistedAuthProfileOwnerAgentDir({
agentDir: params.agentDir,
profileId
}));
for (const ownerAgentDir of ownerAgentDirs) if (!await removeProviderAuthProfilesWithLock({
provider: params.provider,
agentDir: ownerAgentDir
})) return false;
return true;
}
function buildExpiry(remainingMs, expiresAt) {
const normalizedExpiresAt = asDateTimestampMs(expiresAt);
if (normalizedExpiresAt === void 0 || typeof remainingMs !== "number") return;
return {
at: normalizedExpiresAt,
remainingMs,
label: formatRemainingShort(remainingMs)
};
}
function providerDisplayName(provider) {
const usageId = resolveUsageProviderId(provider);
if (usageId && PROVIDER_LABELS[usageId]) return PROVIDER_LABELS[usageId];
return provider;
}
/**
* Aggregate provider status from OAuth profiles only. `buildAuthHealthSummary`
* rolls up across both OAuth and token profiles, which mis-reports providers
* where a healthy OAuth sits alongside an expired/missing bearer token.
* For the dashboard's OAuth-health signal, token profiles are a separate
* concern — we want "is OAuth healthy?", not "is every credential healthy?"
* It also consumes the provider's effective profile subset when auth order
* excludes stale inventory from the runtime credential path.
*
* `expectsOAuth` surfaces the configured-OAuth-but-no-oauth-profile case as
* `missing` instead of silently falling back to the provider's rollup (which
* would report `static` if only api_key credentials exist). Without this,
* switching a provider from api_key to oauth in config but forgetting to
* login hides behind the residual api_key profile until runtime fails.
*
* Exported for direct unit testing of the rollup rules.
*/
function aggregateOAuthStatus(prov, now = Date.now(), expectsOAuth = false) {
const oauth = (prov.effectiveProfiles ?? prov.profiles).filter((p) => p.type === "oauth");
if (oauth.length === 0) {
if (expectsOAuth) return { status: "missing" };
return {
status: prov.status,
expiresAt: prov.expiresAt,
remainingMs: prov.remainingMs
};
}
const statuses = new Set(oauth.map((p) => p.status));
let status;
if (statuses.has("expired")) status = "expired";
else if (statuses.has("missing")) status = "missing";
else if (statuses.has("expiring")) status = "expiring";
else if (statuses.has("ok")) status = "ok";
else if (statuses.has("static")) status = "static";
else {
Array.from(statuses)[0];
status = "static";
}
const expirable = oauth.map((p) => p.expiresAt).filter((v) => asDateTimestampMs(v) !== void 0);
const expiresAt = expirable.length > 0 ? Math.min(...expirable) : void 0;
const remainingMs = expiresAt !== void 0 ? expiresAt - now : void 0;
return {
status,
expiresAt,
remainingMs
};
}
function mapProvider(prov, usageByProvider, expectsOAuthSet) {
const usageProfile = prov.profiles.find((profile) => profile.type === "oauth" || profile.type === "token") ?? prov.profiles.find((profile) => profile.type === "api_key");
const usageKey = resolveUsageProviderId(prov.provider, { credentialType: usageProfile?.type });
const usage = usageKey ? usageByProvider.get(usageKey) : void 0;
const rollup = aggregateOAuthStatus(prov, Date.now(), expectsOAuthSet.has(prov.provider));
return {
provider: prov.provider,
displayName: providerDisplayName(prov.provider),
status: rollup.status,
expiry: buildExpiry(rollup.remainingMs, rollup.expiresAt),
profiles: prov.profiles.map((prof) => ({
profileId: prof.profileId,
type: prof.type,
status: prof.status,
reasonCode: prof.reasonCode,
expiry: buildExpiry(prof.remainingMs, prof.expiresAt)
})),
usage: usage ? {
windows: usage.windows,
...usage.summary ? { summary: usage.summary } : {},
...usage.plan ? { plan: usage.plan } : {}
} : void 0
};
}
/**
* Collect provider IDs with refreshable credentials (OAuth or bearer token)
* so a configured-but-not-logged-in provider surfaces as `missing` rather
* than being silently absent. API-key and AWS-SDK providers are excluded —
* their credentials don't expire on a schedule this endpoint can meaningfully
* monitor, and surfacing them here would flash a red alert on a healthy
* API-key setup.
*
* Providers with `models.providers.<id>.apiKey` set (commonly via a
* SecretRef env binding) are excluded from the "missing" synthesis even
* when their `auth` mode is `oauth` or `token` — an env-backed credential
* is already present, so flagging the dashboard as missing would cry wolf
* for a working auth path. They can still show up with real status if the
* profile store has an entry for them.
*/
function resolveConfiguredProviders(cfg) {
const out = /* @__PURE__ */ new Set();
const expectsOAuth = /* @__PURE__ */ new Set();
const envBacked = /* @__PURE__ */ new Set();
for (const [id, provider] of Object.entries(cfg.models?.providers ?? {})) {
const apiKey = provider?.apiKey;
if (!id || apiKey === void 0 || apiKey === null) continue;
let resolvable = false;
if (typeof apiKey === "string" && apiKey.length > 0) resolvable = true;
else if (isSecretRef(apiKey)) if (apiKey.source === "env") {
const envValue = process.env[apiKey.id];
resolvable = typeof envValue === "string" && envValue.length > 0;
} else resolvable = true;
if (resolvable) envBacked.add(normalizeProviderId(id));
}
for (const [id, provider] of Object.entries(cfg.models?.providers ?? {})) {
if (!id) continue;
const mode = provider?.auth;
if (mode !== "oauth" && mode !== "token") continue;
if (envBacked.has(normalizeProviderId(id))) continue;
out.add(id);
if (mode === "oauth") expectsOAuth.add(normalizeProviderId(id));
}
for (const profile of Object.values(cfg.auth?.profiles ?? {})) {
const provider = profile?.provider;
const mode = profile?.mode;
if (typeof provider !== "string" || provider.length === 0 || mode !== "oauth" && mode !== "token") continue;
if (envBacked.has(normalizeProviderId(provider))) continue;
out.add(provider);
if (mode === "oauth") expectsOAuth.add(normalizeProviderId(provider));
}
return {
providers: Array.from(out),
expectsOAuth
};
}
const modelsAuthStatusHandlers = {
"models.authLogout": async ({ params, respond, context }) => {
const provider = readProviderParam(params);
if (!provider) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "provider is required"));
return;
}
try {
const cfg = context.getRuntimeConfig();
const agentDir = resolveDefaultAgentDir(cfg);
const authProvider = resolveProviderIdForAuth(provider, { config: cfg });
const removedProfiles = listProfilesForProvider(ensureAuthProfileStoreWithoutExternalProfiles(agentDir), provider);
if (!await removeProviderAuthProfilesAcrossOwnerStores({
provider,
agentDir,
profileIds: removedProfiles
})) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, `failed to remove saved auth profiles for provider ${provider}`));
return;
}
await refreshActiveSecretsRuntimeSnapshot();
invalidateModelAuthStatusCache();
clearCurrentProviderAuthState();
warmCurrentProviderAuthStateOffMainThread(context.getRuntimeConfig()).catch((err) => {
log.warn(`provider auth state rewarm after logout failed: ${formatForLog(err)}`);
});
const { runIds: abortedRunIds } = abortChatRunsForProvider(createAuthLogoutAbortOps(context), {
providerId: authProvider,
stopReason: "auth-revoked"
});
respond(true, {
provider,
removedProfiles,
abortedRunIds
}, void 0);
} catch (err) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
}
},
"models.authStatus": async ({ params, respond, context }) => {
const now = Date.now();
const bypassCache = Boolean(params?.refresh);
if (!bypassCache && cached && now - cached.ts < CACHE_TTL_MS) {
respond(true, cached.result, void 0, { cached: true });
return;
}
try {
if (bypassCache) await refreshModelAuthStatusRuntimeState();
const cfg = context.getRuntimeConfig();
const agentDir = resolveDefaultAgentDir(cfg);
const store = ensureAuthProfileStore(agentDir, { externalCli: externalCliDiscoveryForConfigStatus({ cfg }) });
const configured = resolveConfiguredProviders(cfg);
const authHealth = buildAuthHealthSummary({
store,
cfg,
providers: configured.providers.length > 0 ? configured.providers : void 0,
allowKeychainPrompt: false
});
const usageProviderIds = [...new Set(authHealth.profiles.filter((p) => {
if (p.type === "oauth" || p.type === "token") return true;
const usageProvider = resolveUsageProviderId(p.provider, { credentialType: p.type });
return usageProvider ? apiKeyUsageStatusProviders.has(usageProvider) : false;
}).map((p) => resolveUsageProviderId(p.provider, { credentialType: p.type })).filter((id) => Boolean(id)))];
const usageByProvider = /* @__PURE__ */ new Map();
if (usageProviderIds.length > 0) try {
const usage = await loadProviderUsageSummary({
providers: usageProviderIds,
agentDir,
timeoutMs: 3500
});
for (const snap of usage.providers) usageByProvider.set(snap.provider, {
windows: snap.windows,
...snap.summary ? { summary: snap.summary } : {},
...snap.plan ? { plan: snap.plan } : {}
});
} catch (err) {
log.debug(`usage enrichment failed (auth status still returned): providers=${usageProviderIds.join(",")} error=${formatForLog(err)}`);
}
const result = {
ts: now,
providers: authHealth.providers.map((prov) => mapProvider(prov, usageByProvider, configured.expectsOAuth))
};
cached = {
ts: now,
result
};
respond(true, result, void 0);
} catch (err) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
}
}
};
//#endregion
export { aggregateOAuthStatus, invalidateModelAuthStatusCache, modelsAuthStatusHandlers };