Skip to main content

ferrox_webhooks/
lib.rs

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