UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

338 lines (337 loc) 12.7 kB
import { D as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs } from "./number-coercion-CLj0HTDM.js"; import { a as resolveOsHomeRelativePath } from "./home-dir-BPhrG-aM.js"; import { t as loadJsonFileThroughSymlink } from "./json-file-CHTTn1IF.js"; import fs from "node:fs"; import path from "node:path"; import { execSync } from "node:child_process"; import { createHash } from "node:crypto"; //#region src/agents/cli-credentials.ts /** * Reads and refreshes credentials stored by external CLI runtimes such as * Codex, Gemini, and MiniMax. */ const CODEX_CLI_AUTH_FILENAME = "auth.json"; const MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH = ".minimax/oauth_creds.json"; const GEMINI_CLI_CREDENTIALS_RELATIVE_PATH = ".gemini/oauth_creds.json"; const CODEX_CLI_FALLBACK_EXPIRY_MS = 36e5; let codexCliCache = null; let minimaxCliCache = null; let geminiCliCache = null; /** Clears in-memory CLI credential caches for isolated tests. */ function resetCliCredentialCachesForTest() { codexCliCache = null; minimaxCliCache = null; geminiCliCache = null; } function resolveCodexCliHomePath(codexHome, env = process.env) { const configured = codexHome ?? env.CODEX_HOME; const home = resolveOsHomeRelativePath(configured || "~/.codex", { env }); try { return fs.realpathSync.native(home); } catch { return home; } } function codexAuthJsonUsesChatGptTokens(data) { const authMode = typeof data.auth_mode === "string" ? data.auth_mode.toLowerCase() : void 0; if (authMode) return authMode === "chatgpt" || authMode === "chatgptauthtokens"; return typeof data.OPENAI_API_KEY !== "string"; } function codexAuthJsonUsesApiKey(data) { const authMode = typeof data.auth_mode === "string" ? data.auth_mode.toLowerCase() : void 0; if (authMode) return authMode === "apikey" || authMode === "api_key"; return typeof data.OPENAI_API_KEY === "string"; } function resolveMiniMaxCliCredentialsPath(homeDir) { const baseDir = resolveOsHomeRelativePath(homeDir ?? "~"); return path.join(baseDir, MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH); } function resolveGeminiCliCredentialsPath(homeDir) { const baseDir = resolveOsHomeRelativePath(homeDir ?? "~"); return path.join(baseDir, GEMINI_CLI_CREDENTIALS_RELATIVE_PATH); } function readFileMtimeMs(filePath) { try { return fs.statSync(filePath).mtimeMs; } catch { return null; } } function readCachedCliCredential(options) { const { ttlMs, cache, cacheKey, read, setCache, readSourceFingerprint } = options; if (ttlMs <= 0) return read(); const now = Date.now(); const sourceFingerprint = readSourceFingerprint?.(); if (cache && cache.cacheKey === cacheKey && cache.sourceFingerprint === sourceFingerprint && now - cache.readAt < ttlMs) return cache.value; const value = read(); const cachedSourceFingerprint = readSourceFingerprint?.(); if (!readSourceFingerprint || cachedSourceFingerprint === sourceFingerprint) setCache({ value, readAt: now, cacheKey, sourceFingerprint: cachedSourceFingerprint }); else setCache(null); return value; } function computeCodexKeychainAccount(codexHome) { return `cli|${createHash("sha256").update(codexHome).digest("hex").slice(0, 16)}`; } function resolveCodexKeychainParams(options) { return { platform: options?.platform ?? process.platform, execSyncImpl: options?.execSync ?? execSync, codexHome: resolveCodexCliHomePath(options?.codexHome) }; } function decodeJwtExpiryMs(token) { const parts = token.split("."); if (parts.length < 2) return null; const encodedPayload = parts.at(1); if (!encodedPayload) return null; try { const payloadRaw = Buffer.from(encodedPayload, "base64url").toString("utf8"); const payload = JSON.parse(payloadRaw); if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= 0) return null; return asDateTimestampMs(payload.exp * 1e3) ?? null; } catch { return null; } } function decodeJwtIdentityClaims(token) { const parts = token.split("."); if (parts.length < 2) return {}; const encodedPayload = parts.at(1); if (!encodedPayload) return {}; try { const payloadRaw = Buffer.from(encodedPayload, "base64url").toString("utf8"); const payload = JSON.parse(payloadRaw); return { sub: typeof payload.sub === "string" && payload.sub ? payload.sub : void 0, email: typeof payload.email === "string" && payload.email ? payload.email : void 0 }; } catch { return {}; } } function readCodexKeychainAuthRecord(options) { const { platform, execSyncImpl, codexHome } = resolveCodexKeychainParams(options); if (platform !== "darwin" || options?.allowKeychainPrompt === false) return null; const account = computeCodexKeychainAccount(codexHome); try { const secret = execSyncImpl(`security find-generic-password -s "Codex Auth" -a "${account}" -w`, { encoding: "utf8", timeout: 5e3, stdio: [ "pipe", "pipe", "pipe" ] }).trim(); return JSON.parse(secret); } catch { return null; } } function resolveCodexFallbackExpiryMs(nowMs) { return resolveExpiresAtMsFromDurationMs(CODEX_CLI_FALLBACK_EXPIRY_MS, { nowMs: nowMs === void 0 ? void 0 : Math.floor(nowMs) }); } function parseCodexOauthCredential(data, fallbackExpiry) { if (!codexAuthJsonUsesChatGptTokens(data)) return null; const tokens = data.tokens; const accessToken = tokens?.access_token; const refreshToken = tokens?.refresh_token; if (typeof accessToken !== "string" || !accessToken) return null; if (typeof refreshToken !== "string" || !refreshToken) return null; const expires = decodeJwtExpiryMs(accessToken) ?? fallbackExpiry; if (expires === void 0) return null; return { type: "oauth", provider: "openai", access: accessToken, refresh: refreshToken, expires, accountId: typeof tokens?.account_id === "string" ? tokens.account_id : void 0, idToken: typeof tokens?.id_token === "string" ? tokens.id_token : void 0 }; } function parseCodexApiKeyCredential(data) { if (!codexAuthJsonUsesApiKey(data)) return null; const key = typeof data.OPENAI_API_KEY === "string" ? data.OPENAI_API_KEY.trim() : ""; return key ? { type: "api_key", provider: "openai", key } : null; } function readCliOauthTokenFields(data) { const accessToken = data.access_token; const refreshToken = data.refresh_token; const expiresAt = data.expiry_date; if (typeof accessToken !== "string" || !accessToken) return null; if (typeof refreshToken !== "string" || !refreshToken) return null; if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null; return { access: accessToken, refresh: refreshToken, expires: expiresAt }; } function readPortalCliOauthCredentials(credPath, provider) { const raw = loadJsonFileThroughSymlink(credPath); if (!raw || typeof raw !== "object") return null; const tokens = readCliOauthTokenFields(raw); return tokens ? { type: "oauth", provider, ...tokens } : null; } function readMiniMaxCliCredentials(options) { return readPortalCliOauthCredentials(resolveMiniMaxCliCredentialsPath(options?.homeDir), "minimax-portal"); } function readGeminiCliCredentials(options) { const credPath = resolveGeminiCliCredentialsPath(options?.homeDir); const raw = loadJsonFileThroughSymlink(credPath); if (!raw || typeof raw !== "object") return null; const data = raw; const tokens = readCliOauthTokenFields(data); if (!tokens) return null; const idTokenRaw = data.id_token; const identity = typeof idTokenRaw === "string" && idTokenRaw ? decodeJwtIdentityClaims(idTokenRaw) : {}; return { type: "oauth", provider: "google-gemini-cli", ...tokens, ...identity.email ? { email: identity.email } : {}, ...identity.sub ? { accountId: identity.sub } : {} }; } function formatCodexApiKeyForLoginStatus(key) { return key.length <= 13 ? "***" : `${key.slice(0, 8)}***${key.slice(-5)}`; } /** Reads an API key only when Codex confirms that exact credential is active. */ function readCodexCliActiveApiKey(options) { const { execSyncImpl, codexHome } = resolveCodexKeychainParams(options); let status; try { status = execSyncImpl("codex login status 2>&1", { encoding: "utf8", timeout: 5e3, stdio: [ "pipe", "pipe", "pipe" ], env: { ...process.env, CODEX_HOME: codexHome } }).trim(); } catch { return null; } const activeFingerprint = /^Logged in using an API key - (.+)$/mu.exec(status)?.[1]?.trim(); const legacyApiKeyStatus = status.trim() === "Logged in using an API key"; if (!activeFingerprint && !legacyApiKeyStatus) return null; const candidates = []; const authPath = path.join(codexHome, CODEX_CLI_AUTH_FILENAME); const raw = loadJsonFileThroughSymlink(authPath); if (raw && typeof raw === "object") { const fileCredential = parseCodexApiKeyCredential(raw); if (fileCredential) candidates.push(fileCredential); } const keychainRecord = readCodexKeychainAuthRecord({ codexHome, allowKeychainPrompt: options?.allowKeychainPrompt, platform: options?.platform, execSync: options?.execSync }); if (keychainRecord) { const keychainCredential = parseCodexApiKeyCredential(keychainRecord); if (keychainCredential) candidates.push(keychainCredential); } const matchingKeys = new Set(candidates.filter((candidate) => legacyApiKeyStatus || formatCodexApiKeyForLoginStatus(candidate.key) === activeFingerprint).map((candidate) => candidate.key)); if (matchingKeys.size !== 1) return null; const key = [...matchingKeys][0]; return key ? { type: "api_key", provider: "openai", key } : null; } /** Reads Codex CLI OAuth credentials from Keychain or CODEX_HOME auth.json. */ function readCodexCliCredentials(options) { const keychainRecord = readCodexKeychainAuthRecord(options); if (keychainRecord) { const lastRefreshRaw = keychainRecord.last_refresh; const keychainCredential = parseCodexOauthCredential(keychainRecord, resolveCodexFallbackExpiryMs(typeof lastRefreshRaw === "string" || typeof lastRefreshRaw === "number" ? new Date(lastRefreshRaw).getTime() : Date.now()) ?? resolveCodexFallbackExpiryMs()); if (keychainCredential) return keychainCredential; } const authPath = path.join(resolveCodexCliHomePath(options?.codexHome), CODEX_CLI_AUTH_FILENAME); const raw = loadJsonFileThroughSymlink(authPath); if (!raw || typeof raw !== "object") return null; let fallbackExpiry; try { fallbackExpiry = resolveCodexFallbackExpiryMs(fs.statSync(authPath).mtimeMs); } catch { fallbackExpiry = resolveCodexFallbackExpiryMs(); } return parseCodexOauthCredential(raw, fallbackExpiry); } /** Reads Codex CLI credentials with optional short-lived cache and file fingerprinting. */ function readCodexCliCredentialsCached(options) { const platform = options?.platform ?? process.platform; const ttlMs = options?.ttlMs ?? 0; const authPath = path.join(resolveCodexCliHomePath(options?.codexHome), CODEX_CLI_AUTH_FILENAME); const keychainIntent = platform === "darwin" && options?.allowKeychainPrompt !== false ? "keychain" : "file"; return readCachedCliCredential({ ttlMs, cache: codexCliCache, cacheKey: `${platform}|${authPath}:${keychainIntent}`, read: () => readCodexCliCredentials({ codexHome: options?.codexHome, allowKeychainPrompt: options?.allowKeychainPrompt, platform: options?.platform, execSync: options?.execSync }), setCache: (next) => { codexCliCache = next; }, readSourceFingerprint: () => readFileMtimeMs(authPath) }); } /** Reads MiniMax CLI credentials with optional short-lived cache. */ function readMiniMaxCliCredentialsCached(options) { const credPath = resolveMiniMaxCliCredentialsPath(options?.homeDir); return readCachedCliCredential({ ttlMs: options?.ttlMs ?? 0, cache: minimaxCliCache, cacheKey: credPath, read: () => readMiniMaxCliCredentials({ homeDir: options?.homeDir }), setCache: (next) => { minimaxCliCache = next; }, readSourceFingerprint: () => readFileMtimeMs(credPath) }); } /** Reads Gemini CLI credentials with optional short-lived cache. */ function readGeminiCliCredentialsCached(options) { const credPath = resolveGeminiCliCredentialsPath(options?.homeDir); return readCachedCliCredential({ ttlMs: options?.ttlMs ?? 0, cache: geminiCliCache, cacheKey: credPath, read: () => readGeminiCliCredentials({ homeDir: options?.homeDir }), setCache: (next) => { geminiCliCache = next; }, readSourceFingerprint: () => readFileMtimeMs(credPath) }); } if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.cliCredentialsTestApi")] = { readCodexAuth: readCodexCliCredentials, resetCaches: resetCliCredentialCachesForTest }; //#endregion export { resolveCodexCliHomePath as a, readMiniMaxCliCredentialsCached as i, readCodexCliCredentialsCached as n, readGeminiCliCredentialsCached as r, readCodexCliActiveApiKey as t };