use std::path::Path;
use crate::errors::SafeError;
pub use axiom_audit::{
AuditRow, ChainVerdict, ReceiptLink, AUDIT_SCHEMA, GENESIS_HASH, TRAIL_FILENAME,
};
pub const TOOL_NAME: &str = "tsafe";
pub const TOOL_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone)]
pub struct AuditEntry {
pub operation: String,
pub outcome: String,
pub exit_code: i32,
pub receipt: ReceiptLink,
}
pub fn read_trail(path: &Path) -> Result<Vec<AuditRow>, SafeError> {
Ok(axiom_audit::read_rows(path)?)
}
pub fn append(dir: &Path, timestamp: &str, entry: &AuditEntry) -> Result<AuditRow, SafeError> {
let trail = dir.join(TRAIL_FILENAME);
let row = axiom_audit::append(
&trail,
&axiom_audit::AuditEntry {
tool: TOOL_NAME.to_string(),
tool_version: TOOL_VERSION.to_string(),
operation: entry.operation.clone(),
timestamp: timestamp.to_string(),
outcome: entry.outcome.clone(),
exit_code: entry.exit_code,
receipt: entry.receipt.clone(),
},
)?;
Ok(row)
}
pub fn verify_chain(path: &Path) -> Result<ChainVerdict, SafeError> {
Ok(axiom_audit::verify_chain(path)?)
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(op: &str, exit: i32) -> AuditEntry {
AuditEntry {
operation: op.to_string(),
outcome: if exit == 0 { "ok" } else { "failed" }.to_string(),
exit_code: exit,
receipt: ReceiptLink::Empty,
}
}
#[test]
fn append_chains_and_verifies() {
let dir = tempfile::tempdir().unwrap();
let r0 = append(dir.path(), "2026-06-22T00:00:00Z", &entry("attest run", 0)).unwrap();
let r1 = append(dir.path(), "2026-06-22T00:00:01Z", &entry("attest run", 1)).unwrap();
assert_eq!(r0.seq, 0);
assert_eq!(r0.tool, TOOL_NAME);
assert_eq!(r0.prev_hash, GENESIS_HASH);
assert_eq!(r1.seq, 1);
assert_eq!(r1.prev_hash, r0.row_hash);
let trail = dir.path().join(TRAIL_FILENAME);
match verify_chain(&trail).unwrap() {
ChainVerdict::Valid { rows, head_hash } => {
assert_eq!(rows, 2);
assert_eq!(head_hash, r1.row_hash);
}
other @ ChainVerdict::Broken(_) => panic!("expected Valid, got {other:?}"),
}
}
#[test]
fn tampered_row_breaks_chain() {
let dir = tempfile::tempdir().unwrap();
append(dir.path(), "2026-06-22T00:00:00Z", &entry("exec", 0)).unwrap();
let trail = dir.path().join(TRAIL_FILENAME);
let mut rows = read_trail(&trail).unwrap();
rows[0].exit_code = 99;
let line = serde_json::to_string(&rows[0]).unwrap();
std::fs::write(&trail, format!("{line}\n")).unwrap();
assert!(matches!(
verify_chain(&trail).unwrap(),
ChainVerdict::Broken(_)
));
}
#[test]
fn schema_is_axiom_audit_v1() {
assert_eq!(AUDIT_SCHEMA, "axiom.audit.v1");
}
}