use std::sync::Arc;
use talon_core::env::{EnvFile, parse_env};
use talon_security::redact::Redactor;
pub const KNOWN_SECRET_KEYS: &[&str] = &[
"TALON_VLLM_API_KEY",
"HTTP_API_KEY",
"TELEGRAM_BOT_TOKEN",
"WEBHOOK_SECRET",
"EMACS_AUTH_TOKEN",
"ONECLI_API_KEY",
"ANTHROPIC_API_KEY",
"CLAUDE_CODE_OAUTH_TOKEN",
"OPENAI_API_KEY",
];
const SECRET_KEY_MARKERS: &[&str] = &[
"KEY",
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"CREDENTIAL",
"PRIVATE",
];
pub fn is_secret_key(key: &str) -> bool {
let upper = key.to_ascii_uppercase();
KNOWN_SECRET_KEYS.contains(&upper.as_str())
|| SECRET_KEY_MARKERS.iter().any(|m| upper.contains(m))
}
fn looks_like_secret_value(value: &str) -> bool {
let v = value.trim();
!v.is_empty()
&& !v.chars().all(|c| c.is_ascii_digit() || c == '.')
&& !matches!(
v.to_ascii_lowercase().as_str(),
"true" | "false" | "yes" | "no" | "on" | "off"
)
}
pub fn collect_secrets(env: &EnvFile) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
if let Some(path) = env.path() {
if let Ok(content) = std::fs::read_to_string(path) {
out.extend(
parse_env(&content)
.into_iter()
.filter(|(k, _)| is_secret_key(k))
.map(|(_, v)| v),
);
}
}
for key in KNOWN_SECRET_KEYS {
if let Some(v) = env.file_value(key) {
out.push(v.to_string());
}
if let Ok(v) = std::env::var(key) {
out.push(v);
}
}
out.retain(|v| looks_like_secret_value(v));
out.sort();
out.dedup();
out
}
pub fn redactor_for(env: &EnvFile) -> Arc<Redactor> {
Arc::new(Redactor::with_secrets(&collect_secrets(env)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_key_detection() {
assert!(is_secret_key("TALON_VLLM_API_KEY"));
assert!(is_secret_key("HTTP_API_KEY"));
assert!(is_secret_key("MYSQL_PASSWORD"));
assert!(is_secret_key("some_token"));
assert!(!is_secret_key("TALON_VLLM_MODEL"));
assert!(!is_secret_key("TZ"));
}
#[test]
fn collects_secret_values_from_env_file() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".env"),
"TALON_VLLM_API_KEY=vllm-secret-123456\nHTTP_API_KEY=http-secret-abcdef\n\
TALON_VLLM_MODEL=qwen\nCUSTOM_DB_PASSWORD=hunter2hunter2\n\
TALON_VLLM_MAX_TOKENS=131072\nSOME_TOKEN_REQUIRED=true\n",
)
.unwrap();
let env = EnvFile::load(dir.path());
let secrets = collect_secrets(&env);
assert!(secrets.contains(&"vllm-secret-123456".to_string()));
assert!(secrets.contains(&"http-secret-abcdef".to_string()));
assert!(secrets.contains(&"hunter2hunter2".to_string()));
assert!(!secrets.contains(&"qwen".to_string()));
assert!(
!secrets.contains(&"131072".to_string()),
"numbers are not secrets"
);
assert!(!secrets.contains(&"true".to_string()));
let redactor = redactor_for(&env);
let out = redactor.redact("key is vllm-secret-123456 and model qwen");
assert!(!out.contains("vllm-secret-123456"));
assert!(out.contains("qwen"));
}
}