Skip to main content

sirr_server/
webhooks.rs

1//! Per-key, fire-and-forget webhook delivery.
2//!
3//! When a key has a `webhook_url` set, the server POSTs a `WebhookEvent` JSON
4//! payload to that URL after each successful secret lifecycle event. The POST
5//! is dispatched in a background tokio task — it never blocks the HTTP response.
6//! No retries, no queue, no acknowledgement required.
7
8use reqwest::Client;
9use serde::Serialize;
10
11/// The JSON body sent to a webhook URL.
12#[derive(Debug, Clone, Serialize)]
13pub struct WebhookEvent {
14    /// Lifecycle event type. One of:
15    /// `secret.created`, `secret.read`, `secret.patched`, `secret.burned`, `secret.expired`.
16    #[serde(rename = "type")]
17    pub event_type: String,
18    /// The secret hash that triggered the event.
19    pub hash: String,
20    /// Unix seconds when the event occurred.
21    pub at: i64,
22    /// IP address of the caller (empty string if unknown).
23    pub ip: String,
24}
25
26/// Shared HTTP client for webhook delivery. Clone-cheap (Arc internally).
27#[derive(Clone)]
28pub struct WebhookSender {
29    client: Client,
30}
31
32impl WebhookSender {
33    /// Build a sender with a 10-second timeout per request.
34    pub fn new() -> Self {
35        Self {
36            client: Client::builder()
37                .timeout(std::time::Duration::from_secs(10))
38                .build()
39                .unwrap_or_default(),
40        }
41    }
42
43    /// Fire-and-forget: spawn a tokio task, POST `event` to `url`, never block.
44    pub fn fire(&self, url: String, event: WebhookEvent) {
45        let client = self.client.clone();
46        tokio::spawn(async move {
47            match client.post(&url).json(&event).send().await {
48                Ok(resp) => {
49                    tracing::debug!("webhook {} → {}", url, resp.status());
50                }
51                Err(e) => {
52                    tracing::warn!("webhook {} failed: {}", url, e);
53                }
54            }
55        });
56    }
57}
58
59impl Default for WebhookSender {
60    fn default() -> Self {
61        Self::new()
62    }
63}