Skip to main content

aether_cli/
credentials.rs

1use crate::settings_args::SettingsSourceArgs;
2use aether_auth::{
3    EncryptedFileOAuthCredentialStorage, FakeOAuthCredentialStore, OAuthCredentialStorage, OAuthError, OsKeyringStore,
4};
5use aether_project::CredentialsStoreConfig;
6use std::path::Path;
7use std::sync::Arc;
8
9pub fn build_oauth_credential_store(
10    settings_source: &SettingsSourceArgs,
11    cwd: &Path,
12) -> Result<Arc<dyn OAuthCredentialStorage>, OAuthError> {
13    let settings_source =
14        settings_source.load_settings(cwd).map(|settings| settings.credentials_store).map_err(|e| {
15            OAuthError::CredentialStore(format!("Failed to load settings for credential store config: {e}"))
16        })?;
17
18    match settings_source {
19        Some(CredentialsStoreConfig::Memory) => Ok(Arc::new(FakeOAuthCredentialStore::new())),
20        Some(CredentialsStoreConfig::EncryptedFile { path, password_env }) => {
21            Ok(Arc::new(EncryptedFileOAuthCredentialStorage::from_settings(path, password_env.as_deref())?))
22        }
23        Some(CredentialsStoreConfig::Keyring) | None => Ok(Arc::new(OsKeyringStore::with_platform_store())),
24    }
25}
26
27#[cfg(test)]
28mod tests {
29    use super::*;
30
31    fn json_source(json: &str) -> SettingsSourceArgs {
32        SettingsSourceArgs { settings_json: Some(json.to_string()), settings_file: None }
33    }
34
35    #[test]
36    fn memory_store_selected_from_settings() {
37        let source = json_source(
38            r#"{
39                "credentialsStore": { "type": "memory" },
40                "agents": [{"name":"a","description":"a","model":"anthropic:claude-sonnet-4-5","userInvocable":true}]
41            }"#,
42        );
43
44        let store = build_oauth_credential_store(&source, Path::new(".")).unwrap();
45        assert!(!store.has_credential("anything"));
46    }
47
48    #[test]
49    fn encrypted_file_from_settings_propagates_missing_passphrase_error() {
50        let source = json_source(
51            r#"{
52                "credentialsStore": { "type": "encryptedFile", "passwordEnv": "AETHER_DEFINITELY_UNSET_VAR_98765" },
53                "agents": [{"name":"a","description":"a","model":"anthropic:claude-sonnet-4-5","userInvocable":true}]
54            }"#,
55        );
56
57        let result = build_oauth_credential_store(&source, Path::new("."));
58        assert!(result.is_err(), "missing passphrase should surface as a clean error");
59    }
60}