Skip to main content

nostr_double_ratchet/
session_manager.rs

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            // Invalid or corrupt persisted evidence fails closed. A stored event
168            // was already accepted at observation time, so restore validates it
169            // against its own timestamp rather than the current wall clock.
170            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        // Cached roster projections and authorization flags are not owner proof.
190        // Rebuild authorization from exact signed AppKeys evidence first, then
191        // promote only provisional claims covered by that evidence.
192        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    /// Observe a peer roster as an operational projection only.
261    ///
262    /// This method never grants a distinct device (`O != D`) authority to act
263    /// for the owner. Feed the exact signed event to
264    /// [`SessionManager::observe_peer_app_keys_event`] to establish that proof.
265    /// Observations naming the local owner are ignored; local roster changes
266    /// must use [`SessionManager::apply_local_roster`] or
267    /// [`SessionManager::replace_local_roster`].
268    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    /// Observe exact AppKeys evidence signed by a peer owner.
280    ///
281    /// A plain `DeviceRoster` is only an operational projection. Distinct
282    /// owner/device authorization (`O != D`) is granted only through this path.
283    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    /// Prepare a send to the recipient owner's authorized devices without also
375    /// preparing local sibling sender-copy deliveries.
376    ///
377    /// `prepare_send` is the higher-level app default. This lower-level variant
378    /// is useful for runtimes that need a different payload for local sibling
379    /// sync than for peer delivery.
380    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    /// Import a session whose peer device is authenticated by the ratchet but
701    /// whose claimed owner still requires roster verification.
702    ///
703    /// If the owner/device binding is not verified, the session is retained
704    /// under the device's own identity and the claimed owner remains
705    /// provisional. A later roster containing the device promotes the session
706    /// to the claimed owner.
707    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}