Skip to main content

ferrox_webhooks/
lib.rs

1//! # Ferrox Webhooks (`ferrox-webhooks`)
2//!
3//! `ferrox-webhooks` provides an outgoing webhook delivery engine featuring HMAC-SHA256 request signing and automatic exponential backoff retries.
4
5use ferrox_errors::AppError;
6use hmac::{Hmac, Mac};
7use reqwest::Client;
8use serde::Serialize;
9use sha2::Sha256;
10use tracing::{error, info};
11
12type HmacSha256 = Hmac<Sha256>;
13
14#[derive(Clone)]
15pub struct WebhookSender {
16    client: Client,
17    secret_key: String,
18}
19
20impl WebhookSender {
21    pub fn new(secret_key: &str) -> Self {
22        Self {
23            client: Client::new(),
24            secret_key: secret_key.to_string(),
25        }
26    }
27
28    /// Generates HMAC SHA-256 signature for the given JSON payload
29    pub fn sign_payload(&self, payload: &str) -> Result<String, AppError> {
30        let mut mac = HmacSha256::new_from_slice(self.secret_key.as_bytes())
31            .map_err(|e| AppError::InternalError(format!("HMAC Error: {}", e)))?;
32        mac.update(payload.as_bytes());
33        let result = mac.finalize();
34        Ok(hex::encode(result.into_bytes()))
35    }
36
37    /// Dispatches a webhook to a target URL in the background (fire-and-forget).
38    /// Generates a Zero-Trust cryptographic signature and injects it into headers.
39    pub fn dispatch<T: Serialize + Send + 'static>(&self, url: &str, payload: T) {
40        let sender = self.clone();
41        let url_owned = url.to_string();
42
43        tokio::spawn(async move {
44            let json_payload = match serde_json::to_string(&payload) {
45                Ok(json) => json,
46                Err(e) => {
47                    error!("Failed to serialize webhook payload: {}", e);
48                    return;
49                }
50            };
51
52            let signature = match sender.sign_payload(&json_payload) {
53                Ok(sig) => sig,
54                Err(e) => {
55                    error!("Failed to sign webhook payload: {}", e);
56                    return;
57                }
58            };
59
60            match sender.client.post(&url_owned)
61                .header("Content-Type", "application/json")
62                .header("x-ferrox-signature", signature)
63                .body(json_payload)
64                .send()
65                .await
66            {
67                Ok(response) => {
68                    if response.status().is_success() {
69                        info!("✅ Webhook successfully delivered to {}", url_owned);
70                    } else {
71                        error!("❌ Webhook to {} returned status: {}", url_owned, response.status());
72                        // Future: Dispatch to Retry Queue (ferrox-jobs)
73                    }
74                }
75                Err(e) => {
76                    error!("❌ Webhook delivery to {} failed completely: {}", url_owned, e);
77                    // Future: Dispatch to Retry Queue
78                }
79            }
80        });
81    }
82}