Skip to main content

bsv/auth/
session_manager.rs

1//! Session management for the BRC-31 authentication protocol.
2//!
3//! SessionManager tracks authenticated sessions by both session nonce
4//! (primary key) and peer identity key (secondary index), supporting
5//! multiple concurrent sessions per identity key.
6//!
7//! Translated from TS SDK SessionManager.ts and Go SDK session_manager.go.
8
9use std::collections::{HashMap, HashSet};
10
11use super::types::PeerSession;
12
13/// Manages authenticated peer sessions with dual-index tracking.
14///
15/// Sessions are indexed by:
16/// - Session nonce (primary key, unique per session)
17/// - Identity key (secondary index, one-to-many)
18///
19/// This allows lookup by either nonce or identity key, with the identity
20/// key lookup returning the "best" session (preferring authenticated ones).
21pub struct SessionManager {
22    /// Maps session_nonce -> PeerSession (primary index).
23    nonce_to_session: HashMap<String, PeerSession>,
24    /// Maps identity_key -> set of session nonces (secondary index).
25    identity_to_nonces: HashMap<String, HashSet<String>>,
26}
27
28impl SessionManager {
29    /// Create a new empty SessionManager.
30    pub fn new() -> Self {
31        SessionManager {
32            nonce_to_session: HashMap::new(),
33            identity_to_nonces: HashMap::new(),
34        }
35    }
36
37    /// Add a session to the manager.
38    ///
39    /// Indexes by session_nonce (primary) and peer_identity_key (secondary).
40    /// Does NOT overwrite existing sessions for the same identity key,
41    /// allowing multiple concurrent sessions per peer.
42    pub fn add_session(&mut self, session: PeerSession) {
43        let nonce = session.session_nonce.clone();
44        let identity = session.peer_identity_key.clone();
45
46        self.nonce_to_session.insert(nonce.clone(), session);
47
48        self.identity_to_nonces
49            .entry(identity)
50            .or_default()
51            .insert(nonce);
52    }
53
54    /// Get a session by nonce (immutable reference).
55    pub fn get_session(&self, nonce: &str) -> Option<&PeerSession> {
56        self.nonce_to_session.get(nonce)
57    }
58
59    /// Get a session by nonce (mutable reference).
60    pub fn get_session_mut(&mut self, nonce: &str) -> Option<&mut PeerSession> {
61        self.nonce_to_session.get_mut(nonce)
62    }
63
64    /// Get all sessions for a given identity key.
65    pub fn get_sessions_for_identity(&self, identity_key: &str) -> Vec<&PeerSession> {
66        match self.identity_to_nonces.get(identity_key) {
67            Some(nonces) => nonces
68                .iter()
69                .filter_map(|n| self.nonce_to_session.get(n))
70                .collect(),
71            None => Vec::new(),
72        }
73    }
74
75    /// Get the "best" session for an identity key (prefers authenticated).
76    ///
77    /// Matches TS SDK SessionManager.getSession() behavior: if the identifier
78    /// is a session nonce, returns that exact session. If it is an identity key,
79    /// returns the best (authenticated preferred) session.
80    pub fn get_session_by_identifier(&self, identifier: &str) -> Option<&PeerSession> {
81        // Try as direct nonce first
82        if let Some(session) = self.nonce_to_session.get(identifier) {
83            return Some(session);
84        }
85
86        // Try as identity key
87        let nonces = self.identity_to_nonces.get(identifier)?;
88        let mut best: Option<&PeerSession> = None;
89        for nonce in nonces {
90            if let Some(session) = self.nonce_to_session.get(nonce) {
91                match best {
92                    None => best = Some(session),
93                    Some(b) => {
94                        // Prefer authenticated sessions
95                        if session.is_authenticated && !b.is_authenticated {
96                            best = Some(session);
97                        }
98                    }
99                }
100            }
101        }
102        best
103    }
104
105    /// Check if a session exists for a given nonce.
106    pub fn has_session(&self, nonce: &str) -> bool {
107        self.nonce_to_session.contains_key(nonce)
108    }
109
110    /// Check if any session exists for a given identifier (nonce or identity key).
111    pub fn has_session_by_identifier(&self, identifier: &str) -> bool {
112        if self.nonce_to_session.contains_key(identifier) {
113            return true;
114        }
115        match self.identity_to_nonces.get(identifier) {
116            Some(nonces) => !nonces.is_empty(),
117            None => false,
118        }
119    }
120
121    /// Replace a session at the given nonce.
122    pub fn update_session(&mut self, nonce: &str, session: PeerSession) {
123        // Remove old identity mapping if the identity key changed
124        if let Some(old_session) = self.nonce_to_session.get(nonce) {
125            let old_identity = old_session.peer_identity_key.clone();
126            if old_identity != session.peer_identity_key {
127                if let Some(nonces) = self.identity_to_nonces.get_mut(&old_identity) {
128                    nonces.remove(nonce);
129                    if nonces.is_empty() {
130                        self.identity_to_nonces.remove(&old_identity);
131                    }
132                }
133            }
134        }
135
136        let new_identity = session.peer_identity_key.clone();
137        self.nonce_to_session.insert(nonce.to_string(), session);
138        self.identity_to_nonces
139            .entry(new_identity)
140            .or_default()
141            .insert(nonce.to_string());
142    }
143
144    /// Remove a session by nonce. Returns the removed session if found.
145    pub fn remove_session(&mut self, nonce: &str) -> Option<PeerSession> {
146        if let Some(session) = self.nonce_to_session.remove(nonce) {
147            // Clean up identity index
148            if let Some(nonces) = self.identity_to_nonces.get_mut(&session.peer_identity_key) {
149                nonces.remove(nonce);
150                if nonces.is_empty() {
151                    self.identity_to_nonces.remove(&session.peer_identity_key);
152                }
153            }
154            Some(session)
155        } else {
156            None
157        }
158    }
159}
160
161impl Default for SessionManager {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    fn make_session(nonce: &str, identity: &str, authenticated: bool) -> PeerSession {
172        PeerSession {
173            session_nonce: nonce.to_string(),
174            peer_identity_key: identity.to_string(),
175            peer_nonce: format!("peer_{}", nonce),
176            is_authenticated: authenticated,
177        }
178    }
179
180    #[test]
181    fn test_add_and_get_session() {
182        let mut mgr = SessionManager::new();
183        let session = make_session("nonce1", "id_key_A", true);
184        mgr.add_session(session.clone());
185
186        let retrieved = mgr.get_session("nonce1").unwrap();
187        assert_eq!(retrieved.session_nonce, "nonce1");
188        assert_eq!(retrieved.peer_identity_key, "id_key_A");
189        assert!(retrieved.is_authenticated);
190    }
191
192    #[test]
193    fn test_has_session() {
194        let mut mgr = SessionManager::new();
195        assert!(!mgr.has_session("nonce1"));
196
197        mgr.add_session(make_session("nonce1", "id_key_A", true));
198        assert!(mgr.has_session("nonce1"));
199        assert!(!mgr.has_session("nonce2"));
200    }
201
202    #[test]
203    fn test_remove_session() {
204        let mut mgr = SessionManager::new();
205        mgr.add_session(make_session("nonce1", "id_key_A", true));
206
207        let removed = mgr.remove_session("nonce1").unwrap();
208        assert_eq!(removed.session_nonce, "nonce1");
209        assert!(!mgr.has_session("nonce1"));
210
211        // Identity index should also be cleaned up
212        let sessions = mgr.get_sessions_for_identity("id_key_A");
213        assert!(sessions.is_empty());
214    }
215
216    #[test]
217    fn test_get_sessions_for_identity() {
218        let mut mgr = SessionManager::new();
219        mgr.add_session(make_session("nonce1", "id_key_A", true));
220        mgr.add_session(make_session("nonce2", "id_key_A", false));
221        mgr.add_session(make_session("nonce3", "id_key_B", true));
222
223        let a_sessions = mgr.get_sessions_for_identity("id_key_A");
224        assert_eq!(a_sessions.len(), 2);
225
226        let b_sessions = mgr.get_sessions_for_identity("id_key_B");
227        assert_eq!(b_sessions.len(), 1);
228
229        let c_sessions = mgr.get_sessions_for_identity("id_key_C");
230        assert!(c_sessions.is_empty());
231    }
232
233    #[test]
234    fn test_get_session_by_identifier() {
235        let mut mgr = SessionManager::new();
236        mgr.add_session(make_session("nonce1", "id_key_A", false));
237        mgr.add_session(make_session("nonce2", "id_key_A", true));
238
239        // Direct nonce lookup
240        let s = mgr.get_session_by_identifier("nonce1").unwrap();
241        assert_eq!(s.session_nonce, "nonce1");
242
243        // Identity key lookup should prefer authenticated session
244        let best = mgr.get_session_by_identifier("id_key_A").unwrap();
245        assert!(best.is_authenticated);
246    }
247
248    #[test]
249    fn test_has_session_by_identifier() {
250        let mut mgr = SessionManager::new();
251        mgr.add_session(make_session("nonce1", "id_key_A", true));
252
253        assert!(mgr.has_session_by_identifier("nonce1"));
254        assert!(mgr.has_session_by_identifier("id_key_A"));
255        assert!(!mgr.has_session_by_identifier("unknown"));
256    }
257
258    #[test]
259    fn test_update_session() {
260        let mut mgr = SessionManager::new();
261        mgr.add_session(make_session("nonce1", "id_key_A", false));
262
263        // Update to authenticated
264        let updated = make_session("nonce1", "id_key_A", true);
265        mgr.update_session("nonce1", updated);
266
267        let s = mgr.get_session("nonce1").unwrap();
268        assert!(s.is_authenticated);
269    }
270
271    #[test]
272    fn test_get_session_mut() {
273        let mut mgr = SessionManager::new();
274        mgr.add_session(make_session("nonce1", "id_key_A", false));
275
276        let s = mgr.get_session_mut("nonce1").unwrap();
277        s.is_authenticated = true;
278
279        let s2 = mgr.get_session("nonce1").unwrap();
280        assert!(s2.is_authenticated);
281    }
282
283    #[test]
284    fn test_remove_nonexistent() {
285        let mut mgr = SessionManager::new();
286        assert!(mgr.remove_session("nonexistent").is_none());
287    }
288
289    #[test]
290    fn test_identity_cleanup_on_remove_last_session() {
291        let mut mgr = SessionManager::new();
292        mgr.add_session(make_session("nonce1", "id_key_A", true));
293        mgr.add_session(make_session("nonce2", "id_key_A", true));
294
295        mgr.remove_session("nonce1");
296        // Still has one session for identity
297        assert!(mgr.has_session_by_identifier("id_key_A"));
298
299        mgr.remove_session("nonce2");
300        // Now identity should be cleaned up
301        assert!(!mgr.has_session_by_identifier("id_key_A"));
302    }
303}