use serde_json::Value;
pub use mcpmesh_local_api::{AuditKind, AuditRecord};
pub fn args_hash(params: &Value) -> String {
let bytes = serde_json::to_vec(params).unwrap_or_default();
format!("blake3:{}", blake3::hash(&bytes).to_hex())
}
pub fn now_ts() -> String {
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
fmt_rfc3339_millis(millis)
}
pub fn fmt_rfc3339_millis(unix_millis: u128) -> String {
let secs = (unix_millis / 1000) as i64;
let millis = (unix_millis % 1000) as u64;
let days = secs.div_euclid(86_400);
let secs_of_day = secs.rem_euclid(86_400);
let (h, m, s) = (
secs_of_day / 3600,
(secs_of_day % 3600) / 60,
secs_of_day % 60,
);
let (year, month, day) = civil_from_days(days);
format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}.{millis:03}Z")
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; (if m <= 2 { y + 1 } else { y }, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn timestamp_formats_epoch_and_millis() {
assert_eq!(fmt_rfc3339_millis(0), "1970-01-01T00:00:00.000Z");
assert_eq!(fmt_rfc3339_millis(1500), "1970-01-01T00:00:01.500Z");
assert_eq!(
fmt_rfc3339_millis(1_700_000_000_000),
"2023-11-14T22:13:20.000Z"
);
assert_eq!(&fmt_rfc3339_millis(1_700_000_000_000)[..7], "2023-11");
}
#[test]
fn args_hash_is_blake3_prefixed_and_deterministic() {
let p = json!({"path": "/secret/passwords.txt"});
let h = args_hash(&p);
assert!(h.starts_with("blake3:"));
assert_eq!(h, args_hash(&p), "same params → same hash");
assert_ne!(h, args_hash(&json!({"path": "/other"})), "differs by input");
assert_eq!(h.len(), "blake3:".len() + 64);
}
#[test]
fn proxied_request_serialization_never_leaks_raw_arguments() {
let secret = "hunter2-super-secret-token";
let params = json!({"name": "read_file", "arguments": {"token": secret}});
let rec = AuditRecord::proxied_request(
"2026-07-03T14:02:11.480Z".into(),
Some("bob".into()),
"notes".into(),
"tools/call".into(),
Some("read_file".into()),
args_hash(¶ms),
6210,
"ok".into(),
41,
);
let line = serde_json::to_string(&rec).unwrap();
assert!(
!line.contains(secret),
"raw argument leaked into the record: {line}"
);
assert!(line.contains("blake3:"), "args_hash digest must be present");
assert!(
line.contains("\"tool\":\"read_file\""),
"tool NAME is recorded"
);
assert!(line.contains("\"method\":\"tools/call\""));
let s = AuditRecord::session_open(
"2026-07-03T14:02:11.480Z".into(),
Some("bob".into()),
"notes".into(),
);
let sline = serde_json::to_string(&s).unwrap();
assert!(sline.contains("\"kind\":\"session_open\""));
assert!(
!sline.contains("method"),
"session record elides request fields"
);
}
#[test]
fn record_round_trips_through_jsonl() {
let rec = AuditRecord::trust(
"2026-07-03T14:02:11.480Z".into(),
"pair".into(),
Some("bob".into()),
);
let line = serde_json::to_string(&rec).unwrap();
let back: AuditRecord = serde_json::from_str(&line).unwrap();
assert_eq!(back, rec);
assert_eq!(back.kind, AuditKind::Trust);
}
}