Skip to main content

rusty_commit/auth/
vercel_oauth.rs

1use anyhow::{Context, Result};
2use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::sync::Arc;
7use std::time::{Duration, SystemTime};
8use tokio::sync::Mutex;
9use tokio::time::sleep;
10
11// Vercel OAuth endpoints
12#[allow(dead_code)]
13pub const VERCEL_AUTHORIZE_URL: &str = "https://vercel.com/oauth/authorize";
14pub const VERCEL_TOKEN_URL: &str = "https://api.vercel.com/oauth/token";
15#[allow(dead_code)]
16pub const VERCEL_API_URL: &str = "https://api.vercel.com";
17
18#[derive(Debug, Serialize)]
19#[allow(dead_code)]
20struct VercelTokenRequest {
21    grant_type: String,
22    code: String,
23    redirect_uri: String,
24    client_id: String,
25    code_verifier: String,
26}
27
28#[derive(Debug, Serialize)]
29#[allow(dead_code)]
30struct VercelRefreshTokenRequest {
31    grant_type: String,
32    refresh_token: String,
33    client_id: String,
34}
35
36#[derive(Debug, Deserialize)]
37#[allow(dead_code)]
38pub struct VercelTokenResponse {
39    pub access_token: String,
40    pub refresh_token: Option<String>,
41    pub token_type: String,
42    pub expires_in: Option<u64>,
43    pub scope: Option<String>,
44}
45
46#[derive(Debug, Deserialize)]
47struct VercelErrorResponse {
48    error: String,
49    error_description: Option<String>,
50}
51
52/// OAuth client for Vercel AI (AI SDK integration)
53#[allow(dead_code)]
54pub struct VercelOAuthClient {
55    client: Client,
56    client_id: String,
57    #[allow(dead_code)]
58    redirect_uri: String,
59}
60
61impl Default for VercelOAuthClient {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67#[allow(dead_code)]
68impl VercelOAuthClient {
69    pub fn new() -> Self {
70        Self {
71            client: Client::new(),
72            client_id: "rusty-commit-cli".to_string(), // Placeholder - would need registration
73            redirect_uri: "http://localhost:1456/auth/callback".to_string(),
74        }
75    }
76
77    /// Generate PKCE challenge and verifier
78    fn generate_pkce() -> Result<(String, String)> {
79        let mut bytes = [0u8; 32];
80        generate_random_bytes(&mut bytes)?;
81        let verifier = URL_SAFE_NO_PAD.encode(bytes);
82
83        let mut hasher = Sha256::new();
84        hasher.update(verifier.as_bytes());
85        let challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
86
87        Ok((verifier, challenge))
88    }
89
90    /// Generate random state for CSRF protection
91    fn generate_state() -> String {
92        let mut bytes = [0u8; 32];
93        // fill_bytes never fails, so we don't need unwrap_or_default
94        let _ = generate_random_bytes(&mut bytes);
95        URL_SAFE_NO_PAD.encode(bytes)
96    }
97
98    /// Build authorization URL with PKCE
99    pub fn get_authorization_url(&self) -> Result<(String, String)> {
100        let (verifier, challenge) = Self::generate_pkce()?;
101        let state = Self::generate_state();
102
103        let params = [
104            ("client_id", self.client_id.as_str()),
105            ("redirect_uri", self.redirect_uri.as_str()),
106            ("response_type", "code"),
107            ("scope", "openid profile email"),
108            ("state", state.as_str()),
109            ("code_challenge", challenge.as_str()),
110            ("code_challenge_method", "S256"),
111        ];
112
113        let query = serde_urlencoded::to_string(params).context("Failed to encode OAuth params")?;
114        let auth_url = format!("{}?{}", VERCEL_AUTHORIZE_URL, query);
115
116        Ok((auth_url, verifier))
117    }
118
119    /// Start local server to receive OAuth callback
120    pub async fn start_callback_server(&self, verifier: String) -> Result<VercelTokenResponse> {
121        use warp::Filter;
122
123        let code = Arc::new(Mutex::new(None));
124        let code_clone = code.clone();
125
126        let callback = warp::path("auth")
127            .and(warp::path("callback"))
128            .and(warp::query::<std::collections::HashMap<String, String>>())
129            .map(move |params: std::collections::HashMap<String, String>| {
130                if let Some(auth_code) = params.get("code") {
131                    let mut code_lock = code_clone.blocking_lock();
132                    *code_lock = Some(auth_code.clone());
133                }
134                warp::reply::html(r#"<!DOCTYPE html><html><head><title>Authenticated!</title></head><body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #000;"><div style="background: white; padding: 2rem; border-radius: 8px; text-align: center;"><h1 style="color: #000;">Authentication Successful!</h1><p>You can close this window.</p></div></body></html>"#)
135            });
136
137        let server = warp::serve(callback).bind(([127, 0, 0, 1], 1456));
138        let server_handle = tokio::spawn(server);
139
140        let start = std::time::SystemTime::now();
141        let timeout = Duration::from_secs(300);
142
143        loop {
144            if let Some(auth_code) = &*code.lock().await {
145                let token = self.exchange_code_for_token(auth_code, &verifier).await?;
146                server_handle.abort();
147                return Ok(token);
148            }
149
150            if SystemTime::now().duration_since(start)? > timeout {
151                server_handle.abort();
152                anyhow::bail!("Authentication timeout");
153            }
154
155            sleep(Duration::from_millis(100)).await;
156        }
157    }
158
159    /// Exchange authorization code for access token
160    async fn exchange_code_for_token(
161        &self,
162        code: &str,
163        verifier: &str,
164    ) -> Result<VercelTokenResponse> {
165        let params = [
166            ("grant_type", "authorization_code"),
167            ("code", code),
168            ("redirect_uri", self.redirect_uri.as_str()),
169            ("client_id", self.client_id.as_str()),
170            ("code_verifier", verifier),
171        ];
172
173        let response = self
174            .client
175            .post(VERCEL_TOKEN_URL)
176            .form(&params)
177            .send()
178            .await
179            .context("Failed to exchange code for token")?;
180
181        if response.status().is_success() {
182            response
183                .json::<VercelTokenResponse>()
184                .await
185                .context("Failed to parse token response")
186        } else {
187            let error: VercelErrorResponse = response.json().await?;
188            anyhow::bail!(
189                "Token exchange failed: {} - {}",
190                error.error,
191                error.error_description.unwrap_or_default()
192            )
193        }
194    }
195
196    /// Refresh an access token
197    #[allow(dead_code)]
198    pub async fn refresh_token(&self, refresh_token: &str) -> Result<VercelTokenResponse> {
199        let params = [
200            ("grant_type", "refresh_token"),
201            ("refresh_token", refresh_token),
202            ("client_id", self.client_id.as_str()),
203        ];
204
205        let response = self
206            .client
207            .post(VERCEL_TOKEN_URL)
208            .form(&params)
209            .send()
210            .await
211            .context("Failed to refresh token")?;
212
213        if response.status().is_success() {
214            response
215                .json::<VercelTokenResponse>()
216                .await
217                .context("Failed to parse refresh token response")
218        } else {
219            let error: VercelErrorResponse = response.json().await?;
220            anyhow::bail!(
221                "Token refresh failed: {} - {}",
222                error.error,
223                error.error_description.unwrap_or_default()
224            )
225        }
226    }
227}
228
229/// Generate random bytes
230#[allow(dead_code)]
231fn generate_random_bytes(dest: &mut [u8]) -> Result<()> {
232    use rand::RngCore;
233    let mut rng = rand::rng();
234    rng.fill_bytes(dest);
235    Ok(())
236}