use regex::Regex;
use std::sync::OnceLock;
static SCRUB_PATTERNS: OnceLock<Vec<(Regex, &'static str)>> = OnceLock::new();
fn get_patterns() -> &'static Vec<(Regex, &'static str)> {
SCRUB_PATTERNS.get_or_init(|| {
vec![
(Regex::new(r"AKIA[0-9A-Z]{16}").unwrap(), "[REDACTED_AWS_KEY]"),
(
Regex::new(r"gh[ps]_[A-Za-z0-9_]{36,}").unwrap(),
"[REDACTED_GH_TOKEN]",
),
(
Regex::new(r"gho_[A-Za-z0-9_]{36,}").unwrap(),
"[REDACTED_GH_OAUTH]",
),
(
Regex::new(r#"(?i)(api[_-]?key|token|secret|password|passwd|authorization)\s*[=:]\s*['"]?([A-Za-z0-9_\-./+]{16,})['"]?"#).unwrap(),
"$1=[REDACTED]",
),
(
Regex::new(r"(?i)Bearer\s+[A-Za-z0-9_\-./+]{20,}").unwrap(),
"Bearer [REDACTED]",
),
(
Regex::new(r"-----BEGIN[A-Z ]*PRIVATE KEY-----").unwrap(),
"[REDACTED_PRIVATE_KEY]",
),
(
Regex::new(r"sk-ant-[A-Za-z0-9_\-]{20,}").unwrap(),
"[REDACTED_ANTHROPIC_KEY]",
),
(
Regex::new(r"sk-[A-Za-z0-9]{20,}").unwrap(),
"[REDACTED_OPENAI_KEY]",
),
]
})
}
pub fn scrub_secrets(text: &str) -> String {
let mut result = text.to_string();
for (pattern, replacement) in get_patterns() {
result = pattern.replace_all(&result, *replacement).to_string();
}
result
}
pub fn scrub_session(session: &mut crate::models::Session) {
for msg in &mut session.user_messages {
msg.text = scrub_secrets(&msg.text);
}
for msg in &mut session.assistant_messages {
msg.text = scrub_secrets(&msg.text);
if let Some(ref thinking) = msg.thinking_summary {
msg.thinking_summary = Some(scrub_secrets(thinking));
}
}
for err in &mut session.errors {
*err = scrub_secrets(err);
}
for summary in &mut session.summaries {
*summary = scrub_secrets(summary);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scrub_aws_key() {
let input = "aws_key = AKIAIOSFODNN7EXAMPLE";
let result = scrub_secrets(input);
assert!(result.contains("[REDACTED"));
assert!(!result.contains("AKIAIOSFODNN7EXAMPLE"));
}
#[test]
fn test_scrub_github_token() {
let input = "token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij";
let result = scrub_secrets(input);
assert!(result.contains("[REDACTED"));
}
#[test]
fn test_scrub_bearer() {
let input = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
let result = scrub_secrets(input);
assert!(result.contains("Bearer [REDACTED]"));
}
#[test]
fn test_no_scrub_normal_text() {
let input = "This is a normal message about coding";
let result = scrub_secrets(input);
assert_eq!(result, input);
}
}