use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::author::AuthorRef;
use crate::frontmatter::ExtraFields;
use crate::identity::{ContentHash, NoteId, NoteVersion};
use crate::scope::OverrideScope;
use crate::status::NoteStatus;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub note_id: NoteId,
pub event_type: AuditEventType,
pub actor: AuthorRef,
pub occurred_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "ExtraFields::is_empty")]
pub extra: ExtraFields,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation_id: Option<Ulid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum AuditEventType {
Created,
Updated {
fields_changed: Vec<String>,
},
StatusChanged {
from: NoteStatus,
to: NoteStatus,
reason: Option<String>,
},
Embedded {
embedder_id: String,
model_version: String,
dim: u16,
},
Indexed {
fts_tokens: u32,
},
AclChanged {
policy_diff: String,
},
OverrideApplied {
scope: OverrideScope,
override_type: String,
},
OverrideRevoked {
scope: OverrideScope,
override_type: String,
},
ScoreRecomputed {
new_decay: f32,
new_pagerank: f32,
},
DriftDetected {
stored_hash: ContentHash,
computed_hash: ContentHash,
},
Read {
bearer_id: String,
fields_accessed: Vec<String>,
},
Deleted {
reason: Option<String>,
},
Restored {
from_version: NoteVersion,
},
}
pub mod http {
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpAuditActor {
pub kid: String,
pub sub: String,
pub aud: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpAuditEvent {
pub ts: DateTime<Utc>,
pub event: String,
pub actor: HttpAuditActor,
pub tenant_id: String,
pub locus: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub note_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
pub outcome: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub curator: Option<serde_json::Value>,
pub request_id: String,
}
#[async_trait]
pub trait AuditSink: Send + Sync + 'static {
async fn record(&self, event: HttpAuditEvent) -> Result<(), std::io::Error>;
}
pub fn content_hash_jcs(value: &serde_json::Value) -> Result<String, serde_json::Error> {
use sha2::{Digest, Sha256};
let canonical = serde_jcs::to_string(value)?;
let mut h = Sha256::new();
h.update(canonical.as_bytes());
let digest: [u8; 32] = h.finalize().into();
Ok(format!(
"sha256:{}",
digest
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>()
))
}
}