1use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11use time::OffsetDateTime;
12
13use crate::{AuditError, AuditResult, GENESIS_HEAD};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum EventKind {
23 AgentRegistered,
24 AgentRevoked,
25 TransferInitiated,
26 TransferPolicyDecision,
27 TransferScannerVerdict,
28 TransferAccepted,
29 TransferDelivered,
30 TransferRejected,
31 TransferExpired,
32 DeferredDecisionRequested,
36 SignedDecisionReceipt,
43}
44
45impl EventKind {
46 pub fn as_str(&self) -> &'static str {
47 match self {
48 EventKind::AgentRegistered => "agent_registered",
49 EventKind::AgentRevoked => "agent_revoked",
50 EventKind::TransferInitiated => "transfer_initiated",
51 EventKind::TransferPolicyDecision => "transfer_policy_decision",
52 EventKind::TransferScannerVerdict => "transfer_scanner_verdict",
53 EventKind::TransferAccepted => "transfer_accepted",
54 EventKind::TransferDelivered => "transfer_delivered",
55 EventKind::TransferRejected => "transfer_rejected",
56 EventKind::TransferExpired => "transfer_expired",
57 EventKind::DeferredDecisionRequested => "deferred_decision_requested",
58 EventKind::SignedDecisionReceipt => "signed_decision_receipt",
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct Event {
69 pub kind: EventKind,
70 pub actor: String,
73 pub subject: String,
76 pub payload: serde_json::Value,
79}
80
81impl Event {
82 pub fn new(
83 kind: EventKind,
84 actor: impl Into<String>,
85 subject: impl Into<String>,
86 payload: serde_json::Value,
87 ) -> Self {
88 Self {
89 kind,
90 actor: actor.into(),
91 subject: subject.into(),
92 payload,
93 }
94 }
95
96 pub fn canonical_bytes(&self, ts: OffsetDateTime, prev_hash: &str) -> AuditResult<Vec<u8>> {
107 if !matches!(self.payload, serde_json::Value::Object(_)) {
108 return Err(AuditError::InvalidEvent(
109 "payload must be a JSON object".into(),
110 ));
111 }
112 let payload_canonical = canonical_json(&self.payload);
113 let ts_str = ts
114 .format(&time::format_description::well_known::Rfc3339)
115 .map_err(|e| AuditError::InvalidEvent(format!("ts format: {}", e)))?;
116
117 let mut out = Vec::with_capacity(128);
118 out.extend_from_slice(b"{\"kind\":\"");
119 out.extend_from_slice(self.kind.as_str().as_bytes());
120 out.extend_from_slice(b"\",\"actor\":");
121 out.extend_from_slice(json_string(&self.actor).as_bytes());
122 out.extend_from_slice(b",\"subject\":");
123 out.extend_from_slice(json_string(&self.subject).as_bytes());
124 out.extend_from_slice(b",\"payload\":");
125 out.extend_from_slice(payload_canonical.as_bytes());
126 out.extend_from_slice(b",\"ts\":");
127 out.extend_from_slice(json_string(&ts_str).as_bytes());
128 out.extend_from_slice(b",\"prev\":");
129 out.extend_from_slice(json_string(prev_hash).as_bytes());
130 out.extend_from_slice(b"}");
131 Ok(out)
132 }
133
134 pub fn compute_hash(&self, ts: OffsetDateTime, prev_hash: &str) -> AuditResult<String> {
137 let bytes = self.canonical_bytes(ts, prev_hash)?;
138 let digest = Sha256::digest(&bytes);
139 Ok(hex::encode(digest))
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct StoredEvent {
147 pub position: u64,
148 pub event_id: String,
149 #[serde(with = "time::serde::rfc3339")]
150 pub timestamp: OffsetDateTime,
151 pub prev_hash: String,
152 pub this_hash: String,
153 #[serde(flatten)]
154 pub event: Event,
155}
156
157impl StoredEvent {
158 pub fn verify_hash(&self) -> AuditResult<()> {
161 let recomputed = self.event.compute_hash(self.timestamp, &self.prev_hash)?;
162 if recomputed != self.this_hash {
163 return Err(AuditError::HashMismatch {
164 position: self.position,
165 stored: self.this_hash.clone(),
166 recomputed,
167 });
168 }
169 Ok(())
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct EventReceipt {
178 pub event_id: String,
179 pub position: u64,
180 #[serde(with = "time::serde::rfc3339")]
181 pub timestamp: OffsetDateTime,
182 pub chain_head: String,
183}
184
185impl From<&StoredEvent> for EventReceipt {
186 fn from(e: &StoredEvent) -> Self {
187 EventReceipt {
188 event_id: e.event_id.clone(),
189 position: e.position,
190 timestamp: e.timestamp,
191 chain_head: e.this_hash.clone(),
192 }
193 }
194}
195
196fn canonical_json(v: &serde_json::Value) -> String {
200 let mut out = String::new();
201 write_canonical(v, &mut out);
202 out
203}
204
205fn write_canonical(v: &serde_json::Value, out: &mut String) {
206 match v {
207 serde_json::Value::Null => out.push_str("null"),
208 serde_json::Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
209 serde_json::Value::Number(n) => out.push_str(&n.to_string()),
210 serde_json::Value::String(s) => out.push_str(&json_string(s)),
211 serde_json::Value::Array(xs) => {
212 out.push('[');
213 for (i, x) in xs.iter().enumerate() {
214 if i > 0 {
215 out.push(',');
216 }
217 write_canonical(x, out);
218 }
219 out.push(']');
220 }
221 serde_json::Value::Object(map) => {
222 let mut keys: Vec<&String> = map.keys().collect();
223 keys.sort();
224 out.push('{');
225 for (i, k) in keys.iter().enumerate() {
226 if i > 0 {
227 out.push(',');
228 }
229 out.push_str(&json_string(k));
230 out.push(':');
231 write_canonical(&map[*k], out);
232 }
233 out.push('}');
234 }
235 }
236}
237
238fn json_string(s: &str) -> String {
240 serde_json::Value::String(s.to_string()).to_string()
241}
242
243pub fn genesis_head() -> String {
246 GENESIS_HEAD.to_string()
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 fn sample_event() -> Event {
254 Event::new(
255 EventKind::AgentRegistered,
256 "spize:acme/alice:a4f8b2",
257 "spize:acme/alice:a4f8b2",
258 serde_json::json!({"fingerprint": "a4f8b2"}),
259 )
260 }
261
262 #[test]
263 fn canonical_bytes_stable_across_calls() {
264 let e = sample_event();
265 let ts = time::OffsetDateTime::UNIX_EPOCH;
266 let a = e.canonical_bytes(ts, GENESIS_HEAD).unwrap();
267 let b = e.canonical_bytes(ts, GENESIS_HEAD).unwrap();
268 assert_eq!(a, b);
269 }
270
271 #[test]
272 fn canonical_bytes_include_all_fields() {
273 let e = sample_event();
274 let ts = time::OffsetDateTime::UNIX_EPOCH;
275 let bytes = e.canonical_bytes(ts, GENESIS_HEAD).unwrap();
276 let s = std::str::from_utf8(&bytes).unwrap();
277 assert!(s.contains("\"kind\":\"agent_registered\""));
278 assert!(s.contains("\"actor\":\"spize:acme/alice:a4f8b2\""));
279 assert!(s.contains("\"fingerprint\":\"a4f8b2\""));
280 assert!(s.contains("\"prev\":\"0000"));
281 }
282
283 #[test]
284 fn payload_keys_sorted_in_canonical() {
285 let e = Event::new(
286 EventKind::TransferInitiated,
287 "",
288 "tx_1",
289 serde_json::json!({"z": 1, "a": 2, "m": 3}),
290 );
291 let ts = time::OffsetDateTime::UNIX_EPOCH;
292 let bytes = e.canonical_bytes(ts, GENESIS_HEAD).unwrap();
293 let s = std::str::from_utf8(&bytes).unwrap();
294 let a_pos = s.find("\"a\"").unwrap();
295 let m_pos = s.find("\"m\"").unwrap();
296 let z_pos = s.find("\"z\"").unwrap();
297 assert!(a_pos < m_pos && m_pos < z_pos);
298 }
299
300 #[test]
301 fn different_prev_hash_different_hash() {
302 let e = sample_event();
303 let ts = time::OffsetDateTime::UNIX_EPOCH;
304 let h1 = e.compute_hash(ts, GENESIS_HEAD).unwrap();
305 let h2 = e.compute_hash(ts, &"a".repeat(64)).unwrap();
306 assert_ne!(h1, h2);
307 }
308
309 #[test]
310 fn different_kind_different_hash() {
311 let ts = time::OffsetDateTime::UNIX_EPOCH;
312 let a = Event::new(EventKind::AgentRegistered, "", "", serde_json::json!({}))
313 .compute_hash(ts, GENESIS_HEAD)
314 .unwrap();
315 let b = Event::new(EventKind::AgentRevoked, "", "", serde_json::json!({}))
316 .compute_hash(ts, GENESIS_HEAD)
317 .unwrap();
318 assert_ne!(a, b);
319 }
320
321 #[test]
322 fn non_object_payload_rejected() {
323 let e = Event {
324 kind: EventKind::AgentRegistered,
325 actor: "".into(),
326 subject: "".into(),
327 payload: serde_json::json!([1, 2, 3]),
328 };
329 let err = e
330 .canonical_bytes(time::OffsetDateTime::UNIX_EPOCH, GENESIS_HEAD)
331 .unwrap_err();
332 assert!(matches!(err, AuditError::InvalidEvent(_)));
333 }
334}