pub const SESSION_ENV: &str = "LEAN_CTX_SESSION_ID";
pub const SESSION_HEADER: &str = "x-leanctx-session-id";
pub const MAX_SESSION_ID_BYTES: usize = 128;
pub fn session_id(raw: &str) -> Option<String> {
let id = raw.trim();
let valid = !id.is_empty()
&& id.len() <= MAX_SESSION_ID_BYTES
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'));
valid.then(|| id.to_owned())
}
pub const SESSION_NAME_ENV: &str = "LEAN_CTX_SESSION_NAME";
pub const MAX_SESSION_NAME_CHARS: usize = 128;
pub fn session_name(raw: &str) -> Option<String> {
let name = raw.trim();
let valid = !name.is_empty()
&& name.chars().count() <= MAX_SESSION_NAME_CHARS
&& !name.chars().any(char::is_control);
valid.then(|| name.to_owned())
}
pub fn session_name_from_env() -> Option<String> {
std::env::var(SESSION_NAME_ENV)
.ok()
.as_deref()
.and_then(session_name)
}
pub fn session_id_from_env() -> Option<String> {
std::env::var(SESSION_ENV)
.ok()
.as_deref()
.and_then(session_id)
}
#[cfg(test)]
mod tests {
use super::{session_id, session_name};
#[test]
fn only_bounded_plain_ids_are_accepted() {
assert_eq!(
session_id(" app-7f3a:1.x_y ").as_deref(),
Some("app-7f3a:1.x_y")
);
for bad in ["", " ", "../etc", "a b", "a/b", "ü", &"a".repeat(129)] {
assert_eq!(session_id(bad), None, "{bad:?}");
}
assert!(session_id(&"a".repeat(128)).is_some());
}
#[test]
fn session_names_are_bounded_single_lines() {
assert_eq!(
session_name(" LeanCTX · Contracts ").as_deref(),
Some("LeanCTX · Contracts")
);
for bad in ["", " ", "a\nb", "a\u{1b}[31m", &"ü".repeat(129)] {
assert_eq!(session_name(bad), None, "{bad:?}");
}
assert!(session_name(&"ü".repeat(128)).is_some());
}
}