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 fn spawn_with_access_token_login_process(
164 &self,
165 ) -> Result<tokio::process::Child, CodexError> {
166 let mut command = Command::new(self.command_env.binary_path());
167 command
168 .arg("login")
169 .arg("--with-access-token")
170 .stdout(std::process::Stdio::piped())
171 .stderr(std::process::Stdio::piped())
172 .kill_on_drop(true);
173
174 self.command_env.apply(&mut command)?;
175
176 spawn_with_retry(&mut command, self.command_env.binary_path())
177 }
178
179 pub async fn spawn_mcp_login_process(
184 &self,
185 ) -> Result<Option<tokio::process::Child>, CodexError> {
186 let capabilities = self.probe_capabilities().await;
187 let guard = capabilities.guard_mcp_login();
188 if !guard_is_supported(&guard) {
189 log_guard_skip(&guard);
190 return Ok(None);
191 }
192
193 let mut command = Command::new(self.command_env.binary_path());
194 command
195 .arg("login")
196 .arg("--mcp")
197 .stdout(std::process::Stdio::piped())
198 .stderr(std::process::Stdio::piped())
199 .kill_on_drop(true);
200
201 self.command_env.apply(&mut command)?;
202
203 let child = spawn_with_retry(&mut command, self.command_env.binary_path())?;
204
205 Ok(Some(child))
206 }
207
208 pub async fn login_with_api_key(
210 &self,
211 api_key: impl AsRef<str>,
212 ) -> Result<CodexAuthStatus, CodexError> {
213 let api_key = api_key.as_ref().trim();
214 if api_key.is_empty() {
215 return Err(CodexError::EmptyApiKey);
216 }
217
218 let output = self
219 .run_basic_command(["login", "--api-key", api_key])
220 .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 {
230 Err(CodexError::NonZeroExit {
231 status: output.status,
232 stderr: combined,
233 })
234 }
235 }
236
237 pub async fn login_status(&self) -> Result<CodexAuthStatus, CodexError> {
239 let output = self.run_basic_command(["login", "status"]).await?;
240 let combined = preferred_output_channel(&output);
241
242 if output.status.success() {
243 Ok(parse_login_success(&combined).unwrap_or_else(|| {
244 CodexAuthStatus::LoggedIn(CodexAuthMethod::Unknown {
245 raw: combined.clone(),
246 })
247 }))
248 } else if combined.to_lowercase().contains("not logged in") {
249 Ok(CodexAuthStatus::LoggedOut)
250 } else {
251 Err(CodexError::NonZeroExit {
252 status: output.status,
253 stderr: combined,
254 })
255 }
256 }
257
258 pub async fn logout(&self) -> Result<CodexLogoutStatus, CodexError> {
260 let output = self.run_basic_command(["logout"]).await?;
261 let combined = preferred_output_channel(&output);
262
263 if !output.status.success() {
264 return Err(CodexError::NonZeroExit {
265 status: output.status,
266 stderr: combined,
267 });
268 }
269
270 let normalized = combined.to_lowercase();
271 if normalized.contains("successfully logged out") {
272 Ok(CodexLogoutStatus::LoggedOut)
273 } else if normalized.contains("not logged in") {
274 Ok(CodexLogoutStatus::AlreadyLoggedOut)
275 } else {
276 Ok(CodexLogoutStatus::LoggedOut)
277 }
278 }
279}
280
281pub(crate) fn parse_login_success(output: &str) -> Option<CodexAuthStatus> {
282 let lower = output.to_lowercase();
283 if lower.contains("chatgpt") {
284 return Some(CodexAuthStatus::LoggedIn(CodexAuthMethod::ChatGpt));
285 }
286 if lower.contains("api key") || lower.contains("apikey") {
287 let masked = output
289 .split_once(" - ")
290 .map(|(_, value)| value.trim().to_string())
291 .filter(|value| !value.is_empty())
292 .or_else(|| output.split_whitespace().last().map(|v| v.to_string()));
293 return Some(CodexAuthStatus::LoggedIn(CodexAuthMethod::ApiKey {
294 masked_key: masked,
295 }));
296 }
297 None
298}