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_keychain_oauth() -> AppResult<Credential> {
    let output = std::process::Command::new("security")
        .args(["find-generic-password", "-s", "Claude Code-credentials", "-w"])
        .output()
        .map_err(|e| AppError::Provider(format!("failed to run `security`: {e}")))?;

    if !output.status.success() {
        return Err(AppError::Provider(
            "no Claude Code credentials in Keychain".to_string(),
        ));
    }

    let json_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
    let parsed: serde_json::Value = serde_json::from_str(&json_str)
        .map_err(|e| AppError::Provider(format!("failed to parse Keychain JSON: {e}")))?;

    let oauth = parsed
        .get("claudeAiOauth")
        .ok_or_else(|| AppError::Provider("no claudeAiOauth in Keychain data".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,
    })
}