Skip to main content

chio_http_core/
session.rs

1//! Session context for HTTP request evaluation.
2
3use serde::{Deserialize, Serialize};
4
5use crate::identity::CallerIdentity;
6
7/// Per-session context carried through the Chio HTTP pipeline.
8/// A session groups related requests from the same caller over a
9/// bounded time window.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct SessionContext {
12    /// Unique session identifier.
13    pub session_id: String,
14
15    /// The authenticated caller for this session.
16    pub caller: CallerIdentity,
17
18    /// Unix timestamp (seconds) when the session was created.
19    pub created_at: u64,
20
21    /// Unix timestamp (seconds) when the session expires.
22    /// Guards may deny requests after this time.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub expires_at: Option<u64>,
25
26    /// Number of requests evaluated in this session so far.
27    #[serde(default)]
28    pub request_count: u64,
29
30    /// Cumulative bytes read by this session (for data-flow guards).
31    #[serde(default)]
32    pub bytes_read: u64,
33
34    /// Cumulative bytes written by this session (for data-flow guards).
35    #[serde(default)]
36    pub bytes_written: u64,
37
38    /// Current delegation depth (0 = direct caller).
39    #[serde(default)]
40    pub delegation_depth: u32,
41
42    /// Optional metadata for extensibility.
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub metadata: Option<serde_json::Value>,
45}
46
47impl SessionContext {
48    /// Create a new session with the given ID and caller.
49    #[must_use]
50    pub fn new(session_id: String, caller: CallerIdentity) -> Self {
51        let now = chrono::Utc::now().timestamp() as u64;
52        Self {
53            session_id,
54            caller,
55            created_at: now,
56            expires_at: None,
57            request_count: 0,
58            bytes_read: 0,
59            bytes_written: 0,
60            delegation_depth: 0,
61            metadata: None,
62        }
63    }
64
65    /// Whether this session has expired.
66    #[must_use]
67    pub fn is_expired(&self) -> bool {
68        if let Some(exp) = self.expires_at {
69            let now = chrono::Utc::now().timestamp() as u64;
70            now >= exp
71        } else {
72            false
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::identity::CallerIdentity;
81
82    #[test]
83    fn new_session_defaults() {
84        let session = SessionContext::new("sess-001".to_string(), CallerIdentity::anonymous());
85        assert_eq!(session.session_id, "sess-001");
86        assert_eq!(session.request_count, 0);
87        assert_eq!(session.bytes_read, 0);
88        assert_eq!(session.delegation_depth, 0);
89        assert!(session.expires_at.is_none());
90    }
91
92    #[test]
93    fn expired_session() {
94        let mut session = SessionContext::new("sess-002".to_string(), CallerIdentity::anonymous());
95        session.expires_at = Some(0); // epoch = long expired
96        assert!(session.is_expired());
97    }
98
99    #[test]
100    fn not_expired_when_no_expiry() {
101        let session = SessionContext::new("sess-003".to_string(), CallerIdentity::anonymous());
102        assert!(!session.is_expired());
103    }
104
105    #[test]
106    fn serde_roundtrip() {
107        let session = SessionContext::new("sess-004".to_string(), CallerIdentity::anonymous());
108        let json = serde_json::to_string(&session).unwrap();
109        let back: SessionContext = serde_json::from_str(&json).unwrap();
110        assert_eq!(back.session_id, "sess-004");
111    }
112}