use std::path::Path;
use zeroize::Zeroizing;
pub struct Secret(Zeroizing<String>);
impl Secret {
pub fn from_file(path: &Path) -> Result<Self, std::io::Error> {
let contents = std::fs::read_to_string(path)?;
Ok(Self(Zeroizing::new(contents.trim().to_owned())))
}
pub fn new(value: String) -> Self {
Self(Zeroizing::new(value))
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl Clone for Secret {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("[REDACTED]")
}
}
impl std::fmt::Display for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("[REDACTED]")
}
}
#[cfg(all(test, feature = "client", feature = "misanthropic"))]
pub(crate) fn live_test_model() -> misanthropic::model::Model {
misanthropic::model::Model::Custom("claude-haiku-5-5".into())
}
#[cfg(all(test, feature = "client", feature = "misanthropic"))]
pub(crate) const LIVE_TEST_EFFORT: misanthropic::prompt::Effort =
misanthropic::prompt::Effort::Low;
#[cfg(all(test, feature = "client", feature = "misanthropic"))]
pub(crate) fn live_test_api_key() -> Secret {
let dir = std::env::var_os("AGORA_SECRETS_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| {
std::path::PathBuf::from(
std::env::var_os("HOME").expect("HOME is set"),
)
.join("Projects/agora/secrets")
});
let path = dir.join("anthropic_api_key");
Secret::from_file(&path).unwrap_or_else(|e| {
panic!(
"cannot read the API key from {}: {e} (set AGORA_SECRETS_DIR)",
path.display()
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn from_file_and_expose() {
let mut file = tempfile::NamedTempFile::new().unwrap();
writeln!(file, " my-secret-value ").unwrap();
let secret = Secret::from_file(file.path()).unwrap();
assert_eq!(secret.expose(), "my-secret-value");
}
#[test]
fn debug_is_redacted() {
let secret = Secret::new("hunter2".to_string());
assert_eq!(format!("{:?}", secret), "[REDACTED]");
}
#[test]
fn display_is_redacted() {
let secret = Secret::new("hunter2".to_string());
assert_eq!(format!("{}", secret), "[REDACTED]");
}
#[test]
fn nonexistent_file_returns_error() {
let result = Secret::from_file(Path::new("/nonexistent/path"));
assert!(result.is_err());
}
#[test]
fn clone_preserves_value() {
let secret = Secret::new("cloneable".to_string());
let cloned = secret.clone();
assert_eq!(cloned.expose(), "cloneable");
}
}