1use super::*;
2use crate::identity::{AcceptedRecord, ListResponse, MemoryNonceStore, NonceStore};
3use serde::Deserialize;
4use std::collections::BTreeSet;
5
6fn version(value: &Value) -> Result<()> {
7 if value["version"] != 1 {
8 return Err(fail(
9 "invalid_request",
10 "unsupported local Mail state version",
11 ));
12 }
13 Ok(())
14}
15fn mailbox_key(owner: &AgentId, mailbox: &str) -> String {
16 format!("{owner}/{mailbox}")
17}
18
19#[derive(Clone, Default)]
23pub struct MailCardCache {
24 pins: BTreeMap<String, MailboxCard>,
25}
26impl MailCardCache {
27 pub fn new() -> Self {
28 Self::default()
29 }
30 pub fn observe_historical(
33 &mut self,
34 value: &Value,
35 owner: &AgentId,
36 now: i64,
37 ) -> Result<MailboxCard> {
38 let card = validate_card(value)?;
39 clock(now)?;
40 if &card.event.actor != owner {
41 return Err(fail("invalid_actor", "unexpected owner"));
42 }
43 if card.event.created_at > now.saturating_add(FUTURE_SKEW_MS) {
44 return Err(SdkError::TimestampOutOfWindow);
45 }
46 let key = mailbox_key(owner, &card.event.payload.mailbox_id);
47 if let Some(pin) = self.pins.get(&key) {
48 if pin.hash != card.hash && card.event.nonce <= pin.event.nonce {
49 return Err(fail(
50 "stale_card",
51 "card is older than or conflicts with the pinned card",
52 ));
53 }
54 }
55 self.pins.insert(key, card.clone());
56 Ok(card)
57 }
58 pub fn observe(&mut self, value: &Value, owner: &AgentId, now: i64) -> Result<MailboxCard> {
60 let card = self.observe_historical(value, owner, now)?;
61 check_card_usable(&card, owner, now)?;
62 Ok(card)
63 }
64 pub fn prune(&mut self, now: i64) -> Result<()> {
66 clock(now)?;
67 self.pins
68 .retain(|_, pin| now < pin.event.created_at + MAX_TTL_MS + FUTURE_SKEW_MS);
69 Ok(())
70 }
71 pub fn seal(
73 &mut self,
74 message: &MessagePayload,
75 card: &Value,
76 signer: &AgentSigner,
77 nonce: u64,
78 now: i64,
79 ) -> Result<Packet> {
80 validate_message(&serde_json::to_value(message)?)?;
81 let card = self.observe(card, &message.to, now)?;
82 encrypt_message(message, &card, signer, nonce, now)
83 }
84 pub fn snapshot(&self) -> Result<Value> {
85 Ok(json!({"version":1,"pins":self.pins.values().collect::<Vec<_>>()}))
86 }
87 pub fn from_snapshot(value: &Value) -> Result<Self> {
88 version(value)?;
89 let pins: Vec<MailboxCard> = serde_json::from_value(value["pins"].clone())?;
90 Ok(Self {
91 pins: pins
92 .into_iter()
93 .map(|card| {
94 (
95 mailbox_key(&card.event.actor, &card.event.payload.mailbox_id),
96 card,
97 )
98 })
99 .collect(),
100 })
101 }
102}
103
104pub struct MailKeyring {
106 owner: AgentId,
107 entries: BTreeMap<String, (MailboxCard, MailEncryptionKey)>,
108}
109impl MailKeyring {
110 pub fn new(owner: AgentId) -> Self {
111 Self {
112 owner,
113 entries: BTreeMap::new(),
114 }
115 }
116 pub fn owner(&self) -> &AgentId {
117 &self.owner
118 }
119 pub fn add(&mut self, card: &MailboxCard, key: MailEncryptionKey) -> Result<()> {
120 let card = validate_card(&serde_json::to_value(card)?)?;
121 if card.event.actor != self.owner {
122 return Err(fail("invalid_actor", "card owner mismatch"));
123 }
124 if card.event.payload.public_key != key.public_key() {
125 return Err(fail("invalid_private_key", "key does not match card"));
126 }
127 self.entries.insert(card.hash.clone(), (card, key));
128 Ok(())
129 }
130 pub fn open(&self, packet: &Packet, now: i64) -> Result<MessagePayload> {
132 clock(now)?;
133 let (packet, bytes) = checked_packet(&serde_json::to_value(packet)?)?;
134 self.open_checked(&packet, &bytes, now)
135 }
136 pub(crate) fn open_checked(
138 &self,
139 packet: &Packet,
140 bytes: &[u8],
141 now: i64,
142 ) -> Result<MessagePayload> {
143 let (card, key) = self
144 .entries
145 .get(&packet.event.payload.header.card_hash)
146 .ok_or_else(|| fail("invalid_packet", "unknown retained card"))?;
147 open_verified(packet, bytes, card, key, &self.owner, now)
148 }
149 pub fn prune(&mut self, now: i64) -> Result<()> {
152 clock(now)?;
153 self.entries
154 .retain(|_, (card, _)| now < card.event.payload.receive_until);
155 Ok(())
156 }
157 pub fn snapshot(&self) -> Result<Value> {
160 let entries: Vec<_> = self
161 .entries
162 .values()
163 .map(|(card, key)| json!({"card":card,"secret":encode_bytes(&key.export_secret())}))
164 .collect();
165 Ok(json!({"version":1,"owner":self.owner,"entries":entries}))
166 }
167 pub fn from_snapshot(value: &Value) -> Result<Self> {
168 #[derive(Deserialize)]
169 struct Entry {
170 card: MailboxCard,
171 secret: String,
172 }
173 version(value)?;
174 let mut out = Self::new(serde_json::from_value(value["owner"].clone())?);
175 let entries: Vec<Entry> = serde_json::from_value(value["entries"].clone())?;
176 for entry in entries {
177 let raw = Zeroizing::new(fixed_bytes(&entry.secret, 32)?);
178 out.add(&entry.card, MailEncryptionKey::from_secret(&raw)?)?;
179 }
180 Ok(out)
181 }
182}
183
184pub struct MailInbox {
188 keyring: MailKeyring,
189 accepted: BTreeMap<String, (i64, String)>,
190 blocked_senders: BTreeSet<AgentId>,
191}
192impl MailInbox {
193 pub fn new(keyring: MailKeyring) -> Self {
194 Self {
195 keyring,
196 accepted: BTreeMap::new(),
197 blocked_senders: BTreeSet::new(),
198 }
199 }
200 pub fn keyring(&self) -> &MailKeyring {
201 &self.keyring
202 }
203 pub fn keyring_mut(&mut self) -> &mut MailKeyring {
204 &mut self.keyring
205 }
206 pub fn set_sender_blocked(&mut self, sender: AgentId, blocked: bool) {
207 if blocked {
208 self.blocked_senders.insert(sender);
209 } else {
210 self.blocked_senders.remove(&sender);
211 }
212 }
213 pub fn has(&self, sender: &AgentId, message_id: &str) -> bool {
214 self.accepted
215 .contains_key(&format!("{sender}/{message_id}"))
216 }
217 pub fn accept(
219 &mut self,
220 packet: &Packet,
221 claimed_packet_id: Option<&str>,
222 now: i64,
223 ) -> Result<InboxAcceptance> {
224 clock(now)?;
225 let (packet, bytes) = checked_packet(&serde_json::to_value(packet)?)?;
226 if claimed_packet_id.is_some_and(|id| id != packet.hash) {
227 return Err(fail("invalid_packet", "packet ID mismatch"));
228 }
229 if self.blocked_senders.contains(&packet.event.actor) {
230 return Err(SdkError::PermissionDenied);
231 }
232 let message = self.keyring.open_checked(&packet, &bytes, now)?;
233 let key = format!("{}/{}", message.sender, message.message_id);
234 let digest = hash_bytes(&canonical_bytes(&message)?);
235 if let Some((_, old)) = self.accepted.get(&key) {
236 if old != &digest {
237 return Err(fail(
238 "invalid_event",
239 "message ID reused for different content",
240 ));
241 }
242 return Ok(InboxAcceptance::Duplicate(Box::new(message)));
243 }
244 if now >= message.expires_at {
245 return Err(fail(
246 "packet_expired",
247 "expired message cannot be newly accepted",
248 ));
249 }
250 self.accepted.insert(key, (message.expires_at, digest));
251 Ok(InboxAcceptance::Accepted(Box::new(message)))
252 }
253 pub fn prune(&mut self, now: i64) -> Result<()> {
254 clock(now)?;
255 self.accepted.retain(|_, (expires, _)| now < *expires);
256 Ok(())
257 }
258 pub fn snapshot(&self) -> Result<Value> {
259 Ok(
260 json!({"version":1,"owner":self.keyring.owner,"accepted":self.accepted,"blocked_senders":self.blocked_senders}),
261 )
262 }
263 pub fn from_snapshot(keyring: MailKeyring, value: &Value) -> Result<Self> {
264 version(value)?;
265 if value["owner"] != keyring.owner.as_str() {
266 return Err(fail("invalid_request", "inbox owner mismatch"));
267 }
268 Ok(Self {
269 keyring,
270 accepted: serde_json::from_value(value["accepted"].clone())?,
271 blocked_senders: serde_json::from_value(value["blocked_senders"].clone())?,
272 })
273 }
274}
275
276struct RelayMailbox {
277 current: AcceptedRecord<MailboxCardPayload>,
278 last_seq: u64,
279 bytes: usize,
280 packets: BTreeMap<String, (PacketRecord, usize)>,
281 tombstones: BTreeMap<String, (i64, i64)>,
282 blocked_senders: BTreeSet<AgentId>,
283}
284impl RelayMailbox {
285 fn drop_expired(&mut self, now: i64) {
286 let bytes = &mut self.bytes;
287 self.packets.retain(|_, (record, size)| {
288 let live = now < record.packet.event.payload.header.expires_at;
289 if !live {
290 *bytes -= *size;
291 }
292 live
293 });
294 }
295}
296pub struct MailRelayStore {
301 origin: String,
302 max_packets: usize,
303 max_bytes: usize,
304 nonces: MemoryNonceStore,
305 mailboxes: BTreeMap<String, RelayMailbox>,
306}
307impl MailRelayStore {
308 pub fn new(origin: impl Into<String>) -> Result<Self> {
309 Self::with_limits(origin, 10_000, 64 * MAX_PACKET_BYTES)
310 }
311 pub fn with_limits(
313 origin: impl Into<String>,
314 max_packets: usize,
315 max_bytes: usize,
316 ) -> Result<Self> {
317 let origin = origin.into();
318 identity::validate_origin(&origin)?;
319 if max_packets == 0 || max_bytes == 0 {
320 return Err(fail("invalid_request", "invalid relay quota"));
321 }
322 Ok(Self {
323 origin,
324 max_packets,
325 max_bytes,
326 nonces: MemoryNonceStore::new(),
327 mailboxes: BTreeMap::new(),
328 })
329 }
330 pub fn origin(&self) -> &str {
331 &self.origin
332 }
333 pub fn discovery(&self) -> Value {
334 json!({"protocol":PROTOCOL,"service":self.origin})
335 }
336 pub fn publish(
337 &mut self,
338 value: &Value,
339 now: i64,
340 ) -> Result<AcceptedRecord<MailboxCardPayload>> {
341 let mut nonces = std::mem::take(&mut self.nonces);
342 let result = self.publish_with_nonce_store(value, now, &mut nonces);
343 self.nonces = nonces;
344 result
345 }
346 pub fn publish_with_nonce_store<S: NonceStore + ?Sized>(
349 &mut self,
350 value: &Value,
351 now: i64,
352 nonces: &mut S,
353 ) -> Result<AcceptedRecord<MailboxCardPayload>> {
354 clock(now)?;
355 let card = validate_card(value)?;
356 let p = &card.event.payload;
357 if let Some(mailbox) = self.mailboxes.get(&p.mailbox_id) {
358 let current = &mailbox.current.envelope;
359 if current.event.actor != card.event.actor {
360 return Err(fail("mailbox_conflict", "mailbox belongs to another owner"));
361 }
362 if current.hash == card.hash {
363 return Ok(mailbox.current.clone());
364 }
365 if card.event.nonce <= current.event.nonce {
366 return Err(SdkError::NonceNotGreater {
367 max_nonce: current.event.nonce,
368 });
369 }
370 } else if !p.routes.contains(&self.origin) {
371 return Err(SdkError::PermissionDenied);
372 }
373 identity::verify_timestamp(card.event.created_at, now, FUTURE_SKEW_MS)?;
374 nonces.check_and_update(&card.event.actor, card.event.nonce, now, 2 * FUTURE_SKEW_MS)?;
375 let record = AcceptedRecord {
376 envelope: card.clone(),
377 accepted_at: now,
378 };
379 match self.mailboxes.get_mut(&p.mailbox_id) {
380 Some(mailbox) => mailbox.current = record.clone(),
381 None => {
382 self.mailboxes.insert(
383 p.mailbox_id.clone(),
384 RelayMailbox {
385 current: record.clone(),
386 last_seq: 0,
387 bytes: 0,
388 packets: BTreeMap::new(),
389 tombstones: BTreeMap::new(),
390 blocked_senders: BTreeSet::new(),
391 },
392 );
393 }
394 }
395 Ok(record)
396 }
397 pub fn card(&self, mailbox_id: &str) -> Result<AcceptedRecord<MailboxCardPayload>> {
398 path_id(mailbox_id, 16)?;
399 self.mailboxes
400 .get(mailbox_id)
401 .map(|m| m.current.clone())
402 .ok_or_else(|| fail("mailbox_unavailable", "unknown mailbox"))
403 }
404 pub fn deliver(
406 &mut self,
407 mailbox_id: &str,
408 packet: &Packet,
409 now: i64,
410 ) -> Result<DeliveryResult> {
411 clock(now)?;
412 path_id(mailbox_id, 16)?;
413 let (packet, bytes) = checked_packet(&serde_json::to_value(packet)?)?;
414 let h = &packet.event.payload.header;
415 if h.mailbox_id != mailbox_id {
416 return Err(fail("invalid_packet", "path mailbox mismatch"));
417 }
418 let (id, size) = (packet.hash.clone(), bytes.len());
419 let mailbox = self
420 .mailboxes
421 .get_mut(mailbox_id)
422 .ok_or_else(|| fail("mailbox_unavailable", "unknown mailbox"))?;
423 if let Some((accepted_at, _)) = mailbox.tombstones.get(&id) {
424 return Ok(DeliveryResult {
425 packet_id: id,
426 accepted_at: *accepted_at,
427 });
428 }
429 let card = &mailbox.current.envelope;
430 let p = &card.event.payload;
431 if !p.routes.contains(&self.origin) {
432 return Err(fail("mailbox_unavailable", "relay is not a current route"));
433 }
434 if h.card_hash != card.hash || now >= p.expires_at {
435 return Err(fail("stale_card", "current card mismatch or expired"));
436 }
437 if now >= h.expires_at {
438 return Err(fail("packet_expired", "packet expired"));
439 }
440 if h.expires_at
441 > p.receive_until
442 .min(now.saturating_add(MAX_TTL_MS + FUTURE_SKEW_MS))
443 {
444 return Err(fail("invalid_packet", "packet expiry bound"));
445 }
446 if packet.event.created_at > now.saturating_add(FUTURE_SKEW_MS) {
447 return Err(SdkError::TimestampOutOfWindow);
448 }
449 if size > p.max_packet_bytes {
450 return Err(fail("payload_too_large", "card packet limit"));
451 }
452 mailbox.drop_expired(now);
453 if mailbox.packets.len() >= self.max_packets
454 || mailbox.bytes.saturating_add(size) > self.max_bytes
455 || mailbox.last_seq >= identity::MAX_SAFE_NONCE
456 {
457 return Err(fail("rate_limited", "mailbox storage quota exhausted"));
458 }
459 if mailbox.blocked_senders.contains(&packet.event.actor) {
460 return Err(SdkError::PermissionDenied);
461 }
462 mailbox.last_seq += 1;
463 mailbox.bytes += size;
464 mailbox
465 .tombstones
466 .insert(id.clone(), (now, packet.event.payload.header.expires_at));
467 mailbox.packets.insert(
468 id.clone(),
469 (
470 PacketRecord {
471 packet_id: id.clone(),
472 packet,
473 accepted_at: now,
474 seq: mailbox.last_seq,
475 },
476 size,
477 ),
478 );
479 Ok(DeliveryResult {
480 packet_id: id,
481 accepted_at: now,
482 })
483 }
484 pub fn set_sender_blocked(
487 &mut self,
488 mailbox: &str,
489 sender: AgentId,
490 blocked: bool,
491 ) -> Result<()> {
492 path_id(mailbox, 16)?;
493 let box_ = self
494 .mailboxes
495 .get_mut(mailbox)
496 .ok_or_else(|| fail("mailbox_unavailable", "unknown mailbox"))?;
497 if blocked {
498 box_.blocked_senders.insert(sender);
499 } else {
500 box_.blocked_senders.remove(&sender);
501 }
502 Ok(())
503 }
504 fn owner_box(&self, mailbox: &str, jwt: &str, now: i64) -> Result<&RelayMailbox> {
505 path_id(mailbox, 16)?;
506 let claims = owner_claims(jwt, &self.origin, now)?;
507 let found = self
508 .mailboxes
509 .get(mailbox)
510 .ok_or_else(|| fail("mailbox_unavailable", "unknown mailbox"))?;
511 if claims.iss != found.current.envelope.event.actor {
512 return Err(SdkError::PermissionDenied);
513 }
514 Ok(found)
515 }
516 pub fn list(
518 &self,
519 mailbox: &str,
520 jwt: &str,
521 now: i64,
522 limit: usize,
523 cursor: Option<&str>,
524 ) -> Result<ListResponse<PacketRecord>> {
525 let found = self.owner_box(mailbox, jwt, now)?;
526 if !(1..=1000).contains(&limit) {
527 return Err(fail("invalid_request", "list limit must be 1..1000"));
528 }
529 let after = match cursor {
530 None => 0,
531 Some(text) => text
532 .parse::<u64>()
533 .ok()
534 .filter(|n| n.to_string() == text && *n <= identity::MAX_SAFE_NONCE)
535 .ok_or_else(|| fail("invalid_request", "invalid cursor"))?,
536 };
537 let mut rows: Vec<_> = found
538 .packets
539 .values()
540 .map(|(record, _)| record)
541 .filter(|r| r.seq > after && now < r.packet.event.payload.header.expires_at)
542 .cloned()
543 .collect();
544 rows.sort_by_key(|r| r.seq);
545 let more = rows.len() > limit;
546 rows.truncate(limit);
547 Ok(ListResponse {
548 next_cursor: more.then(|| rows[limit - 1].seq.to_string()),
549 result: rows,
550 })
551 }
552 pub fn delete(&mut self, mailbox: &str, id: &str, jwt: &str, now: i64) -> Result<()> {
553 self.owner_box(mailbox, jwt, now)?;
554 path_id(id, 32)?;
555 let found = self.mailboxes.get_mut(mailbox).unwrap();
556 if let Some((_, size)) = found.packets.remove(id) {
557 found.bytes -= size;
558 }
559 Ok(())
560 }
561 pub fn prune(&mut self, now: i64) -> Result<()> {
564 clock(now)?;
565 for m in self.mailboxes.values_mut() {
566 m.drop_expired(now);
567 m.tombstones.retain(|_, (_, expires)| now < *expires);
568 }
569 self.mailboxes
570 .retain(|_, m| now < m.current.envelope.event.payload.receive_until);
571 Ok(())
572 }
573 pub fn snapshot(&self) -> Result<Value> {
574 let mailboxes: Vec<_> = self
575 .mailboxes
576 .values()
577 .map(|m| {
578 json!({
579 "current": m.current,
580 "last_seq": m.last_seq,
581 "blocked_senders":m.blocked_senders,
582 "packets": m.packets.values().map(|(r, _)| r).collect::<Vec<_>>(),
583 "tombstones": m.tombstones.iter().map(|(id, (accepted_at, expires_at))| {
584 json!({"packet_id":id,"accepted_at":accepted_at,"expires_at":expires_at})
585 }).collect::<Vec<_>>(),
586 })
587 })
588 .collect();
589 Ok(json!({"version":1,"origin":self.origin,"mailboxes":mailboxes}))
590 }
591 pub fn from_snapshot(value: &Value, max_packets: usize, max_bytes: usize) -> Result<Self> {
593 #[derive(Deserialize)]
594 struct Tombstone {
595 packet_id: String,
596 accepted_at: i64,
597 expires_at: i64,
598 }
599 #[derive(Deserialize)]
600 struct Saved {
601 current: AcceptedRecord<MailboxCardPayload>,
602 last_seq: u64,
603 packets: Vec<PacketRecord>,
604 tombstones: Vec<Tombstone>,
605 blocked_senders: BTreeSet<AgentId>,
606 }
607 version(value)?;
608 let origin = value["origin"]
609 .as_str()
610 .ok_or_else(|| fail("invalid_request", "snapshot origin"))?;
611 let mut out = Self::with_limits(origin, max_packets, max_bytes)?;
612 let saved: Vec<Saved> = serde_json::from_value(normalized(&value["mailboxes"]))?;
613 for m in saved {
614 let mut mailbox = RelayMailbox {
615 current: m.current,
616 blocked_senders: m.blocked_senders,
617 last_seq: m.last_seq,
618 bytes: 0,
619 packets: BTreeMap::new(),
620 tombstones: m
621 .tombstones
622 .into_iter()
623 .map(|t| (t.packet_id, (t.accepted_at, t.expires_at)))
624 .collect(),
625 };
626 for record in m.packets {
627 let size = canonical_bytes(&record.packet)?.len();
628 mailbox.bytes += size;
629 mailbox
630 .packets
631 .insert(record.packet_id.clone(), (record, size));
632 }
633 let id = mailbox.current.envelope.event.payload.mailbox_id.clone();
634 out.mailboxes.insert(id, mailbox);
635 }
636 Ok(out)
637 }
638}
639fn path_id(value: &str, size: usize) -> Result<()> {
640 fixed_bytes(value, size).map_err(|_| fail("invalid_request", "invalid path ID"))?;
641 Ok(())
642}