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//! 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 =
22        uuid::Uuid::parse_str(value).map_err(|e| IdValidationError::uuid("ClientSessionId", e))?;
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
34#[derive(serde::Deserialize)]
35struct MetadataUserId {
36    #[serde(rename = "session_id")]
37    raw_session: String,
38}
39
40impl ClientSessionId {
41    pub fn from_metadata_user_id(value: &str) -> Result<Option<Self>, IdValidationError> {
42        let value = value.trim();
43        let session = if value.starts_with('{') {
44            let metadata: MetadataUserId = serde_json::from_str(value)
45                .map_err(|e| IdValidationError::json("ClientSessionId", e))?;
46            Some(metadata.raw_session)
47        } else {
48            value
49                .rsplit_once(SESSION_SEGMENT)
50                .map(|(_, suffix)| suffix.to_owned())
51        };
52        session
53            .map(|value| {
54                let parsed = uuid::Uuid::parse_str(value.trim())
55                    .map_err(|e| IdValidationError::uuid("ClientSessionId", e))?;
56                Self::try_new(parsed.hyphenated().to_string())
57            })
58            .transpose()
59    }
60}