//! Credential-Ablage der Pools. Der ApikeyStore abstrahiert den nativen //! Secret-Store (macOS-Keychain / Secret Service / Credential Manager) — //! die Implementierungen liegen in platform/. use sha2::{Digest, Sha256}; use crate::domain::paths::Paths; /// Service-Name der Einträge; Account ist die Pool-ID. Unter Linux legt die /// keyring-Crate die Attribute service/username an — der apiKeyHelper liest /// mit denselben Attributen über secret-tool. pub(crate) const APIKEY_SERVICE: &str = "ai-control-apikey"; /// Fehler-Sentinel an die UI: Store nicht verfügbar und Datei-Ablage (noch) /// nicht erlaubt — die UI fragt dann nach und wiederholt mit allow_file. pub(crate) const KEYCHAIN_UNAVAILABLE: &str = "keychain-unavailable"; /// Ablage der API-Keys im nativen Secret-Store. Die Key-Datei im Pool-Ordner /// bleibt Fallback, wenn der Store beim Schreiben nicht verfügbar ist. pub(crate) trait ApikeyStore { fn set(&self, pool: &str, key: &str) -> Result<(), String>; fn has(&self, pool: &str) -> Result; fn delete(&self, pool: &str) -> Result<(), String>; } /// Keychain-Service-Name des OAuth-Pools: claude legt pro CLAUDE_CONFIG_DIR /// einen suffixierten Eintrag an, Suffix = erste 8 Hex-Zeichen von SHA-256 /// über den Pool-Pfad. pub(crate) fn keychain_service(paths: &Paths, pool: &str) -> String { let hash = Sha256::digest(paths.pool_dir(pool).to_string_lossy().as_bytes()); let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect(); format!("Claude Code-credentials-{}", &hex[..8]) } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; /// Referenzwert vom echten System: privateDefault → 096c4ef9 /// (verifiziert 2026-07-03 gegen den von claude angelegten Eintrag). #[test] fn keychain_service_suffix() { let p = Paths { home: PathBuf::from("/Users/marcus.hinz") }; assert_eq!( keychain_service(&p, "privateDefault"), "Claude Code-credentials-096c4ef9" ); } #[test] fn apikey_helper_kette_referenz() { let dir = PathBuf::from("/pools/abc"); let cmd = crate::platform::apikey_helper_command(&dir, "abc"); assert!(cmd.ends_with("|| cat '/pools/abc/apikey'")); #[cfg(target_os = "macos")] assert!(cmd.starts_with( "security find-generic-password -w -s ai-control-apikey -a abc 2>/dev/null" )); #[cfg(target_os = "linux")] assert!(cmd.starts_with( "secret-tool lookup service ai-control-apikey username abc 2>/dev/null" )); } }