OXYGENHUB

Coverage / crates/spectra-mcp/src/session.rs 95.6%

43 of 45 lines run, 12 of 17 functions · run #31 on main · run missed

1
//! The owner's browser sign-in, shared by Radar and the API (spec §8.3): the owner's CLI signs a request for a
2
//! one-time link (`POST /radar/link`, valid 60 s); opening it starts a
3
//! browser session whose events carry that signed request as their proof
4
//! (level 2). The passkey (level 3) is 0.2. Sessions live in memory: a
5
//! restart signs the browser out.
6
7
use rand::RngCore;
8
use std::collections::HashMap;
9
use std::sync::Mutex;
10
11
/// How long a sign-in link is good for, and a session.
12
pub const LINK_MS: u64 = 60_000;
13
pub const SESSION_MS: u64 = 12 * 3_600_000;
14
15
/// What a session holds: the owner's key and the proof that opened it.
16
#[derive(Clone, Debug)]
17
pub struct Session {
18
    pub key: [u8; 32],
19
    pub proof: Vec<u8>,
20
    pub csrf: String,
21
    pub expires_ms: u64,
22
}
23
24
#[derive(Debug, Default)]
25
pub struct Sessions {
26
    codes: Mutex<HashMap<String, (Session, u64)>>,
27
    live: Mutex<HashMap<String, Session>>,
28
}
29
3011×
fn random_hex() -> String {
3111×
    let mut b = [0u8; 32];
3211×
    rand::rngs::OsRng.fill_bytes(&mut b);
3311×
    hex::encode(b)
3411×
}
35
36
impl Sessions {
37
    /// A one-time code for the owner, from their signed request.
384×
    pub fn link(&self, key: [u8; 32], proof: Vec<u8>, now: u64) -> String {
394×
        let code = random_hex();
404×
        let s = Session { key, proof, csrf: random_hex(), expires_ms: now + SESSION_MS };
414×
        let mut codes = self.codes.lock().unwrap();
424×
        codes.retain(|_, (_, until)| *until > now);
434×
        codes.insert(code.clone(), (s, now + LINK_MS));
444×
        code
454×
    }
46
47
    /// Spend a code: the session id, once, within a minute of the link.
486×
    pub fn open(&self, code: &str, now: u64) -> Option<String> {
496×
        let (s, until) = self.codes.lock().unwrap().remove(code)?;
504×
        if until < now {
511×
            return None;
523×
        }
533×
        let id = random_hex();
543×
        let mut live = self.live.lock().unwrap();
553×
        live.retain(|_, s| s.expires_ms > now);
563×
        live.insert(id.clone(), s);
573×
        Some(id)
586×
    }
59
6024×
    pub fn get(&self, id: &str, now: u64) -> Option<Session> {
6124×
        self.live.lock().unwrap().get(id).filter(|s| s.expires_ms > now).cloned()
6224×
    }
63
641×
    pub fn close(&self, id: &str) {
651×
        self.live.lock().unwrap().remove(id);
661×
    }
67
}
68
69
#[cfg(test)]
70
mod tests {
71
    use super::*;
72
73
    #[test]
741×
    fn a_link_opens_one_session_once_and_only_while_fresh() {
751×
        let s = Sessions::default();
761×
        let code = s.link([1; 32], vec![9], 1_000);
771×
        let id = s.open(&code, 2_000).expect("fresh");
781×
        assert!(s.open(&code, 2_000).is_none(), "spent");
791×
        assert_eq!(s.get(&id, 3_000).unwrap().key, [1; 32]);
801×
        assert!(s.get(&id, 1_000 + SESSION_MS + 1).is_none(), "expired");
811×
        let stale = s.link([1; 32], vec![9], 1_000);
821×
        assert!(s.open(&stale, 1_000 + LINK_MS + 1).is_none());
831×
        s.close(&id);
841×
        assert!(s.get(&id, 3_000).is_none());
851×
    }
86
}