use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JoinOutcome {
Accepted,
Rejected { reason: String },
TokenExpired,
Replayed,
Consumed,
InFlightTimeout,
}
#[derive(Debug, Clone)]
pub struct AuditEvent {
pub ts_ms: u64,
pub token_hash: [u8; 32],
pub joiner_addr: Option<SocketAddr>,
pub claimed_node_id: u64,
pub outcome: JoinOutcome,
}
impl AuditEvent {
pub fn new(
token_hash: [u8; 32],
joiner_addr: Option<SocketAddr>,
claimed_node_id: u64,
outcome: JoinOutcome,
) -> Self {
let ts_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
Self {
ts_ms,
token_hash,
joiner_addr,
claimed_node_id,
outcome,
}
}
}
pub trait AuditWriter: Send + Sync + 'static {
fn append(&self, event: AuditEvent);
}
#[derive(Default, Clone)]
pub struct NoopAuditWriter;
impl AuditWriter for NoopAuditWriter {
fn append(&self, _event: AuditEvent) {}
}
#[derive(Default, Clone)]
pub struct VecAuditWriter {
events: Arc<Mutex<Vec<AuditEvent>>>,
}
impl VecAuditWriter {
pub fn new() -> Self {
Self::default()
}
pub fn drain(&self) -> Vec<AuditEvent> {
let mut guard = self.events.lock().expect("audit lock poisoned");
std::mem::take(&mut *guard)
}
pub fn snapshot(&self) -> Vec<AuditEvent> {
self.events.lock().expect("audit lock poisoned").clone()
}
}
impl AuditWriter for VecAuditWriter {
fn append(&self, event: AuditEvent) {
self.events.lock().expect("audit lock poisoned").push(event);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vec_writer_accumulates_and_drains() {
let w = VecAuditWriter::new();
w.append(AuditEvent::new([1u8; 32], None, 1, JoinOutcome::Accepted));
w.append(AuditEvent::new(
[2u8; 32],
None,
2,
JoinOutcome::Rejected {
reason: "bad mac".into(),
},
));
let events = w.drain();
assert_eq!(events.len(), 2);
assert_eq!(events[0].outcome, JoinOutcome::Accepted);
assert!(matches!(events[1].outcome, JoinOutcome::Rejected { .. }));
assert!(w.drain().is_empty());
}
#[test]
fn noop_writer_does_not_panic() {
let w = NoopAuditWriter;
w.append(AuditEvent::new([0u8; 32], None, 0, JoinOutcome::Consumed));
}
}