stynx-code-auth 3.12.1

Authentication with API keys and macOS Keychain OAuth
Documentation
use stynx_code_errors::{AppError, AppResult};

use crate::domain::Credential;

pub fn resolve_file_oauth() -> AppResult<Credential> {
    let home = std::env::var("HOME")
        .or_else(|_| std::env::var("USERPROFILE"))
        .map_err(|_| AppError::Provider("cannot determine home directory".to_string()))?;
    let home = std::path::PathBuf::from(home);

    let candidates = [
        home.join(".stynx").join(".credentials.json"),
        home.join(".claude").join(".credentials.json"),
    ];

    let path = candidates
        .iter()
        .find(|p| p.exists())
        .ok_or_else(|| AppError::Provider(format!(
            "no credentials file at {} or {}",
            candidates[0].display(),
            candidates[1].display(),
        )))?;

    let contents = std::fs::read_to_string(path)
        .map_err(|e| AppError::Provider(format!("cannot read {}: {e}", path.display())))?;

    let parsed: serde_json::Value = serde_json::from_str(&contents)
        .map_err(|e| AppError::Provider(format!("failed to parse credentials JSON: {e}")))?;

    let oauth = parsed
        .get("claudeAiOauth")
        .ok_or_else(|| AppError::Provider("no claudeAiOauth in credentials file".to_string()))?;

    let access_token = oauth
        .get("accessToken")
        .and_then(|v| v.as_str())
        .ok_or_else(|| AppError::Provider("no accessToken in OAuth data".to_string()))?
        .to_string();

    let expires_at = oauth.get("expiresAt").and_then(|v| v.as_u64()).unwrap_or(0);

    if expires_at > 0 {
        let now_ms = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_millis() as u64)
            .unwrap_or(0);

        if now_ms > expires_at {
            return Err(AppError::Provider(
                "Claude Code OAuth token expired. Run `claude` to refresh your session."
                    .to_string(),
            ));
        }
    }

    Ok(Credential::ClaudeCodeOAuth {
        access_token,
        expires_at,
    })
}

pub fn resolve_settings_json() -> AppResult<Credential> {
    let home = std::env::var("HOME")
        .or_else(|_| std::env::var("USERPROFILE"))
        .map_err(|_| AppError::Provider("cannot determine home directory".to_string()))?;

    let path = std::path::PathBuf::from(home)
        .join(".stynx")
        .join("settings.json");

    let contents = std::fs::read_to_string(&path)
        .map_err(|e| AppError::Provider(format!("cannot read {}: {e}", path.display())))?;

    let parsed: serde_json::Value = serde_json::from_str(&contents)
        .map_err(|e| AppError::Provider(format!("failed to parse settings.json: {e}")))?;

    let env = parsed
        .get("env")
        .ok_or_else(|| AppError::Provider("no env in settings.json".to_string()))?;

    let token = env
        .get("ANTHROPIC_AUTH_TOKEN")
        .and_then(|v| v.as_str())
        .ok_or_else(|| AppError::Provider("no ANTHROPIC_AUTH_TOKEN in settings.json".to_string()))?
        .to_string();

    let base_url = env
        .get("ANTHROPIC_BASE_URL")
        .and_then(|v| v.as_str())
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .unwrap_or("https://api.anthropic.com")
        .to_string();

    tracing::debug!("using ANTHROPIC_AUTH_TOKEN from settings.json");

    Ok(Credential::AuthToken { token, base_url })
}