1use chrono::{DateTime, Utc};
39use serde::{Deserialize, Serialize};
40
41use crate::crypto::{self, SigningKey, VerifyingKey};
42use crate::govlog::{SignatureHex, canonical_json};
43use crate::ids::{AgentId, MessageId};
44use crate::names::AgentName;
45
46pub const EVENT_CONTEXT: &[u8] = b"agora/event/v1\n";
48
49pub const EVENTS_PATH: &str = "events";
51
52pub const EVENTS_KEY_PATH: &str = "events/key";
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
58#[cfg_attr(feature = "schemars", schemars(inline))]
59#[serde(tag = "kind", rename_all = "snake_case")]
60pub enum Event {
61 DirectMessage(MessageNotice),
63 Broadcast(MessageNotice),
66 Gap(GapNotice),
70}
71
72impl Event {
73 pub fn name(&self) -> &'static str {
75 match self {
76 Event::DirectMessage(_) => "direct_message",
77 Event::Broadcast(_) => "broadcast",
78 Event::Gap(_) => "gap",
79 }
80 }
81
82 pub fn recipient_id(&self) -> AgentId {
84 match self {
85 Event::DirectMessage(m) | Event::Broadcast(m) => m.recipient_id,
86 Event::Gap(g) => g.recipient_id,
87 }
88 }
89
90 pub fn message(&self) -> Option<&MessageNotice> {
92 match self {
93 Event::DirectMessage(m) | Event::Broadcast(m) => Some(m),
94 Event::Gap(_) => None,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
103#[cfg_attr(feature = "schemars", schemars(inline))]
104pub struct GapNotice {
105 pub recipient_id: AgentId,
107 pub skipped: u64,
110 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
112 pub at_least: bool,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
118#[serde(transparent)]
119pub struct EventCursor(String);
120
121impl EventCursor {
122 pub fn as_str(&self) -> &str {
123 &self.0
124 }
125}
126
127impl From<String> for EventCursor {
128 fn from(s: String) -> Self {
129 Self(s)
130 }
131}
132
133impl std::fmt::Display for EventCursor {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 f.write_str(&self.0)
136 }
137}
138
139pub const REPLAY_CEILING: usize = 500;
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
146#[cfg_attr(feature = "schemars", schemars(inline))]
147pub struct MessageNotice {
148 pub message_id: MessageId,
149 pub recipient_id: AgentId,
153 pub sender_id: AgentId,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub sender_name: Option<AgentName>,
157 pub sent_at: DateTime<Utc>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
164pub struct SignedEvent {
165 pub event: Event,
166 pub timestamp: i64,
169 pub signature: SignatureHex,
172}
173
174impl SignedEvent {
175 pub fn signing_bytes(event: &Event) -> Vec<u8> {
178 let value =
179 serde_json::to_value(event).expect("an Event always serializes");
180 let mut bytes = EVENT_CONTEXT.to_vec();
181 bytes.extend_from_slice(&canonical_json(&value));
182 bytes
183 }
184
185 pub fn sign(key: &SigningKey, event: Event, timestamp: i64) -> Self {
187 let signature =
188 crypto::sign(key, &Self::signing_bytes(&event), timestamp);
189 Self {
190 event,
191 timestamp,
192 signature: signature.into(),
193 }
194 }
195
196 pub fn verify(&self, key: &VerifyingKey) -> bool {
198 crypto::verify(
199 key,
200 &Self::signing_bytes(&self.event),
201 self.timestamp,
202 &(&self.signature).into(),
203 )
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
214pub struct EventSigningKey {
215 pub public_key: crate::govlog::PublicKeyHex,
217 pub context: String,
219}
220
221impl EventSigningKey {
222 pub fn new(key: &VerifyingKey) -> Self {
223 Self {
224 public_key: key.into(),
225 context: String::from_utf8_lossy(EVENT_CONTEXT).into_owned(),
226 }
227 }
228
229 pub fn verifying_key(&self) -> Option<VerifyingKey> {
231 VerifyingKey::from_bytes(self.public_key.as_bytes()).ok()
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
239#[cfg_attr(feature = "schemars", schemars(inline))]
240pub struct NotificationPreferences {
241 pub direct_messages: bool,
243 pub broadcasts: bool,
245}
246
247impl Default for NotificationPreferences {
248 fn default() -> Self {
249 Self {
250 direct_messages: true,
251 broadcasts: true,
252 }
253 }
254}
255
256impl NotificationPreferences {
257 pub fn wants(&self, event: &Event) -> bool {
259 match event {
260 Event::DirectMessage(_) => self.direct_messages,
261 Event::Broadcast(_) => self.broadcasts,
262 Event::Gap(_) => true,
263 }
264 }
265
266 pub fn with(mut self, change: &NotificationSettings) -> Self {
269 if let Some(on) = change.direct_messages {
270 self.direct_messages = on;
271 }
272 if let Some(on) = change.broadcasts {
273 self.broadcasts = on;
274 }
275 self
276 }
277}
278
279#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
282#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
283#[cfg_attr(feature = "schemars", schemars(inline))]
284#[serde(deny_unknown_fields)]
285pub struct NotificationSettings {
286 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub direct_messages: Option<bool>,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub broadcasts: Option<bool>,
293}
294
295impl NotificationSettings {
296 pub fn is_empty(&self) -> bool {
298 self.direct_messages.is_none() && self.broadcasts.is_none()
299 }
300}
301
302pub const MAX_LINE_BYTES: usize = 64 * 1024;
305
306pub const MAX_FRAME_BYTES: usize = 256 * 1024;
309
310#[derive(Debug, Clone, Default, PartialEq, Eq)]
312pub struct SseFrame {
313 pub id: Option<String>,
315 pub event: Option<String>,
317 pub data: String,
319 pub retry: Option<u64>,
321}
322
323#[derive(Debug, Default)]
331pub struct SseParser {
332 buffer: Vec<u8>,
333 frame: SseFrame,
334 has_data: bool,
335 has_field: bool,
336 oversize: bool,
338 after_cr: bool,
341}
342
343impl SseParser {
344 pub fn new() -> Self {
345 Self::default()
346 }
347
348 pub fn push(&mut self, bytes: &[u8]) -> Vec<SseFrame> {
350 let mut frames = Vec::new();
351 for &b in bytes {
352 if self.after_cr {
353 self.after_cr = false;
354 if b == b'\n' {
355 continue;
356 }
357 }
358 match b {
359 b'\n' | b'\r' => {
360 self.after_cr = b == b'\r';
361 let line = std::mem::take(&mut self.buffer);
362 if let Some(frame) = self.line(&line) {
363 frames.push(frame);
364 }
365 }
366 _ if self.buffer.len() >= MAX_LINE_BYTES => {}
369 _ => self.buffer.push(b),
370 }
371 }
372 frames
373 }
374
375 fn line(&mut self, line: &[u8]) -> Option<SseFrame> {
376 if line.is_empty() {
377 let frame = std::mem::take(&mut self.frame);
378 let oversize = std::mem::take(&mut self.oversize);
379 let dispatch = !oversize && self.has_data
380 || (self.has_field
381 && (frame.id.is_some() || frame.retry.is_some()));
382 self.has_data = false;
383 self.has_field = false;
384 return dispatch.then_some(frame);
385 }
386 if line[0] == b':' {
387 return None;
388 }
389 let line = String::from_utf8_lossy(line);
390 let (field, value) = match line.split_once(':') {
391 Some((field, value)) => {
392 (field, value.strip_prefix(' ').unwrap_or(value))
393 }
394 None => (line.as_ref(), ""),
395 };
396 match field {
397 "data" => {
398 if self.frame.data.len() + value.len() + 1 > MAX_FRAME_BYTES {
399 self.oversize = true;
400 self.frame.data.clear();
401 } else if !self.oversize {
402 if self.has_data {
403 self.frame.data.push('\n');
404 }
405 self.frame.data.push_str(value);
406 }
407 self.has_data = true;
408 }
409 "id" if !value.contains('\0') => {
410 self.frame.id = Some(value.to_owned());
411 self.has_field = true;
412 }
413 "event" => {
414 self.frame.event = Some(value.to_owned());
415 self.has_field = true;
416 }
417 "retry" => {
418 if let Ok(ms) = value.parse() {
419 self.frame.retry = Some(ms);
420 self.has_field = true;
421 }
422 }
423 _ => {}
424 }
425 None
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 use crate::ids::EventSeq;
434
435 fn notice(seq: i64) -> MessageNotice {
436 MessageNotice {
437 recipient_id: AgentId::from(uuid::Uuid::from_u128(9)),
438 message_id: MessageId::from(uuid::Uuid::from_u128(seq as u128)),
439 sender_id: AgentId::from(uuid::Uuid::from_u128(7)),
440 sender_name: Some("alice".parse().unwrap()),
441 sent_at: DateTime::from_timestamp(1_760_000_000, 0).unwrap(),
442 }
443 }
444
445 #[test]
446 fn an_event_says_who_and_which_message_and_nothing_else() {
447 let json =
448 serde_json::to_value(Event::DirectMessage(notice(42))).unwrap();
449 assert_eq!(
450 json,
451 serde_json::json!({
452 "kind": "direct_message",
453 "message_id": "00000000-0000-0000-0000-00000000002a",
454 "recipient_id": "00000000-0000-0000-0000-000000000009",
455 "sender_id": "00000000-0000-0000-0000-000000000007",
456 "sender_name": "alice",
457 "sent_at": "2025-10-09T08:53:20Z",
458 })
459 );
460 let back: Event = serde_json::from_value(json).unwrap();
461 assert_eq!(back, Event::DirectMessage(notice(42)));
462 assert_eq!(back.name(), "direct_message");
463 assert_eq!(
464 back.recipient_id(),
465 AgentId::from(uuid::Uuid::from_u128(9))
466 );
467 assert_eq!(Event::Broadcast(notice(1)).name(), "broadcast");
468 let gap = Event::Gap(GapNotice {
469 recipient_id: AgentId::from(uuid::Uuid::from_u128(9)),
470 skipped: 12,
471 at_least: false,
472 });
473 assert_eq!(
474 serde_json::to_value(&gap).unwrap(),
475 serde_json::json!({
476 "kind": "gap",
477 "recipient_id": "00000000-0000-0000-0000-000000000009",
478 "skipped": 12
479 })
480 );
481 assert!(gap.message().is_none());
482 assert!(
483 NotificationPreferences {
484 direct_messages: false,
485 broadcasts: false
486 }
487 .wants(&gap)
488 );
489 }
490
491 #[test]
492 fn a_signed_event_verifies_and_a_changed_one_does_not() {
493 let (key, public) = crypto::generate_keypair();
494 let signed = SignedEvent::sign(
495 &key,
496 Event::DirectMessage(notice(3)),
497 1_760_000_000,
498 );
499 assert!(signed.verify(&public));
500
501 let wire = serde_json::to_string(&signed).unwrap();
502 let read: SignedEvent = serde_json::from_str(&wire).unwrap();
503 assert!(read.verify(&public));
504
505 let mut moved = read.clone();
506 moved.event = Event::DirectMessage(notice(4));
507 assert!(!moved.verify(&public));
508 let mut forwarded = read.clone();
509 if let Event::DirectMessage(m) = &mut forwarded.event {
510 m.recipient_id = AgentId::from(uuid::Uuid::from_u128(10));
511 }
512 assert!(!forwarded.verify(&public), "an event for another agent");
513 let mut kind = read.clone();
514 kind.event = Event::Broadcast(notice(3));
515 assert!(!kind.verify(&public));
516 let mut late = read;
517 late.timestamp += 1;
518 assert!(!late.verify(&public));
519
520 let (_, other) = crypto::generate_keypair();
521 assert!(!signed.verify(&other));
522 }
523
524 #[test]
527 fn signing_bytes_are_the_context_then_sorted_keys() {
528 let bytes = SignedEvent::signing_bytes(&Event::Broadcast(notice(5)));
529 let text = String::from_utf8(bytes).unwrap();
530 assert_eq!(
531 text,
532 "agora/event/v1\n{\"kind\":\"broadcast\",\
533 \"message_id\":\"00000000-0000-0000-0000-000000000005\",\
534 \"recipient_id\":\"00000000-0000-0000-0000-000000000009\",\
535 \"sender_id\":\"00000000-0000-0000-0000-000000000007\",\
536 \"sender_name\":\"alice\",\"sent_at\":\"2025-10-09T08:53:20Z\"}"
537 );
538 }
539
540 #[test]
541 fn preferences_filter_and_change_field_by_field() {
542 let all = NotificationPreferences::default();
543 assert!(all.wants(&Event::DirectMessage(notice(1))));
544 assert!(all.wants(&Event::Broadcast(notice(1))));
545
546 let quiet = all.with(&NotificationSettings {
547 direct_messages: Some(false),
548 broadcasts: None,
549 });
550 assert!(!quiet.wants(&Event::DirectMessage(notice(1))));
551 assert!(quiet.wants(&Event::Broadcast(notice(1))));
552 assert_eq!(quiet.with(&NotificationSettings::default()), quiet);
553 assert!(NotificationSettings::default().is_empty());
554 }
555
556 #[test]
557 fn event_seq_parses_what_it_prints_and_refuses_the_rest() {
558 assert_eq!("17".parse::<EventSeq>().unwrap(), EventSeq::from(17));
559 assert_eq!(" 0 ".parse::<EventSeq>().unwrap(), EventSeq::ZERO);
560 assert!("-1".parse::<EventSeq>().is_err());
561 assert!("abc".parse::<EventSeq>().is_err());
562 assert_eq!(EventSeq::from(9).to_string(), "9");
563 assert_eq!(serde_json::to_string(&EventSeq::from(9)).unwrap(), "9");
564 }
565
566 #[cfg(feature = "schemars")]
567 #[test]
568 fn schemas_carry_no_ref() {
569 for (name, schema) in [
570 (
571 "SignedEvent",
572 serde_json::to_value(schemars::schema_for!(SignedEvent))
573 .unwrap(),
574 ),
575 (
576 "NotificationSettings",
577 serde_json::to_value(schemars::schema_for!(
578 NotificationSettings
579 ))
580 .unwrap(),
581 ),
582 ] {
583 let text = schema.to_string();
584 assert!(!text.contains("$ref"), "{name}: {text}");
585 assert!(!text.contains("$defs"), "{name}: {text}");
586 }
587 }
588
589 #[test]
590 fn the_parser_reads_frames_across_chunks_and_skips_keep_alives() {
591 let mut p = SseParser::new();
592 assert!(p.push(b": keep-alive\n\nid: 1\nevent: direct").is_empty());
593 let frames = p.push(b"_message\ndata: {\"a\":\ndata: 1}\n\n");
594 assert_eq!(
595 frames,
596 vec![SseFrame {
597 id: Some("1".into()),
598 event: Some("direct_message".into()),
599 data: "{\"a\":\n1}".into(),
600 retry: None,
601 }]
602 );
603 let frames = p.push(b"retry: 3000\r");
605 assert!(frames.is_empty());
606 let frames = p.push(b"\n\r\ndata:x\r\n\r\n");
607 assert_eq!(
608 frames,
609 vec![
610 SseFrame {
611 retry: Some(3000),
612 ..Default::default()
613 },
614 SseFrame {
615 data: "x".into(),
616 ..Default::default()
617 },
618 ]
619 );
620 assert!(p.push(b"foo: bar\n\n").is_empty());
622 let line = format!("data: {}\n", "x".repeat(MAX_LINE_BYTES - 16));
625 let mut big = line.repeat(MAX_FRAME_BYTES / line.len() + 2);
626 big.push_str("\ndata: after\n\n");
627 let frames = p.push(big.as_bytes());
628 assert_eq!(frames.len(), 1);
629 assert_eq!(frames[0].data, "after");
630 }
631}