lean_ctx_activity/
identity.rs1pub const SESSION_ENV: &str = "LEAN_CTX_SESSION_ID";
10pub const SESSION_HEADER: &str = "x-leanctx-session-id";
12pub const MAX_SESSION_ID_BYTES: usize = 128;
14
15pub fn session_id(raw: &str) -> Option<String> {
18 let id = raw.trim();
19 let valid = !id.is_empty()
20 && id.len() <= MAX_SESSION_ID_BYTES
21 && id
22 .bytes()
23 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'));
24 valid.then(|| id.to_owned())
25}
26
27pub const SESSION_NAME_ENV: &str = "LEAN_CTX_SESSION_NAME";
31pub const MAX_SESSION_NAME_CHARS: usize = 128;
33
34pub fn session_name(raw: &str) -> Option<String> {
37 let name = raw.trim();
38 let valid = !name.is_empty()
39 && name.chars().count() <= MAX_SESSION_NAME_CHARS
40 && !name.chars().any(char::is_control);
41 valid.then(|| name.to_owned())
42}
43
44pub fn session_name_from_env() -> Option<String> {
46 std::env::var(SESSION_NAME_ENV)
47 .ok()
48 .as_deref()
49 .and_then(session_name)
50}
51
52pub fn session_id_from_env() -> Option<String> {
54 std::env::var(SESSION_ENV)
55 .ok()
56 .as_deref()
57 .and_then(session_id)
58}
59
60#[cfg(test)]
61mod tests {
62 use super::{session_id, session_name};
63
64 #[test]
65 fn only_bounded_plain_ids_are_accepted() {
66 assert_eq!(
67 session_id(" app-7f3a:1.x_y ").as_deref(),
68 Some("app-7f3a:1.x_y")
69 );
70 for bad in ["", " ", "../etc", "a b", "a/b", "ü", &"a".repeat(129)] {
71 assert_eq!(session_id(bad), None, "{bad:?}");
72 }
73 assert!(session_id(&"a".repeat(128)).is_some());
74 }
75
76 #[test]
77 fn session_names_are_bounded_single_lines() {
78 assert_eq!(
79 session_name(" LeanCTX · Contracts ").as_deref(),
80 Some("LeanCTX · Contracts")
81 );
82 for bad in ["", " ", "a\nb", "a\u{1b}[31m", &"ü".repeat(129)] {
83 assert_eq!(session_name(bad), None, "{bad:?}");
84 }
85 assert!(session_name(&"ü".repeat(128)).is_some());
86 }
87}