Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
233 KiB
JSON

{"repo": "arul28/ade", "files": {"apps/desktop/src/main/services/ai/apiKeyStore.ts": {"249499766d1c5fb237ff36dc83d227d698c5b8b6": {"t": 1785745571.6166008, "n": 21829, "h": "43ac563a0060aea1", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport type { SyncCredentialStore } from \"../../../../../ade-cli/src/services/credentials/credentialStore\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyCredentialStore = SyncCredentialStore;\n\nexport type InitApiKeyStoreOptions = {\n credentialStore?: ApiKeyCredentialStore | null;\n};\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n moonshotai: \"MOONSHOT_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY = \"ai.credentials.legacy_keychain_migrated.v1\";\nconst CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY = \"ai.credentials.legacy_projects_migrated.v1\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\nconst CREDENTIAL_PROVIDER_INDEX_KEY = \"ai.api_key.index.v1\";\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet projectRootPath: string | null = null;\nlet credentialStore: ApiKeyCredentialStore | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\nlet missingCredentialProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n if (credentialStore) return true;\n return isSecureStorageAvailable();\n}\n\nfunction normalizeProvider(provider: string): string {\n return provider.trim().toLowerCase();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction credentialProviderKey(provider: string): string {\n return `ai.api_key.${provider}.v1`;\n}\n\nfunction getSyncCredentialStore(): SyncCredentialStore | null {\n return credentialStore;\n}\n\nfunction readCredentialSecret(key: string): string | null {\n const store = getSyncCredentialStore();\n if (!store) return null;\n try {\n const value = store.getSync(key);\n decryptionFailed = false;\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed.length ? trimmed : null;\n } catch {\n decryptionFailed = true;\n return null;\n }\n}\n\nfunction writeCredentialSecret(key: string, value: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.setSync(key, value);\n decryptionFailed = false;\n}\n\nfunction deleteCredentialSecret(key: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.deleteSync(key);\n decryptionFailed = false;\n}\n\nfunction readCredentialProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeCredentialProviderIndex(providers: Iterable<string>): void {\n writeCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY, JSON.stringify(normalizeProviderList(Array.from(providers))));\n}\n\nfunction readCredentialLegacyMigratedProjectRoots(): Set<string> {\n const raw = readCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY);\n if (!raw) return new Set();\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n const roots = parsed\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => path.resolve(entry));\n return new Set(roots);\n } catch {\n return new Set();\n }\n}\n\nfunction writeCredentialLegacyMigratedProjectRoots(projectRoots: Iterable<string>): void {\n const normalized = Array.from(new Set(Array.from(projectRoots).map((entry) => path.resolve(entry)))).sort();\n writeCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY, JSON.stringify(normalized));\n}\n\nfunction isCredentialLegacyKeychainMigrated(): boolean {\n return Boolean(readCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY));\n}\n\nfunction markCredentialLegacyKeychainMigrated(): void {\n writeCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY, new Date().toISOString());\n}\n\nfunction readCredentialStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n for (const provider of providerCandidates) {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) continue;\n const value = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (value) out[normalizedProvider] = value;\n }\n return out;\n}\n\nfunction mergeLegacyValuesIntoCredentialStore(\n currentStore: StoredKeys,\n legacyStore: StoredKeys,\n): StoredKeys {\n const nextStore = { ...currentStore };\n const changedProviders = new Set<string>();\n for (const [provider, rawValue] of Object.entries(legacyStore)) {\n const normalizedProvider = normalizeProvider(provider);\n const value = rawValue.trim();\n if (!normalizedProvider.length || !value.length || nextStore[normalizedProvider]) continue;\n writeCredentialSecret(credentialProviderKey(normalizedProvider), value);\n nextStore[normalizedProvider] = value;\n changedProviders.add(normalizedProvider);\n }\n if (changedProviders.size) {\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, ...Object.keys(nextStore)]));\n }\n return nextStore;\n}\n\nfunction migrateLegacyProjectStoreIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (!projectRootPath) return currentStore;\n const migratedProjectRoots = readCredentialLegacyMigratedProjectRoots();\n const normalizedProjectRoot = path.resolve(projectRootPath);\n if (migratedProjectRoots.has(normalizedProjectRoot)) return currentStore;\n\n const hadEncryptedStore = Boolean(storePath && fs.existsSync(storePath));\n const legacyStore = loadEncryptedStore();\n const migrationComplete = !hadEncryptedStore || !decryptionFailed;\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, legacyStore);\n\n if (migrationComplete) {\n migratedProjectRoots.add(normalizedProjectRoot);\n writeCredentialLegacyMigratedProjectRoots(migratedProjectRoots);\n }\n return nextStore;\n}\n\nfunction migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (isCredentialLegacyKeychainMigrated()) return currentStore;\n if (!isMacosKeychainAvailable()) return currentStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = new Set([\n ...index.providers,\n ...Object.keys(ENV_KEY_PROVIDERS),\n ]);\n const keychainStore = readMacosKeychainStore(providerCandidates);\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);\n markCredentialLegacyKeychainMigrated();\n return nextStore;\n}\n\nfunction migrateLegacyStoresIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n let nextStore = currentStore;\n try {\n nextStore = migrateLegacyKeychainIntoCredentialStore(nextStore);\n } catch {\n // Keep the machine credential store usable even if a legacy Keychain read\n // or write is blocked. The legacy copy remains available for a later retry.\n }\n try {\n nextStore = migrateLegacyProjectStoreIntoCredentialStore(nextStore);\n } catch {\n // Best effort. A failed migration must not block app or runtime startup.\n }\n return nextStore;\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction canPersistEncryptedStore(): boolean {\n return Boolean(storePath) && isSecureStorageAvailable();\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction deleteMacosKeychainSecretBestEffort(account: string): void {\n try {\n deleteMacosKeychainSecret(account);\n } catch {\n // Legacy Keychain cleanup should not block writes to the active encrypted\n // store. A stale Keychain copy is still superseded by the encrypted value.\n }\n}\n\nfunction migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore: StoredKeys): StoredKeys {\n if (!canPersistEncryptedStore() || !isMacosKeychainAvailable()) return encryptedStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = index.exists ? index.providers : Object.keys(encryptedStore);\n if (providerCandidates.length === 0) return encryptedStore;\n\n const keychainStore = readMacosKeychainStore(providerCandidates);\n if (Object.keys(keychainStore).length === 0) return encryptedStore;\n\n const nextStore = { ...keychainStore, ...encryptedStore };\n persistEncryptedStore(nextStore);\n decryptionFailed = false;\n return nextStore;\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (credentialStore) {\n const index = readCredentialProviderIndex();\n const credentialValues = index.exists ? readCredentialStore(index.providers) : {};\n cache = migrateLegacyStoresIntoCredentialStore(credentialValues);\n return cache;\n }\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n if (canPersistEncryptedStore()) {\n cache = migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore);\n return cache;\n }\n\n const index = readMacosKeychainProviderIndex();\n cache = index.exists ? readMacosKeychainStore(index.providers) : encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string, options: InitApiKeyStoreOptions = {}): void {\n const layout = resolveAdeLayout(projectRoot);\n projectRootPath = path.resolve(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n credentialStore = options.credentialStore ?? null;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n missingCredentialProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: credentialStore ? null : storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingCredentialProviders.delete(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (credentialStore && !missingCredentialProviders.has(normalizedProvider)) {\n const credentialValue = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (credentialValue) {\n store[normalizedProvider] = credentialValue;\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return credentialValue;\n }\n missingCredentialProviders.add(normalizedProvider);\n }\n const allowLegacyKeychainFallback =\n !credentialStore || !isCredentialLegacyKeychainMigrated();\n if (allowLegacyKeychainFallback && isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), keychainValue);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n } else if (canPersistEncryptedStore()) {\n persistEncryptedStore(store);\n }\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (credentialStore) {\n deleteCredentialSecret(credentialProviderKey(normalizedProvider));\n delete store[normalizedProvider];\n missingCredentialProviders.add(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n if (canPersistEncryptedStore()) {\n persistEncryptedStore(nextStore);\n }\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "fdf734fce297c5e728d7a8531dee524777979e10": {"t": 1785754348.972935, "n": 21829, "h": "43ac563a0060aea1", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport type { SyncCredentialStore } from \"../../../../../ade-cli/src/services/credentials/credentialStore\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyCredentialStore = SyncCredentialStore;\n\nexport type InitApiKeyStoreOptions = {\n credentialStore?: ApiKeyCredentialStore | null;\n};\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n moonshotai: \"MOONSHOT_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY = \"ai.credentials.legacy_keychain_migrated.v1\";\nconst CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY = \"ai.credentials.legacy_projects_migrated.v1\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\nconst CREDENTIAL_PROVIDER_INDEX_KEY = \"ai.api_key.index.v1\";\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet projectRootPath: string | null = null;\nlet credentialStore: ApiKeyCredentialStore | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\nlet missingCredentialProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n if (credentialStore) return true;\n return isSecureStorageAvailable();\n}\n\nfunction normalizeProvider(provider: string): string {\n return provider.trim().toLowerCase();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction credentialProviderKey(provider: string): string {\n return `ai.api_key.${provider}.v1`;\n}\n\nfunction getSyncCredentialStore(): SyncCredentialStore | null {\n return credentialStore;\n}\n\nfunction readCredentialSecret(key: string): string | null {\n const store = getSyncCredentialStore();\n if (!store) return null;\n try {\n const value = store.getSync(key);\n decryptionFailed = false;\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed.length ? trimmed : null;\n } catch {\n decryptionFailed = true;\n return null;\n }\n}\n\nfunction writeCredentialSecret(key: string, value: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.setSync(key, value);\n decryptionFailed = false;\n}\n\nfunction deleteCredentialSecret(key: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.deleteSync(key);\n decryptionFailed = false;\n}\n\nfunction readCredentialProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeCredentialProviderIndex(providers: Iterable<string>): void {\n writeCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY, JSON.stringify(normalizeProviderList(Array.from(providers))));\n}\n\nfunction readCredentialLegacyMigratedProjectRoots(): Set<string> {\n const raw = readCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY);\n if (!raw) return new Set();\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n const roots = parsed\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => path.resolve(entry));\n return new Set(roots);\n } catch {\n return new Set();\n }\n}\n\nfunction writeCredentialLegacyMigratedProjectRoots(projectRoots: Iterable<string>): void {\n const normalized = Array.from(new Set(Array.from(projectRoots).map((entry) => path.resolve(entry)))).sort();\n writeCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY, JSON.stringify(normalized));\n}\n\nfunction isCredentialLegacyKeychainMigrated(): boolean {\n return Boolean(readCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY));\n}\n\nfunction markCredentialLegacyKeychainMigrated(): void {\n writeCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY, new Date().toISOString());\n}\n\nfunction readCredentialStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n for (const provider of providerCandidates) {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) continue;\n const value = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (value) out[normalizedProvider] = value;\n }\n return out;\n}\n\nfunction mergeLegacyValuesIntoCredentialStore(\n currentStore: StoredKeys,\n legacyStore: StoredKeys,\n): StoredKeys {\n const nextStore = { ...currentStore };\n const changedProviders = new Set<string>();\n for (const [provider, rawValue] of Object.entries(legacyStore)) {\n const normalizedProvider = normalizeProvider(provider);\n const value = rawValue.trim();\n if (!normalizedProvider.length || !value.length || nextStore[normalizedProvider]) continue;\n writeCredentialSecret(credentialProviderKey(normalizedProvider), value);\n nextStore[normalizedProvider] = value;\n changedProviders.add(normalizedProvider);\n }\n if (changedProviders.size) {\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, ...Object.keys(nextStore)]));\n }\n return nextStore;\n}\n\nfunction migrateLegacyProjectStoreIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (!projectRootPath) return currentStore;\n const migratedProjectRoots = readCredentialLegacyMigratedProjectRoots();\n const normalizedProjectRoot = path.resolve(projectRootPath);\n if (migratedProjectRoots.has(normalizedProjectRoot)) return currentStore;\n\n const hadEncryptedStore = Boolean(storePath && fs.existsSync(storePath));\n const legacyStore = loadEncryptedStore();\n const migrationComplete = !hadEncryptedStore || !decryptionFailed;\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, legacyStore);\n\n if (migrationComplete) {\n migratedProjectRoots.add(normalizedProjectRoot);\n writeCredentialLegacyMigratedProjectRoots(migratedProjectRoots);\n }\n return nextStore;\n}\n\nfunction migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (isCredentialLegacyKeychainMigrated()) return currentStore;\n if (!isMacosKeychainAvailable()) return currentStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = new Set([\n ...index.providers,\n ...Object.keys(ENV_KEY_PROVIDERS),\n ]);\n const keychainStore = readMacosKeychainStore(providerCandidates);\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);\n markCredentialLegacyKeychainMigrated();\n return nextStore;\n}\n\nfunction migrateLegacyStoresIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n let nextStore = currentStore;\n try {\n nextStore = migrateLegacyKeychainIntoCredentialStore(nextStore);\n } catch {\n // Keep the machine credential store usable even if a legacy Keychain read\n // or write is blocked. The legacy copy remains available for a later retry.\n }\n try {\n nextStore = migrateLegacyProjectStoreIntoCredentialStore(nextStore);\n } catch {\n // Best effort. A failed migration must not block app or runtime startup.\n }\n return nextStore;\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction canPersistEncryptedStore(): boolean {\n return Boolean(storePath) && isSecureStorageAvailable();\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction deleteMacosKeychainSecretBestEffort(account: string): void {\n try {\n deleteMacosKeychainSecret(account);\n } catch {\n // Legacy Keychain cleanup should not block writes to the active encrypted\n // store. A stale Keychain copy is still superseded by the encrypted value.\n }\n}\n\nfunction migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore: StoredKeys): StoredKeys {\n if (!canPersistEncryptedStore() || !isMacosKeychainAvailable()) return encryptedStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = index.exists ? index.providers : Object.keys(encryptedStore);\n if (providerCandidates.length === 0) return encryptedStore;\n\n const keychainStore = readMacosKeychainStore(providerCandidates);\n if (Object.keys(keychainStore).length === 0) return encryptedStore;\n\n const nextStore = { ...keychainStore, ...encryptedStore };\n persistEncryptedStore(nextStore);\n decryptionFailed = false;\n return nextStore;\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (credentialStore) {\n const index = readCredentialProviderIndex();\n const credentialValues = index.exists ? readCredentialStore(index.providers) : {};\n cache = migrateLegacyStoresIntoCredentialStore(credentialValues);\n return cache;\n }\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n if (canPersistEncryptedStore()) {\n cache = migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore);\n return cache;\n }\n\n const index = readMacosKeychainProviderIndex();\n cache = index.exists ? readMacosKeychainStore(index.providers) : encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string, options: InitApiKeyStoreOptions = {}): void {\n const layout = resolveAdeLayout(projectRoot);\n projectRootPath = path.resolve(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n credentialStore = options.credentialStore ?? null;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n missingCredentialProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: credentialStore ? null : storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingCredentialProviders.delete(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (credentialStore && !missingCredentialProviders.has(normalizedProvider)) {\n const credentialValue = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (credentialValue) {\n store[normalizedProvider] = credentialValue;\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return credentialValue;\n }\n missingCredentialProviders.add(normalizedProvider);\n }\n const allowLegacyKeychainFallback =\n !credentialStore || !isCredentialLegacyKeychainMigrated();\n if (allowLegacyKeychainFallback && isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), keychainValue);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n } else if (canPersistEncryptedStore()) {\n persistEncryptedStore(store);\n }\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (credentialStore) {\n deleteCredentialSecret(credentialProviderKey(normalizedProvider));\n delete store[normalizedProvider];\n missingCredentialProviders.add(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n if (canPersistEncryptedStore()) {\n persistEncryptedStore(nextStore);\n }\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "8fdf95ab2051fed8a116feec1595f40601fc737a": {"t": 1785754350.4442177, "n": 21795, "h": "3d35afaaa01ad6f6", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport type { SyncCredentialStore } from \"../../../../../ade-cli/src/services/credentials/credentialStore\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyCredentialStore = SyncCredentialStore;\n\nexport type InitApiKeyStoreOptions = {\n credentialStore?: ApiKeyCredentialStore | null;\n};\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY = \"ai.credentials.legacy_keychain_migrated.v1\";\nconst CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY = \"ai.credentials.legacy_projects_migrated.v1\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\nconst CREDENTIAL_PROVIDER_INDEX_KEY = \"ai.api_key.index.v1\";\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet projectRootPath: string | null = null;\nlet credentialStore: ApiKeyCredentialStore | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\nlet missingCredentialProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n if (credentialStore) return true;\n return isSecureStorageAvailable();\n}\n\nfunction normalizeProvider(provider: string): string {\n return provider.trim().toLowerCase();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction credentialProviderKey(provider: string): string {\n return `ai.api_key.${provider}.v1`;\n}\n\nfunction getSyncCredentialStore(): SyncCredentialStore | null {\n return credentialStore;\n}\n\nfunction readCredentialSecret(key: string): string | null {\n const store = getSyncCredentialStore();\n if (!store) return null;\n try {\n const value = store.getSync(key);\n decryptionFailed = false;\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed.length ? trimmed : null;\n } catch {\n decryptionFailed = true;\n return null;\n }\n}\n\nfunction writeCredentialSecret(key: string, value: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.setSync(key, value);\n decryptionFailed = false;\n}\n\nfunction deleteCredentialSecret(key: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.deleteSync(key);\n decryptionFailed = false;\n}\n\nfunction readCredentialProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeCredentialProviderIndex(providers: Iterable<string>): void {\n writeCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY, JSON.stringify(normalizeProviderList(Array.from(providers))));\n}\n\nfunction readCredentialLegacyMigratedProjectRoots(): Set<string> {\n const raw = readCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY);\n if (!raw) return new Set();\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n const roots = parsed\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => path.resolve(entry));\n return new Set(roots);\n } catch {\n return new Set();\n }\n}\n\nfunction writeCredentialLegacyMigratedProjectRoots(projectRoots: Iterable<string>): void {\n const normalized = Array.from(new Set(Array.from(projectRoots).map((entry) => path.resolve(entry)))).sort();\n writeCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY, JSON.stringify(normalized));\n}\n\nfunction isCredentialLegacyKeychainMigrated(): boolean {\n return Boolean(readCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY));\n}\n\nfunction markCredentialLegacyKeychainMigrated(): void {\n writeCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY, new Date().toISOString());\n}\n\nfunction readCredentialStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n for (const provider of providerCandidates) {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) continue;\n const value = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (value) out[normalizedProvider] = value;\n }\n return out;\n}\n\nfunction mergeLegacyValuesIntoCredentialStore(\n currentStore: StoredKeys,\n legacyStore: StoredKeys,\n): StoredKeys {\n const nextStore = { ...currentStore };\n const changedProviders = new Set<string>();\n for (const [provider, rawValue] of Object.entries(legacyStore)) {\n const normalizedProvider = normalizeProvider(provider);\n const value = rawValue.trim();\n if (!normalizedProvider.length || !value.length || nextStore[normalizedProvider]) continue;\n writeCredentialSecret(credentialProviderKey(normalizedProvider), value);\n nextStore[normalizedProvider] = value;\n changedProviders.add(normalizedProvider);\n }\n if (changedProviders.size) {\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, ...Object.keys(nextStore)]));\n }\n return nextStore;\n}\n\nfunction migrateLegacyProjectStoreIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (!projectRootPath) return currentStore;\n const migratedProjectRoots = readCredentialLegacyMigratedProjectRoots();\n const normalizedProjectRoot = path.resolve(projectRootPath);\n if (migratedProjectRoots.has(normalizedProjectRoot)) return currentStore;\n\n const hadEncryptedStore = Boolean(storePath && fs.existsSync(storePath));\n const legacyStore = loadEncryptedStore();\n const migrationComplete = !hadEncryptedStore || !decryptionFailed;\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, legacyStore);\n\n if (migrationComplete) {\n migratedProjectRoots.add(normalizedProjectRoot);\n writeCredentialLegacyMigratedProjectRoots(migratedProjectRoots);\n }\n return nextStore;\n}\n\nfunction migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (isCredentialLegacyKeychainMigrated()) return currentStore;\n if (!isMacosKeychainAvailable()) return currentStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = new Set([\n ...index.providers,\n ...Object.keys(ENV_KEY_PROVIDERS),\n ]);\n const keychainStore = readMacosKeychainStore(providerCandidates);\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);\n markCredentialLegacyKeychainMigrated();\n return nextStore;\n}\n\nfunction migrateLegacyStoresIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n let nextStore = currentStore;\n try {\n nextStore = migrateLegacyKeychainIntoCredentialStore(nextStore);\n } catch {\n // Keep the machine credential store usable even if a legacy Keychain read\n // or write is blocked. The legacy copy remains available for a later retry.\n }\n try {\n nextStore = migrateLegacyProjectStoreIntoCredentialStore(nextStore);\n } catch {\n // Best effort. A failed migration must not block app or runtime startup.\n }\n return nextStore;\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction canPersistEncryptedStore(): boolean {\n return Boolean(storePath) && isSecureStorageAvailable();\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction deleteMacosKeychainSecretBestEffort(account: string): void {\n try {\n deleteMacosKeychainSecret(account);\n } catch {\n // Legacy Keychain cleanup should not block writes to the active encrypted\n // store. A stale Keychain copy is still superseded by the encrypted value.\n }\n}\n\nfunction migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore: StoredKeys): StoredKeys {\n if (!canPersistEncryptedStore() || !isMacosKeychainAvailable()) return encryptedStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = index.exists ? index.providers : Object.keys(encryptedStore);\n if (providerCandidates.length === 0) return encryptedStore;\n\n const keychainStore = readMacosKeychainStore(providerCandidates);\n if (Object.keys(keychainStore).length === 0) return encryptedStore;\n\n const nextStore = { ...keychainStore, ...encryptedStore };\n persistEncryptedStore(nextStore);\n decryptionFailed = false;\n return nextStore;\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (credentialStore) {\n const index = readCredentialProviderIndex();\n const credentialValues = index.exists ? readCredentialStore(index.providers) : {};\n cache = migrateLegacyStoresIntoCredentialStore(credentialValues);\n return cache;\n }\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n if (canPersistEncryptedStore()) {\n cache = migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore);\n return cache;\n }\n\n const index = readMacosKeychainProviderIndex();\n cache = index.exists ? readMacosKeychainStore(index.providers) : encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string, options: InitApiKeyStoreOptions = {}): void {\n const layout = resolveAdeLayout(projectRoot);\n projectRootPath = path.resolve(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n credentialStore = options.credentialStore ?? null;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n missingCredentialProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: credentialStore ? null : storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingCredentialProviders.delete(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (credentialStore && !missingCredentialProviders.has(normalizedProvider)) {\n const credentialValue = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (credentialValue) {\n store[normalizedProvider] = credentialValue;\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return credentialValue;\n }\n missingCredentialProviders.add(normalizedProvider);\n }\n const allowLegacyKeychainFallback =\n !credentialStore || !isCredentialLegacyKeychainMigrated();\n if (allowLegacyKeychainFallback && isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), keychainValue);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n } else if (canPersistEncryptedStore()) {\n persistEncryptedStore(store);\n }\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (credentialStore) {\n deleteCredentialSecret(credentialProviderKey(normalizedProvider));\n delete store[normalizedProvider];\n missingCredentialProviders.add(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n if (canPersistEncryptedStore()) {\n persistEncryptedStore(nextStore);\n }\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "6bd0a115e89734f8dabf113f501025502285a925": {"t": 1785754351.7704885, "n": 23533, "h": "e15620bfb1d89cca", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport type { SyncCredentialStore } from \"../../../../../ade-cli/src/services/credentials/credentialStore\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyCredentialStore = SyncCredentialStore;\n\nexport type InitApiKeyStoreOptions = {\n credentialStore?: ApiKeyCredentialStore | null;\n};\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY = \"ai.credentials.legacy_keychain_migrated.v1\";\nconst CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY = \"ai.credentials.legacy_projects_migrated.v1\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\nconst CREDENTIAL_PROVIDER_INDEX_KEY = \"ai.api_key.index.v1\";\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet projectRootPath: string | null = null;\nlet credentialStore: ApiKeyCredentialStore | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\nlet missingCredentialProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n if (credentialStore) return true;\n return isMacosKeychainAvailable() || isSecureStorageAvailable();\n}\n\nfunction normalizeProvider(provider: string): string {\n return provider.trim().toLowerCase();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction writeMacosKeychainSecret(account: string, value: string): void {\n if (!isMacosKeychainAvailable()) {\n throw new Error(\"macOS Keychain is unavailable. Cannot persist API keys.\");\n }\n // `/usr/bin/security add-generic-password` does not support a non-interactive\n // stdin password sentinel: `-w -` stores a literal dash. Keep this path\n // synchronous and tightly scoped until we replace it with a native binding.\n const result = runSecurity([\n \"add-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n value,\n \"-U\",\n ]);\n if (!result.ok) {\n rememberKeychainError(\"write\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain write failed.\");\n }\n clearKeychainError();\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction credentialProviderKey(provider: string): string {\n return `ai.api_key.${provider}.v1`;\n}\n\nfunction getSyncCredentialStore(): SyncCredentialStore | null {\n return credentialStore;\n}\n\nfunction readCredentialSecret(key: string): string | null {\n const store = getSyncCredentialStore();\n if (!store) return null;\n try {\n const value = store.getSync(key);\n decryptionFailed = false;\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed.length ? trimmed : null;\n } catch {\n decryptionFailed = true;\n return null;\n }\n}\n\nfunction writeCredentialSecret(key: string, value: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.setSync(key, value);\n decryptionFailed = false;\n}\n\nfunction deleteCredentialSecret(key: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.deleteSync(key);\n decryptionFailed = false;\n}\n\nfunction readCredentialProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeCredentialProviderIndex(providers: Iterable<string>): void {\n writeCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY, JSON.stringify(normalizeProviderList(Array.from(providers))));\n}\n\nfunction readCredentialLegacyMigratedProjectRoots(): Set<string> {\n const raw = readCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY);\n if (!raw) return new Set();\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n const roots = parsed\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => path.resolve(entry));\n return new Set(roots);\n } catch {\n return new Set();\n }\n}\n\nfunction writeCredentialLegacyMigratedProjectRoots(projectRoots: Iterable<string>): void {\n const normalized = Array.from(new Set(Array.from(projectRoots).map((entry) => path.resolve(entry)))).sort();\n writeCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY, JSON.stringify(normalized));\n}\n\nfunction isCredentialLegacyKeychainMigrated(): boolean {\n return Boolean(readCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY));\n}\n\nfunction markCredentialLegacyKeychainMigrated(): void {\n writeCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY, new Date().toISOString());\n}\n\nfunction readCredentialStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n for (const provider of providerCandidates) {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) continue;\n const value = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (value) out[normalizedProvider] = value;\n }\n return out;\n}\n\nfunction mergeLegacyValuesIntoCredentialStore(\n currentStore: StoredKeys,\n legacyStore: StoredKeys,\n): StoredKeys {\n const nextStore = { ...currentStore };\n const changedProviders = new Set<string>();\n for (const [provider, rawValue] of Object.entries(legacyStore)) {\n const normalizedProvider = normalizeProvider(provider);\n const value = rawValue.trim();\n if (!normalizedProvider.length || !value.length || nextStore[normalizedProvider]) continue;\n writeCredentialSecret(credentialProviderKey(normalizedProvider), value);\n nextStore[normalizedProvider] = value;\n changedProviders.add(normalizedProvider);\n }\n if (changedProviders.size) {\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, ...Object.keys(nextStore)]));\n }\n return nextStore;\n}\n\nfunction migrateLegacyProjectStoreIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (!projectRootPath) return currentStore;\n const migratedProjectRoots = readCredentialLegacyMigratedProjectRoots();\n const normalizedProjectRoot = path.resolve(projectRootPath);\n if (migratedProjectRoots.has(normalizedProjectRoot)) return currentStore;\n\n const hadEncryptedStore = Boolean(storePath && fs.existsSync(storePath));\n const legacyStore = loadEncryptedStore();\n const migrationComplete = !hadEncryptedStore || !decryptionFailed;\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, legacyStore);\n\n if (migrationComplete) {\n migratedProjectRoots.add(normalizedProjectRoot);\n writeCredentialLegacyMigratedProjectRoots(migratedProjectRoots);\n }\n return nextStore;\n}\n\nfunction migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (isCredentialLegacyKeychainMigrated()) return currentStore;\n if (!isMacosKeychainAvailable()) return currentStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = new Set([\n ...index.providers,\n ...Object.keys(ENV_KEY_PROVIDERS),\n ]);\n const keychainStore = readMacosKeychainStore(providerCandidates);\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);\n markCredentialLegacyKeychainMigrated();\n return nextStore;\n}\n\nfunction migrateLegacyStoresIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n let nextStore = currentStore;\n try {\n nextStore = migrateLegacyKeychainIntoCredentialStore(nextStore);\n } catch {\n // Keep the machine credential store usable even if a legacy Keychain read\n // or write is blocked. The legacy copy remains available for a later retry.\n }\n try {\n nextStore = migrateLegacyProjectStoreIntoCredentialStore(nextStore);\n } catch {\n // Best effort. A failed migration must not block app or runtime startup.\n }\n return nextStore;\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeMacosKeychainProviderIndex(providers: Iterable<string>): void {\n writeMacosKeychainSecret(\n MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT,\n JSON.stringify(normalizeProviderList(Array.from(providers))),\n );\n}\n\nfunction tryWriteMacosKeychainProviderIndex(providers: Iterable<string>): void {\n try {\n writeMacosKeychainProviderIndex(providers);\n } catch {\n // The provider index only powers discovery/listing. Keep the just-written\n // secret usable in this process even if the secondary index write fails.\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction migrateEncryptedStoreToMacosKeychain(encryptedStore: StoredKeys): void {\n if (!isMacosKeychainAvailable()) return;\n const providers = Object.keys(encryptedStore);\n if (!providers.length) return;\n\n const existingIndex = readMacosKeychainProviderIndex();\n const nextProviders = new Set<string>(existingIndex.providers);\n let changed = false;\n\n for (const provider of providers) {\n const value = encryptedStore[provider]?.trim();\n if (!value) continue;\n const existingValue = readMacosKeychainSecret(provider);\n if (!existingValue) {\n writeMacosKeychainSecret(provider, value);\n }\n nextProviders.add(provider);\n changed = true;\n }\n\n if (changed) {\n writeMacosKeychainProviderIndex(nextProviders);\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (credentialStore) {\n const index = readCredentialProviderIndex();\n const credentialValues = index.exists ? readCredentialStore(index.providers) : {};\n cache = migrateLegacyStoresIntoCredentialStore(credentialValues);\n return cache;\n }\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n const indexBeforeMigration = readMacosKeychainProviderIndex();\n if (!indexBeforeMigration.exists) {\n try {\n migrateEncryptedStoreToMacosKeychain(encryptedStore);\n } catch {\n // If Keychain migration is blocked, keep the decryptable encrypted\n // store as the active fallback instead of dropping existing keys.\n }\n }\n\n const index = readMacosKeychainProviderIndex();\n if (index.exists) {\n cache = readMacosKeychainStore(index.providers);\n return cache;\n }\n\n // Without an index, avoid probing every known provider synchronously on the\n // Electron main thread. The old encrypted store remains the migration\n // source of truth; direct Keychain reads happen on-demand by provider.\n cache = encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string, options: InitApiKeyStoreOptions = {}): void {\n const layout = resolveAdeLayout(projectRoot);\n projectRootPath = path.resolve(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n credentialStore = options.credentialStore ?? null;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n missingCredentialProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: credentialStore ? null : storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingCredentialProviders.delete(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n if (isMacosKeychainAvailable()) {\n writeMacosKeychainSecret(normalizedProvider, normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingMacosKeychainProviders.delete(normalizedProvider);\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (credentialStore && !missingCredentialProviders.has(normalizedProvider)) {\n const credentialValue = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (credentialValue) {\n store[normalizedProvider] = credentialValue;\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return credentialValue;\n }\n missingCredentialProviders.add(normalizedProvider);\n }\n const allowLegacyKeychainFallback =\n !credentialStore || !isCredentialLegacyKeychainMigrated();\n if (allowLegacyKeychainFallback && isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (credentialStore) {\n deleteCredentialSecret(credentialProviderKey(normalizedProvider));\n delete store[normalizedProvider];\n missingCredentialProviders.add(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n if (isMacosKeychainAvailable()) {\n deleteMacosKeychainSecret(normalizedProvider);\n delete store[normalizedProvider];\n missingMacosKeychainProviders.add(normalizedProvider);\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n persistEncryptedStore(nextStore);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "8638e7023fc17b97f9fbb9bcb1a7e19d4f81ebc2": {"t": 1785754353.272457, "n": 24161, "h": "166846bc7f987237", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyCredentialStore = {\n get?: (key: string) => Promise<string | null> | string | null;\n set?: (key: string, value: string) => Promise<void> | void;\n delete?: (key: string) => Promise<void> | void;\n getSync?: (key: string) => string | null;\n setSync?: (key: string, value: string) => void;\n deleteSync?: (key: string) => void;\n};\n\nexport type InitApiKeyStoreOptions = {\n credentialStore?: ApiKeyCredentialStore | null;\n};\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY = \"ai.credentials.legacy_keychain_migrated.v1\";\nconst CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY = \"ai.credentials.legacy_projects_migrated.v1\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\nconst CREDENTIAL_PROVIDER_INDEX_KEY = \"ai.api_key.index.v1\";\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet projectRootPath: string | null = null;\nlet credentialStore: ApiKeyCredentialStore | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\nlet missingCredentialProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n if (credentialStore) return true;\n return isMacosKeychainAvailable() || isSecureStorageAvailable();\n}\n\nfunction normalizeProvider(provider: string): string {\n return provider.trim().toLowerCase();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction writeMacosKeychainSecret(account: string, value: string): void {\n if (!isMacosKeychainAvailable()) {\n throw new Error(\"macOS Keychain is unavailable. Cannot persist API keys.\");\n }\n // `/usr/bin/security add-generic-password` does not support a non-interactive\n // stdin password sentinel: `-w -` stores a literal dash. Keep this path\n // synchronous and tightly scoped until we replace it with a native binding.\n const result = runSecurity([\n \"add-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n value,\n \"-U\",\n ]);\n if (!result.ok) {\n rememberKeychainError(\"write\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain write failed.\");\n }\n clearKeychainError();\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction credentialProviderKey(provider: string): string {\n return `ai.api_key.${provider}.v1`;\n}\n\nfunction getSyncCredentialStore(): Required<Pick<ApiKeyCredentialStore, \"getSync\" | \"setSync\" | \"deleteSync\">> | null {\n if (!credentialStore) return null;\n if (\n typeof credentialStore.getSync === \"function\"\n && typeof credentialStore.setSync === \"function\"\n && typeof credentialStore.deleteSync === \"function\"\n ) {\n return credentialStore as Required<Pick<ApiKeyCredentialStore, \"getSync\" | \"setSync\" | \"deleteSync\">>;\n }\n throw new Error(\"API key credentialStore must provide getSync, setSync, and deleteSync.\");\n}\n\nfunction readCredentialSecret(key: string): string | null {\n const store = getSyncCredentialStore();\n if (!store) return null;\n try {\n const value = store.getSync(key);\n decryptionFailed = false;\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed.length ? trimmed : null;\n } catch {\n decryptionFailed = true;\n return null;\n }\n}\n\nfunction writeCredentialSecret(key: string, value: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.setSync(key, value);\n decryptionFailed = false;\n}\n\nfunction deleteCredentialSecret(key: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.deleteSync(key);\n decryptionFailed = false;\n}\n\nfunction readCredentialProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeCredentialProviderIndex(providers: Iterable<string>): void {\n writeCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY, JSON.stringify(normalizeProviderList(Array.from(providers))));\n}\n\nfunction readCredentialLegacyMigratedProjectRoots(): Set<string> {\n const raw = readCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY);\n if (!raw) return new Set();\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n const roots = parsed\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => path.resolve(entry));\n return new Set(roots);\n } catch {\n return new Set();\n }\n}\n\nfunction writeCredentialLegacyMigratedProjectRoots(projectRoots: Iterable<string>): void {\n const normalized = Array.from(new Set(Array.from(projectRoots).map((entry) => path.resolve(entry)))).sort();\n writeCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY, JSON.stringify(normalized));\n}\n\nfunction isCredentialLegacyKeychainMigrated(): boolean {\n return Boolean(readCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY));\n}\n\nfunction markCredentialLegacyKeychainMigrated(): void {\n writeCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY, new Date().toISOString());\n}\n\nfunction readCredentialStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n for (const provider of providerCandidates) {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) continue;\n const value = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (value) out[normalizedProvider] = value;\n }\n return out;\n}\n\nfunction mergeLegacyValuesIntoCredentialStore(\n currentStore: StoredKeys,\n legacyStore: StoredKeys,\n): StoredKeys {\n const nextStore = { ...currentStore };\n const changedProviders = new Set<string>();\n for (const [provider, rawValue] of Object.entries(legacyStore)) {\n const normalizedProvider = normalizeProvider(provider);\n const value = rawValue.trim();\n if (!normalizedProvider.length || !value.length || nextStore[normalizedProvider]) continue;\n writeCredentialSecret(credentialProviderKey(normalizedProvider), value);\n nextStore[normalizedProvider] = value;\n changedProviders.add(normalizedProvider);\n }\n if (changedProviders.size) {\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, ...Object.keys(nextStore)]));\n }\n return nextStore;\n}\n\nfunction migrateLegacyProjectStoreIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (!projectRootPath) return currentStore;\n const migratedProjectRoots = readCredentialLegacyMigratedProjectRoots();\n const normalizedProjectRoot = path.resolve(projectRootPath);\n if (migratedProjectRoots.has(normalizedProjectRoot)) return currentStore;\n\n const hadEncryptedStore = Boolean(storePath && fs.existsSync(storePath));\n const legacyStore = loadEncryptedStore();\n const migrationComplete = !hadEncryptedStore || !decryptionFailed;\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, legacyStore);\n\n if (migrationComplete) {\n migratedProjectRoots.add(normalizedProjectRoot);\n writeCredentialLegacyMigratedProjectRoots(migratedProjectRoots);\n }\n return nextStore;\n}\n\nfunction migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (isCredentialLegacyKeychainMigrated()) return currentStore;\n if (!isMacosKeychainAvailable()) return currentStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = new Set([\n ...index.providers,\n ...Object.keys(ENV_KEY_PROVIDERS),\n ]);\n const keychainStore = readMacosKeychainStore(providerCandidates);\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);\n markCredentialLegacyKeychainMigrated();\n return nextStore;\n}\n\nfunction migrateLegacyStoresIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n let nextStore = currentStore;\n try {\n nextStore = migrateLegacyKeychainIntoCredentialStore(nextStore);\n } catch {\n // Keep the machine credential store usable even if a legacy Keychain read\n // or write is blocked. The legacy copy remains available for a later retry.\n }\n try {\n nextStore = migrateLegacyProjectStoreIntoCredentialStore(nextStore);\n } catch {\n // Best effort. A failed migration must not block app or runtime startup.\n }\n return nextStore;\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeMacosKeychainProviderIndex(providers: Iterable<string>): void {\n writeMacosKeychainSecret(\n MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT,\n JSON.stringify(normalizeProviderList(Array.from(providers))),\n );\n}\n\nfunction tryWriteMacosKeychainProviderIndex(providers: Iterable<string>): void {\n try {\n writeMacosKeychainProviderIndex(providers);\n } catch {\n // The provider index only powers discovery/listing. Keep the just-written\n // secret usable in this process even if the secondary index write fails.\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction migrateEncryptedStoreToMacosKeychain(encryptedStore: StoredKeys): void {\n if (!isMacosKeychainAvailable()) return;\n const providers = Object.keys(encryptedStore);\n if (!providers.length) return;\n\n const existingIndex = readMacosKeychainProviderIndex();\n const nextProviders = new Set<string>(existingIndex.providers);\n let changed = false;\n\n for (const provider of providers) {\n const value = encryptedStore[provider]?.trim();\n if (!value) continue;\n const existingValue = readMacosKeychainSecret(provider);\n if (!existingValue) {\n writeMacosKeychainSecret(provider, value);\n }\n nextProviders.add(provider);\n changed = true;\n }\n\n if (changed) {\n writeMacosKeychainProviderIndex(nextProviders);\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (credentialStore) {\n const index = readCredentialProviderIndex();\n const credentialValues = index.exists ? readCredentialStore(index.providers) : {};\n cache = migrateLegacyStoresIntoCredentialStore(credentialValues);\n return cache;\n }\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n const indexBeforeMigration = readMacosKeychainProviderIndex();\n if (!indexBeforeMigration.exists) {\n try {\n migrateEncryptedStoreToMacosKeychain(encryptedStore);\n } catch {\n // If Keychain migration is blocked, keep the decryptable encrypted\n // store as the active fallback instead of dropping existing keys.\n }\n }\n\n const index = readMacosKeychainProviderIndex();\n if (index.exists) {\n cache = readMacosKeychainStore(index.providers);\n return cache;\n }\n\n // Without an index, avoid probing every known provider synchronously on the\n // Electron main thread. The old encrypted store remains the migration\n // source of truth; direct Keychain reads happen on-demand by provider.\n cache = encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string, options: InitApiKeyStoreOptions = {}): void {\n const layout = resolveAdeLayout(projectRoot);\n projectRootPath = path.resolve(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n credentialStore = options.credentialStore ?? null;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n missingCredentialProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: credentialStore ? null : storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingCredentialProviders.delete(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n if (isMacosKeychainAvailable()) {\n writeMacosKeychainSecret(normalizedProvider, normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingMacosKeychainProviders.delete(normalizedProvider);\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (credentialStore && !missingCredentialProviders.has(normalizedProvider)) {\n const credentialValue = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (credentialValue) {\n store[normalizedProvider] = credentialValue;\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return credentialValue;\n }\n missingCredentialProviders.add(normalizedProvider);\n }\n const allowLegacyKeychainFallback =\n !credentialStore || !isCredentialLegacyKeychainMigrated();\n if (allowLegacyKeychainFallback && isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (credentialStore) {\n deleteCredentialSecret(credentialProviderKey(normalizedProvider));\n delete store[normalizedProvider];\n missingCredentialProviders.add(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n if (isMacosKeychainAvailable()) {\n deleteMacosKeychainSecret(normalizedProvider);\n delete store[normalizedProvider];\n missingMacosKeychainProviders.add(normalizedProvider);\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n persistEncryptedStore(nextStore);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "3be658856289ccbe8d5d37b537aefe4cf4e32675": {"t": 1785754354.8321438, "n": 19791, "h": "12f98d02d2b9176a", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyCredentialStore = {\n get?: (key: string) => Promise<string | null> | string | null;\n set?: (key: string, value: string) => Promise<void> | void;\n delete?: (key: string) => Promise<void> | void;\n getSync?: (key: string) => string | null;\n setSync?: (key: string, value: string) => void;\n deleteSync?: (key: string) => void;\n};\n\nexport type InitApiKeyStoreOptions = {\n credentialStore?: ApiKeyCredentialStore | null;\n};\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\nconst CREDENTIAL_PROVIDER_INDEX_KEY = \"ai.api_key.index.v1\";\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet credentialStore: ApiKeyCredentialStore | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\nlet missingCredentialProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n if (credentialStore) return true;\n return isMacosKeychainAvailable() || isSecureStorageAvailable();\n}\n\nfunction normalizeProvider(provider: string): string {\n return provider.trim().toLowerCase();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction writeMacosKeychainSecret(account: string, value: string): void {\n if (!isMacosKeychainAvailable()) {\n throw new Error(\"macOS Keychain is unavailable. Cannot persist API keys.\");\n }\n // `/usr/bin/security add-generic-password` does not support a non-interactive\n // stdin password sentinel: `-w -` stores a literal dash. Keep this path\n // synchronous and tightly scoped until we replace it with a native binding.\n const result = runSecurity([\n \"add-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n value,\n \"-U\",\n ]);\n if (!result.ok) {\n rememberKeychainError(\"write\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain write failed.\");\n }\n clearKeychainError();\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction credentialProviderKey(provider: string): string {\n return `ai.api_key.${provider}.v1`;\n}\n\nfunction getSyncCredentialStore(): Required<Pick<ApiKeyCredentialStore, \"getSync\" | \"setSync\" | \"deleteSync\">> | null {\n if (!credentialStore) return null;\n if (\n typeof credentialStore.getSync === \"function\"\n && typeof credentialStore.setSync === \"function\"\n && typeof credentialStore.deleteSync === \"function\"\n ) {\n return credentialStore as Required<Pick<ApiKeyCredentialStore, \"getSync\" | \"setSync\" | \"deleteSync\">>;\n }\n throw new Error(\"API key credentialStore must provide getSync, setSync, and deleteSync.\");\n}\n\nfunction readCredentialSecret(key: string): string | null {\n const store = getSyncCredentialStore();\n if (!store) return null;\n try {\n const value = store.getSync(key);\n decryptionFailed = false;\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed.length ? trimmed : null;\n } catch {\n decryptionFailed = true;\n return null;\n }\n}\n\nfunction writeCredentialSecret(key: string, value: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.setSync(key, value);\n decryptionFailed = false;\n}\n\nfunction deleteCredentialSecret(key: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.deleteSync(key);\n decryptionFailed = false;\n}\n\nfunction readCredentialProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n decryptionFailed = true;\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeCredentialProviderIndex(providers: Iterable<string>): void {\n writeCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY, JSON.stringify(normalizeProviderList(Array.from(providers))));\n}\n\nfunction readCredentialStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n for (const provider of providerCandidates) {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) continue;\n const value = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (value) out[normalizedProvider] = value;\n }\n return out;\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeMacosKeychainProviderIndex(providers: Iterable<string>): void {\n writeMacosKeychainSecret(\n MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT,\n JSON.stringify(normalizeProviderList(Array.from(providers))),\n );\n}\n\nfunction tryWriteMacosKeychainProviderIndex(providers: Iterable<string>): void {\n try {\n writeMacosKeychainProviderIndex(providers);\n } catch {\n // The provider index only powers discovery/listing. Keep the just-written\n // secret usable in this process even if the secondary index write fails.\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction migrateEncryptedStoreToMacosKeychain(encryptedStore: StoredKeys): void {\n if (!isMacosKeychainAvailable()) return;\n const providers = Object.keys(encryptedStore);\n if (!providers.length) return;\n\n const existingIndex = readMacosKeychainProviderIndex();\n const nextProviders = new Set<string>(existingIndex.providers);\n let changed = false;\n\n for (const provider of providers) {\n const value = encryptedStore[provider]?.trim();\n if (!value) continue;\n const existingValue = readMacosKeychainSecret(provider);\n if (!existingValue) {\n writeMacosKeychainSecret(provider, value);\n }\n nextProviders.add(provider);\n changed = true;\n }\n\n if (changed) {\n writeMacosKeychainProviderIndex(nextProviders);\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (credentialStore) {\n const index = readCredentialProviderIndex();\n cache = index.exists ? readCredentialStore(index.providers) : {};\n return cache;\n }\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n const indexBeforeMigration = readMacosKeychainProviderIndex();\n if (!indexBeforeMigration.exists) {\n try {\n migrateEncryptedStoreToMacosKeychain(encryptedStore);\n } catch {\n // If Keychain migration is blocked, keep the decryptable encrypted\n // store as the active fallback instead of dropping existing keys.\n }\n }\n\n const index = readMacosKeychainProviderIndex();\n if (index.exists) {\n cache = readMacosKeychainStore(index.providers);\n return cache;\n }\n\n // Without an index, avoid probing every known provider synchronously on the\n // Electron main thread. The old encrypted store remains the migration\n // source of truth; direct Keychain reads happen on-demand by provider.\n cache = encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string, options: InitApiKeyStoreOptions = {}): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n credentialStore = options.credentialStore ?? null;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n missingCredentialProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: credentialStore ? null : storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingCredentialProviders.delete(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n if (isMacosKeychainAvailable()) {\n writeMacosKeychainSecret(normalizedProvider, normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingMacosKeychainProviders.delete(normalizedProvider);\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (credentialStore && !missingCredentialProviders.has(normalizedProvider)) {\n const credentialValue = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (credentialValue) {\n store[normalizedProvider] = credentialValue;\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return credentialValue;\n }\n missingCredentialProviders.add(normalizedProvider);\n }\n if (isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (credentialStore) {\n deleteCredentialSecret(credentialProviderKey(normalizedProvider));\n delete store[normalizedProvider];\n missingCredentialProviders.add(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n if (isMacosKeychainAvailable()) {\n deleteMacosKeychainSecret(normalizedProvider);\n delete store[normalizedProvider];\n missingMacosKeychainProviders.add(normalizedProvider);\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n persistEncryptedStore(nextStore);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "4c1764609b88fcf775564743c2815528e1f280c8": {"t": 1785754356.1155386, "n": 15121, "h": "5a12359ff8f12a78", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n return isMacosKeychainAvailable() || isSecureStorageAvailable();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction writeMacosKeychainSecret(account: string, value: string): void {\n if (!isMacosKeychainAvailable()) {\n throw new Error(\"macOS Keychain is unavailable. Cannot persist API keys.\");\n }\n // `/usr/bin/security add-generic-password` does not support a non-interactive\n // stdin password sentinel: `-w -` stores a literal dash. Keep this path\n // synchronous and tightly scoped until we replace it with a native binding.\n const result = runSecurity([\n \"add-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n value,\n \"-U\",\n ]);\n if (!result.ok) {\n rememberKeychainError(\"write\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain write failed.\");\n }\n clearKeychainError();\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeMacosKeychainProviderIndex(providers: Iterable<string>): void {\n writeMacosKeychainSecret(\n MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT,\n JSON.stringify(normalizeProviderList(Array.from(providers))),\n );\n}\n\nfunction tryWriteMacosKeychainProviderIndex(providers: Iterable<string>): void {\n try {\n writeMacosKeychainProviderIndex(providers);\n } catch {\n // The provider index only powers discovery/listing. Keep the just-written\n // secret usable in this process even if the secondary index write fails.\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction migrateEncryptedStoreToMacosKeychain(encryptedStore: StoredKeys): void {\n if (!isMacosKeychainAvailable()) return;\n const providers = Object.keys(encryptedStore);\n if (!providers.length) return;\n\n const existingIndex = readMacosKeychainProviderIndex();\n const nextProviders = new Set<string>(existingIndex.providers);\n let changed = false;\n\n for (const provider of providers) {\n const value = encryptedStore[provider]?.trim();\n if (!value) continue;\n const existingValue = readMacosKeychainSecret(provider);\n if (!existingValue) {\n writeMacosKeychainSecret(provider, value);\n }\n nextProviders.add(provider);\n changed = true;\n }\n\n if (changed) {\n writeMacosKeychainProviderIndex(nextProviders);\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n const indexBeforeMigration = readMacosKeychainProviderIndex();\n if (!indexBeforeMigration.exists) {\n try {\n migrateEncryptedStoreToMacosKeychain(encryptedStore);\n } catch {\n // If Keychain migration is blocked, keep the decryptable encrypted\n // store as the active fallback instead of dropping existing keys.\n }\n }\n\n const index = readMacosKeychainProviderIndex();\n if (index.exists) {\n cache = readMacosKeychainStore(index.providers);\n return cache;\n }\n\n // Without an index, avoid probing every known provider synchronously on the\n // Electron main thread. The old encrypted store remains the migration\n // source of truth; direct Keychain reads happen on-demand by provider.\n cache = encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (isMacosKeychainAvailable()) {\n writeMacosKeychainSecret(normalizedProvider, normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingMacosKeychainProviders.delete(normalizedProvider);\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (isMacosKeychainAvailable()) {\n deleteMacosKeychainSecret(normalizedProvider);\n delete store[normalizedProvider];\n missingMacosKeychainProviders.add(normalizedProvider);\n const index = readMacosKeychainProviderIndex();\n tryWriteMacosKeychainProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n persistEncryptedStore(nextStore);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "c33b8f96ab2fadcf1e2930c23a4234cbf1de6851": {"t": 1785754357.3019679, "n": 5753, "h": "1880c0a6b81c7f7d", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { SafeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n cache = {};\n return cache;\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n cache = {};\n return cache;\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n cache = normalizeStoredKeys(JSON.parse(decrypted));\n decryptionFailed = false;\n return cache;\n } catch {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n}\n\nfunction persist(): void {\n if (!storePath || !cache) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(cache));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n cache = null;\n decryptionFailed = false;\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n store[normalizedProvider] = normalizedKey;\n persist();\n}\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n};\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n delete store[normalizedProvider];\n persist();\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "b93ca5d03adf33a0a1bc64b80b382554de268b12": {"t": 1785754358.7454567, "n": 5725, "h": "609532690f0dc75a", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { SafeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n cache = {};\n return cache;\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n cache = {};\n return cache;\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n cache = normalizeStoredKeys(JSON.parse(decrypted));\n decryptionFailed = false;\n return cache;\n } catch {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n}\n\nfunction persist(): void {\n if (!storePath || !cache) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(cache));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n cache = null;\n decryptionFailed = false;\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n store[normalizedProvider] = normalizedKey;\n persist();\n}\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n};\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n delete store[normalizedProvider];\n persist();\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "45027248c4e9f35740bd6149ae85f4a2eac81126": {"t": 1785754360.030911, "n": 5804, "h": "47764d125f2c93b8", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { SafeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the headless MCP server (spawned by\n// Claude Agent SDK / Codex App Server as a plain Node process), `electron`\n// is not present. Gracefully degrade so the MCP server can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n cache = {};\n return cache;\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n cache = {};\n return cache;\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n cache = normalizeStoredKeys(JSON.parse(decrypted));\n decryptionFailed = false;\n return cache;\n } catch {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n}\n\nfunction persist(): void {\n if (!storePath || !cache) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(cache));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n cache = null;\n decryptionFailed = false;\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n store[normalizedProvider] = normalizedKey;\n persist();\n}\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n};\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n delete store[normalizedProvider];\n persist();\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "88496e5fed4c9d634adda876552df1a33038a81d": {"t": 1785754361.4786797, "n": 5259, "h": "f2ddd0534bdef91a", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { SafeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the headless MCP server (spawned by\n// Claude Agent SDK / Codex App Server as a plain Node process), `electron`\n// is not present. Gracefully degrade so the MCP server can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n cache = {};\n return cache;\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n cache = {};\n return cache;\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n cache = normalizeStoredKeys(JSON.parse(decrypted));\n decryptionFailed = false;\n return cache;\n } catch {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n}\n\nfunction persist(): void {\n if (!storePath || !cache) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(cache));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n cache = null;\n decryptionFailed = false;\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n store[normalizedProvider] = normalizedKey;\n persist();\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n return store[normalizedProvider] ?? null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n delete store[normalizedProvider];\n persist();\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "f54dcbc87a8e0e1505db4b362a5b6843b73d315f": {"t": 1785754363.1936555, "n": 5039, "h": "b8a95373cd448c65", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { SafeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the headless MCP server (spawned by\n// Claude Agent SDK / Codex App Server as a plain Node process), `electron`\n// is not present. Gracefully degrade so the MCP server can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch {\n // Not running inside Electron — secure storage unavailable.\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n cache = {};\n return cache;\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n cache = {};\n return cache;\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n cache = normalizeStoredKeys(JSON.parse(decrypted));\n decryptionFailed = false;\n return cache;\n } catch {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n}\n\nfunction persist(): void {\n if (!storePath || !cache) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(cache));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n cache = null;\n decryptionFailed = false;\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n store[normalizedProvider] = normalizedKey;\n persist();\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n return store[normalizedProvider] ?? null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n delete store[normalizedProvider];\n persist();\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "9b3e6455206eab75a2bd6800a75ed16c9119c2c2": {"t": 1785754364.835445, "n": 5020, "h": "bf9dd5773c73bd15", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the headless MCP server (spawned by\n// Claude Agent SDK / Codex App Server as a plain Node process), `electron`\n// is not present. Gracefully degrade so the MCP server can start.\nlet safeStorage: typeof import(\"electron\").safeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch {\n // Not running inside Electron — secure storage unavailable.\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n cache = {};\n return cache;\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n cache = {};\n return cache;\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n cache = normalizeStoredKeys(JSON.parse(decrypted));\n decryptionFailed = false;\n return cache;\n } catch {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n}\n\nfunction persist(): void {\n if (!storePath || !cache) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(cache));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n cache = null;\n decryptionFailed = false;\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n store[normalizedProvider] = normalizedKey;\n persist();\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n return store[normalizedProvider] ?? null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n delete store[normalizedProvider];\n persist();\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "4fdca5beae0459d45881c38250157adcbc922643": {"t": 1785754366.116963, "n": 4498, "h": "fb2a8b6bed24d016", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { safeStorage } from \"electron\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n cache = {};\n return cache;\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n cache = {};\n return cache;\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage.decryptString(raw);\n cache = normalizeStoredKeys(JSON.parse(decrypted));\n decryptionFailed = false;\n return cache;\n } catch {\n decryptionFailed = true;\n cache = {};\n return cache;\n }\n}\n\nfunction persist(): void {\n if (!storePath || !cache) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage.encryptString(JSON.stringify(cache));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string): void {\n const layout = resolveAdeLayout(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n cache = null;\n decryptionFailed = false;\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isSecureStorageAvailable(),\n encryptedStorePath: storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n store[normalizedProvider] = normalizedKey;\n persist();\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n return store[normalizedProvider] ?? null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = provider.trim().toLowerCase();\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n delete store[normalizedProvider];\n persist();\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "26c0d1be65fede77ae24a1085d8607f8a53065d2": {"t": 1786067409.2032118, "n": 21829, "h": "43ac563a0060aea1", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport type { SyncCredentialStore } from \"../../../../../ade-cli/src/services/credentials/credentialStore\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyCredentialStore = SyncCredentialStore;\n\nexport type InitApiKeyStoreOptions = {\n credentialStore?: ApiKeyCredentialStore | null;\n};\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n moonshotai: \"MOONSHOT_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY = \"ai.credentials.legacy_keychain_migrated.v1\";\nconst CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY = \"ai.credentials.legacy_projects_migrated.v1\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\nconst CREDENTIAL_PROVIDER_INDEX_KEY = \"ai.api_key.index.v1\";\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet projectRootPath: string | null = null;\nlet credentialStore: ApiKeyCredentialStore | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\nlet missingCredentialProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n if (credentialStore) return true;\n return isSecureStorageAvailable();\n}\n\nfunction normalizeProvider(provider: string): string {\n return provider.trim().toLowerCase();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction credentialProviderKey(provider: string): string {\n return `ai.api_key.${provider}.v1`;\n}\n\nfunction getSyncCredentialStore(): SyncCredentialStore | null {\n return credentialStore;\n}\n\nfunction readCredentialSecret(key: string): string | null {\n const store = getSyncCredentialStore();\n if (!store) return null;\n try {\n const value = store.getSync(key);\n decryptionFailed = false;\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed.length ? trimmed : null;\n } catch {\n decryptionFailed = true;\n return null;\n }\n}\n\nfunction writeCredentialSecret(key: string, value: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.setSync(key, value);\n decryptionFailed = false;\n}\n\nfunction deleteCredentialSecret(key: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.deleteSync(key);\n decryptionFailed = false;\n}\n\nfunction readCredentialProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeCredentialProviderIndex(providers: Iterable<string>): void {\n writeCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY, JSON.stringify(normalizeProviderList(Array.from(providers))));\n}\n\nfunction readCredentialLegacyMigratedProjectRoots(): Set<string> {\n const raw = readCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY);\n if (!raw) return new Set();\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n const roots = parsed\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => path.resolve(entry));\n return new Set(roots);\n } catch {\n return new Set();\n }\n}\n\nfunction writeCredentialLegacyMigratedProjectRoots(projectRoots: Iterable<string>): void {\n const normalized = Array.from(new Set(Array.from(projectRoots).map((entry) => path.resolve(entry)))).sort();\n writeCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY, JSON.stringify(normalized));\n}\n\nfunction isCredentialLegacyKeychainMigrated(): boolean {\n return Boolean(readCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY));\n}\n\nfunction markCredentialLegacyKeychainMigrated(): void {\n writeCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY, new Date().toISOString());\n}\n\nfunction readCredentialStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n for (const provider of providerCandidates) {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) continue;\n const value = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (value) out[normalizedProvider] = value;\n }\n return out;\n}\n\nfunction mergeLegacyValuesIntoCredentialStore(\n currentStore: StoredKeys,\n legacyStore: StoredKeys,\n): StoredKeys {\n const nextStore = { ...currentStore };\n const changedProviders = new Set<string>();\n for (const [provider, rawValue] of Object.entries(legacyStore)) {\n const normalizedProvider = normalizeProvider(provider);\n const value = rawValue.trim();\n if (!normalizedProvider.length || !value.length || nextStore[normalizedProvider]) continue;\n writeCredentialSecret(credentialProviderKey(normalizedProvider), value);\n nextStore[normalizedProvider] = value;\n changedProviders.add(normalizedProvider);\n }\n if (changedProviders.size) {\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, ...Object.keys(nextStore)]));\n }\n return nextStore;\n}\n\nfunction migrateLegacyProjectStoreIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (!projectRootPath) return currentStore;\n const migratedProjectRoots = readCredentialLegacyMigratedProjectRoots();\n const normalizedProjectRoot = path.resolve(projectRootPath);\n if (migratedProjectRoots.has(normalizedProjectRoot)) return currentStore;\n\n const hadEncryptedStore = Boolean(storePath && fs.existsSync(storePath));\n const legacyStore = loadEncryptedStore();\n const migrationComplete = !hadEncryptedStore || !decryptionFailed;\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, legacyStore);\n\n if (migrationComplete) {\n migratedProjectRoots.add(normalizedProjectRoot);\n writeCredentialLegacyMigratedProjectRoots(migratedProjectRoots);\n }\n return nextStore;\n}\n\nfunction migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (isCredentialLegacyKeychainMigrated()) return currentStore;\n if (!isMacosKeychainAvailable()) return currentStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = new Set([\n ...index.providers,\n ...Object.keys(ENV_KEY_PROVIDERS),\n ]);\n const keychainStore = readMacosKeychainStore(providerCandidates);\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);\n markCredentialLegacyKeychainMigrated();\n return nextStore;\n}\n\nfunction migrateLegacyStoresIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n let nextStore = currentStore;\n try {\n nextStore = migrateLegacyKeychainIntoCredentialStore(nextStore);\n } catch {\n // Keep the machine credential store usable even if a legacy Keychain read\n // or write is blocked. The legacy copy remains available for a later retry.\n }\n try {\n nextStore = migrateLegacyProjectStoreIntoCredentialStore(nextStore);\n } catch {\n // Best effort. A failed migration must not block app or runtime startup.\n }\n return nextStore;\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction canPersistEncryptedStore(): boolean {\n return Boolean(storePath) && isSecureStorageAvailable();\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction deleteMacosKeychainSecretBestEffort(account: string): void {\n try {\n deleteMacosKeychainSecret(account);\n } catch {\n // Legacy Keychain cleanup should not block writes to the active encrypted\n // store. A stale Keychain copy is still superseded by the encrypted value.\n }\n}\n\nfunction migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore: StoredKeys): StoredKeys {\n if (!canPersistEncryptedStore() || !isMacosKeychainAvailable()) return encryptedStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = index.exists ? index.providers : Object.keys(encryptedStore);\n if (providerCandidates.length === 0) return encryptedStore;\n\n const keychainStore = readMacosKeychainStore(providerCandidates);\n if (Object.keys(keychainStore).length === 0) return encryptedStore;\n\n const nextStore = { ...keychainStore, ...encryptedStore };\n persistEncryptedStore(nextStore);\n decryptionFailed = false;\n return nextStore;\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (credentialStore) {\n const index = readCredentialProviderIndex();\n const credentialValues = index.exists ? readCredentialStore(index.providers) : {};\n cache = migrateLegacyStoresIntoCredentialStore(credentialValues);\n return cache;\n }\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n if (canPersistEncryptedStore()) {\n cache = migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore);\n return cache;\n }\n\n const index = readMacosKeychainProviderIndex();\n cache = index.exists ? readMacosKeychainStore(index.providers) : encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string, options: InitApiKeyStoreOptions = {}): void {\n const layout = resolveAdeLayout(projectRoot);\n projectRootPath = path.resolve(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n credentialStore = options.credentialStore ?? null;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n missingCredentialProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: credentialStore ? null : storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingCredentialProviders.delete(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (credentialStore && !missingCredentialProviders.has(normalizedProvider)) {\n const credentialValue = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (credentialValue) {\n store[normalizedProvider] = credentialValue;\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return credentialValue;\n }\n missingCredentialProviders.add(normalizedProvider);\n }\n const allowLegacyKeychainFallback =\n !credentialStore || !isCredentialLegacyKeychainMigrated();\n if (allowLegacyKeychainFallback && isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), keychainValue);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n } else if (canPersistEncryptedStore()) {\n persistEncryptedStore(store);\n }\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (credentialStore) {\n deleteCredentialSecret(credentialProviderKey(normalizedProvider));\n delete store[normalizedProvider];\n missingCredentialProviders.add(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n if (canPersistEncryptedStore()) {\n persistEncryptedStore(nextStore);\n }\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}, "0e53ea96e5f1df1fbb96a17073a49f54edf5e24c": {"t": 1786068274.9227984, "n": 21829, "h": "43ac563a0060aea1", "c": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport type { SafeStorage } from \"electron\";\nimport type { SyncCredentialStore } from \"../../../../../ade-cli/src/services/credentials/credentialStore\";\nimport { resolveAdeLayout } from \"../../../shared/adeLayout\";\n\n// electron.safeStorage is only available inside an Electron main process.\n// When this module is bundled into the ADE CLI headless runtime, `electron`\n// is not present. Gracefully degrade so the CLI can start.\nlet safeStorage: SafeStorage | null = null;\ntry {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n safeStorage = require(\"electron\").safeStorage;\n} catch (err) {\n // Not running inside Electron — secure storage unavailable.\n // Log at debug level so silent failures don't hide useful diagnostics.\n if (typeof process !== \"undefined\" && process.env.DEBUG) {\n console.debug(\"[apiKeyStore] electron.safeStorage unavailable:\", err);\n }\n}\n\ntype StoredKeys = Record<string, string>;\n\nexport type ApiKeyCredentialStore = SyncCredentialStore;\n\nexport type InitApiKeyStoreOptions = {\n credentialStore?: ApiKeyCredentialStore | null;\n};\n\nexport type ApiKeyStoreStatus = {\n secureStorageAvailable: boolean;\n macosKeychainAvailable: boolean;\n macosKeychainService: string | null;\n macosKeychainError: string | null;\n encryptedStorePath: string | null;\n legacyPlaintextDetected: boolean;\n legacyPlaintextPath: string | null;\n decryptionFailed: boolean;\n};\n\nconst ENV_KEY_PROVIDERS: Record<string, string> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n deepseek: \"DEEPSEEK_API_KEY\",\n xai: \"XAI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n together: \"TOGETHER_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n cursor: \"CURSOR_API_KEY\",\n moonshotai: \"MOONSHOT_API_KEY\",\n};\n\nconst MACOS_SECURITY_BIN = \"/usr/bin/security\";\nconst MACOS_KEYCHAIN_SERVICE = \"com.ade.desktop.api-keys.v1\";\nconst MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT = \"__ade_provider_index__\";\nconst CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY = \"ai.credentials.legacy_keychain_migrated.v1\";\nconst CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY = \"ai.credentials.legacy_projects_migrated.v1\";\nconst MACOS_KEYCHAIN_MISSING_PATTERNS = [\n /could not be found/i,\n /item could not be found/i,\n /the specified item could not be found/i,\n];\nconst SECURITY_TIMEOUT_MS = 5_000;\nconst CREDENTIAL_PROVIDER_INDEX_KEY = \"ai.api_key.index.v1\";\n\nlet storePath: string | null = null;\nlet legacyStorePath: string | null = null;\nlet projectRootPath: string | null = null;\nlet credentialStore: ApiKeyCredentialStore | null = null;\nlet cache: StoredKeys | null = null;\nlet decryptionFailed = false;\nlet macosKeychainError: string | null = null;\nlet missingMacosKeychainProviders = new Set<string>();\nlet missingCredentialProviders = new Set<string>();\n\nexport function __setSafeStorageForTests(next: SafeStorage | null): void {\n safeStorage = next;\n cache = null;\n missingMacosKeychainProviders = new Set<string>();\n}\n\nfunction isSecureStorageAvailable(): boolean {\n return Boolean(safeStorage && typeof safeStorage.isEncryptionAvailable === \"function\" && safeStorage.isEncryptionAvailable());\n}\n\nfunction isMacosKeychainAvailable(): boolean {\n if (process.env.ADE_API_KEY_STORE_DISABLE_KEYCHAIN === \"1\") return false;\n if (process.env.NODE_ENV === \"test\" && process.env.ADE_API_KEY_STORE_FORCE_KEYCHAIN === \"1\") return true;\n return process.platform === \"darwin\" && fs.existsSync(MACOS_SECURITY_BIN);\n}\n\nfunction isPersistentSecureStorageAvailable(): boolean {\n if (credentialStore) return true;\n return isSecureStorageAvailable();\n}\n\nfunction normalizeProvider(provider: string): string {\n return provider.trim().toLowerCase();\n}\n\nfunction normalizeStoredKeys(value: unknown): StoredKeys {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const out: StoredKeys = {};\n for (const [provider, rawValue] of Object.entries(value as Record<string, unknown>)) {\n if (typeof rawValue !== \"string\") continue;\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = rawValue.trim();\n if (!normalizedProvider.length || !normalizedKey.length) continue;\n out[normalizedProvider] = normalizedKey;\n }\n return out;\n}\n\nfunction ensureInitialized(): void {\n if (!storePath || !legacyStorePath) {\n throw new Error(\"API key store not initialized. Call initApiKeyStore first.\");\n }\n}\n\ntype SecurityResult = {\n ok: boolean;\n stdout: string;\n stderr: string;\n status: number | null;\n};\n\nfunction runSecurity(args: string[]): SecurityResult {\n const result = spawnSync(MACOS_SECURITY_BIN, args, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: SECURITY_TIMEOUT_MS,\n });\n return {\n ok: result.status === 0 && !result.error,\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n stderr: typeof result.stderr === \"string\" ? result.stderr : result.error?.message ?? \"\",\n status: typeof result.status === \"number\" ? result.status : null,\n };\n}\n\nfunction securityMissing(result: SecurityResult): boolean {\n if (result.status === 44) return true;\n return MACOS_KEYCHAIN_MISSING_PATTERNS.some((pattern) => pattern.test(result.stderr));\n}\n\nfunction rememberKeychainError(action: string, result: SecurityResult): void {\n const detail = result.stderr.trim().split(/\\r?\\n/)[0] || `status ${result.status ?? \"unknown\"}`;\n macosKeychainError = `macOS Keychain ${action} failed: ${detail}`;\n}\n\nfunction clearKeychainError(): void {\n macosKeychainError = null;\n}\n\nfunction trimTrailingNewline(value: string): string {\n return value.replace(/(?:\\r?\\n)+$/, \"\");\n}\n\nfunction readMacosKeychainSecret(account: string): string | null {\n if (!isMacosKeychainAvailable()) return null;\n const result = runSecurity([\n \"find-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n \"-w\",\n ]);\n if (result.ok) {\n clearKeychainError();\n const value = trimTrailingNewline(result.stdout).trim();\n return value.length ? value : null;\n }\n if (!securityMissing(result)) {\n rememberKeychainError(\"read\", result);\n }\n return null;\n}\n\nfunction deleteMacosKeychainSecret(account: string): void {\n if (!isMacosKeychainAvailable()) return;\n const result = runSecurity([\n \"delete-generic-password\",\n \"-a\",\n account,\n \"-s\",\n MACOS_KEYCHAIN_SERVICE,\n ]);\n if (result.ok || securityMissing(result)) {\n clearKeychainError();\n return;\n }\n rememberKeychainError(\"delete\", result);\n throw new Error(macosKeychainError ?? \"macOS Keychain delete failed.\");\n}\n\nfunction normalizeProviderList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const providers = new Set<string>();\n for (const raw of value) {\n if (typeof raw !== \"string\") continue;\n const provider = raw.trim().toLowerCase();\n if (provider.length) providers.add(provider);\n }\n return Array.from(providers).sort();\n}\n\nfunction credentialProviderKey(provider: string): string {\n return `ai.api_key.${provider}.v1`;\n}\n\nfunction getSyncCredentialStore(): SyncCredentialStore | null {\n return credentialStore;\n}\n\nfunction readCredentialSecret(key: string): string | null {\n const store = getSyncCredentialStore();\n if (!store) return null;\n try {\n const value = store.getSync(key);\n decryptionFailed = false;\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed.length ? trimmed : null;\n } catch {\n decryptionFailed = true;\n return null;\n }\n}\n\nfunction writeCredentialSecret(key: string, value: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.setSync(key, value);\n decryptionFailed = false;\n}\n\nfunction deleteCredentialSecret(key: string): void {\n const store = getSyncCredentialStore();\n if (!store) return;\n store.deleteSync(key);\n decryptionFailed = false;\n}\n\nfunction readCredentialProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n return { exists: true, providers: [] };\n }\n}\n\nfunction writeCredentialProviderIndex(providers: Iterable<string>): void {\n writeCredentialSecret(CREDENTIAL_PROVIDER_INDEX_KEY, JSON.stringify(normalizeProviderList(Array.from(providers))));\n}\n\nfunction readCredentialLegacyMigratedProjectRoots(): Set<string> {\n const raw = readCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY);\n if (!raw) return new Set();\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n const roots = parsed\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => path.resolve(entry));\n return new Set(roots);\n } catch {\n return new Set();\n }\n}\n\nfunction writeCredentialLegacyMigratedProjectRoots(projectRoots: Iterable<string>): void {\n const normalized = Array.from(new Set(Array.from(projectRoots).map((entry) => path.resolve(entry)))).sort();\n writeCredentialSecret(CREDENTIAL_LEGACY_PROJECTS_MIGRATED_KEY, JSON.stringify(normalized));\n}\n\nfunction isCredentialLegacyKeychainMigrated(): boolean {\n return Boolean(readCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY));\n}\n\nfunction markCredentialLegacyKeychainMigrated(): void {\n writeCredentialSecret(CREDENTIAL_LEGACY_KEYCHAIN_MIGRATED_KEY, new Date().toISOString());\n}\n\nfunction readCredentialStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n for (const provider of providerCandidates) {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) continue;\n const value = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (value) out[normalizedProvider] = value;\n }\n return out;\n}\n\nfunction mergeLegacyValuesIntoCredentialStore(\n currentStore: StoredKeys,\n legacyStore: StoredKeys,\n): StoredKeys {\n const nextStore = { ...currentStore };\n const changedProviders = new Set<string>();\n for (const [provider, rawValue] of Object.entries(legacyStore)) {\n const normalizedProvider = normalizeProvider(provider);\n const value = rawValue.trim();\n if (!normalizedProvider.length || !value.length || nextStore[normalizedProvider]) continue;\n writeCredentialSecret(credentialProviderKey(normalizedProvider), value);\n nextStore[normalizedProvider] = value;\n changedProviders.add(normalizedProvider);\n }\n if (changedProviders.size) {\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, ...Object.keys(nextStore)]));\n }\n return nextStore;\n}\n\nfunction migrateLegacyProjectStoreIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (!projectRootPath) return currentStore;\n const migratedProjectRoots = readCredentialLegacyMigratedProjectRoots();\n const normalizedProjectRoot = path.resolve(projectRootPath);\n if (migratedProjectRoots.has(normalizedProjectRoot)) return currentStore;\n\n const hadEncryptedStore = Boolean(storePath && fs.existsSync(storePath));\n const legacyStore = loadEncryptedStore();\n const migrationComplete = !hadEncryptedStore || !decryptionFailed;\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, legacyStore);\n\n if (migrationComplete) {\n migratedProjectRoots.add(normalizedProjectRoot);\n writeCredentialLegacyMigratedProjectRoots(migratedProjectRoots);\n }\n return nextStore;\n}\n\nfunction migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n if (isCredentialLegacyKeychainMigrated()) return currentStore;\n if (!isMacosKeychainAvailable()) return currentStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = new Set([\n ...index.providers,\n ...Object.keys(ENV_KEY_PROVIDERS),\n ]);\n const keychainStore = readMacosKeychainStore(providerCandidates);\n const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);\n markCredentialLegacyKeychainMigrated();\n return nextStore;\n}\n\nfunction migrateLegacyStoresIntoCredentialStore(currentStore: StoredKeys): StoredKeys {\n let nextStore = currentStore;\n try {\n nextStore = migrateLegacyKeychainIntoCredentialStore(nextStore);\n } catch {\n // Keep the machine credential store usable even if a legacy Keychain read\n // or write is blocked. The legacy copy remains available for a later retry.\n }\n try {\n nextStore = migrateLegacyProjectStoreIntoCredentialStore(nextStore);\n } catch {\n // Best effort. A failed migration must not block app or runtime startup.\n }\n return nextStore;\n}\n\nfunction readMacosKeychainProviderIndex(): { exists: boolean; providers: string[] } {\n const raw = readMacosKeychainSecret(MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT);\n if (!raw) return { exists: false, providers: [] };\n try {\n return { exists: true, providers: normalizeProviderList(JSON.parse(raw)) };\n } catch {\n macosKeychainError = \"macOS Keychain provider index is unreadable.\";\n return { exists: true, providers: [] };\n }\n}\n\nfunction readMacosKeychainStore(providerCandidates: Iterable<string>): StoredKeys {\n const out: StoredKeys = {};\n if (!isMacosKeychainAvailable()) return out;\n for (const provider of providerCandidates) {\n if (provider === MACOS_KEYCHAIN_PROVIDER_INDEX_ACCOUNT) continue;\n const value = readMacosKeychainSecret(provider);\n if (value) out[provider] = value;\n }\n return out;\n}\n\nfunction canPersistEncryptedStore(): boolean {\n return Boolean(storePath) && isSecureStorageAvailable();\n}\n\nfunction loadEncryptedStore(): StoredKeys {\n ensureInitialized();\n\n if (!storePath || !legacyStorePath) {\n return {};\n }\n\n if (!fs.existsSync(storePath)) {\n decryptionFailed = false;\n return {};\n }\n\n if (!isSecureStorageAvailable()) {\n decryptionFailed = true;\n return {};\n }\n\n try {\n const raw = fs.readFileSync(storePath);\n const decrypted = safeStorage!.decryptString(raw);\n decryptionFailed = false;\n return normalizeStoredKeys(JSON.parse(decrypted));\n } catch {\n decryptionFailed = true;\n return {};\n }\n}\n\nfunction deleteMacosKeychainSecretBestEffort(account: string): void {\n try {\n deleteMacosKeychainSecret(account);\n } catch {\n // Legacy Keychain cleanup should not block writes to the active encrypted\n // store. A stale Keychain copy is still superseded by the encrypted value.\n }\n}\n\nfunction migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore: StoredKeys): StoredKeys {\n if (!canPersistEncryptedStore() || !isMacosKeychainAvailable()) return encryptedStore;\n\n const index = readMacosKeychainProviderIndex();\n const providerCandidates = index.exists ? index.providers : Object.keys(encryptedStore);\n if (providerCandidates.length === 0) return encryptedStore;\n\n const keychainStore = readMacosKeychainStore(providerCandidates);\n if (Object.keys(keychainStore).length === 0) return encryptedStore;\n\n const nextStore = { ...keychainStore, ...encryptedStore };\n persistEncryptedStore(nextStore);\n decryptionFailed = false;\n return nextStore;\n}\n\nfunction ensureStore(): StoredKeys {\n if (cache) return cache;\n ensureInitialized();\n\n if (credentialStore) {\n const index = readCredentialProviderIndex();\n const credentialValues = index.exists ? readCredentialStore(index.providers) : {};\n cache = migrateLegacyStoresIntoCredentialStore(credentialValues);\n return cache;\n }\n\n const encryptedStore = loadEncryptedStore();\n if (isMacosKeychainAvailable()) {\n if (canPersistEncryptedStore()) {\n cache = migrateLegacyMacosKeychainIntoEncryptedStore(encryptedStore);\n return cache;\n }\n\n const index = readMacosKeychainProviderIndex();\n cache = index.exists ? readMacosKeychainStore(index.providers) : encryptedStore;\n return cache;\n }\n\n cache = encryptedStore;\n return cache;\n}\n\nfunction persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {\n if (!storePath) return;\n if (!isSecureStorageAvailable()) {\n throw new Error(\"OS secure storage is unavailable. Cannot persist API keys.\");\n }\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));\n fs.writeFileSync(storePath, encrypted);\n try {\n fs.chmodSync(storePath, 0o600);\n } catch {\n // Best effort\n }\n}\n\nexport function initApiKeyStore(projectRoot: string, options: InitApiKeyStoreOptions = {}): void {\n const layout = resolveAdeLayout(projectRoot);\n projectRootPath = path.resolve(projectRoot);\n storePath = layout.apiKeysPath;\n legacyStorePath = layout.legacyApiKeysPath;\n credentialStore = options.credentialStore ?? null;\n cache = null;\n decryptionFailed = false;\n macosKeychainError = null;\n missingMacosKeychainProviders = new Set<string>();\n missingCredentialProviders = new Set<string>();\n}\n\nexport function getApiKeyStoreStatus(): ApiKeyStoreStatus {\n if (!storePath || !legacyStorePath) {\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: null,\n legacyPlaintextDetected: false,\n legacyPlaintextPath: null,\n decryptionFailed,\n };\n }\n return {\n secureStorageAvailable: isPersistentSecureStorageAvailable(),\n macosKeychainAvailable: isMacosKeychainAvailable(),\n macosKeychainService: isMacosKeychainAvailable() ? MACOS_KEYCHAIN_SERVICE : null,\n macosKeychainError,\n encryptedStorePath: credentialStore ? null : storePath,\n legacyPlaintextDetected: Boolean(legacyStorePath && fs.existsSync(legacyStorePath)),\n legacyPlaintextPath: legacyStorePath && fs.existsSync(legacyStorePath) ? legacyStorePath : null,\n decryptionFailed,\n };\n}\n\nexport function storeApiKey(provider: string, key: string): void {\n const normalizedProvider = normalizeProvider(provider);\n const normalizedKey = key.trim();\n if (!normalizedProvider.length || !normalizedKey.length) {\n throw new Error(\"Provider and key are required.\");\n }\n const store = ensureStore();\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), normalizedKey);\n store[normalizedProvider] = normalizedKey;\n missingCredentialProviders.delete(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return;\n }\n const nextStore = { ...store, [normalizedProvider]: normalizedKey };\n persistEncryptedStore(nextStore);\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function getApiKey(provider: string): string | null {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return null;\n const store = ensureStore();\n const stored = store[normalizedProvider];\n if (stored) return stored;\n if (credentialStore && !missingCredentialProviders.has(normalizedProvider)) {\n const credentialValue = readCredentialSecret(credentialProviderKey(normalizedProvider));\n if (credentialValue) {\n store[normalizedProvider] = credentialValue;\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n return credentialValue;\n }\n missingCredentialProviders.add(normalizedProvider);\n }\n const allowLegacyKeychainFallback =\n !credentialStore || !isCredentialLegacyKeychainMigrated();\n if (allowLegacyKeychainFallback && isMacosKeychainAvailable() && !missingMacosKeychainProviders.has(normalizedProvider)) {\n const keychainValue = readMacosKeychainSecret(normalizedProvider);\n if (keychainValue) {\n store[normalizedProvider] = keychainValue;\n if (credentialStore) {\n writeCredentialSecret(credentialProviderKey(normalizedProvider), keychainValue);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(new Set([...index.providers, normalizedProvider]));\n } else if (canPersistEncryptedStore()) {\n persistEncryptedStore(store);\n }\n return keychainValue;\n }\n missingMacosKeychainProviders.add(normalizedProvider);\n }\n const envVar = ENV_KEY_PROVIDERS[normalizedProvider];\n if (envVar) {\n const envValue = (process.env[envVar] ?? \"\").trim();\n if (envValue.length > 0) return envValue;\n }\n return null;\n}\n\nexport function deleteApiKey(provider: string): void {\n const normalizedProvider = normalizeProvider(provider);\n if (!normalizedProvider.length) return;\n const store = ensureStore();\n if (credentialStore) {\n deleteCredentialSecret(credentialProviderKey(normalizedProvider));\n delete store[normalizedProvider];\n missingCredentialProviders.add(normalizedProvider);\n const index = readCredentialProviderIndex();\n writeCredentialProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));\n return;\n }\n const nextStore = { ...store };\n delete nextStore[normalizedProvider];\n if (canPersistEncryptedStore()) {\n persistEncryptedStore(nextStore);\n }\n deleteMacosKeychainSecretBestEffort(normalizedProvider);\n missingMacosKeychainProviders.add(normalizedProvider);\n cache = nextStore;\n}\n\nexport function listStoredProviders(): string[] {\n const store = ensureStore();\n return Object.keys(store);\n}\n\nexport function getAllApiKeys(): Record<string, string> {\n return { ...ensureStore() };\n}\n"}}}}