Skip to main content

wi_api/
webhook.rs

1use bytes::Bytes;
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4
5use crate::{error::WiError, types::Event};
6
7type HmacSha256 = Hmac<Sha256>;
8
9const SIGNATURE_HEADER: &str = "x-wi-signature";
10
11/// Verify the HMAC-SHA256 signature on a webhook payload.
12///
13/// The server sends `x-wi-signature: sha256=<hex>`. Pass the raw body bytes,
14/// the header value, and your webhook secret.
15///
16/// Returns `false` (not an error) when the signature does not match — treat
17/// that as an HTTP 401.
18pub fn verify_signature(body: &[u8], signature: &str, secret: &[u8]) -> bool {
19    let hex_part = match signature.strip_prefix("sha256=") {
20        Some(h) => h,
21        None => return false,
22    };
23
24    let expected = match hex::decode(hex_part) {
25        Ok(b) => b,
26        Err(_) => return false,
27    };
28
29    let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC accepts any key length");
30    mac.update(body);
31    mac.verify_slice(&expected).is_ok()
32}
33
34/// Parse raw webhook body bytes into an [`Event`].
35pub fn parse_event(body: &[u8]) -> Result<Event, WiError> {
36    serde_json::from_slice(body).map_err(WiError::Serialize)
37}
38
39/// A verified, parsed webhook request.
40pub struct WebhookPayload {
41    pub event: Event,
42    pub raw: Bytes,
43}
44
45impl WebhookPayload {
46    /// Verify the signature and parse the event from raw bytes and a header value.
47    ///
48    /// Returns `None` when the signature is invalid (caller should respond 401).
49    pub fn from_parts(body: Bytes, signature: &str, secret: &[u8]) -> Option<Result<Self, WiError>> {
50        if !verify_signature(&body, signature, secret) {
51            return None;
52        }
53        Some(parse_event(&body).map(|event| WebhookPayload { event, raw: body }))
54    }
55
56    /// Signature header name: `x-wi-signature`
57    pub const HEADER: &'static str = SIGNATURE_HEADER;
58}