stynx_code_auth/infrastructure/
file_provider.rs1use stynx_code_errors::{AppError, AppResult};
2
3use crate::domain::Credential;
4
5pub fn resolve_file_oauth() -> AppResult<Credential> {
6 let home = std::env::var("HOME")
7 .or_else(|_| std::env::var("USERPROFILE"))
8 .map_err(|_| AppError::Provider("cannot determine home directory".to_string()))?;
9 let home = std::path::PathBuf::from(home);
10
11 let candidates = [
12 home.join(".stynx").join(".credentials.json"),
13 home.join(".claude").join(".credentials.json"),
14 ];
15
16 let path = candidates
17 .iter()
18 .find(|p| p.exists())
19 .ok_or_else(|| AppError::Provider(format!(
20 "no credentials file at {} or {}",
21 candidates[0].display(),
22 candidates[1].display(),
23 )))?;
24
25 let contents = std::fs::read_to_string(path)
26 .map_err(|e| AppError::Provider(format!("cannot read {}: {e}", path.display())))?;
27
28 let parsed: serde_json::Value = serde_json::from_str(&contents)
29 .map_err(|e| AppError::Provider(format!("failed to parse credentials JSON: {e}")))?;
30
31 let oauth = parsed
32 .get("claudeAiOauth")
33 .ok_or_else(|| AppError::Provider("no claudeAiOauth in credentials file".to_string()))?;
34
35 let access_token = oauth
36 .get("accessToken")
37 .and_then(|v| v.as_str())
38 .ok_or_else(|| AppError::Provider("no accessToken in OAuth data".to_string()))?
39 .to_string();
40
41 let expires_at = oauth.get("expiresAt").and_then(|v| v.as_u64()).unwrap_or(0);
42
43 if expires_at > 0 {
44 let now_ms = std::time::SystemTime::now()
45 .duration_since(std::time::UNIX_EPOCH)
46 .map(|d| d.as_millis() as u64)
47 .unwrap_or(0);
48
49 if now_ms > expires_at {
50 return Err(AppError::Provider(
51 "Claude Code OAuth token expired. Run `claude` to refresh your session."
52 .to_string(),
53 ));
54 }
55 }
56
57 Ok(Credential::ClaudeCodeOAuth {
58 access_token,
59 expires_at,
60 })
61}
62
63pub fn resolve_settings_json() -> AppResult<Credential> {
64 let home = std::env::var("HOME")
65 .or_else(|_| std::env::var("USERPROFILE"))
66 .map_err(|_| AppError::Provider("cannot determine home directory".to_string()))?;
67
68 let path = std::path::PathBuf::from(home)
69 .join(".stynx")
70 .join("settings.json");
71
72 let contents = std::fs::read_to_string(&path)
73 .map_err(|e| AppError::Provider(format!("cannot read {}: {e}", path.display())))?;
74
75 let parsed: serde_json::Value = serde_json::from_str(&contents)
76 .map_err(|e| AppError::Provider(format!("failed to parse settings.json: {e}")))?;
77
78 let env = parsed
79 .get("env")
80 .ok_or_else(|| AppError::Provider("no env in settings.json".to_string()))?;
81
82 let token = env
83 .get("ANTHROPIC_AUTH_TOKEN")
84 .and_then(|v| v.as_str())
85 .ok_or_else(|| AppError::Provider("no ANTHROPIC_AUTH_TOKEN in settings.json".to_string()))?
86 .to_string();
87
88 let base_url = env
89 .get("ANTHROPIC_BASE_URL")
90 .and_then(|v| v.as_str())
91 .map(str::trim)
92 .filter(|s| !s.is_empty())
93 .unwrap_or("https://api.anthropic.com")
94 .to_string();
95
96 tracing::debug!("using ANTHROPIC_AUTH_TOKEN from settings.json");
97
98 Ok(Credential::AuthToken { token, base_url })
99}