use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use crate::constants::{
AGENT_ID_PARAM, AGENTCAT_TAG_AGENT_ID, AGENTCAT_TAG_AGENT_SOURCE,
AGENTCAT_TAG_PROTOCOL_VERSION, AGENTCAT_TAG_SESSION_SOURCE, MINT_BACK_HEADER_ISSUED,
MINT_BACK_HEADER_UNRECOGNIZED, MINT_BACK_ISSUED_BODY, MINT_BACK_UNRECOGNIZED_BODY,
SESSION_ID_PARAM, SESSION_START_SENTINEL, mint_back_session_line,
};
use crate::wire;
pub(crate) mod ksuid;
#[cfg(test)]
mod tests;
const SESSION_PREFIX: &str = "ses";
#[cfg_attr(
not(test),
allow(
dead_code,
reason = "read by the delivery pipeline; rustc 1.88 counts a use inside another dead item as a use"
),
expect(
clippy::allow_attributes,
reason = "fires on newer rustc only, where dead code is found transitively"
)
)]
const EVENT_PREFIX: &str = "evt";
const SESSION_BODY_LEN: usize = 27;
const DERIVE_EPOCH_MS: u64 = 1_704_067_200_000;
const _: () = assert!(
DERIVE_EPOCH_MS >= ksuid::EPOCH_MS && DERIVE_EPOCH_MS + 0xFFFF_FFFF <= ksuid::MAX_TIME_MS
);
const AGENT_ID_TAG_MAX_UNITS: usize = 200;
const AGENT_SOURCE_SUPPLIED: &str = "supplied";
pub(crate) fn new_session_id() -> Option<String> {
if minting_fails() {
return None;
}
ksuid::random(SESSION_PREFIX)
}
#[cfg(test)]
thread_local! {
static MINTING_FAILS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
fn minting_fails() -> bool {
MINTING_FAILS.get()
}
#[cfg(not(test))]
const fn minting_fails() -> bool {
false
}
#[cfg(test)]
#[derive(Debug)]
pub(crate) struct SessionIdFailure(());
#[cfg(test)]
impl SessionIdFailure {
pub(crate) fn start() -> Self {
MINTING_FAILS.set(true);
Self(())
}
}
#[cfg(test)]
impl Drop for SessionIdFailure {
fn drop(&mut self) {
MINTING_FAILS.set(false);
}
}
pub(crate) fn new_event_id() -> Option<String> {
ksuid::random(EVENT_PREFIX)
}
#[cfg_attr(
not(test),
allow(
dead_code,
reason = "read by the delivery pipeline; rustc 1.88 counts a use inside another dead item as a use"
),
expect(
clippy::allow_attributes,
reason = "fires on newer rustc only, where dead code is found transitively"
)
)]
pub(crate) fn derive_session_id(id: &str, project_id: Option<&str>) -> String {
let hash = match project_id.filter(|project| !project.is_empty()) {
Some(project) => Sha256::digest(format!("{id}:{project}")),
None => Sha256::digest(id),
};
let (time, rest) = hash.split_at(4);
let mut offset = [0; 4];
offset.copy_from_slice(time);
let mut payload = [0; 16];
payload.copy_from_slice(&rest[..16]);
let time_ms = DERIVE_EPOCH_MS + u64::from(u32::from_be_bytes(offset));
ksuid::encode(
SESSION_PREFIX,
(time_ms - ksuid::EPOCH_MS) / 1_000,
&payload,
)
}
pub(crate) fn is_valid_session_id(value: &str) -> bool {
value.strip_prefix("ses_").is_some_and(|body| {
body.len() == SESSION_BODY_LEN && body.bytes().all(|b| b.is_ascii_alphanumeric())
})
}
pub(crate) fn extract_handle(args: Option<&Map<String, Value>>, name: &str) -> Option<String> {
let trimmed = wire::trim(args?.get(name)?.as_str()?);
(!trimmed.is_empty()).then(|| trimmed.to_owned())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SessionSource {
Hook,
Supplied,
Minted,
Invalid,
Foreign,
}
impl SessionSource {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Hook => "hook",
Self::Supplied => "supplied",
Self::Minted => "minted",
Self::Invalid => "invalid",
Self::Foreign => "foreign",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct HandleResolution {
pub(crate) session_id: String,
pub(crate) session_source: SessionSource,
pub(crate) hook_mode: bool,
pub(crate) agent_id: Option<String>,
}
impl HandleResolution {
fn agent(&self) -> Option<&str> {
self.agent_id.as_deref().filter(|agent| !agent.is_empty())
}
}
pub(crate) fn resolve_handles(
args: Option<&Map<String, Value>>,
hook_mode: bool,
agent_tracking: bool,
session_param_is_ours: bool,
) -> Option<HandleResolution> {
let (session_id, session_source) = if hook_mode {
(String::new(), SessionSource::Hook)
} else if !session_param_is_ours {
(String::new(), SessionSource::Foreign)
} else {
match extract_handle(args, SESSION_ID_PARAM) {
Some(supplied) if !supplied.eq_ignore_ascii_case(SESSION_START_SENTINEL) => {
if is_valid_session_id(&supplied) {
(supplied, SessionSource::Supplied)
} else {
(String::new(), SessionSource::Invalid)
}
}
_ => (new_session_id()?, SessionSource::Minted),
}
};
let agent_id = if agent_tracking {
extract_handle(args, AGENT_ID_PARAM)
} else {
None
};
Some(HandleResolution {
session_id,
session_source,
hook_mode,
agent_id,
})
}
pub(crate) fn build_mint_back_text(res: &HandleResolution) -> Option<String> {
if res.hook_mode {
return None;
}
match res.session_source {
SessionSource::Minted => Some(format!(
"{MINT_BACK_HEADER_ISSUED}\n{}\n{MINT_BACK_ISSUED_BODY}",
mint_back_session_line(&res.session_id)
)),
SessionSource::Invalid => Some(format!(
"{MINT_BACK_HEADER_UNRECOGNIZED}\n{MINT_BACK_UNRECOGNIZED_BODY}"
)),
SessionSource::Hook | SessionSource::Supplied | SessionSource::Foreign => None,
}
}
pub(crate) fn build_structured_mint_back(res: &HandleResolution) -> Option<Map<String, Value>> {
let session_ours = !res.hook_mode && res.session_source != SessionSource::Foreign;
let mut mint = Map::new();
if session_ours && res.session_source != SessionSource::Invalid {
mint.insert(SESSION_ID_PARAM.to_owned(), res.session_id.clone().into());
}
if let Some(agent) = res.agent() {
mint.insert(AGENT_ID_PARAM.to_owned(), agent.into());
}
if session_ours {
let status = match res.session_source {
SessionSource::Minted => "issued",
SessionSource::Invalid => "unrecognized",
SessionSource::Hook | SessionSource::Supplied | SessionSource::Foreign => "active",
};
mint.insert("status".to_owned(), status.into());
}
(!mint.is_empty()).then_some(mint)
}
pub(crate) fn build_handle_tags(
res: &HandleResolution,
protocol_version: Option<&str>,
) -> Map<String, Value> {
let mut tags = Map::new();
tags.insert(
AGENTCAT_TAG_SESSION_SOURCE.to_owned(),
res.session_source.as_str().into(),
);
if let Some(agent) = res.agent() {
let flat = agent.replace(['\r', '\n'], " ");
tags.insert(
AGENTCAT_TAG_AGENT_ID.to_owned(),
wire::cut_utf16(&flat, AGENT_ID_TAG_MAX_UNITS).into(),
);
tags.insert(
AGENTCAT_TAG_AGENT_SOURCE.to_owned(),
AGENT_SOURCE_SUPPLIED.into(),
);
}
if let Some(version) = protocol_version.filter(|version| !version.is_empty()) {
tags.insert(AGENTCAT_TAG_PROTOCOL_VERSION.to_owned(), version.into());
}
tags
}
pub(crate) fn session_from_hook_value(
hook_value: Option<&str>,
project_id: Option<&str>,
) -> Option<(String, SessionSource)> {
match hook_value.map(wire::trim).filter(|value| !value.is_empty()) {
Some(value) => Some((derive_session_id(value, project_id), SessionSource::Hook)),
None => Some((new_session_id()?, SessionSource::Minted)),
}
}