use crate::credential::{Credential, UsageStats};
use crate::pool::AuthPool;
use anyhow::{Context, Result};
use std::path::Path;
impl AuthPool {
pub fn import_from_auth_profiles_file(&mut self, path: &Path) -> Result<Vec<String>> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read: {}", path.display()))?;
let json: serde_json::Value = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse JSON: {}", path.display()))?;
Ok(self.import_from_auth_profiles_json(&json))
}
pub fn import_from_auth_profiles_json(&mut self, profiles_json: &serde_json::Value) -> Vec<String> {
let mut imported = Vec::new();
if let Some(profiles) = profiles_json.get("profiles").and_then(|v| v.as_object()) {
for (cred_name, profile_data) in profiles {
let provider = profile_data
.get("provider")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
cred_name
.split(':')
.next()
.unwrap_or(cred_name)
.to_string()
});
let cred_type = profile_data
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("token")
.to_string();
let token = if cred_type == "oauth" {
profile_data
.get("access")
.or_else(|| profile_data.get("token"))
.and_then(|v| v.as_str())
.filter(|s| *s != "keychain")
.map(|s| s.to_string())
} else {
profile_data
.get("token")
.or_else(|| profile_data.get("apiKey"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
};
let keychain_service = if cred_type == "oauth" {
profile_data
.get("access")
.and_then(|v| v.as_str())
.filter(|s| *s == "keychain")
.map(|_| format!("auth-profiles:{}", cred_name))
} else {
None
};
let cred = Credential {
provider: provider.clone(),
cred_type,
token,
keychain_service,
};
self.pool.insert(cred_name.clone(), cred);
imported.push(cred_name.clone());
}
}
if let Some(order) = profiles_json.get("order").and_then(|v| v.as_object()) {
for (provider, order_arr) in order {
if let Some(arr) = order_arr.as_array() {
let names: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str())
.map(|name| name.to_string())
.collect();
self.order.insert(provider.clone(), names);
}
}
}
if let Some(last_good) = profiles_json.get("lastGood").and_then(|v| v.as_object()) {
for (provider, name) in last_good {
if let Some(name_str) = name.as_str() {
let full_name = if name_str.contains(':') {
name_str.to_string()
} else {
format!("{}:{}", provider, name_str)
};
self.defaults.insert(provider.clone(), full_name);
}
}
}
if let Some(usage) = profiles_json.get("usageStats").and_then(|v| v.as_object()) {
for (cred_name, stats) in usage {
let usage_stat = UsageStats {
last_used: stats.get("lastUsed").and_then(|v| v.as_u64()),
error_count: stats
.get("errorCount")
.and_then(|v| v.as_u64())
.map(|v| v as u32),
cooldown_until: stats.get("cooldownUntil").and_then(|v| v.as_u64()),
};
self.usage_stats.insert(cred_name.clone(), usage_stat);
}
}
for provider in self.providers() {
if !self.defaults.contains_key(&provider) {
if let Some(order) = self.order.get(&provider) {
if let Some(first) = order.first() {
self.defaults.insert(provider, first.clone());
}
}
}
}
imported
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_import_from_json() {
let json = serde_json::json!({
"profiles": {
"anthropic:default": {
"type": "token",
"provider": "anthropic",
"token": "sk-ant-1"
},
"anthropic:tonitang273": {
"type": "token",
"provider": "anthropic",
"token": "sk-ant-2"
}
},
"order": {
"anthropic": ["anthropic:default", "anthropic:tonitang273"]
},
"lastGood": {
"anthropic": "anthropic:default"
},
"usageStats": {
"anthropic:default": {"lastUsed": 12345, "errorCount": 0}
}
});
let mut pool = AuthPool::default();
let imported = pool.import_from_auth_profiles_json(&json);
assert_eq!(imported.len(), 2);
assert!(pool.get("anthropic:default").is_some());
assert!(pool.get("anthropic:tonitang273").is_some());
assert_eq!(
pool.defaults.get("anthropic").map(|s| s.as_str()),
Some("anthropic:default")
);
}
#[test]
fn test_import_oauth_with_keychain() {
let json = serde_json::json!({
"profiles": {
"anthropic:keychain": {
"type": "oauth",
"provider": "anthropic",
"access": "keychain",
"refresh": "keychain",
"expires": 9999999999999u64
}
},
"order": {
"anthropic": ["anthropic:keychain"]
}
});
let mut pool = AuthPool::default();
pool.import_from_auth_profiles_json(&json);
let cred = pool.get("anthropic:keychain").unwrap();
assert_eq!(cred.cred_type, "oauth");
assert!(cred.token.is_none());
assert!(cred.keychain_service.is_some());
}
}