Skip to main content

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//! The value is opaque: a bounded, printable, whitespace-free string, so a
10//! harness's own id (an `x-session-id` such as `ses_…` from `OpenCode`) is
11//! recorded as is.
12//!
13//! Distinct from [`crate::SessionId`]: that is the gateway's own attested
14//! `sess_` session, minted once per credential and shared by every Claude Code
15//! run that credential drives.
16//!
17//! Copyright (c) systemprompt.io — Business Source License 1.1.
18//! See <https://systemprompt.io> for licensing details.
19
20use crate::error::IdValidationError;
21
22const SESSION_SEGMENT: &str = "_session_";
23
24const MAX_LEN: usize = 128;
25
26fn validate(value: &str) -> Result<(), IdValidationError> {
27    if value.is_empty() || value.len() > MAX_LEN {
28        return Err(IdValidationError::invalid(
29            "ClientSessionId",
30            "must be 1 to 128 characters",
31        ));
32    }
33    if !value.bytes().all(|b| b.is_ascii_graphic()) {
34        return Err(IdValidationError::invalid(
35            "ClientSessionId",
36            "must be printable ASCII with no whitespace",
37        ));
38    }
39    Ok(())
40}
41
42crate::define_id!(ClientSessionId, validated, schema, validate);
43
44#[derive(serde::Deserialize)]
45struct MetadataUserId {
46    #[serde(rename = "session_id")]
47    raw_session: String,
48}
49
50impl ClientSessionId {
51    pub fn from_metadata_user_id(value: &str) -> Result<Option<Self>, IdValidationError> {
52        let value = value.trim();
53        let session = if value.starts_with('{') {
54            let metadata: MetadataUserId = serde_json::from_str(value)
55                .map_err(|e| IdValidationError::json("ClientSessionId", e))?;
56            Some(metadata.raw_session)
57        } else {
58            value
59                .rsplit_once(SESSION_SEGMENT)
60                .map(|(_, suffix)| suffix.to_owned())
61        };
62        session
63            .map(|value| {
64                let parsed = uuid::Uuid::parse_str(value.trim())
65                    .map_err(|e| IdValidationError::uuid("ClientSessionId", e))?;
66                Self::try_new(parsed.hyphenated().to_string())
67            })
68            .transpose()
69    }
70}