Skip to main content

nestrs_webhooks/
lib.rs

1//! NestJS [`@nestjs/webhooks`](https://docs.nestjs.com/techniques/webhooks) analogue —
2//! [Standard Webhooks](https://www.standardwebhooks.com/) signing/verification and
3//! optional outbox-backed outbound dispatch.
4//!
5//! Always verify on the **raw** request body. Prefer `whsec_`-encoded secrets and never
6//! log them. Pair [`WebhooksOptions::outbox`] with [`nestrs_outbox::OutboxRelay`] for
7//! durable delivery.
8//!
9//! Umbrella feature: `nestrs` / `webhooks` (implies `outbox`) → `nestrs::webhooks`.
10#![doc(html_root_url = "https://docs.rs/nestrs-webhooks/1.7.1")]
11
12mod module;
13mod options;
14mod sign;
15mod verify;
16
17pub use module::WebhooksModule;
18pub use options::WebhooksOptions;
19pub use sign::{sign_standard_webhook, SignedWebhook};
20pub use verify::{verify_standard_webhook, WebhookVerifyError};
21
22pub use nestrs_core::{DynamicModule, Injectable, ProviderRegistry};
23use nestrs_outbox::OutboxRelay;
24use serde_json::json;
25use std::sync::Arc;
26
27pub struct WebhooksService {
28    secret: String,
29    outbox: Option<Arc<OutboxRelay>>,
30}
31
32impl WebhooksService {
33    pub fn new(secret: impl Into<String>, outbox: Option<Arc<OutboxRelay>>) -> Self {
34        Self {
35            secret: secret.into(),
36            outbox,
37        }
38    }
39
40    /// Sign a payload for immediate HTTP delivery by the caller.
41    pub fn sign(&self, msg_id: &str, timestamp: i64, body: &[u8]) -> SignedWebhook {
42        sign_standard_webhook(&self.secret, msg_id, timestamp, body)
43    }
44
45    /// Enqueue an outbound webhook through the transactional outbox.
46    pub async fn dispatch(
47        &self,
48        topic: &str,
49        url: &str,
50        body: serde_json::Value,
51    ) -> Result<(), String> {
52        let Some(outbox) = &self.outbox else {
53            return Err("webhooks: outbox not configured".into());
54        };
55        outbox
56            .add(
57                topic,
58                json!({
59                    "url": url,
60                    "body": body,
61                }),
62            )
63            .await;
64        Ok(())
65    }
66
67    pub fn verify(
68        &self,
69        msg_id: &str,
70        timestamp: &str,
71        signature: &str,
72        body: &[u8],
73    ) -> Result<(), WebhookVerifyError> {
74        verify_standard_webhook(&self.secret, msg_id, timestamp, signature, body)
75    }
76}
77
78impl Injectable for WebhooksService {
79    fn construct(_registry: &nestrs_core::ProviderRegistry) -> Arc<Self> {
80        panic!("WebhooksService must be provided by WebhooksModule::register(...)");
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn sign_and_verify_roundtrip() {
90        let secret = "whsec_test";
91        let body = br#"{"hello":"world"}"#;
92        let msg_id = "msg_1";
93        let ts = 1_700_000_000_i64;
94        let signed = sign_standard_webhook(secret, msg_id, ts, body);
95        verify_standard_webhook(
96            secret,
97            msg_id,
98            &ts.to_string(),
99            &signed.signature_header,
100            body,
101        )
102        .expect("verify");
103    }
104}