Skip to main content

aptu_core/
auth.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Token provider abstraction for multi-platform credential resolution.
4//!
5//! This module defines the `TokenProvider` trait, which abstracts credential
6//! resolution across different platforms (CLI, iOS, etc.). Each platform
7//! implements this trait to provide GitHub and AI provider tokens from their
8//! respective credential sources.
9
10use secrecy::SecretString;
11
12/// Provides GitHub and AI provider credentials for API calls.
13///
14/// This trait abstracts credential resolution across platforms:
15/// - **CLI:** Resolves from environment variables, GitHub CLI, or system keyring
16/// - **iOS:** Resolves from iOS keychain via FFI
17///
18/// Implementations should handle credential lookup and return `None` if
19/// credentials are not available.
20pub trait TokenProvider: Send + Sync {
21    /// Retrieves the GitHub API token.
22    ///
23    /// Returns `None` if no token is available from any source.
24    fn github_token(&self) -> Option<SecretString>;
25
26    /// Retrieves an AI provider API key.
27    ///
28    /// # Arguments
29    /// * `provider` - The AI provider name (must match a registered provider in the registry)
30    ///
31    /// Returns `None` if no API key is available from any source.
32    fn ai_api_key(&self, provider: &str) -> Option<SecretString>;
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::ai::registry::all_providers;
39    use std::collections::HashMap;
40
41    /// Mock implementation for testing.
42    struct MockTokenProvider {
43        github_token: Option<SecretString>,
44        ai_keys: HashMap<String, SecretString>,
45    }
46
47    impl Drop for MockTokenProvider {
48        fn drop(&mut self) {
49            use zeroize::Zeroize;
50            if let Some(ref mut gh_token) = self.github_token {
51                gh_token.zeroize();
52            }
53            for ai_key in self.ai_keys.values_mut() {
54                ai_key.zeroize();
55            }
56        }
57    }
58
59    impl TokenProvider for MockTokenProvider {
60        fn github_token(&self) -> Option<SecretString> {
61            self.github_token.clone()
62        }
63
64        fn ai_api_key(&self, provider: &str) -> Option<SecretString> {
65            self.ai_keys.get(provider).cloned()
66        }
67    }
68
69    #[test]
70    fn test_mock_provider_with_tokens() {
71        let mut ai_keys = HashMap::new();
72        for provider_config in all_providers() {
73            ai_keys.insert(
74                provider_config.name.to_string(),
75                SecretString::from(format!("{}_key", provider_config.name)),
76            );
77        }
78
79        let provider = MockTokenProvider {
80            github_token: Some(SecretString::from("gh_token")),
81            ai_keys,
82        };
83
84        assert!(provider.github_token().is_some());
85        for provider_config in all_providers() {
86            assert!(
87                provider.ai_api_key(provider_config.name).is_some(),
88                "Expected key for provider: {}",
89                provider_config.name
90            );
91        }
92    }
93
94    #[test]
95    fn test_mock_provider_without_tokens() {
96        let provider = MockTokenProvider {
97            github_token: None,
98            ai_keys: HashMap::new(),
99        };
100
101        assert!(provider.github_token().is_none());
102        for provider_config in all_providers() {
103            assert!(
104                provider.ai_api_key(provider_config.name).is_none(),
105                "Expected no key for provider: {}",
106                provider_config.name
107            );
108        }
109    }
110}