coyote-ai 0.8.3

An all-in-one, batteries included LLM CLI Tool
use url::Url;

use super::oauth::{OAuthConfig, OAuthFlow, OAuthProvider, TokenRequestFormat};

pub struct OpenAICompatibleOAuthProvider {
    pub config: OAuthConfig,
    pub client_name: String,
}

fn is_loopback_uri(uri: &str) -> bool {
    Url::parse(uri)
        .ok()
        .and_then(|u| u.host_str().map(str::to_string))
        .is_some_and(|host| matches!(host.as_str(), "127.0.0.1" | "localhost" | "[::1]" | "::1"))
}

impl OAuthProvider for OpenAICompatibleOAuthProvider {
    fn provider_name(&self) -> &str {
        &self.client_name
    }

    fn client_id(&self) -> &str {
        &self.config.client_id
    }

    fn authorize_url(&self) -> &str {
        self.config.authorize_url.as_deref().unwrap_or("")
    }

    fn token_url(&self) -> &str {
        &self.config.token_url
    }

    fn redirect_uri(&self) -> &str {
        self.config.redirect_uri.as_deref().unwrap_or("")
    }

    fn scopes(&self) -> String {
        self.config.scopes.join(" ")
    }

    fn client_secret(&self) -> Option<&str> {
        self.config.client_secret.as_deref()
    }

    fn extra_authorize_params(&self) -> Vec<(&str, &str)> {
        self.config
            .extra_authorize_params
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect()
    }

    fn token_request_format(&self) -> TokenRequestFormat {
        self.config
            .token_request_format
            .unwrap_or(TokenRequestFormat::FormUrlEncoded)
    }

    fn uses_localhost_redirect(&self) -> bool {
        self.config.redirect_uri.is_none() && self.config.redirect_port.is_none()
    }

    fn extra_token_headers(&self) -> Vec<(&str, &str)> {
        self.config
            .extra_token_headers
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect()
    }

    fn extra_request_headers(&self) -> Vec<(&str, &str)> {
        self.config
            .extra_request_headers
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect()
    }

    fn fixed_redirect_uri(&self) -> Option<String> {
        if let Some(uri) = &self.config.redirect_uri {
            return if is_loopback_uri(uri) {
                Some(uri.clone())
            } else {
                None
            };
        }
        if let Some(port) = self.config.redirect_port {
            return Some(format!("http://127.0.0.1:{port}/callback"));
        }
        None
    }

    fn include_state_in_token_exchange(&self) -> bool {
        self.config.include_state_in_token_exchange
    }

    fn flow(&self) -> OAuthFlow {
        self.config.flow
    }

    fn echo_pkce_in_token_exchange(&self) -> bool {
        self.config.echo_pkce_in_token_exchange
    }

    fn device_authorization_url(&self) -> Option<&str> {
        self.config.device_authorization_url.as_deref()
    }

    fn use_pkce_in_device_flow(&self) -> bool {
        self.config.use_pkce_in_device_flow
    }
}