UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

492 lines (491 loc) 22.7 kB
import { o as asDateTimestampMs } from "./number-coercion-CLj0HTDM.js"; import { w as tryResolveAmbientOwnerAgentId } from "./agent-scope-config-DcbEhP0R.js"; import { r as normalizeProviderId } from "./provider-id-DMd-TDFp.js"; import { l as hasConfiguredSecretInput } from "./types.secrets-kC0nOetj.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { n as resolveProviderIdForAuth } from "./provider-auth-aliases-DhA9c2am.js"; import "./operator-scopes-Dw7Gu2cA.js"; import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import { g as getRuntimeExternalCliProfileIds } from "./persisted-B_qhhBlh.js"; import { a as ensureAuthProfileStoreWithoutExternalProfiles, g as resolvePersistedAuthProfileOwnerAgentDir } from "./store-F1B2duCT.js"; import { i as removeAuthProfilesAcrossOwnerStores, o as removeProviderAuthProfilesWithLock } from "./profiles-DOTqXcYA.js"; import { n as listProfilesForProvider } from "./profile-list-DyfWX-d2.js"; import { n as resolveAuthProfileMetadata } from "./identity-BamcuBvi.js"; import { s as resolveExplicitAuthOrderSelection } from "./order-CC2RBzI5.js"; import { c as isNonSecretApiKeyMarker, g as resolveProviderEnvAuthLookupMaps, h as listProviderEnvAuthLookupKeys, s as isKnownEnvApiKeyMarker } from "./model-auth-markers-jBKQn38x.js"; import { c as resolveUsageProviderId, o as providerUsageLabel } from "./provider-usage.shared-BKgwfwAS.js"; import { r as resolveProviderEnvAuthEvidence } from "./model-auth-env-Dq9W4xg9.js"; import { t as externalCliDiscoveryForConfigStatus } from "./external-cli-discovery-CbeZXk1q.js"; import { S as resolveUsableCustomProviderApiKey, b as resolveProviderEntryApiKeyProfileReference, v as resolveProviderConfigSecretInput } from "./model-auth-provider-config-C_kr_q2g.js"; import "./auth-profiles-BdUEhE7u.js"; import "./model-auth-C48_DZ-I.js"; import { r as clearCurrentProviderAuthState } from "./model-provider-auth-state-Bn1pG368.js"; import { i as warmCurrentProviderAuthStateOffMainThread } from "./model-provider-auth-jVhbzrw7.js"; import { n as abortChatRunsForProvider } from "./chat-abort-qLn3eFOg.js"; import { n as readPreparedCatalog, t as loadDeferredCatalog } from "./server-model-catalog-auth-d5Ty5VGR.js"; import { t as formatForLog } from "./ws-log-Dkg9wKNU.js"; import { n as buildAuthHealthSummary, r as formatRemainingShort } from "./auth-health-BDRAK0UV.js"; import { i as getProviderUsageRuntimeSnapshot, r as readProviderUsageStaleWhileRevalidate, t as clearModelAuthStatusUsageCache } from "./models-auth-status-usage-cache-DTKjRTY8.js"; import { d as refreshActiveProviderAuthRuntimeSnapshot } from "./runtime-C3AQ3lWb.js"; import { n as resolveModelAuthAgentScope, t as modelAuthAgentScopeError } from "./model-auth-agent-scope-Bkn59-AH.js"; import { t as resolveConfigBoundProfileIds } from "./models-auth-status-config-BOZ7BO6r.js"; import { t as resolveModelProviderCapabilities } from "./model-provider-capabilities-DoGihhvt.js"; //#region src/gateway/server-methods/models-auth-status-api-keys.ts function resolveEnvVarName(source) { return /^(?:shell env|env): ([A-Z][A-Z0-9_]*)$/u.exec(source)?.[1]; } function resolveProviderApiKeys(cfg, store, authAliasLookupParams) { const lookupMaps = resolveProviderEnvAuthLookupMaps({ ...authAliasLookupParams, config: cfg, env: process.env }); const providerIds = /* @__PURE__ */ new Set([ ...Object.keys(cfg.models?.providers ?? {}), ...Object.values(cfg.auth?.profiles ?? {}).map((profile) => profile?.provider).filter((provider) => typeof provider === "string"), ...listProviderEnvAuthLookupKeys(lookupMaps) ]); const apiKeys = /* @__PURE__ */ new Map(); for (const rawProvider of providerIds) { const provider = normalizeProviderId(rawProvider); if (!provider) continue; const { providerConfig, ref } = resolveProviderConfigSecretInput(cfg, provider); if (hasConfiguredSecretInput(providerConfig?.apiKey, cfg.secrets?.defaults)) { const profileReference = resolveProviderEntryApiKeyProfileReference({ cfg, authAliasLookupParams, provider, store }); if (profileReference.kind !== "profile" && profileReference.kind !== "profile-incompatible") { if (ref && ref.source !== "env") { apiKeys.set(provider, { source: "config" }); continue; } const available = resolveUsableCustomProviderApiKey({ cfg, provider, env: process.env }); if (available) { const rawKey = typeof providerConfig?.apiKey === "string" ? providerConfig.apiKey.trim() : ""; if (rawKey && isNonSecretApiKeyMarker(rawKey, { includeEnvVarName: false })) continue; const envVar = ref?.source === "env" ? ref.id : profileReference.kind === "marker" && isKnownEnvApiKeyMarker(rawKey) ? rawKey : resolveEnvVarName(available.source); apiKeys.set(provider, envVar ? { source: "env", envVar } : { source: "config" }); continue; } } } const envEvidence = resolveProviderEnvAuthEvidence(provider, process.env, { aliasMap: lookupMaps.aliasMap, candidateMap: lookupMaps.envCandidateMap, authEvidenceMap: lookupMaps.authEvidenceMap }); if (envEvidence?.mode === "api-key") { const envVar = resolveEnvVarName(envEvidence.source); apiKeys.set(provider, { source: "env", ...envVar ? { envVar } : {} }); } } return apiKeys; } //#endregion //#region src/gateway/server-methods/models-auth-status.ts const log = createSubsystemLogger("models-auth-status"); const apiKeyUsageStatusProviders = /* @__PURE__ */ new Set(["clawrouter", "deepseek"]); function buildProviderCapabilities(params) { return resolveModelProviderCapabilities(params).capabilities; } function resolveAuthRefreshScope(cfg) { const discovery = externalCliDiscoveryForConfigStatus({ cfg }); if (discovery.mode !== "scoped") return { providerIds: [] }; const providerIds = [...discovery.providerIds ?? []]; const profileIds = [...discovery.profileIds ?? []]; return { providerIds, ...profileIds.length > 0 ? { profileIds } : {} }; } /** * Invalidate auxiliary usage and prepared provider-auth state after an auth * mutation. Auth health itself is rebuilt on every request; only outbound * usage enrichment is cached. */ function invalidateModelAuthStatusCache() { clearModelAuthStatusUsageCache(); clearCurrentProviderAuthState(); } async function refreshModelAuthStatusRuntimeState() { try { await refreshActiveProviderAuthRuntimeSnapshot(); } catch (err) { log.warn(`runtime auth snapshot refresh before auth status failed: ${formatForLog(err)}`); } } function readProviderParam(params) { const raw = params.provider; if (typeof raw !== "string") return null; return normalizeProviderId(raw) || null; } function readLogoutProfileSelection(params) { if (!("profileIds" in params)) return { ok: true }; if (!Array.isArray(params.profileIds) || params.profileIds.length === 0) return { ok: false, message: "profileIds must be a non-empty string array" }; const profileIds = []; for (const value of params.profileIds) { if (typeof value !== "string" || !value.trim()) return { ok: false, message: "profileIds must be a non-empty string array" }; const profileId = value.trim(); if (!profileIds.includes(profileId)) profileIds.push(profileId); } return { ok: true, profileIds }; } function createAuthLogoutAbortOps(context) { return { chatAbortControllers: context.chatAbortControllers, chatRunState: context.chatRunState, removeChatRun: context.removeChatRun, agentRunSeq: context.agentRunSeq, broadcast: context.broadcast, nodeSendToSession: context.nodeSendToSession }; } async function removeProviderAuthProfilesAcrossOwnerStores(params) { const ownerAgentDirs = /* @__PURE__ */ 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); const usageLabel = usageId ? providerUsageLabel(usageId) : void 0; if (usageLabel) return usageLabel; return provider; } function aggregateProfileStatus(profiles, now) { const statuses = new Set(profiles.map((profile) => profile.status)); const status = [ "expired", "missing", "expiring", "ok", "static" ].find((candidate) => statuses.has(candidate)); const expirable = profiles.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: status ?? "static", expiresAt, remainingMs }; } /** * Aggregate the effective refreshable credential status for the dashboard. * OAuth remains authoritative when present; token credentials are the * supported fallback after an OAuth-to-token migration. Explicit auth-order * exclusions remain authoritative through `effectiveProfiles`. * * `expectsOAuth` keeps an API-key-only provider `missing` after config switches * to OAuth but login has not completed. */ function aggregateRefreshableAuthStatus(prov, now = Date.now(), expectsOAuth = false) { const profiles = prov.effectiveProfiles ?? prov.profiles; const oauth = profiles.filter((profile) => profile.type === "oauth"); if (oauth.length > 0) return aggregateProfileStatus(oauth, now); const tokens = profiles.filter((profile) => profile.type === "token"); if (tokens.length > 0) return aggregateProfileStatus(tokens, now); if (expectsOAuth) return { status: "missing" }; return { status: prov.status, expiresAt: prov.expiresAt, remainingMs: prov.remainingMs }; } function mapProvider(prov, cfg, store, authAliasLookupParams, usageByProvider, expectsOAuthSet, apiKeys, logoutProfileIds, configBoundProfileIds, externalProfileIds, externalCliProfileIds, includeProfileIdentity) { const providerKey = normalizeProviderId(prov.provider); const authProviderKey = resolveProviderIdForAuth(prov.provider, authAliasLookupParams); const profileOrder = resolveExplicitAuthOrderSelection({ storeOrder: store.order, configuredOrder: cfg.auth?.order, providerKey, providerAuthKey: authProviderKey }); const localProfileIds = new Set(store.runtimeLocalProfileIds ?? Object.keys(store.profiles).filter((profileId) => !externalProfileIds.has(profileId))); const providerOrderLocked = prov.profiles.some((profile) => configBoundProfileIds.has(profile.profileId)); const configuredOrderLocked = profileOrder.order !== void 0 && !profileOrder.fromStore; 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 rawRollup = aggregateRefreshableAuthStatus(prov, Date.now(), expectsOAuthSet.has(prov.provider)); const refreshableProfiles = (prov.effectiveProfiles ?? prov.profiles).filter((profile) => profile.type === "oauth" || profile.type === "token"); const rollup = refreshableProfiles.length > 0 && refreshableProfiles.every((profile) => profile.type === "oauth" && externalCliProfileIds.has(profile.profileId)) && (rawRollup.status === "expired" || rawRollup.status === "expiring") ? { status: "ok" } : rawRollup; const apiKey = apiKeys.get(normalizeProviderId(prov.provider)); const hasRefreshableProfile = prov.profiles.some((profile) => profile.type === "oauth" || profile.type === "token"); return { provider: prov.provider, authProvider: authProviderKey, displayName: providerDisplayName(prov.provider), status: apiKey && !hasRefreshableProfile && rollup.status === "missing" ? "static" : rollup.status, expiry: buildExpiry(rollup.remainingMs, rollup.expiresAt), profiles: prov.profiles.map((prof) => { const metadata = resolveAuthProfileMetadata({ cfg, store, profileId: prof.profileId }); const lastUsedAt = store.usageStats?.[prof.profileId]?.lastUsed; return { profileId: prof.profileId, type: prof.type, status: prof.status, reasonCode: prof.reasonCode, source: configBoundProfileIds.has(prof.profileId) ? "config" : externalProfileIds.has(prof.profileId) ? "external" : localProfileIds.has(prof.profileId) ? "saved" : "inherited", expiry: buildExpiry(prof.remainingMs, prof.expiresAt), ...externalCliProfileIds.has(prof.profileId) ? { externallyManaged: true } : {}, ...includeProfileIdentity && metadata.displayName ? { displayName: metadata.displayName } : {}, ...includeProfileIdentity && metadata.email ? { email: metadata.email } : {}, ...includeProfileIdentity && lastUsedAt ? { lastUsedAt } : {}, ...(prof.type === "oauth" || prof.type === "token") && logoutProfileIds.has(prof.profileId) && !configBoundProfileIds.has(prof.profileId) ? { logoutSupported: true } : {} }; }), ...profileOrder.order !== void 0 ? { profileOrder: profileOrder.order } : {}, ...profileOrder.fromStore ? { profileOrderStored: true } : {}, ...providerOrderLocked ? { profileOrderLocked: "provider-config" } : configuredOrderLocked ? { profileOrderLocked: "auth-config" } : {}, ...apiKey ? { apiKey } : {}, usage: usage && usageKey ? { providerId: usageKey, windows: usage.windows, ...usage.summary ? { summary: usage.summary } : {}, ...usage.plan ? { plan: usage.plan } : {}, ...usage.billing?.length ? { billing: usage.billing } : {}, ...includeProfileIdentity && usage.accountEmail ? { accountEmail: usage.accountEmail } : {} } : void 0 }; } function resolveConfiguredProviders(cfg, apiKeys) { const out = /* @__PURE__ */ new Set(); const expectsOAuth = /* @__PURE__ */ new Set(); for (const [id, provider] of Object.entries(cfg.models?.providers ?? {})) { const normalized = normalizeProviderId(id); if (!normalized) continue; const rawKey = typeof provider?.apiKey === "string" ? provider.apiKey.trim() : ""; const hasApiKey = hasConfiguredSecretInput(provider?.apiKey, cfg.secrets?.defaults) && (rawKey === "secretref-managed" || !isNonSecretApiKeyMarker(rawKey, { includeEnvVarName: false })); const mode = provider?.auth; if (mode !== "oauth" && mode !== "token" && !hasApiKey) continue; if (apiKeys.has(normalized)) continue; out.add(normalized); if (mode === "oauth") expectsOAuth.add(normalized); } 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; const normalized = normalizeProviderId(provider); if (!normalized) continue; if (apiKeys.has(normalized)) continue; out.add(normalized); if (mode === "oauth") expectsOAuth.add(normalized); } 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; } const selection = readLogoutProfileSelection(params); if (!selection.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, selection.message)); return; } try { const cfg = context.getRuntimeConfig(); const scope = resolveModelAuthAgentScope(cfg, params.agentId); if (!scope.ok) { respond(false, void 0, modelAuthAgentScopeError(scope)); return; } const { agentDir } = scope; const authProvider = resolveProviderIdForAuth(provider, { config: cfg }); const store = ensureAuthProfileStoreWithoutExternalProfiles(agentDir); const availableProfiles = listProfilesForProvider(store, provider); const removedProfiles = selection.profileIds ?? availableProfiles; if (selection.profileIds && selection.profileIds.some((profileId) => { const profile = store.profiles[profileId]; return !availableProfiles.includes(profileId) || profile?.type !== "oauth" && profile?.type !== "token"; })) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "profileIds contain unavailable auth profiles")); return; } const configBoundProfileIds = selection.profileIds ? resolveConfigBoundProfileIds(cfg, store) : null; if (selection.profileIds?.some((profileId) => configBoundProfileIds?.has(profileId))) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "profileIds contain config-bound auth profiles")); return; } if (!(selection.profileIds ? await removeAuthProfilesAcrossOwnerStores({ agentDir, profileIds: removedProfiles }) : await removeProviderAuthProfilesAcrossOwnerStores({ provider, agentDir, profileIds: removedProfiles }))) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, `failed to remove saved auth profiles for provider ${provider}`)); return; } invalidateModelAuthStatusCache(); await refreshActiveProviderAuthRuntimeSnapshot(); warmCurrentProviderAuthStateOffMainThread(context.getRuntimeConfig()).catch((err) => { log.warn(`provider auth state rewarm after logout failed: ${formatForLog(err)}`); }); const { runIds: abortedRunIds } = selection.profileIds ? { runIds: [] } : abortChatRunsForProvider(createAuthLogoutAbortOps(context), { cfg, providerId: authProvider, agentId: scope.agentId, 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, client }) => { const now = Date.now(); const refreshRequested = Boolean(params.refresh); const includeProfileIdentity = Array.isArray(client?.connect?.scopes) && client.connect.scopes.includes("operator.admin"); const resolveScope = (cfg) => resolveModelAuthAgentScope(cfg, params.agentId === void 0 || params.agentId === "" ? tryResolveAmbientOwnerAgentId(cfg) : params.agentId); try { let cfg = context.getRuntimeConfig(); let scope = resolveScope(cfg); if (!scope.ok) { respond(false, void 0, modelAuthAgentScopeError(scope)); return; } if (refreshRequested) { await refreshModelAuthStatusRuntimeState(); cfg = context.getRuntimeConfig(); scope = resolveScope(cfg); if (!scope.ok) { respond(false, void 0, modelAuthAgentScopeError(scope)); return; } } const preparedSnapshot = refreshRequested ? await loadDeferredCatalog(context, scope.agentId, { readOnly: true, authScope: resolveAuthRefreshScope(cfg), refreshAuth: true, refreshFullCatalog: false }) : await readPreparedCatalog(context, scope.agentId); if (!preparedSnapshot) { respond(true, { ts: now, providers: [], unavailable: { code: "PREPARED_MODEL_AUTH_UNAVAILABLE", message: "Model authentication status is unavailable. Refresh Models after setup finishes; restart the Gateway if it persists." } }, void 0); return; } cfg = preparedSnapshot.config; const { agentId, agentDir, authStore: store, workspaceDir } = preparedSnapshot; const authAliasLookupParams = { config: cfg, workspaceDir, metadataSnapshot: preparedSnapshot.metadataSnapshot, includeUntrustedWorkspacePlugins: false }; const apiKeys = resolveProviderApiKeys(cfg, store, authAliasLookupParams); const configured = resolveConfiguredProviders(cfg, apiKeys); const statusProviderIds = new Set(configured.providers); for (const provider of apiKeys.keys()) statusProviderIds.add(provider); for (const profile of Object.values(store.profiles)) { const provider = normalizeProviderId(profile.provider); if (provider) statusProviderIds.add(provider); } const authHealth = buildAuthHealthSummary({ store, cfg, providers: statusProviderIds.size > 0 ? [...statusProviderIds] : void 0, allowKeychainPrompt: false, authAliasLookupParams }); 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 providerUsageRuntime = getProviderUsageRuntimeSnapshot({ config: cfg, agentId, agentDir, store }); const usageByProvider = readProviderUsageStaleWhileRevalidate({ agentId, agentDir, authStore: providerUsageRuntime.store, configRef: cfg, credentialKey: providerUsageRuntime.credentialKey, forceRefresh: refreshRequested, providerIds: usageProviderIds, now }); const externalProfileIds = new Set(store.runtimeExternalProfileIds ?? []); const externalCliProfileIds = new Set(getRuntimeExternalCliProfileIds(store)); const logoutProfileIds = new Set(Object.entries(store.profiles).filter(([profileId, profile]) => !externalProfileIds.has(profileId) && (profile.type === "oauth" || profile.type === "token")).map(([profileId]) => profileId)); const configBoundProfileIds = resolveConfigBoundProfileIds(cfg, store, authAliasLookupParams); respond(true, { ts: now, providers: authHealth.providers.map((prov) => mapProvider(prov, cfg, store, authAliasLookupParams, usageByProvider, configured.expectsOAuth, apiKeys, logoutProfileIds, configBoundProfileIds, externalProfileIds, externalCliProfileIds, includeProfileIdentity)), providerCapabilities: buildProviderCapabilities({ config: cfg, workspaceDir, metadataSnapshot: preparedSnapshot.metadataSnapshot }) }, void 0); } catch (err) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err))); } } }; //#endregion export { modelsAuthStatusHandlers };