1use crate::credential::{Credential, UsageStats};
4use crate::pool::AuthPool;
5use anyhow::{Context, Result};
6use std::path::Path;
7
8impl AuthPool {
9 pub fn import_from_auth_profiles_file(&mut self, path: &Path) -> Result<Vec<String>> {
11 let content = std::fs::read_to_string(path)
12 .with_context(|| format!("Failed to read: {}", path.display()))?;
13 let json: serde_json::Value = serde_json::from_str(&content)
14 .with_context(|| format!("Failed to parse JSON: {}", path.display()))?;
15 Ok(self.import_from_auth_profiles_json(&json))
16 }
17
18 pub fn import_from_auth_profiles_json(&mut self, profiles_json: &serde_json::Value) -> Vec<String> {
20 let mut imported = Vec::new();
21
22 if let Some(profiles) = profiles_json.get("profiles").and_then(|v| v.as_object()) {
24 for (cred_name, profile_data) in profiles {
25 let provider = profile_data
26 .get("provider")
27 .and_then(|v| v.as_str())
28 .map(|s| s.to_string())
29 .unwrap_or_else(|| {
30 cred_name
31 .split(':')
32 .next()
33 .unwrap_or(cred_name)
34 .to_string()
35 });
36
37 let cred_type = profile_data
38 .get("type")
39 .and_then(|v| v.as_str())
40 .unwrap_or("token")
41 .to_string();
42
43 let token = if cred_type == "oauth" {
44 profile_data
45 .get("access")
46 .or_else(|| profile_data.get("token"))
47 .and_then(|v| v.as_str())
48 .filter(|s| *s != "keychain")
49 .map(|s| s.to_string())
50 } else {
51 profile_data
52 .get("token")
53 .or_else(|| profile_data.get("apiKey"))
54 .and_then(|v| v.as_str())
55 .map(|s| s.to_string())
56 };
57
58 let keychain_service = if cred_type == "oauth" {
59 profile_data
60 .get("access")
61 .and_then(|v| v.as_str())
62 .filter(|s| *s == "keychain")
63 .map(|_| format!("auth-profiles:{}", cred_name))
64 } else {
65 None
66 };
67
68 let cred = Credential {
69 provider: provider.clone(),
70 cred_type,
71 token,
72 keychain_service,
73 };
74
75 self.pool.insert(cred_name.clone(), cred);
76 imported.push(cred_name.clone());
77 }
78 }
79
80 if let Some(order) = profiles_json.get("order").and_then(|v| v.as_object()) {
82 for (provider, order_arr) in order {
83 if let Some(arr) = order_arr.as_array() {
84 let names: Vec<String> = arr
85 .iter()
86 .filter_map(|v| v.as_str())
87 .map(|name| name.to_string())
88 .collect();
89 self.order.insert(provider.clone(), names);
90 }
91 }
92 }
93
94 if let Some(last_good) = profiles_json.get("lastGood").and_then(|v| v.as_object()) {
96 for (provider, name) in last_good {
97 if let Some(name_str) = name.as_str() {
98 let full_name = if name_str.contains(':') {
99 name_str.to_string()
100 } else {
101 format!("{}:{}", provider, name_str)
102 };
103 self.defaults.insert(provider.clone(), full_name);
104 }
105 }
106 }
107
108 if let Some(usage) = profiles_json.get("usageStats").and_then(|v| v.as_object()) {
110 for (cred_name, stats) in usage {
111 let usage_stat = UsageStats {
112 last_used: stats.get("lastUsed").and_then(|v| v.as_u64()),
113 error_count: stats
114 .get("errorCount")
115 .and_then(|v| v.as_u64())
116 .map(|v| v as u32),
117 cooldown_until: stats.get("cooldownUntil").and_then(|v| v.as_u64()),
118 };
119 self.usage_stats.insert(cred_name.clone(), usage_stat);
120 }
121 }
122
123 for provider in self.providers() {
125 if !self.defaults.contains_key(&provider) {
126 if let Some(order) = self.order.get(&provider) {
127 if let Some(first) = order.first() {
128 self.defaults.insert(provider, first.clone());
129 }
130 }
131 }
132 }
133
134 imported
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_import_from_json() {
144 let json = serde_json::json!({
145 "profiles": {
146 "anthropic:default": {
147 "type": "token",
148 "provider": "anthropic",
149 "token": "sk-ant-1"
150 },
151 "anthropic:tonitang273": {
152 "type": "token",
153 "provider": "anthropic",
154 "token": "sk-ant-2"
155 }
156 },
157 "order": {
158 "anthropic": ["anthropic:default", "anthropic:tonitang273"]
159 },
160 "lastGood": {
161 "anthropic": "anthropic:default"
162 },
163 "usageStats": {
164 "anthropic:default": {"lastUsed": 12345, "errorCount": 0}
165 }
166 });
167
168 let mut pool = AuthPool::default();
169 let imported = pool.import_from_auth_profiles_json(&json);
170
171 assert_eq!(imported.len(), 2);
172 assert!(pool.get("anthropic:default").is_some());
173 assert!(pool.get("anthropic:tonitang273").is_some());
174 assert_eq!(
175 pool.defaults.get("anthropic").map(|s| s.as_str()),
176 Some("anthropic:default")
177 );
178 }
179
180 #[test]
181 fn test_import_oauth_with_keychain() {
182 let json = serde_json::json!({
183 "profiles": {
184 "anthropic:keychain": {
185 "type": "oauth",
186 "provider": "anthropic",
187 "access": "keychain",
188 "refresh": "keychain",
189 "expires": 9999999999999u64
190 }
191 },
192 "order": {
193 "anthropic": ["anthropic:keychain"]
194 }
195 });
196
197 let mut pool = AuthPool::default();
198 pool.import_from_auth_profiles_json(&json);
199
200 let cred = pool.get("anthropic:keychain").unwrap();
201 assert_eq!(cred.cred_type, "oauth");
202 assert!(cred.token.is_none());
203 assert!(cred.keychain_service.is_some());
204 }
205}