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    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}