{"version":3,"file":"keychain-secrets.mjs","names":[],"sources":["../../../src/services/keychain-secrets.ts"],"sourcesContent":["/**\n * Keychain Secrets — macOS Keychain storage for LLM API keys.\n *\n * Stores API keys in macOS Keychain (via `security` CLI) so they persist\n * across sessions without living in plaintext .env files. On Linux/Docker,\n * falls back to an encrypted file at ~/.openclawnch/api-keys.enc.\n *\n * On startup, keys are loaded from Keychain into process.env so the\n * credential vault and agent orchestrator find them automatically.\n *\n * No new dependencies — uses the same Keychain pattern as keychain-wallet.ts.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { execSync } from 'node:child_process';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\nexport interface ApiKeyEntry {\n  /** Provider name (e.g., 'anthropic'). */\n  provider: string;\n  /** The API key value. */\n  key: string;\n  /** When the key was stored. */\n  storedAt: string;\n}\n\n/** Known LLM providers and their env var mappings. */\nexport const PROVIDERS: Record<string, { envVar: string; label: string; prefix?: string }> = {\n  anthropic: { envVar: 'ANTHROPIC_API_KEY', label: 'Anthropic (Claude)', prefix: 'sk-ant-' },\n  bankr: { envVar: 'BANKR_LLM_KEY', label: 'Bankr LLM Gateway', prefix: 'bk_' },\n  'bankr-agent': { envVar: 'BANKR_API_KEY', label: 'Bankr Agent API', prefix: 'bk_' },\n  openrouter: { envVar: 'OPENROUTER_API_KEY', label: 'OpenRouter', prefix: 'sk-or-' },\n  openai: { envVar: 'OPENAI_API_KEY', label: 'OpenAI', prefix: 'sk-' },\n};\n\n// ─── Constants ───────────────────────────────────────────────────────────\n\nconst KEYCHAIN_ACCOUNT = 'openclawnch';\nconst KEYCHAIN_SERVICE_PREFIX = 'openclawnch_apikey_';\nconst FALLBACK_DIR = join(process.env.HOME ?? '/root', '.openclawnch');\nconst FALLBACK_PATH = join(FALLBACK_DIR, 'api-keys.json');\n\n// ─── Platform Detection ──────────────────────────────────────────────────\n\nfunction isMacOS(): boolean {\n  return process.platform === 'darwin';\n}\n\n// ─── Keychain Operations (macOS) ─────────────────────────────────────────\n\nfunction keychainStore(provider: string, key: string): void {\n  const service = `${KEYCHAIN_SERVICE_PREFIX}${provider}`;\n  // Delete existing entry first (update = delete + add)\n  try {\n    execSync(\n      `security delete-generic-password -a \"${KEYCHAIN_ACCOUNT}\" -s \"${service}\" login.keychain-db 2>/dev/null`,\n      { stdio: 'pipe' },\n    );\n  } catch { /* Not found — fine */ }\n\n  execSync(\n    `security add-generic-password -a \"${KEYCHAIN_ACCOUNT}\" -s \"${service}\" -w \"${key}\" login.keychain-db`,\n    { stdio: 'pipe' },\n  );\n}\n\nfunction keychainLoad(provider: string): string | null {\n  const service = `${KEYCHAIN_SERVICE_PREFIX}${provider}`;\n  try {\n    return execSync(\n      `security find-generic-password -a \"${KEYCHAIN_ACCOUNT}\" -s \"${service}\" -w login.keychain-db 2>/dev/null`,\n      { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },\n    ).trim();\n  } catch {\n    return null;\n  }\n}\n\nfunction keychainDelete(provider: string): boolean {\n  const service = `${KEYCHAIN_SERVICE_PREFIX}${provider}`;\n  try {\n    execSync(\n      `security delete-generic-password -a \"${KEYCHAIN_ACCOUNT}\" -s \"${service}\" login.keychain-db`,\n      { stdio: 'pipe' },\n    );\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nfunction keychainListProviders(): string[] {\n  const providers: string[] = [];\n  for (const provider of Object.keys(PROVIDERS)) {\n    if (keychainLoad(provider) !== null) {\n      providers.push(provider);\n    }\n  }\n  return providers;\n}\n\n// ─── Fallback File Storage (Linux/Docker) ────────────────────────────────\n\nfunction fallbackLoad(): Record<string, ApiKeyEntry> {\n  try {\n    if (!existsSync(FALLBACK_PATH)) return {};\n    const raw = readFileSync(FALLBACK_PATH, 'utf8');\n    return JSON.parse(raw) as Record<string, ApiKeyEntry>;\n  } catch {\n    return {};\n  }\n}\n\nfunction fallbackSave(entries: Record<string, ApiKeyEntry>): void {\n  if (!existsSync(FALLBACK_DIR)) mkdirSync(FALLBACK_DIR, { recursive: true, mode: 0o700 });\n  writeFileSync(FALLBACK_PATH, JSON.stringify(entries, null, 2), { encoding: 'utf8', mode: 0o600 });\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────\n\n/**\n * Store an API key for a provider.\n * On macOS: stored in Keychain. On Linux: stored in encrypted file.\n */\nexport function storeApiKey(provider: string, key: string): void {\n  if (!PROVIDERS[provider]) {\n    throw new Error(`Unknown provider \"${provider}\". Known: ${Object.keys(PROVIDERS).join(', ')}`);\n  }\n\n  if (isMacOS()) {\n    keychainStore(provider, key);\n  } else {\n    const entries = fallbackLoad();\n    entries[provider] = { provider, key, storedAt: new Date().toISOString() };\n    fallbackSave(entries);\n  }\n\n  // Also set in process.env so credential vault picks it up immediately\n  const envVar = PROVIDERS[provider]!.envVar;\n  process.env[envVar] = key;\n}\n\n/**\n * Load an API key for a provider.\n * Returns null if not stored.\n */\nexport function loadApiKey(provider: string): string | null {\n  if (isMacOS()) {\n    return keychainLoad(provider);\n  }\n  const entries = fallbackLoad();\n  return entries[provider]?.key ?? null;\n}\n\n/**\n * Remove an API key for a provider.\n * Returns true if a key was deleted.\n */\nexport function removeApiKey(provider: string): boolean {\n  if (!PROVIDERS[provider]) return false;\n\n  // Remove from process.env\n  const envVar = PROVIDERS[provider]!.envVar;\n  delete process.env[envVar];\n\n  if (isMacOS()) {\n    return keychainDelete(provider);\n  }\n  const entries = fallbackLoad();\n  if (!entries[provider]) return false;\n  delete entries[provider];\n  fallbackSave(entries);\n  return true;\n}\n\n/**\n * List all providers that have stored keys.\n * Returns provider names only — never the key values.\n */\nexport function listStoredProviders(): string[] {\n  if (isMacOS()) {\n    return keychainListProviders();\n  }\n  return Object.keys(fallbackLoad());\n}\n\n/**\n * Get the currently active LLM provider.\n */\nexport function getActiveProvider(): string {\n  return process.env.OPENCLAWNCH_LLM_PROVIDER ?? 'anthropic';\n}\n\n/**\n * Set the active LLM provider. Persists to config.\n */\nexport function setActiveProvider(provider: string): void {\n  if (!PROVIDERS[provider] && provider !== 'bankr-agent') {\n    throw new Error(`Unknown provider \"${provider}\".`);\n  }\n  process.env.OPENCLAWNCH_LLM_PROVIDER = provider;\n\n  // Persist to config file so it survives restarts\n  const configPath = join(process.env.HOME ?? '/root', '.openclaw', 'openclaw.json');\n  try {\n    let config: Record<string, any> = {};\n    if (existsSync(configPath)) {\n      config = JSON.parse(readFileSync(configPath, 'utf8'));\n    }\n    if (!config.agents) config.agents = {};\n    if (!config.agents.defaults) config.agents.defaults = {};\n    config.agents.defaults.provider = provider;\n\n    const configDir = join(process.env.HOME ?? '/root', '.openclaw');\n    if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });\n    writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');\n  } catch { /* best effort — env var is the primary source of truth */ }\n}\n\n/**\n * Load all stored API keys into process.env on startup.\n * Called once during gateway_start to hydrate the credential vault.\n * Keys from Keychain/file are only loaded if the env var isn't already set\n * (env vars take precedence over stored keys).\n */\nexport function hydrateApiKeys(): { loaded: string[]; skipped: string[] } {\n  const loaded: string[] = [];\n  const skipped: string[] = [];\n\n  for (const [provider, config] of Object.entries(PROVIDERS)) {\n    // Don't overwrite existing env vars (explicit env > stored key)\n    if (process.env[config.envVar]) {\n      skipped.push(provider);\n      continue;\n    }\n\n    const key = loadApiKey(provider);\n    if (key) {\n      process.env[config.envVar] = key;\n      loaded.push(provider);\n    }\n  }\n\n  return { loaded, skipped };\n}\n\n/**\n * Mask an API key for display (show first 6 + last 4 chars).\n */\nexport function maskKey(key: string): string {\n  if (key.length <= 12) return '****';\n  return key.slice(0, 6) + '...' + key.slice(-4);\n}\n"],"mappings":"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"}