use crate::context::Context;
use std::path::PathBuf;
pub(crate) const AUTH_FILE: &str = "auth.json";
pub(crate) fn home(ctx: &Context) -> PathBuf {
match ctx.codex_home() {
Some(dir) if !dir.is_empty() => PathBuf::from(dir),
_ => ctx.home().join(".codex"),
}
}
pub(crate) fn auth_file(ctx: &Context) -> PathBuf {
home(ctx).join(AUTH_FILE)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Backend {
File,
Keyring,
Either,
Ephemeral,
Secrets,
}
pub(crate) fn backend(ctx: &Context) -> Backend {
std::fs::read_to_string(home(ctx).join("config.toml"))
.map_or(Backend::File, |config| backend_in(&config))
}
fn backend_in(config: &str) -> Backend {
let mut table = String::new();
let mut store = Backend::File;
let mut secrets = false;
for line in config.lines() {
let line = line.trim();
if let Some(header) = line.strip_prefix('[') {
table = header
.split(']')
.next()
.unwrap_or_default()
.trim()
.to_string();
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let (key, value) = (key.trim(), bare(value));
match (table.as_str(), key) {
("", "cli_auth_credentials_store") => {
store = match value {
"keyring" => Backend::Keyring,
"auto" => Backend::Either,
"ephemeral" => Backend::Ephemeral,
_ => Backend::File,
};
}
("features", "secret_auth_storage") => secrets = value == "true",
_ => {}
}
}
match store {
Backend::Keyring | Backend::Either if secrets => Backend::Secrets,
other => other,
}
}
fn bare(value: &str) -> &str {
let value = value.trim();
for quote in ['"', '\''] {
if let Some(rest) = value.strip_prefix(quote) {
return rest.split(quote).next().unwrap_or_default();
}
}
value.split('#').next().unwrap_or_default().trim()
}
#[cfg(test)]
mod tests {
use super::*;
fn at(dir: &std::path::Path, config: &str) -> Context {
std::fs::create_dir_all(dir).expect("a scratch codex home");
std::fs::write(dir.join("config.toml"), config).expect("a config");
Context::new(PathBuf::from("/nowhere")).with_codex_home(dir.to_string_lossy().into())
}
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"pitboard-codex-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
dir
}
#[test]
fn no_setting_means_the_file() {
let dir = scratch("default");
let ctx = at(&dir, "model = \"gpt-5\"\n");
assert_eq!(backend(&ctx), Backend::File);
assert_eq!(auth_file(&ctx), dir.join("auth.json"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn each_store_is_recognised() {
let dir = scratch("stores");
for (written, expected) in [
("cli_auth_credentials_store = \"keyring\"", Backend::Keyring),
("cli_auth_credentials_store=\"auto\"", Backend::Either),
(
" cli_auth_credentials_store = 'ephemeral'",
Backend::Ephemeral,
),
("cli_auth_credentials_store = \"file\"", Backend::File),
("cli_auth_credentials_store = \"nonsense\"", Backend::File),
] {
let ctx = at(&dir, written);
assert_eq!(backend(&ctx), expected, "{written}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_comment_after_the_value_is_not_the_value() {
assert_eq!(
backend_in("cli_auth_credentials_store = \"keyring\" # on this mac\n"),
Backend::Keyring
);
assert_eq!(
backend_in("cli_auth_credentials_store = keyring # bare\n"),
Backend::Keyring
);
}
#[test]
fn only_the_top_level_key_is_the_setting() {
let config = "[profiles.work]\ncli_auth_credentials_store = \"keyring\"\n";
assert_eq!(backend_in(config), Backend::File);
let config =
"cli_auth_credentials_store = \"keyring\"\n[features]\nsecret_auth_storage = true\n";
assert_eq!(
backend_in(config),
Backend::Secrets,
"an encrypted file whose key is in the keychain is a store of its own"
);
let config = "[features]\nsecret_auth_storage = true\n";
assert_eq!(
backend_in(config),
Backend::File,
"the feature only changes a keyring store"
);
}
}