use serde_json::Value;
use crate::config::schema::{Config, SECRET_PATHS};
pub const REDACTION: &str = "[redacted]";
const MIN_SCRUBBABLE_LENGTH: usize = 8;
pub fn redact_config(config: &Config) -> Value {
let mut clone =
serde_json::to_value(config).expect("the resolved configuration always renders as JSON");
for path in SECRET_PATHS {
let Some((parent_path, last)) = path.rsplit_once('.') else {
continue;
};
if let Some(Value::Object(fields)) = read_path_mut(&mut clone, parent_path) {
fields.insert(last.to_owned(), Value::String(REDACTION.to_owned()));
}
}
if let Some(Value::Object(providers)) = read_path_mut(&mut clone, "agent.providers") {
for definition in providers.values_mut() {
if let Value::Object(fields) = definition
&& fields.contains_key("credential")
{
fields.insert("credential".to_owned(), Value::String(REDACTION.to_owned()));
}
}
}
clone
}
pub fn secret_values(config: &Config) -> Vec<String> {
let mut values = Vec::new();
let mut hold = |value: Option<&str>| {
if let Some(value) = value.filter(|value| value.len() >= MIN_SCRUBBABLE_LENGTH) {
values.push(value.to_owned());
}
};
hold(Some(&config.chat.token));
if let Some(github) = &config.github {
hold(Some(&github.token));
}
for definition in config.agent.providers.values() {
hold(definition.get("credential").and_then(Value::as_str));
}
values
}
pub fn redact_text(text: &str, secrets: &[String]) -> String {
let mut scrubbed = text.to_owned();
for secret in secrets {
if secret.len() < MIN_SCRUBBABLE_LENGTH {
continue;
}
scrubbed = scrubbed.replace(secret.as_str(), REDACTION);
}
scrubbed
}
fn read_path_mut<'a>(source: &'a mut Value, path: &str) -> Option<&'a mut Value> {
let mut current = source;
for segment in path.split('.') {
current = current.as_object_mut()?.get_mut(segment)?;
}
Some(current)
}
#[cfg(test)]
mod tests;