1use std::path::PathBuf;
2
3use tokio::process::Command;
4
5use crate::{
6 capabilities::{guard_is_supported, log_guard_skip},
7 process::{preferred_output_channel, spawn_with_retry},
8 CodexClient, CodexError,
9};
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum CodexAuthStatus {
14 LoggedIn(CodexAuthMethod),
16 LoggedOut,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub enum CodexAuthMethod {
23 ChatGpt,
24 ApiKey {
25 masked_key: Option<String>,
26 },
27 Unknown {
29 raw: String,
30 },
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
35pub enum CodexLogoutStatus {
36 LoggedOut,
37 AlreadyLoggedOut,
38}
39
40#[derive(Clone, Debug)]
44pub struct AuthSessionHelper {
45 client: CodexClient,
46}
47
48impl AuthSessionHelper {
49 pub fn new(app_codex_home: impl Into<PathBuf>) -> Self {
51 let client = CodexClient::builder()
52 .codex_home(app_codex_home)
53 .create_home_dirs(true)
54 .build();
55 Self { client }
56 }
57
58 pub fn with_client(client: CodexClient) -> Self {
60 Self { client }
61 }
62
63 pub fn client(&self) -> CodexClient {
65 self.client.clone()
66 }
67
68 pub async fn status(&self) -> Result<CodexAuthStatus, CodexError> {
70 self.client.login_status().await
71 }
72
73 pub async fn ensure_api_key_login(
75 &self,
76 api_key: impl AsRef<str>,
77 ) -> Result<CodexAuthStatus, CodexError> {
78 match self.status().await? {
79 logged @ CodexAuthStatus::LoggedIn(_) => Ok(logged),
80 CodexAuthStatus::LoggedOut => self.client.login_with_api_key(api_key).await,
81 }
82 }
83
84 pub async fn ensure_chatgpt_login(&self) -> Result<Option<tokio::process::Child>, CodexError> {
89 match self.status().await? {
90 CodexAuthStatus::LoggedIn(_) => Ok(None),
91 CodexAuthStatus::LoggedOut => self.client.spawn_login_process().map(Some),
92 }
93 }
94
95 pub fn spawn_chatgpt_login(&self) -> Result<tokio::process::Child, CodexError> {
97 self.client.spawn_login_process()
98 }
99
100 pub async fn login_with_api_key(
102 &self,
103 api_key: impl AsRef<str>,
104 ) -> Result<CodexAuthStatus, CodexError> {
105 self.client.login_with_api_key(api_key).await
106 }
107}
108
109impl CodexClient {
110 pub fn spawn_login_process(&self) -> Result<tokio::process::Child, CodexError> {
114 let mut command = Command::new(self.command_env.binary_path());
115 command
116 .arg("login")
117 .stdout(std::process::Stdio::piped())
118 .stderr(std::process::Stdio::piped())
119 .kill_on_drop(true);
120
121 self.command_env.apply(&mut command)?;
122
123 spawn_with_retry(&mut command, self.command_env.binary_path())
124 }
125
126 pub fn spawn_device_auth_login_process(&self) -> Result<tokio::process::Child, CodexError> {
130 let mut command = Command::new(self.command_env.binary_path());
131 command
132 .arg("login")
133 .arg("--device-auth")
134 .stdout(std::process::Stdio::piped())
135 .stderr(std::process::Stdio::piped())
136 .kill_on_drop(true);
137
138 self.command_env.apply(&mut command)?;
139
140 spawn_with_retry(&mut command, self.command_env.binary_path())
141 }
142
143 pub fn spawn_with_api_key_login_process(&self) -> Result<tokio::process::Child, CodexError> {
147 let mut command = Command::new(self.command_env.binary_path());
148 command
149 .arg("login")
150 .arg("--with-api-key")
151 .stdout(std::process::Stdio::piped())
152 .stderr(std::process::Stdio::piped())
153 .kill_on_drop(true);
154
155 self.command_env.apply(&mut command)?;
156
157 spawn_with_retry(&mut command, self.command_env.binary_path())
158 }
159
160 pub async fn spawn_mcp_login_process(
165 &self,
166 ) -> Result<Option<tokio::process::Child>, CodexError> {
167 let capabilities = self.probe_capabilities().await;
168 let guard = capabilities.guard_mcp_login();
169 if !guard_is_supported(&guard) {
170 log_guard_skip(&guard);
171 return Ok(None);
172 }
173
174 let mut command = Command::new(self.command_env.binary_path());
175 command
176 .arg("login")
177 .arg("--mcp")
178 .stdout(std::process::Stdio::piped())
179 .stderr(std::process::Stdio::piped())
180 .kill_on_drop(true);
181
182 self.command_env.apply(&mut command)?;
183
184 let child = spawn_with_retry(&mut command, self.command_env.binary_path())?;
185
186 Ok(Some(child))
187 }
188
189 pub async fn login_with_api_key(
191 &self,
192 api_key: impl AsRef<str>,
193 ) -> Result<CodexAuthStatus, CodexError> {
194 let api_key = api_key.as_ref().trim();
195 if api_key.is_empty() {
196 return Err(CodexError::EmptyApiKey);
197 }
198
199 let output = self
200 .run_basic_command(["login", "--api-key", api_key])
201 .await?;
202 let combined = preferred_output_channel(&output);
203
204 if output.status.success() {
205 Ok(parse_login_success(&combined).unwrap_or_else(|| {
206 CodexAuthStatus::LoggedIn(CodexAuthMethod::Unknown {
207 raw: combined.clone(),
208 })
209 }))
210 } else {
211 Err(CodexError::NonZeroExit {
212 status: output.status,
213 stderr: combined,
214 })
215 }
216 }
217
218 pub async fn login_status(&self) -> Result<CodexAuthStatus, CodexError> {
220 let output = self.run_basic_command(["login", "status"]).await?;
221 let combined = preferred_output_channel(&output);
222
223 if output.status.success() {
224 Ok(parse_login_success(&combined).unwrap_or_else(|| {
225 CodexAuthStatus::LoggedIn(CodexAuthMethod::Unknown {
226 raw: combined.clone(),
227 })
228 }))
229 } else if combined.to_lowercase().contains("not logged in") {
230 Ok(CodexAuthStatus::LoggedOut)
231 } else {
232 Err(CodexError::NonZeroExit {
233 status: output.status,
234 stderr: combined,
235 })
236 }
237 }
238
239 pub async fn logout(&self) -> Result<CodexLogoutStatus, CodexError> {
241 let output = self.run_basic_command(["logout"]).await?;
242 let combined = preferred_output_channel(&output);
243
244 if !output.status.success() {
245 return Err(CodexError::NonZeroExit {
246 status: output.status,
247 stderr: combined,
248 });
249 }
250
251 let normalized = combined.to_lowercase();
252 if normalized.contains("successfully logged out") {
253 Ok(CodexLogoutStatus::LoggedOut)
254 } else if normalized.contains("not logged in") {
255 Ok(CodexLogoutStatus::AlreadyLoggedOut)
256 } else {
257 Ok(CodexLogoutStatus::LoggedOut)
258 }
259 }
260}
261
262pub(crate) fn parse_login_success(output: &str) -> Option<CodexAuthStatus> {
263 let lower = output.to_lowercase();
264 if lower.contains("chatgpt") {
265 return Some(CodexAuthStatus::LoggedIn(CodexAuthMethod::ChatGpt));
266 }
267 if lower.contains("api key") || lower.contains("apikey") {
268 let masked = output
270 .split_once(" - ")
271 .map(|(_, value)| value.trim().to_string())
272 .filter(|value| !value.is_empty())
273 .or_else(|| output.split_whitespace().last().map(|v| v.to_string()));
274 return Some(CodexAuthStatus::LoggedIn(CodexAuthMethod::ApiKey {
275 masked_key: masked,
276 }));
277 }
278 None
279}