use anyhow::{Context, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::Mutex;
use tokio::time::sleep;
#[allow(dead_code)]
pub const VERCEL_AUTHORIZE_URL: &str = "https://vercel.com/oauth/authorize";
pub const VERCEL_TOKEN_URL: &str = "https://api.vercel.com/oauth/token";
#[allow(dead_code)]
pub const VERCEL_API_URL: &str = "https://api.vercel.com";
#[derive(Debug, Serialize)]
#[allow(dead_code)]
struct VercelTokenRequest {
grant_type: String,
code: String,
redirect_uri: String,
client_id: String,
code_verifier: String,
}
#[derive(Debug, Serialize)]
#[allow(dead_code)]
struct VercelRefreshTokenRequest {
grant_type: String,
refresh_token: String,
client_id: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct VercelTokenResponse {
pub access_token: String,
pub refresh_token: Option<String>,
pub token_type: String,
pub expires_in: Option<u64>,
pub scope: Option<String>,
}
#[derive(Debug, Deserialize)]
struct VercelErrorResponse {
error: String,
error_description: Option<String>,
}
#[allow(dead_code)]
pub struct VercelOAuthClient {
client: Client,
client_id: String,
#[allow(dead_code)]
redirect_uri: String,
}
impl Default for VercelOAuthClient {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl VercelOAuthClient {
pub fn new() -> Self {
Self {
client: Client::new(),
client_id: "rusty-commit-cli".to_string(), redirect_uri: "http://localhost:1456/auth/callback".to_string(),
}
}
fn generate_pkce() -> Result<(String, String)> {
let mut bytes = [0u8; 32];
generate_random_bytes(&mut bytes)?;
let verifier = URL_SAFE_NO_PAD.encode(bytes);
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
let challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
Ok((verifier, challenge))
}
fn generate_state() -> String {
let mut bytes = [0u8; 32];
let _ = generate_random_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
}
pub fn get_authorization_url(&self) -> Result<(String, String)> {
let (verifier, challenge) = Self::generate_pkce()?;
let state = Self::generate_state();
let params = [
("client_id", self.client_id.as_str()),
("redirect_uri", self.redirect_uri.as_str()),
("response_type", "code"),
("scope", "openid profile email"),
("state", state.as_str()),
("code_challenge", challenge.as_str()),
("code_challenge_method", "S256"),
];
let query = serde_urlencoded::to_string(params).context("Failed to encode OAuth params")?;
let auth_url = format!("{}?{}", VERCEL_AUTHORIZE_URL, query);
Ok((auth_url, verifier))
}
pub async fn start_callback_server(&self, verifier: String) -> Result<VercelTokenResponse> {
use warp::Filter;
let code = Arc::new(Mutex::new(None));
let code_clone = code.clone();
let callback = warp::path("auth")
.and(warp::path("callback"))
.and(warp::query::<std::collections::HashMap<String, String>>())
.map(move |params: std::collections::HashMap<String, String>| {
if let Some(auth_code) = params.get("code") {
let mut code_lock = code_clone.blocking_lock();
*code_lock = Some(auth_code.clone());
}
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>"#)
});
let server = warp::serve(callback).bind(([127, 0, 0, 1], 1456));
let server_handle = tokio::spawn(server);
let start = std::time::SystemTime::now();
let timeout = Duration::from_secs(300);
loop {
if let Some(auth_code) = &*code.lock().await {
let token = self.exchange_code_for_token(auth_code, &verifier).await?;
server_handle.abort();
return Ok(token);
}
if SystemTime::now().duration_since(start)? > timeout {
server_handle.abort();
anyhow::bail!("Authentication timeout");
}
sleep(Duration::from_millis(100)).await;
}
}
async fn exchange_code_for_token(
&self,
code: &str,
verifier: &str,
) -> Result<VercelTokenResponse> {
let params = [
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", self.redirect_uri.as_str()),
("client_id", self.client_id.as_str()),
("code_verifier", verifier),
];
let response = self
.client
.post(VERCEL_TOKEN_URL)
.form(¶ms)
.send()
.await
.context("Failed to exchange code for token")?;
if response.status().is_success() {
response
.json::<VercelTokenResponse>()
.await
.context("Failed to parse token response")
} else {
let error: VercelErrorResponse = response.json().await?;
anyhow::bail!(
"Token exchange failed: {} - {}",
error.error,
error.error_description.unwrap_or_default()
)
}
}
#[allow(dead_code)]
pub async fn refresh_token(&self, refresh_token: &str) -> Result<VercelTokenResponse> {
let params = [
("grant_type", "refresh_token"),
("refresh_token", refresh_token),
("client_id", self.client_id.as_str()),
];
let response = self
.client
.post(VERCEL_TOKEN_URL)
.form(¶ms)
.send()
.await
.context("Failed to refresh token")?;
if response.status().is_success() {
response
.json::<VercelTokenResponse>()
.await
.context("Failed to parse refresh token response")
} else {
let error: VercelErrorResponse = response.json().await?;
anyhow::bail!(
"Token refresh failed: {} - {}",
error.error,
error.error_description.unwrap_or_default()
)
}
}
}
#[allow(dead_code)]
fn generate_random_bytes(dest: &mut [u8]) -> Result<()> {
use rand::RngCore;
let mut rng = rand::rng();
rng.fill_bytes(dest);
Ok(())
}