Skip to main content

fraiseql_server/webhooks/
mod.rs

1//! # FraiseQL Webhook Runtime
2//!
3//! Webhook processing with signature verification, idempotency, and event routing.
4//!
5//! ## Features
6//!
7//! - **15+ provider support**: Stripe, GitHub, Shopify, and more
8//! - **Signature verification**: Constant-time comparison for security
9//! - **Idempotency**: Prevent duplicate event processing
10//! - **Event routing**: Map webhook events to database functions
11//! - **Transaction boundaries**: Correct isolation levels for data consistency
12
13pub mod config;
14pub mod signature;
15pub mod testing;
16pub mod traits;
17pub mod transaction;
18
19// Re-exports
20pub use config::{WebhookConfig, WebhookEventConfig};
21pub use signature::SignatureError;
22// Re-export testing mocks for tests
23#[cfg(test)]
24pub use testing::mocks;
25// Also export mocks for integration tests (tests/ directory)
26#[cfg(not(test))]
27pub use testing::mocks;
28pub use traits::{Clock, EventHandler, IdempotencyStore, SecretProvider, SignatureVerifier};
29pub use transaction::{WebhookIsolation, execute_in_transaction};
30
31/// Webhook-specific errors
32#[derive(Debug, thiserror::Error)]
33pub enum WebhookError {
34    #[error("Missing signature header")]
35    MissingSignature,
36
37    #[error("Invalid signature format: {0}")]
38    InvalidSignature(String),
39
40    #[error("Signature verification failed")]
41    SignatureVerificationFailed,
42
43    #[error("Timestamp expired (received: {received}, now: {now}, tolerance: {tolerance}s)")]
44    TimestampExpired {
45        received:  i64,
46        now:       i64,
47        tolerance: u64,
48    },
49
50    #[error("Missing timestamp header")]
51    MissingTimestamp,
52
53    #[error("Missing webhook secret: {0}")]
54    MissingSecret(String),
55
56    #[error("Unknown webhook provider: {0}")]
57    UnknownProvider(String),
58
59    #[error("Unknown event type: {0}")]
60    UnknownEvent(String),
61
62    #[error("Invalid payload: {0}")]
63    InvalidPayload(String),
64
65    #[error("Handler execution failed: {0}")]
66    HandlerFailed(String),
67
68    #[error("Database error: {0}")]
69    Database(String),
70
71    #[error("Condition evaluation error: {0}")]
72    Condition(String),
73
74    #[error("Mapping error: {0}")]
75    Mapping(String),
76
77    #[error("Provider not configured: {0}")]
78    ProviderNotConfigured(String),
79}
80
81impl From<sqlx::Error> for WebhookError {
82    fn from(err: sqlx::Error) -> Self {
83        Self::Database(err.to_string())
84    }
85}
86
87impl From<serde_json::Error> for WebhookError {
88    fn from(err: serde_json::Error) -> Self {
89        Self::InvalidPayload(err.to_string())
90    }
91}
92
93/// Result type for webhook operations
94pub type Result<T> = std::result::Result<T, WebhookError>;