Skip to main content

stynx_code_auth/infrastructure/
file_provider.rs

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