cc-switch-tui 0.2.1

All-in-One Assistant for Claude Code, Codex, Gemini, OpenCode, OpenClaw & Hermes
Documentation
use std::path::PathBuf;
use std::sync::{Arc, OnceLock, RwLock};

use crate::config::get_app_config_dir;
use crate::proxy::providers::codex_oauth_auth::{
    CodexOAuthError, CodexOAuthManager, CodexOAuthStatus, ManagedAuthAccount,
    ManagedAuthDeviceCodeResponse,
};
use crate::services::subscription::{query_codex_quota, CredentialStatus, SubscriptionQuota};

fn manager_store() -> &'static RwLock<Option<(PathBuf, Arc<CodexOAuthManager>)>> {
    static STORE: OnceLock<RwLock<Option<(PathBuf, Arc<CodexOAuthManager>)>>> = OnceLock::new();
    STORE.get_or_init(|| RwLock::new(None))
}

pub struct CodexOAuthService;

impl CodexOAuthService {
    pub fn manager() -> Arc<CodexOAuthManager> {
        let path = get_app_config_dir();
        {
            let guard = manager_store().read().expect("read codex oauth manager");
            if let Some((cached_path, manager)) = guard.as_ref() {
                if cached_path == &path {
                    return Arc::clone(manager);
                }
            }
        }

        let manager = Arc::new(CodexOAuthManager::new(path.clone()));
        let mut guard = manager_store().write().expect("write codex oauth manager");
        *guard = Some((path, Arc::clone(&manager)));
        manager
    }

    #[cfg(test)]
    pub(crate) fn reset_for_tests() {
        let mut guard = manager_store().write().expect("write codex oauth manager");
        *guard = None;
    }

    pub async fn start_device_flow() -> Result<ManagedAuthDeviceCodeResponse, CodexOAuthError> {
        Self::manager().start_device_flow().await
    }

    pub async fn poll_for_token(
        device_code: &str,
    ) -> Result<Option<ManagedAuthAccount>, CodexOAuthError> {
        Self::manager().poll_for_token(device_code).await
    }

    pub async fn get_valid_token_for_account(account_id: &str) -> Result<String, CodexOAuthError> {
        Self::manager()
            .get_valid_token_for_account(account_id)
            .await
    }

    pub async fn get_valid_token() -> Result<String, CodexOAuthError> {
        Self::manager().get_valid_token().await
    }

    pub async fn default_account_id() -> Option<String> {
        Self::manager().default_account_id().await
    }

    pub async fn remove_account(account_id: &str) -> Result<(), CodexOAuthError> {
        Self::manager().remove_account(account_id).await
    }

    pub async fn set_default_account(account_id: &str) -> Result<(), CodexOAuthError> {
        Self::manager().set_default_account(account_id).await
    }

    pub async fn clear_auth() -> Result<(), CodexOAuthError> {
        Self::manager().clear_auth().await
    }

    pub async fn get_status() -> CodexOAuthStatus {
        Self::manager().get_status().await
    }

    pub async fn get_quota(account_id: Option<&str>) -> SubscriptionQuota {
        let manager = Self::manager();
        let resolved_account_id = match account_id {
            Some(account_id) => Some(account_id.to_string()),
            None => manager.default_account_id().await,
        };

        let Some(account_id) = resolved_account_id else {
            return SubscriptionQuota::not_found("codex_oauth");
        };

        let token = match manager.get_valid_token_for_account(&account_id).await {
            Ok(token) => token,
            Err(error) => {
                return SubscriptionQuota::error(
                    "codex_oauth",
                    CredentialStatus::Expired,
                    format!("Codex OAuth token unavailable: {error}"),
                );
            }
        };

        query_codex_quota(
            &token,
            Some(&account_id),
            "codex_oauth",
            "Codex OAuth access token expired or rejected. Please re-login via cc-switch.",
        )
        .await
    }

    #[cfg(test)]
    pub(crate) async fn seed_account_for_tests(
        account_id: &str,
        refresh_token: &str,
        email: Option<&str>,
        access_token: Option<&str>,
        expires_at_ms: Option<i64>,
    ) -> Result<(), CodexOAuthError> {
        Self::manager()
            .seed_account_for_tests(
                account_id,
                refresh_token,
                email,
                access_token,
                expires_at_ms,
            )
            .await
    }
}