use anyhow::{bail, Context, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use sha2::{Digest, Sha256};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use url::Url;
const API: &str = "https://openrouter.ai/api/v1/auth/keys";
fn random_token() -> Result<String> {
let mut bytes = [0u8; 32];
getrandom::getrandom(&mut bytes).map_err(|_| anyhow::anyhow!("OS randomness unavailable"))?;
Ok(URL_SAFE_NO_PAD.encode(bytes))
}
fn authorization_url(verifier: &str, callback: Option<&str>) -> Result<Url> {
let mut url = Url::parse("https://openrouter.ai/auth")?;
{
let mut params = url.query_pairs_mut();
params.append_pair(
"code_challenge",
&URL_SAFE_NO_PAD.encode(Sha256::digest(verifier)),
);
params.append_pair("code_challenge_method", "S256");
params.append_pair("key_label", "Volter Harness");
if let Some(callback) = callback {
params.append_pair("callback_url", callback);
}
}
Ok(url)
}
pub async fn authorize(headless: bool) -> Result<String> {
let verifier = random_token()?;
let code = if headless {
let url = authorization_url(&verifier, None)?;
eprintln!(
"Open this URL, authorize Volter Harness, then paste the authorization code:\n{url}"
);
crate::hidden_input::read_hidden_line().context("reading authorization code")?
} else {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.context("opening OAuth callback listener; use --oauth --headless over SSH")?;
let path = format!("/callback/{}", random_token()?);
let callback = format!("http://127.0.0.1:{}{path}", listener.local_addr()?.port());
let url = authorization_url(&verifier, Some(&callback))?;
eprintln!("Authorize Volter Harness in your browser:\n{url}");
open_browser(url.as_str());
tokio::time::timeout(Duration::from_secs(600), receive_callback(&listener, &path))
.await
.context("OpenRouter sign-in timed out; run login again")??
};
if code.trim().is_empty() || code.len() > 4096 {
bail!("invalid authorization code");
}
exchange(API, code.trim(), &verifier).await
}
fn open_browser(url: &str) {
#[cfg(target_os = "macos")]
let command = Some(("open", vec![url]));
#[cfg(target_os = "linux")]
let command = Some(("xdg-open", vec![url]));
#[cfg(target_os = "windows")]
let command = Some(("rundll32", vec!["url.dll,FileProtocolHandler", url]));
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let command: Option<(&str, Vec<&str>)> = None;
if let Some((program, args)) = command {
let _ = std::process::Command::new(program)
.args(args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
}
fn callback_code(target: &str, path: &str) -> Result<Option<String>> {
if !target.starts_with('/') || target.starts_with("//") {
return Ok(None);
}
let url = Url::parse(&format!("http://localhost{target}"))?;
if url.path() != path {
return Ok(None);
}
if url.query_pairs().any(|(name, _)| name == "error") {
bail!("OpenRouter authorization was declined");
}
let codes: Vec<_> = url
.query_pairs()
.filter(|(name, _)| name == "code")
.map(|(_, code)| code.into_owned())
.collect();
if codes.len() != 1 || codes[0].is_empty() || codes[0].len() > 4096 {
bail!("OpenRouter callback has no unique authorization code");
}
Ok(Some(codes[0].clone()))
}
async fn receive_callback(listener: &tokio::net::TcpListener, path: &str) -> Result<String> {
loop {
let (mut stream, _) = listener.accept().await?;
let mut bytes = Vec::new();
let read = tokio::time::timeout(Duration::from_secs(5), async {
let mut byte = [0u8; 1];
while bytes.len() < 8192 {
if stream.read(&mut byte).await? == 0 {
break;
}
bytes.push(byte[0]);
if bytes.ends_with(b"\r\n\r\n") {
break;
}
}
Ok::<_, std::io::Error>(())
})
.await;
if !matches!(read, Ok(Ok(()))) {
continue;
}
let request = String::from_utf8_lossy(&bytes);
let mut line = request
.lines()
.next()
.unwrap_or_default()
.split_whitespace();
let method = line.next();
let target = line.next().unwrap_or_default();
let result = if method == Some("GET") {
callback_code(target, path)
} else {
Ok(None)
};
let (status, message) = match &result {
Ok(Some(_)) => (
"200 OK",
"Authorization received. Return to Volter Harness to finish signing in.",
),
Ok(None) => ("404 Not Found", "Unknown callback."),
Err(_) => (
"400 Bad Request",
"Authorization failed. Return to Volter Harness.",
),
};
let response = format!("HTTP/1.1 {status}\r\nContent-Type: text/plain\r\nCache-Control: no-store\r\nReferrer-Policy: no-referrer\r\nConnection: close\r\nContent-Length: {}\r\n\r\n{message}", message.len());
let _ = stream.write_all(response.as_bytes()).await;
if let Some(code) = result? {
return Ok(code);
}
}
}
async fn exchange(endpoint: &str, code: &str, verifier: &str) -> Result<String> {
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(30))
.build()?;
let response = client
.post(endpoint)
.json(&serde_json::json!({
"code": code, "code_verifier": verifier, "code_challenge_method": "S256"
}))
.send()
.await
.context("OpenRouter authorization exchange failed")?;
if !response.status().is_success() {
bail!(
"OpenRouter authorization exchange returned HTTP {}; run login again",
response.status()
);
}
let body: serde_json::Value = response
.json()
.await
.context("invalid OpenRouter authorization response")?;
let key = body
.get("key")
.and_then(|value| value.as_str())
.filter(|key| {
(8..=4096).contains(&key.len())
&& key
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
})
.context("OpenRouter returned no valid API key")?;
Ok(key.to_owned())
}