use chrono::DateTime; use chrono::Utc; use serde::Deserialize; use serde::Serialize; use std::fs::OpenOptions; use std::io::Read; use std::io::Write; #[cfg(unix)] use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use std::process::Stdio; use tokio::process::Command; const SOURCE_FOR_PYTHON_SERVER: &str = include_str!("./login_with_chatgpt.py"); const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann"; /// Run `python3 -c {{SOURCE_FOR_PYTHON_SERVER}}` with the CODEX_HOME /// environment variable set to the provided `codex_home` path. If the /// subprocess exits 0, read the OPENAI_API_KEY property out of /// CODEX_HOME/auth.json and return Ok(OPENAI_API_KEY). Otherwise, return Err /// with any information from the subprocess. /// /// If `capture_output` is true, the subprocess's output will be captured and /// recorded in memory. Otherwise, the subprocess's output will be sent to the /// current process's stdout/stderr. pub async fn login_with_chatgpt( codex_home: &Path, capture_output: bool, ) -> std::io::Result { let child = Command::new("python3") .arg("-c") .arg(SOURCE_FOR_PYTHON_SERVER) .env("CODEX_HOME", codex_home) .stdin(Stdio::null()) .stdout(if capture_output { Stdio::piped() } else { Stdio::inherit() }) .stderr(if capture_output { Stdio::piped() } else { Stdio::inherit() }) .spawn()?; let output = child.wait_with_output().await?; if output.status.success() { try_read_openai_api_key(codex_home).await } else { let stderr = String::from_utf8_lossy(&output.stderr); Err(std::io::Error::other(format!( "login_with_chatgpt subprocess failed: {stderr}" ))) } } /// Attempt to read the `OPENAI_API_KEY` from the `auth.json` file in the given /// `CODEX_HOME` directory, refreshing it, if necessary. pub async fn try_read_openai_api_key(codex_home: &Path) -> std::io::Result { let auth_dot_json = try_read_auth_json(codex_home).await?; Ok(auth_dot_json.openai_api_key) } /// Attempt to read and refresh the `auth.json` file in the given `CODEX_HOME` directory. /// Returns the full AuthDotJson structure after refreshing if necessary. pub async fn try_read_auth_json(codex_home: &Path) -> std::io::Result { let auth_path = codex_home.join("auth.json"); let mut file = std::fs::File::open(&auth_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; let auth_dot_json: AuthDotJson = serde_json::from_str(&contents)?; if is_expired(&auth_dot_json) { let refresh_response = try_refresh_token(&auth_dot_json).await?; let mut auth_dot_json = auth_dot_json; auth_dot_json.tokens.id_token = refresh_response.id_token; if let Some(refresh_token) = refresh_response.refresh_token { auth_dot_json.tokens.refresh_token = refresh_token; } auth_dot_json.last_refresh = Utc::now(); let mut options = OpenOptions::new(); options.truncate(true).write(true).create(true); #[cfg(unix)] { options.mode(0o600); } let json_data = serde_json::to_string(&auth_dot_json)?; { let mut file = options.open(&auth_path)?; file.write_all(json_data.as_bytes())?; file.flush()?; } Ok(auth_dot_json) } else { Ok(auth_dot_json) } } fn is_expired(auth_dot_json: &AuthDotJson) -> bool { let last_refresh = auth_dot_json.last_refresh; last_refresh < Utc::now() - chrono::Duration::days(28) } async fn try_refresh_token(auth_dot_json: &AuthDotJson) -> std::io::Result { let refresh_request = RefreshRequest { client_id: CLIENT_ID, grant_type: "refresh_token", refresh_token: auth_dot_json.tokens.refresh_token.clone(), scope: "openid profile email", }; let client = reqwest::Client::new(); let response = client .post("https://auth.openai.com/oauth/token") .header("Content-Type", "application/json") .json(&refresh_request) .send() .await .map_err(std::io::Error::other)?; if response.status().is_success() { let refresh_response = response .json::() .await .map_err(std::io::Error::other)?; Ok(refresh_response) } else { Err(std::io::Error::other(format!( "Failed to refresh token: {}", response.status() ))) } } #[derive(Serialize)] struct RefreshRequest { client_id: &'static str, grant_type: &'static str, refresh_token: String, scope: &'static str, } #[derive(Deserialize)] struct RefreshResponse { id_token: String, refresh_token: Option, } /// Expected structure for $CODEX_HOME/auth.json. #[derive(Deserialize, Serialize)] pub struct AuthDotJson { #[serde(rename = "OPENAI_API_KEY")] pub openai_api_key: String, pub tokens: TokenData, pub last_refresh: DateTime, } #[derive(Deserialize, Serialize, Clone)] pub struct TokenData { /// This is a JWT. pub id_token: String, /// This is a JWT. pub access_token: String, pub refresh_token: String, pub account_id: String, }