mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Summary Stacked on #26706. Adds the shared auth/system-proxy contract that later platform resolver PRs plug into. This PR moves Codex-owned auth and startup HTTP clients through a common route-aware boundary, but does not yet add Windows or macOS system proxy resolution. The default path remains unchanged when `respect_system_proxy` is absent or disabled. ## Implementation - Adds `codex-client/src/outbound_proxy.rs` with the shared route-selection model: - `OutboundProxyConfig`; - `ClientRouteClass`; - `RouteFailureClass`; - `build_reqwest_client_for_route`. - Preserves the existing reqwest/default-client behavior when no route config is supplied. - Uses the fixed MVP routing policy when route config is supplied: platform system/PAC/WPAD discovery, then explicit env proxy variables, then direct connection. - Keeps platform-specific system discovery behind the shared client boundary. This PR provides the contract and fallback behavior; later resolver PRs plug in Windows and macOS discovery. - Adds `login::AuthRouteConfig` so auth call sites depend on a small policy type instead of platform resolver details. - Maps the resolved `Config.respect_system_proxy` boolean into `AuthRouteConfig` for auth-owned clients. - Wires the route config through browser login, device-code login, access-token login, login status, logout/revoke, token refresh, API-key exchange, app-server account login, TUI/app startup, cloud-config bootstrap, cloud tasks, plugin auth, and exec startup config loading. ## End-user behavior - No behavior changes by default. - When `respect_system_proxy = true`, auth-owned clients opt into the shared route-aware client path. - On platforms without a resolver implementation in this PR, system discovery is unavailable and the route-aware path falls back to explicit env proxy handling, then direct connection. - Custom CA handling remains separate from proxy route selection and still runs through the shared client builder. - No proxy URLs, PAC contents, or resolved platform details are exposed through the public config surface introduced here. ## Tests Adds or updates coverage for: - preserving default auth-client fallback behavior when no route config is provided; - injected environment-proxy fallback without mutating process environment; - existing login-server E2E flows using explicit `auth_route_config: None` to guard unchanged default behavior; - updated auth manager, login, logout, cloud-config, startup, and plugin-auth call sites passing route config explicitly.
582 lines
19 KiB
Rust
582 lines
19 KiB
Rust
//! CLI login commands and their direct-user observability surfaces.
|
|
//!
|
|
//! The TUI path already installs a broader tracing stack with feedback, OpenTelemetry, and other
|
|
//! interactive-session layers. Direct `codex login` intentionally does less: it preserves the
|
|
//! existing stderr/browser UX and adds only a small file-backed tracing layer for login-specific
|
|
//! targets. Keeping that setup local avoids pulling the TUI's session-oriented logging machinery
|
|
//! into a one-shot CLI command while still producing a durable `codex-login.log` artifact that
|
|
//! support can request from users.
|
|
|
|
use codex_app_server_protocol::AuthMode;
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use codex_core::config::Config;
|
|
use codex_login::AuthKeyringBackendKind;
|
|
use codex_login::AuthRouteConfig;
|
|
use codex_login::CLIENT_ID;
|
|
use codex_login::CodexAuth;
|
|
use codex_login::ServerOptions;
|
|
use codex_login::login_with_access_token;
|
|
use codex_login::login_with_api_key;
|
|
use codex_login::logout_with_revoke;
|
|
use codex_login::run_device_code_login;
|
|
use codex_login::run_login_server;
|
|
use codex_protocol::config_types::ForcedLoginMethod;
|
|
use codex_utils_cli::CliConfigOverrides;
|
|
use std::fs::OpenOptions;
|
|
use std::io::IsTerminal;
|
|
use std::io::Read;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use tracing_appender::non_blocking;
|
|
use tracing_appender::non_blocking::WorkerGuard;
|
|
use tracing_subscriber::EnvFilter;
|
|
use tracing_subscriber::Layer;
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
const CHATGPT_LOGIN_DISABLED_MESSAGE: &str =
|
|
"ChatGPT login is disabled. Use API key login instead.";
|
|
const API_KEY_LOGIN_DISABLED_MESSAGE: &str =
|
|
"API key login is disabled. Use ChatGPT login instead.";
|
|
const ACCESS_TOKEN_LOGIN_DISABLED_MESSAGE: &str =
|
|
"Access token login is disabled. Use API key login instead.";
|
|
const LOGIN_SUCCESS_MESSAGE: &str = "Successfully logged in";
|
|
|
|
/// Installs a small file-backed tracing layer for direct `codex login` flows.
|
|
///
|
|
/// This deliberately duplicates a narrow slice of the TUI logging setup instead of reusing it
|
|
/// wholesale. The TUI stack includes session-oriented layers that are valuable for interactive
|
|
/// runs but unnecessary for a one-shot login command. Keeping the direct CLI path local lets this
|
|
/// command produce a durable `codex-login.log` artifact without coupling it to the TUI's broader
|
|
/// telemetry and feedback initialization.
|
|
fn init_login_file_logging(config: &Config) -> Option<WorkerGuard> {
|
|
let log_dir = match codex_core::config::log_dir(config) {
|
|
Ok(log_dir) => log_dir,
|
|
Err(err) => {
|
|
eprintln!("Warning: failed to resolve login log directory: {err}");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
if let Err(err) = std::fs::create_dir_all(&log_dir) {
|
|
eprintln!(
|
|
"Warning: failed to create login log directory {}: {err}",
|
|
log_dir.display()
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let mut log_file_opts = OpenOptions::new();
|
|
log_file_opts.create(true).append(true);
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
log_file_opts.mode(0o600);
|
|
}
|
|
|
|
let log_path = log_dir.join("codex-login.log");
|
|
let log_file = match log_file_opts.open(&log_path) {
|
|
Ok(log_file) => log_file,
|
|
Err(err) => {
|
|
eprintln!(
|
|
"Warning: failed to open login log file {}: {err}",
|
|
log_path.display()
|
|
);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let (non_blocking, guard) = non_blocking(log_file);
|
|
let env_filter = EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| EnvFilter::new("codex_cli=info,codex_core=info,codex_login=info"));
|
|
let file_layer = tracing_subscriber::fmt::layer()
|
|
.with_writer(non_blocking)
|
|
.with_target(true)
|
|
.with_ansi(false)
|
|
.with_filter(env_filter);
|
|
|
|
// Direct `codex login` otherwise relies on ephemeral stderr and browser output.
|
|
// Persist the same login targets to a file so support can inspect auth failures
|
|
// without reproducing them through TUI or app-server.
|
|
if let Err(err) = tracing_subscriber::registry().with(file_layer).try_init() {
|
|
eprintln!(
|
|
"Warning: failed to initialize login log file {}: {err}",
|
|
log_path.display()
|
|
);
|
|
return None;
|
|
}
|
|
|
|
Some(guard)
|
|
}
|
|
|
|
fn print_login_server_start(actual_port: u16, auth_url: &str) {
|
|
eprintln!(
|
|
"Starting local login server on http://localhost:{actual_port}.\nIf your browser did not open, navigate to this URL to authenticate:\n\n{auth_url}\n\nOn a remote or headless machine? Use `codex login --device-auth` instead."
|
|
);
|
|
}
|
|
|
|
async fn clear_existing_auth_before_login(
|
|
codex_home: &Path,
|
|
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
|
auth_keyring_backend_kind: AuthKeyringBackendKind,
|
|
auth_route_config: Option<&AuthRouteConfig>,
|
|
) {
|
|
if let Err(err) = logout_with_revoke(
|
|
codex_home,
|
|
auth_credentials_store_mode,
|
|
auth_keyring_backend_kind,
|
|
auth_route_config,
|
|
)
|
|
.await
|
|
{
|
|
tracing::warn!("failed to clear existing auth before login: {err}");
|
|
}
|
|
}
|
|
|
|
pub async fn login_with_chatgpt(
|
|
codex_home: PathBuf,
|
|
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
|
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
|
auth_keyring_backend_kind: AuthKeyringBackendKind,
|
|
auth_route_config: Option<AuthRouteConfig>,
|
|
) -> std::io::Result<()> {
|
|
clear_existing_auth_before_login(
|
|
&codex_home,
|
|
cli_auth_credentials_store_mode,
|
|
auth_keyring_backend_kind,
|
|
auth_route_config.as_ref(),
|
|
)
|
|
.await;
|
|
|
|
let opts = ServerOptions::new(
|
|
codex_home,
|
|
CLIENT_ID.to_string(),
|
|
forced_chatgpt_workspace_id,
|
|
cli_auth_credentials_store_mode,
|
|
auth_keyring_backend_kind,
|
|
auth_route_config,
|
|
);
|
|
let server = run_login_server(opts)?;
|
|
|
|
print_login_server_start(server.actual_port, &server.auth_url);
|
|
|
|
server.block_until_done().await
|
|
}
|
|
|
|
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let _login_log_guard = init_login_file_logging(&config);
|
|
tracing::info!("starting browser login flow");
|
|
|
|
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
|
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
|
match login_with_chatgpt(
|
|
config.codex_home.to_path_buf(),
|
|
forced_chatgpt_workspace_id,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
config.auth_route_config(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_login_with_api_key(
|
|
cli_config_overrides: CliConfigOverrides,
|
|
api_key: String,
|
|
) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let _login_log_guard = init_login_file_logging(&config);
|
|
tracing::info!("starting api key login flow");
|
|
|
|
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Chatgpt)) {
|
|
eprintln!("{API_KEY_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
match login_with_api_key(
|
|
&config.codex_home,
|
|
&api_key,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
) {
|
|
Ok(_) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_login_with_access_token(
|
|
cli_config_overrides: CliConfigOverrides,
|
|
access_token: String,
|
|
) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let _login_log_guard = init_login_file_logging(&config);
|
|
tracing::info!("starting access token login flow");
|
|
|
|
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
|
eprintln!("{ACCESS_TOKEN_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let auth_route_config = config.auth_route_config();
|
|
match login_with_access_token(
|
|
&config.codex_home,
|
|
&access_token,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.forced_chatgpt_workspace_id.as_deref(),
|
|
Some(&config.chatgpt_base_url),
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config.as_ref(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in with access token: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn read_api_key_from_stdin() -> String {
|
|
read_stdin_secret(
|
|
"--with-api-key expects the API key on stdin. Try piping it, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`.",
|
|
"Reading API key from stdin...",
|
|
"No API key provided via stdin.",
|
|
)
|
|
}
|
|
|
|
pub fn read_access_token_from_stdin() -> String {
|
|
read_stdin_secret(
|
|
"--with-access-token expects the access token on stdin. Try piping it, e.g. `printenv CODEX_ACCESS_TOKEN | codex login --with-access-token`.",
|
|
"Reading access token from stdin...",
|
|
"No access token provided via stdin.",
|
|
)
|
|
}
|
|
|
|
fn read_stdin_secret(terminal_message: &str, reading_message: &str, empty_message: &str) -> String {
|
|
let mut stdin = std::io::stdin();
|
|
|
|
if stdin.is_terminal() {
|
|
eprintln!("{terminal_message}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
eprintln!("{reading_message}");
|
|
|
|
let mut buffer = String::new();
|
|
if let Err(err) = stdin.read_to_string(&mut buffer) {
|
|
eprintln!("Failed to read stdin: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let secret = buffer.trim().to_string();
|
|
if secret.is_empty() {
|
|
eprintln!("{empty_message}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
secret
|
|
}
|
|
|
|
/// Login using the OAuth device code flow.
|
|
pub async fn run_login_with_device_code(
|
|
cli_config_overrides: CliConfigOverrides,
|
|
issuer_base_url: Option<String>,
|
|
client_id: Option<String>,
|
|
) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let _login_log_guard = init_login_file_logging(&config);
|
|
tracing::info!("starting device code login flow");
|
|
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
|
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
let auth_route_config = config.auth_route_config();
|
|
clear_existing_auth_before_login(
|
|
&config.codex_home,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config.as_ref(),
|
|
)
|
|
.await;
|
|
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
|
let mut opts = ServerOptions::new(
|
|
config.codex_home.to_path_buf(),
|
|
client_id.unwrap_or(CLIENT_ID.to_string()),
|
|
forced_chatgpt_workspace_id,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config,
|
|
);
|
|
if let Some(iss) = issuer_base_url {
|
|
opts.issuer = iss;
|
|
}
|
|
match run_device_code_login(opts).await {
|
|
Ok(()) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in with device code: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Prefers device-code login (with `open_browser = false`) when headless environment is detected, but keeps
|
|
/// `codex login` working in environments where device-code may be disabled/feature-gated.
|
|
/// If `run_device_code_login` returns `ErrorKind::NotFound` ("device-code unsupported"), this
|
|
/// falls back to starting the local browser login server.
|
|
pub async fn run_login_with_device_code_fallback_to_browser(
|
|
cli_config_overrides: CliConfigOverrides,
|
|
issuer_base_url: Option<String>,
|
|
client_id: Option<String>,
|
|
) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let _login_log_guard = init_login_file_logging(&config);
|
|
tracing::info!("starting login flow with device code fallback");
|
|
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
|
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
let auth_route_config = config.auth_route_config();
|
|
clear_existing_auth_before_login(
|
|
&config.codex_home,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config.as_ref(),
|
|
)
|
|
.await;
|
|
|
|
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
|
let mut opts = ServerOptions::new(
|
|
config.codex_home.to_path_buf(),
|
|
client_id.unwrap_or(CLIENT_ID.to_string()),
|
|
forced_chatgpt_workspace_id,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config,
|
|
);
|
|
if let Some(iss) = issuer_base_url {
|
|
opts.issuer = iss;
|
|
}
|
|
opts.open_browser = false;
|
|
|
|
match run_device_code_login(opts.clone()).await {
|
|
Ok(()) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
if e.kind() == std::io::ErrorKind::NotFound {
|
|
eprintln!("Device code login is not enabled; falling back to browser login.");
|
|
match run_login_server(opts) {
|
|
Ok(server) => {
|
|
print_login_server_start(server.actual_port, &server.auth_url);
|
|
match server.block_until_done().await {
|
|
Ok(()) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
eprintln!("Error logging in with device code: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let auth_route_config = config.auth_route_config();
|
|
|
|
match CodexAuth::from_auth_storage(
|
|
&config.codex_home,
|
|
config.cli_auth_credentials_store_mode,
|
|
Some(&config.chatgpt_base_url),
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config.as_ref(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(Some(auth)) => match auth.auth_mode() {
|
|
AuthMode::ApiKey => match auth.get_token() {
|
|
Ok(api_key) => {
|
|
eprintln!("Logged in using an API key - {}", safe_format_key(&api_key));
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Unexpected error retrieving API key: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
},
|
|
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => {
|
|
eprintln!("Logged in using ChatGPT");
|
|
std::process::exit(0);
|
|
}
|
|
AuthMode::AgentIdentity => {
|
|
eprintln!("Logged in using access token");
|
|
std::process::exit(0);
|
|
}
|
|
AuthMode::PersonalAccessToken => {
|
|
eprintln!("Logged in using personal access token");
|
|
std::process::exit(0);
|
|
}
|
|
AuthMode::BedrockApiKey => {
|
|
eprintln!("Logged in using Amazon Bedrock API key");
|
|
std::process::exit(0);
|
|
}
|
|
},
|
|
Ok(None) => {
|
|
eprintln!("Not logged in");
|
|
std::process::exit(1);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error checking login status: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let auth_route_config = config.auth_route_config();
|
|
|
|
match logout_with_revoke(
|
|
&config.codex_home,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config.as_ref(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(true) => {
|
|
eprintln!("Successfully logged out");
|
|
std::process::exit(0);
|
|
}
|
|
Ok(false) => {
|
|
eprintln!("Not logged in");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging out: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
|
|
let cli_overrides = match cli_config_overrides.parse_overrides() {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("Error parsing -c overrides: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
match Config::load_with_cli_overrides(cli_overrides).await {
|
|
Ok(config) => config,
|
|
Err(e) => {
|
|
eprintln!("Error loading configuration: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn safe_format_key(key: &str) -> String {
|
|
if key.len() <= 13 {
|
|
return "***".to_string();
|
|
}
|
|
let prefix = &key[..8];
|
|
let suffix = &key[key.len() - 5..];
|
|
format!("{prefix}***{suffix}")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use codex_login::AuthKeyringBackendKind;
|
|
use codex_login::load_auth_dot_json;
|
|
use codex_login::login_with_api_key;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::tempdir;
|
|
|
|
use super::clear_existing_auth_before_login;
|
|
use super::safe_format_key;
|
|
|
|
#[tokio::test]
|
|
async fn clears_existing_auth_before_login() {
|
|
let codex_home = tempdir().expect("create temporary Codex home");
|
|
login_with_api_key(
|
|
codex_home.path(),
|
|
"sk-existing",
|
|
AuthCredentialsStoreMode::File,
|
|
AuthKeyringBackendKind::default(),
|
|
)
|
|
.expect("save existing auth");
|
|
|
|
clear_existing_auth_before_login(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
AuthKeyringBackendKind::default(),
|
|
/*auth_route_config*/ None,
|
|
)
|
|
.await;
|
|
|
|
let auth = load_auth_dot_json(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
AuthKeyringBackendKind::default(),
|
|
)
|
|
.expect("load auth after cleanup");
|
|
assert_eq!(auth, None);
|
|
}
|
|
|
|
#[test]
|
|
fn formats_long_key() {
|
|
let key = "sk-proj-1234567890ABCDE";
|
|
assert_eq!(safe_format_key(key), "sk-proj-***ABCDE");
|
|
}
|
|
|
|
#[test]
|
|
fn short_key_returns_stars() {
|
|
let key = "sk-proj-12345";
|
|
assert_eq!(safe_format_key(key), "***");
|
|
}
|
|
}
|