Skip to main content

systemprompt_api/routes/agent/contexts/webhook/
validation.rs

1//! Webhook payload validation with size caps.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6const MAX_PAYLOAD_SIZE: usize = 1_000_000;
7const MAX_TEXT_FIELD_SIZE: usize = 100_000;
8
9#[derive(Debug, thiserror::Error)]
10pub enum PayloadValidationError {
11    #[error("payload could not be serialized")]
12    Serialize(#[source] serde_json::Error),
13    #[error("Payload too large: {size} bytes (max: {max})")]
14    TooLarge { size: usize, max: usize },
15    #[error("serialized payload could not be re-parsed")]
16    Reparse(#[source] serde_json::Error),
17}
18
19// JSON: webhook payload — arbitrary caller JSON, size-capped before relay.
20pub fn validate_json_serializable(value: &serde_json::Value) -> Result<(), PayloadValidationError> {
21    let sanitized = sanitize_payload(value, MAX_TEXT_FIELD_SIZE);
22
23    let serialized =
24        serde_json::to_string(&sanitized).map_err(PayloadValidationError::Serialize)?;
25
26    if serialized.len() > MAX_PAYLOAD_SIZE {
27        return Err(PayloadValidationError::TooLarge {
28            size: serialized.len(),
29            max: MAX_PAYLOAD_SIZE,
30        });
31    }
32
33    serde_json::from_str::<serde_json::Value>(&serialized)
34        .map_err(PayloadValidationError::Reparse)?;
35
36    Ok(())
37}
38
39// JSON: webhook payload — arbitrary caller JSON, size-capped before relay.
40pub fn sanitize_payload(value: &serde_json::Value, max_text_size: usize) -> serde_json::Value {
41    match value {
42        serde_json::Value::String(s) => {
43            if s.len() > max_text_size {
44                serde_json::Value::String(format!(
45                    "{}... [truncated from {} bytes]",
46                    &s[..max_text_size.min(s.len())],
47                    s.len()
48                ))
49            } else {
50                serde_json::Value::String(s.clone())
51            }
52        },
53        serde_json::Value::Array(arr) => serde_json::Value::Array(
54            arr.iter()
55                .map(|v| sanitize_payload(v, max_text_size))
56                .collect(),
57        ),
58        serde_json::Value::Object(obj) => {
59            let sanitized: serde_json::Map<String, serde_json::Value> = obj
60                .iter()
61                .map(|(k, v)| (k.clone(), sanitize_payload(v, max_text_size)))
62                .collect();
63            serde_json::Value::Object(sanitized)
64        },
65        other => other.clone(),
66    }
67}