systemprompt_identifiers/client_session.rs
1//! The caller's own session identifier, carried inside `metadata.user_id`.
2//!
3//! Claude Code stamps every `/v1/messages` call with
4//! `metadata.user_id = "user_<sha256>_account_<uuid>_session_<uuid>"`, and the
5//! trailing UUID is the session id it also reports through its hook events.
6//! Parsing it lets the gateway land a request on the same context the hooks
7//! pipeline writes, without the caller having to send a dedicated header.
8//!
9//! Distinct from [`crate::SessionId`]: that is the gateway's own attested
10//! `sess_` session, minted once per credential and shared by every Claude Code
11//! run that credential drives.
12//!
13//! Copyright (c) systemprompt.io — Business Source License 1.1.
14//! See <https://systemprompt.io> for licensing details.
15
16use crate::error::IdValidationError;
17
18const SESSION_SEGMENT: &str = "_session_";
19
20fn validate(value: &str) -> Result<(), IdValidationError> {
21 let parsed = uuid::Uuid::parse_str(value)
22 .map_err(|e| IdValidationError::invalid("ClientSessionId", e.to_string()))?;
23 if parsed.hyphenated().to_string() != value {
24 return Err(IdValidationError::invalid(
25 "ClientSessionId",
26 "must be a lowercase hyphenated UUID",
27 ));
28 }
29 Ok(())
30}
31
32crate::define_id!(ClientSessionId, validated, schema, validate);
33
34impl ClientSessionId {
35 // Why: the suffix after the last `_session_` is the only part with a
36 // stable shape; the prefix segments vary by client and account.
37 #[must_use]
38 pub fn from_metadata_user_id(user_id: &str) -> Option<Self> {
39 let (_, suffix) = user_id.rsplit_once(SESSION_SEGMENT)?;
40 let parsed = uuid::Uuid::parse_str(suffix.trim()).ok()?;
41 Some(Self::new_unchecked(parsed.hyphenated().to_string()))
42 }
43}