use super::KeyStore;
use super::dotenv;
use super::file_store::FileKeyStore;
#[cfg(feature = "keyring-store")]
use super::keyring_store::KeyringStore;
use super::memory_store::MemoryKeyStore;
use super::registry::env_var_for;
pub fn resolve_key(provider: &str) -> Option<String> {
dotenv::load_env_local_once();
resolve_key_with(provider, default_store().as_ref())
}
pub fn resolve_key_with(provider: &str, store: &dyn KeyStore) -> Option<String> {
if let Some(value) = env_tier(provider) {
return Some(value);
}
store.get(provider)
}
fn env_tier(provider: &str) -> Option<String> {
let var = env_var_for(provider)?;
std::env::var(var).ok().filter(|v| !v.is_empty())
}
pub fn default_store() -> Box<dyn KeyStore> {
#[cfg(feature = "keyring-store")]
{
let keyring = KeyringStore::new();
if keyring.probe_available() {
return Box::new(keyring);
}
}
match FileKeyStore::new() {
Ok(store) => Box::new(store),
Err(_) => Box::new(MemoryKeyStore::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::credentials::env_guard::EnvVarGuard;
use serial_test::serial;
fn describe(value: Option<&str>) -> String {
match value {
None => "None".to_string(),
Some(v) => format!("Some({})", crate::credentials::redact_secret(v)),
}
}
fn assert_resolved(actual: Option<String>, expected: Option<&str>) {
assert!(
actual.as_deref() == expected,
"expected {expected:?}, got {} (actual value redacted: this \
assertion can observe a real credential from the process \
environment)",
describe(actual.as_deref())
);
}
#[test]
fn a_failed_resolution_assert_never_prints_the_secret() {
let secret = "sk-or-v1-0123456789abcdef0123456789abcdef";
let panic = std::panic::catch_unwind(|| {
assert_resolved(Some(secret.to_string()), Some("from-dotenv"));
})
.expect_err("a mismatched resolution must panic");
let msg = panic
.downcast_ref::<String>()
.expect("assert! panics with a String payload");
assert!(
!msg.contains(secret),
"the failure message echoed the secret verbatim"
);
assert!(
!msg.contains("0123456789"),
"the failure message echoed secret material past the redaction head"
);
assert!(
msg.contains(&crate::credentials::redact_secret(secret)),
"the failure message must still identify WHICH credential it saw"
);
}
#[test]
#[serial(dotenv_credential_env)]
fn env_beats_store() {
let _guard = EnvVarGuard::set("FIREWORKS_API_KEY", "from-env");
let store = MemoryKeyStore::new();
store.set("fireworks", "from-store").unwrap();
assert_resolved(resolve_key_with("fireworks", &store), Some("from-env"));
}
#[test]
#[serial(dotenv_credential_env)]
fn dotenv_loaded_value_beats_store() {
let _guard = EnvVarGuard::remove("OPENROUTER_API_KEY");
let tmp = tempfile::TempDir::new().unwrap();
let env_path = tmp.path().join(".env.local");
std::fs::write(&env_path, "OPENROUTER_API_KEY=from-dotenv\n").unwrap();
assert!(dotenv::load_env_from_path(&env_path));
let store = MemoryKeyStore::new();
store.set("openrouter", "from-store").unwrap();
assert_resolved(resolve_key_with("openrouter", &store), Some("from-dotenv"));
}
#[test]
#[serial(dotenv_credential_env)]
fn falls_through_to_store() {
let _guard = EnvVarGuard::remove("ANTHROPIC_API_KEY");
let store = MemoryKeyStore::new();
store.set("anthropic", "from-store").unwrap();
assert_resolved(resolve_key_with("anthropic", &store), Some("from-store"));
}
#[test]
#[serial(dotenv_credential_env)]
fn absent_everywhere_is_none() {
let _guard = EnvVarGuard::remove("OPENAI_API_KEY");
let store = MemoryKeyStore::new();
assert_resolved(resolve_key_with("openai", &store), None);
}
#[test]
#[serial(dotenv_credential_env)]
fn empty_env_var_falls_through_to_store() {
let _guard = EnvVarGuard::set("FIREWORKS_API_KEY", "");
let store = MemoryKeyStore::new();
store.set("fireworks", "from-store").unwrap();
assert_resolved(resolve_key_with("fireworks", &store), Some("from-store"));
}
}