Skip to main content

worldinterface_http_trigger/
receipt.rs

1//! Trigger receipt generation for webhook boundary crossings.
2
3use chrono::Utc;
4use uuid::Uuid;
5use worldinterface_core::id::{trigger_input_node_id, FlowRunId, StepRunId};
6use worldinterface_core::receipt::{sha256_hex, Receipt, ReceiptStatus};
7
8use crate::trigger::TriggerInput;
9
10/// Generate a Receipt for a webhook trigger boundary crossing.
11///
12/// The webhook is a boundary crossing: external HTTP request -> internal FlowRun.
13/// The receipt captures the trigger metadata as immutable evidence per IBP section 6.
14pub fn create_trigger_receipt(flow_run_id: FlowRunId, trigger_input: &TriggerInput) -> Receipt {
15    let input_bytes = serde_json::to_vec(trigger_input).unwrap_or_default();
16    let input_hash = sha256_hex(&input_bytes);
17
18    Receipt::new(
19        flow_run_id,
20        trigger_input_node_id(),
21        StepRunId::new(),
22        "webhook.trigger".into(),
23        Utc::now(),
24        Uuid::new_v4(), // attempt_id = unique (triggers are not retried)
25        input_hash.clone(),
26        Some(input_hash), // output_hash = input_hash (trigger passes through)
27        ReceiptStatus::Success,
28        None, // no error
29        0,    // duration_ms = 0 (trigger is instantaneous)
30    )
31}
32
33#[cfg(test)]
34mod tests {
35    use serde_json::json;
36
37    use super::*;
38
39    fn sample_trigger_input() -> TriggerInput {
40        TriggerInput {
41            body: json!({"event": "push"}),
42            headers: json!({"content-type": "application/json"}),
43            method: "POST".to_string(),
44            path: "github/push".to_string(),
45            source_addr: None,
46            received_at: 1741200000,
47        }
48    }
49
50    #[test]
51    fn receipt_has_webhook_trigger_connector() {
52        let receipt = create_trigger_receipt(FlowRunId::new(), &sample_trigger_input());
53        assert_eq!(receipt.connector, "webhook.trigger");
54    }
55
56    #[test]
57    fn receipt_has_success_status() {
58        let receipt = create_trigger_receipt(FlowRunId::new(), &sample_trigger_input());
59        assert_eq!(receipt.status, ReceiptStatus::Success);
60    }
61
62    #[test]
63    fn receipt_has_correct_flow_run_id() {
64        let frid = FlowRunId::new();
65        let receipt = create_trigger_receipt(frid, &sample_trigger_input());
66        assert_eq!(receipt.flow_run_id, frid);
67    }
68
69    #[test]
70    fn receipt_has_trigger_node_id() {
71        let receipt = create_trigger_receipt(FlowRunId::new(), &sample_trigger_input());
72        assert_eq!(receipt.node_id, trigger_input_node_id());
73    }
74
75    #[test]
76    fn receipt_has_zero_duration() {
77        let receipt = create_trigger_receipt(FlowRunId::new(), &sample_trigger_input());
78        assert_eq!(receipt.duration_ms, 0);
79    }
80}