1use crate::{
2 AuthorizedDevice, DeviceMembership, DevicePubkey, DeviceRoster, DomainError, Error, Invite,
3 InviteResponse, InviteResponseEnvelope, MessageEnvelope, OwnerPubkey, ProtocolContext, Result,
4 RosterSnapshotDecision, Session, SessionState, UnixSeconds, VerifiedAppKeysIndex,
5};
6use rand::{CryptoRng, RngCore};
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9
10const MAX_INACTIVE_SESSIONS: usize = 10;
11#[derive(Debug, Clone)]
12pub struct SessionManager {
13 local_owner_pubkey: OwnerPubkey,
14 local_device_pubkey: DevicePubkey,
15 local_device_secret_key: [u8; 32],
16 local_invite: Option<Invite>,
17 verified_peer_app_keys: VerifiedAppKeysIndex,
18 users: BTreeMap<OwnerPubkey, UserRecord>,
19}
20
21#[derive(Debug, Clone)]
22struct UserRecord {
23 owner_pubkey: OwnerPubkey,
24 roster: Option<DeviceRoster>,
25 devices: BTreeMap<DevicePubkey, DeviceRecord>,
26}
27
28#[derive(Debug, Clone)]
29struct DeviceRecord {
30 device_pubkey: DevicePubkey,
31 authorized: bool,
32 is_stale: bool,
33 stale_since: Option<UnixSeconds>,
34 claimed_owner_pubkey: Option<OwnerPubkey>,
35 public_invite: Option<Invite>,
36 invite_response_generated: bool,
37 active_session: Option<Session>,
38 inactive_sessions: Vec<Session>,
39 last_activity: Option<UnixSeconds>,
40 created_at: UnixSeconds,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44pub struct SessionManagerSnapshot {
45 pub local_owner_pubkey: OwnerPubkey,
46 pub local_device_pubkey: DevicePubkey,
47 pub local_invite: Option<Invite>,
48 #[serde(default)]
49 pub verified_peer_app_keys_events: Vec<nostr::Event>,
50 pub users: Vec<UserRecordSnapshot>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54pub struct UserRecordSnapshot {
55 pub owner_pubkey: OwnerPubkey,
56 pub roster: Option<DeviceRoster>,
57 pub devices: Vec<DeviceRecordSnapshot>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61pub struct DeviceRecordSnapshot {
62 pub device_pubkey: DevicePubkey,
63 pub authorized: bool,
64 pub is_stale: bool,
65 pub stale_since: Option<UnixSeconds>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub claimed_owner_pubkey: Option<OwnerPubkey>,
68 pub public_invite: Option<Invite>,
69 #[serde(default)]
70 pub invite_response_generated: bool,
71 pub active_session: Option<SessionState>,
72 pub inactive_sessions: Vec<SessionState>,
73 pub last_activity: Option<UnixSeconds>,
74 pub created_at: UnixSeconds,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct PreparedSend {
79 pub recipient_owner: OwnerPubkey,
80 pub payload: Vec<u8>,
81 pub deliveries: Vec<Delivery>,
82 pub invite_responses: Vec<InviteResponseEnvelope>,
83 pub relay_gaps: Vec<RelayGap>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Delivery {
88 pub owner_pubkey: OwnerPubkey,
89 pub device_pubkey: DevicePubkey,
90 pub envelope: MessageEnvelope,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ProcessedInviteResponse {
95 pub owner_pubkey: OwnerPubkey,
96 pub device_pubkey: DevicePubkey,
97 pub claimed_owner_pubkey: Option<OwnerPubkey>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ReceivedMessage {
102 pub owner_pubkey: OwnerPubkey,
103 pub device_pubkey: DevicePubkey,
104 pub payload: Vec<u8>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
108pub enum RelayGap {
109 MissingRoster {
110 owner_pubkey: OwnerPubkey,
111 },
112 MissingDeviceInvite {
113 owner_pubkey: OwnerPubkey,
114 device_pubkey: DevicePubkey,
115 },
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct PruneReport {
120 pub removed_devices: Vec<(OwnerPubkey, DevicePubkey)>,
121 pub removed_users: Vec<OwnerPubkey>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
125struct TargetDevice {
126 owner_pubkey: OwnerPubkey,
127 device_pubkey: DevicePubkey,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131enum SendSessionSource {
132 Active,
133 Inactive(usize),
134}
135
136impl SessionManager {
137 pub fn new(local_owner_pubkey: OwnerPubkey, local_device_secret_key: [u8; 32]) -> Self {
138 let local_device_pubkey = crate::device_pubkey_from_secret_bytes(&local_device_secret_key)
139 .expect("local device secret key must derive a valid device public key");
140
141 Self {
142 local_owner_pubkey,
143 local_device_pubkey,
144 local_device_secret_key,
145 local_invite: None,
146 verified_peer_app_keys: VerifiedAppKeysIndex::default(),
147 users: BTreeMap::new(),
148 }
149 }
150
151 pub fn from_snapshot(
152 snapshot: SessionManagerSnapshot,
153 local_device_secret_key: [u8; 32],
154 ) -> Result<Self> {
155 let derived_local_device_pubkey =
156 crate::device_pubkey_from_secret_bytes(&local_device_secret_key)?;
157 if derived_local_device_pubkey != snapshot.local_device_pubkey {
158 return Err(DomainError::InvalidState(
159 "snapshot local device pubkey does not match provided secret key".to_string(),
160 )
161 .into());
162 }
163
164 let mut verified_peer_app_keys = VerifiedAppKeysIndex::default();
165 for event in snapshot.verified_peer_app_keys_events {
166 let observed_at = event.created_at.as_secs();
167 let _ = verified_peer_app_keys.ingest(event, observed_at);
171 }
172
173 let users = snapshot
174 .users
175 .into_iter()
176 .map(UserRecord::from_snapshot)
177 .map(|record| (record.owner_pubkey, record))
178 .collect();
179
180 let mut manager = Self {
181 local_owner_pubkey: snapshot.local_owner_pubkey,
182 local_device_pubkey: snapshot.local_device_pubkey,
183 local_device_secret_key,
184 local_invite: snapshot.local_invite,
185 verified_peer_app_keys,
186 users,
187 };
188
189 let restored_owners = manager.users.keys().copied().collect::<Vec<_>>();
193 for owner_pubkey in restored_owners.iter().copied() {
194 manager.recompute_authorization_for_owner(owner_pubkey);
195 }
196 let verified_owners = manager
197 .verified_peer_app_keys
198 .owners()
199 .into_iter()
200 .map(|owner| OwnerPubkey::from_bytes(owner.to_bytes()))
201 .collect::<Vec<_>>();
202 for owner_pubkey in verified_owners {
203 let now = owner_pubkey
204 .to_nostr()
205 .ok()
206 .and_then(|owner| manager.verified_peer_app_keys.head_created_at(owner))
207 .map(UnixSeconds)
208 .unwrap_or(UnixSeconds(0));
209 manager.reconcile_verified_claimed_devices(owner_pubkey, now);
210 manager.recompute_authorization_for_owner(owner_pubkey);
211 }
212
213 Ok(manager)
214 }
215
216 pub fn snapshot(&self) -> SessionManagerSnapshot {
217 SessionManagerSnapshot {
218 local_owner_pubkey: self.local_owner_pubkey,
219 local_device_pubkey: self.local_device_pubkey,
220 local_invite: self.local_invite.clone(),
221 verified_peer_app_keys_events: self.verified_peer_app_keys.events(),
222 users: self.users.values().map(UserRecord::snapshot).collect(),
223 }
224 }
225
226 pub fn local_device_pubkey(&self) -> DevicePubkey {
227 self.local_device_pubkey
228 }
229
230 pub fn replace_local_invite(&mut self, invite: Invite) {
231 self.local_invite = Some(invite);
232 }
233
234 pub fn ensure_local_invite<R>(&mut self, ctx: &mut ProtocolContext<'_, R>) -> Result<&Invite>
235 where
236 R: RngCore + CryptoRng,
237 {
238 if self.local_invite.is_none() {
239 let invite = Invite::create_new_with_context(
240 ctx,
241 self.local_device_pubkey,
242 Some(self.local_owner_pubkey),
243 None,
244 )?;
245 self.observe_public_invite(self.local_owner_pubkey, invite.clone())?;
246 self.local_invite = Some(invite);
247 }
248
249 Ok(self.local_invite.as_ref().expect("local invite must exist"))
250 }
251
252 pub fn apply_local_roster(&mut self, roster: DeviceRoster) -> RosterSnapshotDecision {
253 self.apply_roster_for_owner(self.local_owner_pubkey, roster)
254 }
255
256 pub fn replace_local_roster(&mut self, roster: DeviceRoster) -> RosterSnapshotDecision {
257 self.apply_roster_for_owner_inner(self.local_owner_pubkey, roster, true)
258 }
259
260 pub fn observe_peer_roster(
269 &mut self,
270 owner_pubkey: OwnerPubkey,
271 roster: DeviceRoster,
272 ) -> RosterSnapshotDecision {
273 if owner_pubkey == self.local_owner_pubkey {
274 return RosterSnapshotDecision::Stale;
275 }
276 self.apply_roster_for_owner(owner_pubkey, roster)
277 }
278
279 pub fn observe_peer_app_keys_event(
284 &mut self,
285 event: nostr::Event,
286 observed_at: UnixSeconds,
287 ) -> Result<bool> {
288 let owner_nostr = event.pubkey;
289 let event_id = event.id;
290 let owner = OwnerPubkey::from_bytes(owner_nostr.to_bytes());
291 let created_at = UnixSeconds(event.created_at.as_secs());
292 if !self
293 .verified_peer_app_keys
294 .ingest(event, observed_at.get())?
295 {
296 return Ok(false);
297 }
298 let app_keys = self
299 .verified_peer_app_keys
300 .app_keys_for_event(owner_nostr, event_id)
301 .expect("successfully ingested AppKeys candidate must remain indexed");
302
303 let roster = DeviceRoster::new(
304 created_at,
305 app_keys
306 .get_all_devices()
307 .into_iter()
308 .map(|device| {
309 AuthorizedDevice::new(
310 DevicePubkey::from_bytes(device.identity_pubkey.to_bytes()),
311 UnixSeconds(device.created_at),
312 )
313 })
314 .collect(),
315 );
316 self.apply_roster_for_owner(owner, roster);
317 Ok(true)
318 }
319
320 pub fn observe_device_invite(
321 &mut self,
322 owner_pubkey: OwnerPubkey,
323 invite: Invite,
324 ) -> Result<()> {
325 self.observe_public_invite(owner_pubkey, invite)
326 }
327
328 pub fn observe_invite_response<R>(
329 &mut self,
330 ctx: &mut ProtocolContext<'_, R>,
331 envelope: &InviteResponseEnvelope,
332 ) -> Result<Option<ProcessedInviteResponse>>
333 where
334 R: RngCore + CryptoRng,
335 {
336 let Some(invite) = self.local_invite.clone() else {
337 return Ok(None);
338 };
339
340 let mut owned_invite = invite;
341 let InviteResponse {
342 session,
343 invitee_device_pubkey,
344 invitee_owner_pubkey,
345 ..
346 } = owned_invite.process_response(ctx, envelope, self.local_device_secret_key)?;
347
348 self.local_invite = Some(owned_invite);
349
350 let invitee_owner_pubkey = invitee_owner_pubkey.ok_or_else(|| {
351 DomainError::InvalidState("invite response missing owner claim".to_string())
352 })?;
353
354 Ok(Some(self.store_claimed_session(
355 invitee_owner_pubkey,
356 invitee_device_pubkey,
357 session,
358 ctx.now,
359 )))
360 }
361
362 pub fn prepare_send<R>(
363 &mut self,
364 ctx: &mut ProtocolContext<'_, R>,
365 recipient_owner: OwnerPubkey,
366 payload: Vec<u8>,
367 ) -> Result<PreparedSend>
368 where
369 R: RngCore + CryptoRng,
370 {
371 self.prepare_send_inner(ctx, recipient_owner, payload, true)
372 }
373
374 pub fn prepare_remote_send<R>(
381 &mut self,
382 ctx: &mut ProtocolContext<'_, R>,
383 recipient_owner: OwnerPubkey,
384 payload: Vec<u8>,
385 ) -> Result<PreparedSend>
386 where
387 R: RngCore + CryptoRng,
388 {
389 self.prepare_send_inner(ctx, recipient_owner, payload, false)
390 }
391
392 pub fn prepare_remote_send_to_devices<R>(
393 &mut self,
394 ctx: &mut ProtocolContext<'_, R>,
395 recipient_owner: OwnerPubkey,
396 device_pubkeys: impl IntoIterator<Item = DevicePubkey>,
397 payload: Vec<u8>,
398 ) -> Result<PreparedSend>
399 where
400 R: RngCore + CryptoRng,
401 {
402 let targets = device_pubkeys
403 .into_iter()
404 .map(|device_pubkey| TargetDevice {
405 owner_pubkey: recipient_owner,
406 device_pubkey,
407 })
408 .collect();
409 self.prepare_explicit_send(ctx, recipient_owner, targets, payload, false)
410 }
411
412 pub fn prepare_local_sibling_send<R>(
413 &mut self,
414 ctx: &mut ProtocolContext<'_, R>,
415 payload: Vec<u8>,
416 ) -> Result<PreparedSend>
417 where
418 R: RngCore + CryptoRng,
419 {
420 self.prepare_local_sibling_send_inner(ctx, payload, false)
421 }
422
423 pub fn prepare_local_sibling_send_to_devices<R>(
424 &mut self,
425 ctx: &mut ProtocolContext<'_, R>,
426 device_pubkeys: impl IntoIterator<Item = DevicePubkey>,
427 payload: Vec<u8>,
428 ) -> Result<PreparedSend>
429 where
430 R: RngCore + CryptoRng,
431 {
432 let targets = device_pubkeys
433 .into_iter()
434 .map(|device_pubkey| TargetDevice {
435 owner_pubkey: self.local_owner_pubkey,
436 device_pubkey,
437 })
438 .collect();
439 self.prepare_explicit_send(ctx, self.local_owner_pubkey, targets, payload, false)
440 }
441
442 pub fn prepare_local_sibling_send_reusing_sessions<R>(
443 &mut self,
444 ctx: &mut ProtocolContext<'_, R>,
445 payload: Vec<u8>,
446 ) -> Result<PreparedSend>
447 where
448 R: RngCore + CryptoRng,
449 {
450 self.prepare_local_sibling_send_inner(ctx, payload, false)
451 }
452
453 pub fn prepare_local_sibling_send_refreshing_one_way_sessions<R>(
454 &mut self,
455 ctx: &mut ProtocolContext<'_, R>,
456 payload: Vec<u8>,
457 ) -> Result<PreparedSend>
458 where
459 R: RngCore + CryptoRng,
460 {
461 self.prepare_local_sibling_send_inner(ctx, payload, true)
462 }
463
464 pub fn prepare_local_sibling_send_reusing_all_sessions<R>(
465 &mut self,
466 ctx: &mut ProtocolContext<'_, R>,
467 payload: Vec<u8>,
468 ) -> Result<PreparedSend>
469 where
470 R: RngCore + CryptoRng,
471 {
472 let mut targets = BTreeSet::new();
473 self.collect_local_sibling_targets(&mut targets);
474
475 let mut deliveries = Vec::new();
476 let mut invite_responses = Vec::new();
477 let mut relay_gaps = Vec::new();
478
479 for target in targets {
480 let mut target_deliveries = self.prepare_device_deliveries_for_all_send_sessions(
481 ctx,
482 target.owner_pubkey,
483 target.device_pubkey,
484 &payload,
485 )?;
486 if target_deliveries.is_empty() {
487 match self.prepare_device_delivery(
488 ctx,
489 target.owner_pubkey,
490 target.device_pubkey,
491 &payload,
492 false,
493 )? {
494 Some((delivery, maybe_response)) => {
495 target_deliveries.push(delivery);
496 if let Some(response) = maybe_response {
497 invite_responses.push(response);
498 }
499 }
500 None => {
501 relay_gaps.push(RelayGap::MissingDeviceInvite {
502 owner_pubkey: target.owner_pubkey,
503 device_pubkey: target.device_pubkey,
504 });
505 }
506 }
507 }
508 deliveries.extend(target_deliveries);
509 }
510
511 relay_gaps.sort();
512
513 Ok(PreparedSend {
514 recipient_owner: self.local_owner_pubkey,
515 payload,
516 deliveries,
517 invite_responses,
518 relay_gaps,
519 })
520 }
521
522 fn prepare_local_sibling_send_inner<R>(
523 &mut self,
524 ctx: &mut ProtocolContext<'_, R>,
525 payload: Vec<u8>,
526 refresh_one_way_bootstrap: bool,
527 ) -> Result<PreparedSend>
528 where
529 R: RngCore + CryptoRng,
530 {
531 let mut targets = BTreeSet::new();
532 self.collect_local_sibling_targets(&mut targets);
533
534 let mut deliveries = Vec::new();
535 let mut invite_responses = Vec::new();
536 let mut relay_gaps = Vec::new();
537
538 for target in targets {
539 match self.prepare_device_delivery(
540 ctx,
541 target.owner_pubkey,
542 target.device_pubkey,
543 &payload,
544 refresh_one_way_bootstrap,
545 )? {
546 Some((delivery, maybe_response)) => {
547 deliveries.push(delivery);
548 if let Some(response) = maybe_response {
549 invite_responses.push(response);
550 }
551 }
552 None => {
553 relay_gaps.push(RelayGap::MissingDeviceInvite {
554 owner_pubkey: target.owner_pubkey,
555 device_pubkey: target.device_pubkey,
556 });
557 }
558 }
559 }
560
561 relay_gaps.sort();
562
563 Ok(PreparedSend {
564 recipient_owner: self.local_owner_pubkey,
565 payload,
566 deliveries,
567 invite_responses,
568 relay_gaps,
569 })
570 }
571
572 pub(crate) fn has_authorized_local_siblings(&self) -> bool {
573 let Some(user) = self.users.get(&self.local_owner_pubkey) else {
574 return false;
575 };
576 if user.roster.is_none() {
577 return false;
578 }
579 user.authorized_non_stale_devices()
580 .into_iter()
581 .any(|device_pubkey| device_pubkey != self.local_device_pubkey)
582 }
583
584 pub fn receive<R>(
585 &mut self,
586 ctx: &mut ProtocolContext<'_, R>,
587 sender_owner: OwnerPubkey,
588 envelope: &MessageEnvelope,
589 ) -> Result<Option<ReceivedMessage>>
590 where
591 R: RngCore + CryptoRng,
592 {
593 let owner_authorized_devices = self.authorized_devices_for_owner(sender_owner);
594 let Some(user) = self.users.get_mut(&sender_owner) else {
595 return Ok(None);
596 };
597
598 let device_pubkeys: Vec<DevicePubkey> = user.devices.keys().copied().collect();
599 for device_pubkey in device_pubkeys {
600 let record = user
601 .devices
602 .get_mut(&device_pubkey)
603 .expect("device key collected from map");
604
605 let has_owner_binding = crate::owner_pubkey_from_device_pubkey(device_pubkey)
606 == sender_owner
607 || owner_authorized_devices.contains(&device_pubkey);
608 if !has_owner_binding || !record.authorized || record.is_stale {
609 continue;
610 }
611
612 if let Some(active_session) = record.active_session.as_ref() {
613 if active_session.matches_sender(envelope.sender) {
614 let plan = active_session.plan_receive(ctx, envelope)?;
615 let outcome = record
616 .active_session
617 .as_mut()
618 .expect("active session must still exist")
619 .apply_receive(plan);
620 record.last_activity = Some(ctx.now);
621 return Ok(Some(ReceivedMessage {
622 owner_pubkey: sender_owner,
623 device_pubkey,
624 payload: outcome.payload,
625 }));
626 }
627 }
628
629 let mut matched_inactive = None;
630 for (index, session) in record.inactive_sessions.iter().enumerate() {
631 if !session.matches_sender(envelope.sender) {
632 continue;
633 }
634 let plan = session.plan_receive(ctx, envelope)?;
635 matched_inactive = Some((index, plan));
636 break;
637 }
638
639 if let Some((index, plan)) = matched_inactive {
640 let mut session = record.inactive_sessions.remove(index);
641 let outcome = session.apply_receive(plan);
642 record.promote_inactive_session(session);
643 record.last_activity = Some(ctx.now);
644 return Ok(Some(ReceivedMessage {
645 owner_pubkey: sender_owner,
646 device_pubkey,
647 payload: outcome.payload,
648 }));
649 }
650 }
651
652 Ok(None)
653 }
654
655 pub fn prune_stale(&mut self, _now: UnixSeconds) -> PruneReport {
656 let mut removed_devices = Vec::new();
657 let mut removed_users = Vec::new();
658
659 self.users.retain(|owner_pubkey, user| {
660 user.devices.retain(|device_pubkey, record| {
661 let keep = !record.is_stale;
662 if !keep {
663 removed_devices.push((*owner_pubkey, *device_pubkey));
664 }
665 keep
666 });
667
668 let keep_user = !user.devices.is_empty() || user.roster.is_some();
669 if !keep_user {
670 removed_users.push(*owner_pubkey);
671 }
672 keep_user
673 });
674
675 removed_devices.sort();
676 removed_users.sort();
677
678 PruneReport {
679 removed_devices,
680 removed_users,
681 }
682 }
683
684 pub fn delete_user(&mut self, owner_pubkey: OwnerPubkey) {
685 if owner_pubkey != self.local_owner_pubkey {
686 self.users.remove(&owner_pubkey);
687 }
688 }
689
690 pub fn import_session_state(
691 &mut self,
692 owner_pubkey: OwnerPubkey,
693 device_pubkey: DevicePubkey,
694 state: SessionState,
695 now: UnixSeconds,
696 ) {
697 self.import_claimed_session_state(owner_pubkey, device_pubkey, state, now);
698 }
699
700 pub fn import_claimed_session_state(
708 &mut self,
709 claimed_owner_pubkey: OwnerPubkey,
710 authenticated_device_pubkey: DevicePubkey,
711 state: SessionState,
712 now: UnixSeconds,
713 ) -> ProcessedInviteResponse {
714 self.store_claimed_session(
715 claimed_owner_pubkey,
716 authenticated_device_pubkey,
717 Session::from_state(state),
718 now,
719 )
720 }
721
722 fn store_claimed_session(
723 &mut self,
724 claimed_owner_pubkey: OwnerPubkey,
725 authenticated_device_pubkey: DevicePubkey,
726 session: Session,
727 now: UnixSeconds,
728 ) -> ProcessedInviteResponse {
729 let device_owner_pubkey =
730 crate::owner_pubkey_from_device_pubkey(authenticated_device_pubkey);
731 let claim_is_verified =
732 self.owner_device_is_authorized(claimed_owner_pubkey, authenticated_device_pubkey);
733 let owner_pubkey = if claim_is_verified {
734 claimed_owner_pubkey
735 } else {
736 device_owner_pubkey
737 };
738 let provisional_claim =
739 (claimed_owner_pubkey != owner_pubkey).then_some(claimed_owner_pubkey);
740 let should_seed_single_device_roster = owner_pubkey == device_owner_pubkey
741 && self
742 .users
743 .get(&owner_pubkey)
744 .and_then(|user| user.roster.as_ref())
745 .is_none();
746
747 let user = self.user_record_mut(owner_pubkey);
748 if should_seed_single_device_roster {
749 user.roster = Some(DeviceRoster::new(
750 now,
751 vec![AuthorizedDevice::new(authenticated_device_pubkey, now)],
752 ));
753 }
754 let record = user.device_record_mut(authenticated_device_pubkey, now);
755 record.claimed_owner_pubkey = provisional_claim;
756 record.invite_response_generated = true;
757 record.upsert_session(session, now);
758 self.recompute_authorization_for_owner(owner_pubkey);
759
760 ProcessedInviteResponse {
761 owner_pubkey,
762 device_pubkey: authenticated_device_pubkey,
763 claimed_owner_pubkey: provisional_claim,
764 }
765 }
766
767 fn prepare_device_delivery<R>(
768 &mut self,
769 ctx: &mut ProtocolContext<'_, R>,
770 owner_pubkey: OwnerPubkey,
771 device_pubkey: DevicePubkey,
772 payload: &[u8],
773 refresh_one_way_bootstrap: bool,
774 ) -> Result<Option<(Delivery, Option<InviteResponseEnvelope>)>>
775 where
776 R: RngCore + CryptoRng,
777 {
778 let claimed_owner = Some(self.local_owner_pubkey);
779 let local_owner_pubkey = self.local_owner_pubkey;
780 let local_device_pubkey = self.local_device_pubkey;
781 let local_device_secret_key = self.local_device_secret_key;
782 let user = self.user_record_mut(owner_pubkey);
783 let record = user.device_record_mut(device_pubkey, ctx.now);
784
785 if !record.authorized || record.is_stale {
786 return Ok(None);
787 }
788
789 let source = record.best_send_session_source();
790 let should_refresh_local_sibling_bootstrap = refresh_one_way_bootstrap
791 && owner_pubkey == local_owner_pubkey
792 && device_pubkey != local_device_pubkey
793 && record.public_invite.is_some()
794 && source
795 .as_ref()
796 .and_then(|source| record.session_for_send_source(source))
797 .is_some_and(is_one_way_bootstrap_session);
798
799 if should_refresh_local_sibling_bootstrap {
800 let public_invite = record
801 .public_invite
802 .clone()
803 .expect("checked public invite presence");
804 match public_invite.accept_with_owner_context(
805 ctx,
806 local_device_pubkey,
807 local_device_secret_key,
808 claimed_owner,
809 ) {
810 Ok((mut session, invite_response)) => {
811 let mut envelope = session
812 .apply_send(session.plan_send(payload, ctx.now)?)
813 .envelope;
814 envelope.recipient = Some(device_pubkey);
815 record.invite_response_generated = true;
816 record.upsert_session(session, ctx.now);
817
818 return Ok(Some((
819 Delivery {
820 owner_pubkey,
821 device_pubkey,
822 envelope,
823 },
824 Some(invite_response),
825 )));
826 }
827 Err(Error::Domain(
828 DomainError::InviteAlreadyUsed | DomainError::InviteExhausted,
829 )) => {}
830 Err(error) => return Err(error),
831 }
832 }
833
834 if let Some(source) = source {
835 let plan = match source {
836 SendSessionSource::Active => record
837 .active_session
838 .as_ref()
839 .expect("active session must exist")
840 .plan_send(payload, ctx.now)?,
841 SendSessionSource::Inactive(index) => {
842 record.inactive_sessions[index].plan_send(payload, ctx.now)?
843 }
844 };
845
846 let mut envelope = match source {
847 SendSessionSource::Active => {
848 record
849 .active_session
850 .as_mut()
851 .expect("active session must exist")
852 .apply_send(plan)
853 .envelope
854 }
855 SendSessionSource::Inactive(index) => {
856 let mut session = record.inactive_sessions.remove(index);
857 let outcome = session.apply_send(plan);
858 record.upsert_session(session, ctx.now);
859 outcome.envelope
860 }
861 };
862 envelope.recipient = Some(device_pubkey);
863
864 record.last_activity = Some(ctx.now);
865 return Ok(Some((
866 Delivery {
867 owner_pubkey,
868 device_pubkey,
869 envelope,
870 },
871 None,
872 )));
873 }
874
875 let Some(public_invite) = record.public_invite.clone() else {
876 return Ok(None);
877 };
878
879 let (mut session, invite_response) = match public_invite.accept_with_owner_context(
880 ctx,
881 local_device_pubkey,
882 local_device_secret_key,
883 claimed_owner,
884 ) {
885 Ok(result) => result,
886 Err(Error::Domain(DomainError::InviteAlreadyUsed | DomainError::InviteExhausted)) => {
887 return Ok(None)
888 }
889 Err(error) => return Err(error),
890 };
891 let mut envelope = session
892 .apply_send(session.plan_send(payload, ctx.now)?)
893 .envelope;
894 envelope.recipient = Some(device_pubkey);
895 record.invite_response_generated = true;
896 record.upsert_session(session, ctx.now);
897
898 Ok(Some((
899 Delivery {
900 owner_pubkey,
901 device_pubkey,
902 envelope,
903 },
904 Some(invite_response),
905 )))
906 }
907
908 fn prepare_device_deliveries_for_all_send_sessions<R>(
909 &mut self,
910 ctx: &mut ProtocolContext<'_, R>,
911 owner_pubkey: OwnerPubkey,
912 device_pubkey: DevicePubkey,
913 payload: &[u8],
914 ) -> Result<Vec<Delivery>>
915 where
916 R: RngCore + CryptoRng,
917 {
918 let user = self.user_record_mut(owner_pubkey);
919 let record = user.device_record_mut(device_pubkey, ctx.now);
920
921 if !record.authorized || record.is_stale {
922 return Ok(Vec::new());
923 }
924
925 let mut deliveries = Vec::new();
926
927 if let Some(active_session) = record.active_session.as_mut() {
928 if active_session.can_send() {
929 let plan = active_session.plan_send(payload, ctx.now)?;
930 let mut envelope = active_session.apply_send(plan).envelope;
931 envelope.recipient = Some(device_pubkey);
932 deliveries.push(Delivery {
933 owner_pubkey,
934 device_pubkey,
935 envelope,
936 });
937 }
938 }
939
940 let inactive_sessions = std::mem::take(&mut record.inactive_sessions);
941 for mut session in inactive_sessions {
942 if session.can_send() {
943 let plan = session.plan_send(payload, ctx.now)?;
944 let mut envelope = session.apply_send(plan).envelope;
945 envelope.recipient = Some(device_pubkey);
946 deliveries.push(Delivery {
947 owner_pubkey,
948 device_pubkey,
949 envelope,
950 });
951 }
952 record.upsert_session(session, ctx.now);
953 }
954
955 if !deliveries.is_empty() {
956 record.last_activity = Some(ctx.now);
957 }
958
959 Ok(deliveries)
960 }
961
962 fn prepare_send_inner<R>(
963 &mut self,
964 ctx: &mut ProtocolContext<'_, R>,
965 recipient_owner: OwnerPubkey,
966 payload: Vec<u8>,
967 include_local_siblings: bool,
968 ) -> Result<PreparedSend>
969 where
970 R: RngCore + CryptoRng,
971 {
972 let mut relay_gaps = Vec::new();
973 let mut targets = BTreeSet::new();
974
975 self.collect_recipient_targets(recipient_owner, &mut targets, &mut relay_gaps);
976 if include_local_siblings {
977 self.collect_local_sibling_targets(&mut targets);
978 }
979
980 let mut deliveries = Vec::new();
981 let mut invite_responses = Vec::new();
982
983 for target in targets {
984 match self.prepare_device_delivery(
985 ctx,
986 target.owner_pubkey,
987 target.device_pubkey,
988 &payload,
989 false,
990 )? {
991 Some((delivery, maybe_response)) => {
992 deliveries.push(delivery);
993 if let Some(response) = maybe_response {
994 invite_responses.push(response);
995 }
996 }
997 None => {
998 relay_gaps.push(RelayGap::MissingDeviceInvite {
999 owner_pubkey: target.owner_pubkey,
1000 device_pubkey: target.device_pubkey,
1001 });
1002 }
1003 }
1004 }
1005
1006 relay_gaps.sort();
1007
1008 Ok(PreparedSend {
1009 recipient_owner,
1010 payload,
1011 deliveries,
1012 invite_responses,
1013 relay_gaps,
1014 })
1015 }
1016
1017 fn prepare_explicit_send<R>(
1018 &mut self,
1019 ctx: &mut ProtocolContext<'_, R>,
1020 recipient_owner: OwnerPubkey,
1021 targets: BTreeSet<TargetDevice>,
1022 payload: Vec<u8>,
1023 refresh_one_way_bootstrap: bool,
1024 ) -> Result<PreparedSend>
1025 where
1026 R: RngCore + CryptoRng,
1027 {
1028 let mut deliveries = Vec::new();
1029 let mut invite_responses = Vec::new();
1030 let mut relay_gaps = Vec::new();
1031
1032 for target in targets {
1033 match self.prepare_device_delivery(
1034 ctx,
1035 target.owner_pubkey,
1036 target.device_pubkey,
1037 &payload,
1038 refresh_one_way_bootstrap,
1039 )? {
1040 Some((delivery, maybe_response)) => {
1041 deliveries.push(delivery);
1042 if let Some(response) = maybe_response {
1043 invite_responses.push(response);
1044 }
1045 }
1046 None => {
1047 relay_gaps.push(RelayGap::MissingDeviceInvite {
1048 owner_pubkey: target.owner_pubkey,
1049 device_pubkey: target.device_pubkey,
1050 });
1051 }
1052 }
1053 }
1054
1055 relay_gaps.sort();
1056
1057 Ok(PreparedSend {
1058 recipient_owner,
1059 payload,
1060 deliveries,
1061 invite_responses,
1062 relay_gaps,
1063 })
1064 }
1065
1066 fn collect_recipient_targets(
1067 &self,
1068 recipient_owner: OwnerPubkey,
1069 targets: &mut BTreeSet<TargetDevice>,
1070 relay_gaps: &mut Vec<RelayGap>,
1071 ) {
1072 let Some(user) = self.users.get(&recipient_owner) else {
1073 relay_gaps.push(RelayGap::MissingRoster {
1074 owner_pubkey: recipient_owner,
1075 });
1076 return;
1077 };
1078
1079 if user
1080 .roster
1081 .as_ref()
1082 .is_none_or(|roster| roster.devices().is_empty())
1083 {
1084 relay_gaps.push(RelayGap::MissingRoster {
1085 owner_pubkey: recipient_owner,
1086 });
1087 return;
1088 }
1089
1090 for device_pubkey in user.authorized_non_stale_devices() {
1091 targets.insert(TargetDevice {
1092 owner_pubkey: recipient_owner,
1093 device_pubkey,
1094 });
1095 }
1096 }
1097
1098 fn collect_local_sibling_targets(&self, targets: &mut BTreeSet<TargetDevice>) {
1099 let Some(user) = self.users.get(&self.local_owner_pubkey) else {
1100 return;
1101 };
1102
1103 if user.roster.is_none() {
1104 return;
1105 }
1106
1107 for device_pubkey in user.authorized_non_stale_devices() {
1108 if device_pubkey == self.local_device_pubkey {
1109 continue;
1110 }
1111 targets.insert(TargetDevice {
1112 owner_pubkey: self.local_owner_pubkey,
1113 device_pubkey,
1114 });
1115 }
1116 }
1117
1118 fn observe_public_invite(&mut self, owner_pubkey: OwnerPubkey, invite: Invite) -> Result<()> {
1119 if let Some(inviter_owner_pubkey) = invite.inviter_owner_pubkey {
1120 if inviter_owner_pubkey != owner_pubkey {
1121 return Err(DomainError::InvalidState(format!(
1122 "invite owner mismatch: expected {owner_pubkey}, got {inviter_owner_pubkey}"
1123 ))
1124 .into());
1125 }
1126 }
1127
1128 let device_pubkey = invite.inviter_device_pubkey;
1129 let mut public_invite = invite;
1130 public_invite.inviter_ephemeral_private_key = None;
1131
1132 let user = self.user_record_mut(owner_pubkey);
1133 let record = user.device_record_mut(device_pubkey, public_invite.created_at);
1134
1135 let should_replace_invite = record
1136 .public_invite
1137 .as_ref()
1138 .is_none_or(|existing| public_invite.created_at >= existing.created_at);
1139
1140 record.created_at = merge_created_at(record.created_at, public_invite.created_at);
1141 if should_replace_invite {
1142 record.public_invite = Some(public_invite);
1143 }
1144 Ok(())
1145 }
1146
1147 fn apply_roster_for_owner(
1148 &mut self,
1149 owner_pubkey: OwnerPubkey,
1150 incoming_roster: DeviceRoster,
1151 ) -> RosterSnapshotDecision {
1152 self.apply_roster_for_owner_inner(owner_pubkey, incoming_roster, false)
1153 }
1154
1155 fn apply_roster_for_owner_inner(
1156 &mut self,
1157 owner_pubkey: OwnerPubkey,
1158 incoming_roster: DeviceRoster,
1159 replace_existing: bool,
1160 ) -> RosterSnapshotDecision {
1161 let (decision, next_roster) = {
1162 let user = self.user_record_mut(owner_pubkey);
1163 let current_roster = user.roster.as_ref();
1164 if replace_existing {
1165 (RosterSnapshotDecision::Advanced, incoming_roster)
1166 } else {
1167 apply_roster_snapshot(current_roster, &incoming_roster)
1168 }
1169 };
1170
1171 let next_created_at = next_roster.created_at;
1172 self.user_record_mut(owner_pubkey).roster = Some(next_roster);
1173 self.recompute_authorization_for_owner(owner_pubkey);
1174 self.reconcile_verified_claimed_devices(owner_pubkey, next_created_at);
1175 self.recompute_authorization_for_owner(owner_pubkey);
1176
1177 decision
1178 }
1179
1180 fn reconcile_verified_claimed_devices(&mut self, owner_pubkey: OwnerPubkey, now: UnixSeconds) {
1181 let source_owners: Vec<OwnerPubkey> = self
1182 .users
1183 .keys()
1184 .copied()
1185 .filter(|candidate_owner_pubkey| *candidate_owner_pubkey != owner_pubkey)
1186 .collect();
1187
1188 let mut migrated = Vec::new();
1189 let mut empty_sources = Vec::new();
1190
1191 for source_owner_pubkey in source_owners {
1192 let matching_devices = self
1193 .users
1194 .get(&source_owner_pubkey)
1195 .map(|user| {
1196 user.devices
1197 .values()
1198 .filter(|record| {
1199 if !self.owner_device_is_authorized(owner_pubkey, record.device_pubkey)
1200 {
1201 return false;
1202 }
1203 if record.claimed_owner_pubkey == Some(owner_pubkey) {
1204 return true;
1205 }
1206 user.roster
1207 .as_ref()
1208 .and_then(|roster| roster.get_device(&record.device_pubkey))
1209 .is_none()
1210 })
1211 .map(|record| record.device_pubkey)
1212 .collect::<Vec<_>>()
1213 })
1214 .unwrap_or_default();
1215
1216 if matching_devices.is_empty() {
1217 continue;
1218 }
1219
1220 if let Some(user) = self.users.get_mut(&source_owner_pubkey) {
1221 let source_roster_is_provisional = user.roster.as_ref().is_some_and(|roster| {
1222 roster.devices().iter().all(|device| {
1223 matching_devices.contains(&device.device_pubkey)
1224 && crate::owner_pubkey_from_device_pubkey(device.device_pubkey)
1225 == source_owner_pubkey
1226 })
1227 });
1228
1229 for device_pubkey in matching_devices {
1230 if let Some(mut record) = user.devices.remove(&device_pubkey) {
1231 record.claimed_owner_pubkey = None;
1232 migrated.push(record);
1233 }
1234 }
1235
1236 if user.devices.is_empty()
1237 && (user.roster.is_none() || source_roster_is_provisional)
1238 {
1239 empty_sources.push(source_owner_pubkey);
1240 }
1241 }
1242 }
1243
1244 for source_owner_pubkey in empty_sources {
1245 self.users.remove(&source_owner_pubkey);
1246 }
1247
1248 if migrated.is_empty() {
1249 return;
1250 }
1251
1252 let user = self.user_record_mut(owner_pubkey);
1253 for record in migrated {
1254 let device_pubkey = record.device_pubkey;
1255 user.device_record_mut(device_pubkey, record.created_at)
1256 .absorb(record, now);
1257 }
1258 }
1259
1260 fn owner_device_is_authorized(
1261 &self,
1262 owner_pubkey: OwnerPubkey,
1263 device_pubkey: DevicePubkey,
1264 ) -> bool {
1265 if crate::owner_pubkey_from_device_pubkey(device_pubkey) == owner_pubkey {
1266 return true;
1267 }
1268 if owner_pubkey == self.local_owner_pubkey {
1269 return self
1270 .users
1271 .get(&owner_pubkey)
1272 .and_then(|user| user.roster.as_ref())
1273 .and_then(|roster| roster.get_device(&device_pubkey))
1274 .is_some();
1275 }
1276 let Ok(owner) = owner_pubkey.to_nostr() else {
1277 return false;
1278 };
1279 let Ok(device) = device_pubkey.to_nostr() else {
1280 return false;
1281 };
1282 self.verified_peer_app_keys.membership(owner, device) == DeviceMembership::Authorized
1283 }
1284
1285 fn authorized_devices_for_owner(&self, owner_pubkey: OwnerPubkey) -> BTreeSet<DevicePubkey> {
1286 self.users
1287 .get(&owner_pubkey)
1288 .map(|user| {
1289 user.roster
1290 .as_ref()
1291 .map(|roster| {
1292 roster
1293 .devices()
1294 .iter()
1295 .filter(|device| {
1296 self.owner_device_is_authorized(owner_pubkey, device.device_pubkey)
1297 })
1298 .map(|device| device.device_pubkey)
1299 .collect()
1300 })
1301 .unwrap_or_default()
1302 })
1303 .unwrap_or_default()
1304 }
1305
1306 fn recompute_authorization_for_owner(&mut self, owner_pubkey: OwnerPubkey) {
1307 let authorized_devices = self.authorized_devices_for_owner(owner_pubkey);
1308 if let Some(user) = self.users.get_mut(&owner_pubkey) {
1309 user.recompute_authorization(&authorized_devices);
1310 }
1311 }
1312
1313 fn user_record_mut(&mut self, owner_pubkey: OwnerPubkey) -> &mut UserRecord {
1314 self.users
1315 .entry(owner_pubkey)
1316 .or_insert_with(|| UserRecord::new(owner_pubkey))
1317 }
1318}
1319
1320impl UserRecord {
1321 fn new(owner_pubkey: OwnerPubkey) -> Self {
1322 Self {
1323 owner_pubkey,
1324 roster: None,
1325 devices: BTreeMap::new(),
1326 }
1327 }
1328
1329 fn from_snapshot(snapshot: UserRecordSnapshot) -> Self {
1330 Self {
1331 owner_pubkey: snapshot.owner_pubkey,
1332 roster: snapshot.roster,
1333 devices: snapshot
1334 .devices
1335 .into_iter()
1336 .map(DeviceRecord::from_snapshot)
1337 .map(|record| (record.device_pubkey, record))
1338 .collect(),
1339 }
1340 }
1341
1342 fn snapshot(&self) -> UserRecordSnapshot {
1343 UserRecordSnapshot {
1344 owner_pubkey: self.owner_pubkey,
1345 roster: self.roster.clone(),
1346 devices: self.devices.values().map(DeviceRecord::snapshot).collect(),
1347 }
1348 }
1349
1350 fn device_record_mut(
1351 &mut self,
1352 device_pubkey: DevicePubkey,
1353 created_at: UnixSeconds,
1354 ) -> &mut DeviceRecord {
1355 self.devices
1356 .entry(device_pubkey)
1357 .or_insert_with(|| DeviceRecord::new(device_pubkey, created_at))
1358 }
1359
1360 fn recompute_authorization(&mut self, authorized_devices: &BTreeSet<DevicePubkey>) {
1361 let owner_pubkey = self.owner_pubkey;
1362 let roster_created_at = self.roster.as_ref().map(|roster| roster.created_at);
1363 let roster_devices = self
1364 .roster
1365 .as_ref()
1366 .map(|roster| {
1367 roster
1368 .devices()
1369 .iter()
1370 .map(|device| (device.device_pubkey, device.created_at))
1371 .collect::<Vec<_>>()
1372 })
1373 .unwrap_or_default();
1374 for record in self.devices.values_mut() {
1375 let self_owned =
1376 crate::owner_pubkey_from_device_pubkey(record.device_pubkey) == self.owner_pubkey;
1377 record.authorized = self_owned || authorized_devices.contains(&record.device_pubkey);
1378 if record.authorized {
1379 record.is_stale = false;
1380 record.stale_since = None;
1381 } else if let Some(roster_created_at) = roster_created_at {
1382 record.is_stale = true;
1383 if record.stale_since.is_none() {
1384 record.stale_since = Some(roster_created_at);
1385 }
1386 } else {
1387 record.is_stale = false;
1388 record.stale_since = None;
1389 }
1390 }
1391
1392 for (device_pubkey, created_at) in roster_devices {
1393 let record = self.device_record_mut(device_pubkey, created_at);
1394 record.authorized = authorized_devices.contains(&device_pubkey)
1395 || crate::owner_pubkey_from_device_pubkey(device_pubkey) == owner_pubkey;
1396 if record.authorized {
1397 record.is_stale = false;
1398 record.stale_since = None;
1399 }
1400 record.created_at = merge_created_at(record.created_at, created_at);
1401 }
1402 }
1403
1404 fn authorized_non_stale_devices(&self) -> Vec<DevicePubkey> {
1405 self.devices
1406 .values()
1407 .filter(|record| record.authorized && !record.is_stale)
1408 .map(|record| record.device_pubkey)
1409 .collect()
1410 }
1411}
1412
1413impl DeviceRecord {
1414 fn new(device_pubkey: DevicePubkey, created_at: UnixSeconds) -> Self {
1415 Self {
1416 device_pubkey,
1417 authorized: false,
1418 is_stale: false,
1419 stale_since: None,
1420 claimed_owner_pubkey: None,
1421 public_invite: None,
1422 invite_response_generated: false,
1423 active_session: None,
1424 inactive_sessions: Vec::new(),
1425 last_activity: None,
1426 created_at,
1427 }
1428 }
1429
1430 fn from_snapshot(snapshot: DeviceRecordSnapshot) -> Self {
1431 Self {
1432 device_pubkey: snapshot.device_pubkey,
1433 authorized: snapshot.authorized,
1434 is_stale: snapshot.is_stale,
1435 stale_since: snapshot.stale_since,
1436 claimed_owner_pubkey: snapshot.claimed_owner_pubkey,
1437 public_invite: snapshot.public_invite,
1438 invite_response_generated: snapshot.invite_response_generated,
1439 active_session: snapshot.active_session.map(Session::from_state),
1440 inactive_sessions: snapshot
1441 .inactive_sessions
1442 .into_iter()
1443 .map(Session::from_state)
1444 .collect(),
1445 last_activity: snapshot.last_activity,
1446 created_at: snapshot.created_at,
1447 }
1448 }
1449
1450 fn snapshot(&self) -> DeviceRecordSnapshot {
1451 DeviceRecordSnapshot {
1452 device_pubkey: self.device_pubkey,
1453 authorized: self.authorized,
1454 is_stale: self.is_stale,
1455 stale_since: self.stale_since,
1456 claimed_owner_pubkey: self.claimed_owner_pubkey,
1457 public_invite: self.public_invite.clone(),
1458 invite_response_generated: self.invite_response_generated,
1459 active_session: self
1460 .active_session
1461 .as_ref()
1462 .map(|session| session.state.clone()),
1463 inactive_sessions: self
1464 .inactive_sessions
1465 .iter()
1466 .map(|session| session.state.clone())
1467 .collect(),
1468 last_activity: self.last_activity,
1469 created_at: self.created_at,
1470 }
1471 }
1472
1473 fn best_send_session_source(&self) -> Option<SendSessionSource> {
1474 let mut best: Option<(SendSessionSource, (u8, u32, u32))> = None;
1475
1476 if let Some(active_session) = self.active_session.as_ref() {
1477 if active_session.can_send() {
1478 best = Some((SendSessionSource::Active, session_priority(active_session)));
1479 }
1480 }
1481
1482 for (index, session) in self.inactive_sessions.iter().enumerate() {
1483 if !session.can_send() {
1484 continue;
1485 }
1486 let priority = session_priority(session);
1487 if best
1488 .as_ref()
1489 .is_none_or(|(_, current_priority)| priority > *current_priority)
1490 {
1491 best = Some((SendSessionSource::Inactive(index), priority));
1492 }
1493 }
1494
1495 best.map(|(source, _)| source)
1496 }
1497
1498 fn session_for_send_source(&self, source: &SendSessionSource) -> Option<&Session> {
1499 match source {
1500 SendSessionSource::Active => self.active_session.as_ref(),
1501 SendSessionSource::Inactive(index) => self.inactive_sessions.get(*index),
1502 }
1503 }
1504
1505 fn upsert_session(&mut self, session: Session, now: UnixSeconds) {
1506 if self.contains_state(&session.state) {
1507 self.compact_duplicate_sessions();
1508 self.last_activity = Some(now);
1509 return;
1510 }
1511
1512 let new_priority = session_priority(&session);
1513 let old_priority = self
1514 .active_session
1515 .as_ref()
1516 .map(session_priority)
1517 .unwrap_or((0, 0, 0));
1518
1519 if let Some(old_active) = self.active_session.take() {
1520 if old_priority >= new_priority {
1521 self.inactive_sessions.push(session);
1522 self.active_session = Some(old_active);
1523 } else {
1524 self.inactive_sessions.push(old_active);
1525 self.active_session = Some(session);
1526 }
1527 } else {
1528 self.active_session = Some(session);
1529 }
1530
1531 self.compact_duplicate_sessions();
1532 if self.inactive_sessions.len() > MAX_INACTIVE_SESSIONS {
1533 self.inactive_sessions.truncate(MAX_INACTIVE_SESSIONS);
1534 }
1535 self.last_activity = Some(now);
1536 }
1537
1538 fn absorb(&mut self, mut other: DeviceRecord, now: UnixSeconds) {
1539 self.authorized |= other.authorized;
1540 self.is_stale &= other.is_stale;
1541 self.stale_since = match (self.stale_since, other.stale_since) {
1542 (Some(existing), Some(incoming)) => Some(existing.min(incoming)),
1543 (None, incoming) => incoming,
1544 (existing, None) => existing,
1545 };
1546 self.claimed_owner_pubkey = self
1547 .claimed_owner_pubkey
1548 .or(other.claimed_owner_pubkey.take());
1549 self.created_at = merge_created_at(self.created_at, other.created_at);
1550
1551 if let Some(public_invite) = other.public_invite.take() {
1552 let should_replace_invite = self
1553 .public_invite
1554 .as_ref()
1555 .is_none_or(|existing| public_invite.created_at >= existing.created_at);
1556 if should_replace_invite {
1557 self.public_invite = Some(public_invite);
1558 }
1559 }
1560 self.invite_response_generated |= other.invite_response_generated;
1561
1562 if let Some(session) = other.active_session.take() {
1563 self.upsert_session(session, now);
1564 }
1565
1566 for session in other.inactive_sessions.drain(..) {
1567 self.upsert_session(session, now);
1568 }
1569
1570 self.last_activity = match (self.last_activity, other.last_activity) {
1571 (Some(existing), Some(incoming)) => Some(existing.max(incoming)),
1572 (None, incoming) => incoming,
1573 (existing, None) => existing,
1574 };
1575 }
1576
1577 fn promote_inactive_session(&mut self, session: Session) {
1578 let new_priority = session_priority(&session);
1579 if let Some(old_active) = self.active_session.take() {
1580 let old_priority = session_priority(&old_active);
1581 if new_priority > old_priority {
1582 if old_active.state != session.state {
1583 self.inactive_sessions.push(old_active);
1584 }
1585 self.active_session = Some(session);
1586 } else {
1587 self.inactive_sessions.push(session);
1588 self.active_session = Some(old_active);
1589 }
1590 } else {
1591 self.active_session = Some(session);
1592 }
1593 self.compact_duplicate_sessions();
1594 if self.inactive_sessions.len() > MAX_INACTIVE_SESSIONS {
1595 self.inactive_sessions.truncate(MAX_INACTIVE_SESSIONS);
1596 }
1597 }
1598
1599 fn contains_state(&self, state: &SessionState) -> bool {
1600 self.active_session
1601 .as_ref()
1602 .is_some_and(|session| session.state == *state)
1603 || self
1604 .inactive_sessions
1605 .iter()
1606 .any(|session| session.state == *state)
1607 }
1608
1609 fn compact_duplicate_sessions(&mut self) {
1610 let active_state = self
1611 .active_session
1612 .as_ref()
1613 .map(|session| session.state.clone());
1614 let mut unique_states = Vec::new();
1615 let mut inactive_sessions = Vec::with_capacity(self.inactive_sessions.len());
1616
1617 for session in self.inactive_sessions.drain(..) {
1618 let is_duplicate = active_state
1619 .as_ref()
1620 .is_some_and(|state| *state == session.state)
1621 || unique_states.contains(&session.state);
1622 if is_duplicate {
1623 continue;
1624 }
1625 unique_states.push(session.state.clone());
1626 inactive_sessions.push(session);
1627 }
1628
1629 self.inactive_sessions = inactive_sessions;
1630 }
1631}
1632
1633fn apply_roster_snapshot(
1634 current_roster: Option<&DeviceRoster>,
1635 incoming_roster: &DeviceRoster,
1636) -> (RosterSnapshotDecision, DeviceRoster) {
1637 let Some(current_roster) = current_roster else {
1638 return (RosterSnapshotDecision::Advanced, incoming_roster.clone());
1639 };
1640
1641 if incoming_roster.created_at > current_roster.created_at {
1642 return (RosterSnapshotDecision::Advanced, incoming_roster.clone());
1643 }
1644
1645 if incoming_roster.created_at < current_roster.created_at {
1646 return (RosterSnapshotDecision::Stale, current_roster.clone());
1647 }
1648
1649 (
1650 RosterSnapshotDecision::MergedEqualTimestamp,
1651 current_roster.merge(incoming_roster),
1652 )
1653}
1654
1655fn session_priority(session: &Session) -> (u8, u32, u32) {
1656 let can_send = session.can_send();
1657 let can_receive = session.state.receiving_chain_key.is_some()
1658 || session.state.their_current_nostr_public_key.is_some()
1659 || session.state.receiving_chain_message_number > 0;
1660
1661 let directionality = match (can_send, can_receive) {
1662 (true, true) => 3,
1663 (true, false) => 2,
1664 (false, true) => 1,
1665 (false, false) => 0,
1666 };
1667
1668 (
1669 directionality,
1670 session.state.receiving_chain_message_number,
1671 session.state.sending_chain_message_number,
1672 )
1673}
1674
1675fn is_one_way_bootstrap_session(session: &Session) -> bool {
1676 session.state.receiving_chain_key.is_none()
1677 && session.state.their_current_nostr_public_key.is_none()
1678}
1679
1680fn merge_created_at(current: UnixSeconds, observed: UnixSeconds) -> UnixSeconds {
1681 match (current.get(), observed.get()) {
1682 (0, _) => observed,
1683 (_, 0) => current,
1684 _ => current.min(observed),
1685 }
1686}