use once_cell::sync::Lazy;
use regex::Regex;
use serde_json::Value;
const DEBUG_MSG_LIMIT: usize = 2000;
static SECRET_FIELD_NAMES: [&str; 5] = [
"session_ingress_token",
"environment_secret",
"access_token",
"secret",
"token",
];
static SECRET_PATTERN: Lazy<Regex> = Lazy::new(|| {
let pattern = format!(r#""({})"\s*:\s*"([^"]*)""#, SECRET_FIELD_NAMES.join("|"));
Regex::new(&pattern).unwrap()
});
const REDACT_MIN_LENGTH: usize = 16;
pub fn redact_secrets(s: &str) -> String {
SECRET_PATTERN
.replace_all(s, |caps: ®ex::Captures| {
let field = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let value = caps.get(2).map(|m| m.as_str()).unwrap_or("");
if value.len() < REDACT_MIN_LENGTH {
return format!(r#""{}":"[REDACTED]""#, field);
}
let redacted = format!("{}...{}", &value[..8], &value[value.len() - 4..]);
format!(r#""{}":"{}""#, field, redacted)
})
.to_string()
}
pub fn debug_truncate(s: &str) -> String {
let flat = s.replace('\n', "\\n");
if flat.len() <= DEBUG_MSG_LIMIT {
return flat;
}
format!("{}... ({} chars)", &flat[..DEBUG_MSG_LIMIT], flat.len())
}
pub fn debug_body(data: &str) -> String {
let raw = if let Ok(parsed) = serde_json::from_str::<Value>(data) {
serde_json::to_string(&parsed).unwrap_or_else(|_| data.to_string())
} else {
data.to_string()
};
let s = redact_secrets(&raw);
if s.len() <= DEBUG_MSG_LIMIT {
return s;
}
format!("{}... ({} chars)", &s[..DEBUG_MSG_LIMIT], s.len())
}
fn error_message(err: &dyn std::error::Error) -> String {
err.to_string()
}
pub fn describe_axios_error(err: &serde_json::Value) -> String {
let msg = if let Some(err_str) = err.get("message").and_then(|v| v.as_str()) {
err_str.to_string()
} else {
"Unknown error".to_string()
};
if let Some(response) = err.get("response").and_then(|v| v.as_object()) {
if let Some(data) = response.get("data").and_then(|v| v.as_object()) {
let detail = data.get("message").and_then(|v| v.as_str()).or_else(|| {
data.get("error")
.and_then(|v| v.get("message"))
.and_then(|v| v.as_str())
});
if let Some(detail) = detail {
return format!("{}: {}", msg, detail);
}
}
}
msg
}
pub fn extract_http_status(err: &serde_json::Value) -> Option<u16> {
let response = err.get("response")?;
let status = response.get("status")?;
status.as_u64().map(|v| v as u16)
}
pub fn extract_error_detail(data: &serde_json::Value) -> Option<String> {
if let Some(msg) = data.get("message").and_then(|v| v.as_str()) {
return Some(msg.to_string());
}
if let Some(error) = data.get("error").and_then(|v| v.as_object()) {
if let Some(msg) = error.get("message").and_then(|v| v.as_str()) {
return Some(msg.to_string());
}
}
None
}
pub fn log_bridge_skip(reason: &str, debug_msg: Option<&str>, v2: Option<bool>) {
if let Some(msg) = debug_msg {
eprintln!("[bridge:debug] {}", msg);
}
let mut event = serde_json::json!({ "reason": reason });
if let Some(v2_val) = v2 {
event["v2"] = serde_json::json!(v2_val);
}
eprintln!("[bridge:analytics] tengu_bridge_repl_skipped: {}", event);
}