use std::path::PathBuf;
use tokio::process::Command;
use crate::{
capabilities::{guard_is_supported, log_guard_skip},
process::{preferred_output_channel, spawn_with_retry},
CodexClient, CodexError,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CodexAuthStatus {
LoggedIn(CodexAuthMethod),
LoggedOut,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CodexAuthMethod {
ChatGpt,
ApiKey {
masked_key: Option<String>,
},
Unknown {
raw: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CodexLogoutStatus {
LoggedOut,
AlreadyLoggedOut,
}
#[derive(Clone, Debug)]
pub struct AuthSessionHelper {
client: CodexClient,
}
impl AuthSessionHelper {
pub fn new(app_codex_home: impl Into<PathBuf>) -> Self {
let client = CodexClient::builder()
.codex_home(app_codex_home)
.create_home_dirs(true)
.build();
Self { client }
}
pub fn with_client(client: CodexClient) -> Self {
Self { client }
}
pub fn client(&self) -> CodexClient {
self.client.clone()
}
pub async fn status(&self) -> Result<CodexAuthStatus, CodexError> {
self.client.login_status().await
}
pub async fn ensure_api_key_login(
&self,
api_key: impl AsRef<str>,
) -> Result<CodexAuthStatus, CodexError> {
match self.status().await? {
logged @ CodexAuthStatus::LoggedIn(_) => Ok(logged),
CodexAuthStatus::LoggedOut => self.client.login_with_api_key(api_key).await,
}
}
pub async fn ensure_chatgpt_login(&self) -> Result<Option<tokio::process::Child>, CodexError> {
match self.status().await? {
CodexAuthStatus::LoggedIn(_) => Ok(None),
CodexAuthStatus::LoggedOut => self.client.spawn_login_process().map(Some),
}
}
pub fn spawn_chatgpt_login(&self) -> Result<tokio::process::Child, CodexError> {
self.client.spawn_login_process()
}
pub async fn login_with_api_key(
&self,
api_key: impl AsRef<str>,
) -> Result<CodexAuthStatus, CodexError> {
self.client.login_with_api_key(api_key).await
}
}
impl CodexClient {
pub fn spawn_login_process(&self) -> Result<tokio::process::Child, CodexError> {
let mut command = Command::new(self.command_env.binary_path());
command
.arg("login")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
self.command_env.apply(&mut command)?;
spawn_with_retry(&mut command, self.command_env.binary_path())
}
pub fn spawn_device_auth_login_process(&self) -> Result<tokio::process::Child, CodexError> {
let mut command = Command::new(self.command_env.binary_path());
command
.arg("login")
.arg("--device-auth")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
self.command_env.apply(&mut command)?;
spawn_with_retry(&mut command, self.command_env.binary_path())
}
pub fn spawn_with_api_key_login_process(&self) -> Result<tokio::process::Child, CodexError> {
let mut command = Command::new(self.command_env.binary_path());
command
.arg("login")
.arg("--with-api-key")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
self.command_env.apply(&mut command)?;
spawn_with_retry(&mut command, self.command_env.binary_path())
}
pub async fn spawn_mcp_login_process(
&self,
) -> Result<Option<tokio::process::Child>, CodexError> {
let capabilities = self.probe_capabilities().await;
let guard = capabilities.guard_mcp_login();
if !guard_is_supported(&guard) {
log_guard_skip(&guard);
return Ok(None);
}
let mut command = Command::new(self.command_env.binary_path());
command
.arg("login")
.arg("--mcp")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
self.command_env.apply(&mut command)?;
let child = spawn_with_retry(&mut command, self.command_env.binary_path())?;
Ok(Some(child))
}
pub async fn login_with_api_key(
&self,
api_key: impl AsRef<str>,
) -> Result<CodexAuthStatus, CodexError> {
let api_key = api_key.as_ref().trim();
if api_key.is_empty() {
return Err(CodexError::EmptyApiKey);
}
let output = self
.run_basic_command(["login", "--api-key", api_key])
.await?;
let combined = preferred_output_channel(&output);
if output.status.success() {
Ok(parse_login_success(&combined).unwrap_or_else(|| {
CodexAuthStatus::LoggedIn(CodexAuthMethod::Unknown {
raw: combined.clone(),
})
}))
} else {
Err(CodexError::NonZeroExit {
status: output.status,
stderr: combined,
})
}
}
pub async fn login_status(&self) -> Result<CodexAuthStatus, CodexError> {
let output = self.run_basic_command(["login", "status"]).await?;
let combined = preferred_output_channel(&output);
if output.status.success() {
Ok(parse_login_success(&combined).unwrap_or_else(|| {
CodexAuthStatus::LoggedIn(CodexAuthMethod::Unknown {
raw: combined.clone(),
})
}))
} else if combined.to_lowercase().contains("not logged in") {
Ok(CodexAuthStatus::LoggedOut)
} else {
Err(CodexError::NonZeroExit {
status: output.status,
stderr: combined,
})
}
}
pub async fn logout(&self) -> Result<CodexLogoutStatus, CodexError> {
let output = self.run_basic_command(["logout"]).await?;
let combined = preferred_output_channel(&output);
if !output.status.success() {
return Err(CodexError::NonZeroExit {
status: output.status,
stderr: combined,
});
}
let normalized = combined.to_lowercase();
if normalized.contains("successfully logged out") {
Ok(CodexLogoutStatus::LoggedOut)
} else if normalized.contains("not logged in") {
Ok(CodexLogoutStatus::AlreadyLoggedOut)
} else {
Ok(CodexLogoutStatus::LoggedOut)
}
}
}
pub(crate) fn parse_login_success(output: &str) -> Option<CodexAuthStatus> {
let lower = output.to_lowercase();
if lower.contains("chatgpt") {
return Some(CodexAuthStatus::LoggedIn(CodexAuthMethod::ChatGpt));
}
if lower.contains("api key") || lower.contains("apikey") {
let masked = output
.split_once(" - ")
.map(|(_, value)| value.trim().to_string())
.filter(|value| !value.is_empty())
.or_else(|| output.split_whitespace().last().map(|v| v.to_string()));
return Some(CodexAuthStatus::LoggedIn(CodexAuthMethod::ApiKey {
masked_key: masked,
}));
}
None
}