github_bot_sdk/webhook/mod.rs
1//! GitHub webhook processing and validation.
2//!
3//! This module provides comprehensive webhook handling for GitHub Apps, including
4//! signature validation, event processing, and async handler execution using a
5//! fire-and-forget pattern to ensure fast HTTP responses.
6//!
7//! # Core Components
8//!
9//! - [`WebhookHandler`] - Trait for application-provided event processing logic
10//! - [`WebhookReceiver`] - HTTP webhook intake with validation and async dispatch
11//! - [`SignatureValidator`] - HMAC-SHA256 signature validation
12//! - [`WebhookRequest`]/[`WebhookResponse`] - HTTP request/response types
13//!
14//! # Fire-and-Forget Pattern
15//!
16//! The receiver ensures GitHub receives responses within the 10-second timeout:
17//!
18//! 1. Validate signature (< 10ms)
19//! 2. Process/normalize event (< 5ms)
20//! 3. Return HTTP response immediately (target < 100ms)
21//! 4. Spawn async tasks for handlers (non-blocking)
22//!
23//! # Security
24//!
25//! Webhook signature validation uses HMAC-SHA256 with constant-time comparison
26//! to prevent timing attacks. All validation operations complete in under 100ms.
27//!
28//! # Complete Usage Example
29//!
30//! ## Basic Webhook Handler
31//!
32//! ```rust,no_run
33//! use github_bot_sdk::webhook::{WebhookHandler, WebhookReceiver, WebhookRequest};
34//! use github_bot_sdk::auth::SecretProvider;
35//! use github_bot_sdk::events::{EventProcessor, ProcessorConfig, EventEnvelope};
36//! use async_trait::async_trait;
37//! use std::sync::Arc;
38//! use std::collections::HashMap;
39//!
40//! // Define your handler
41//! struct MyBotHandler;
42//!
43//! #[async_trait]
44//! impl WebhookHandler for MyBotHandler {
45//! async fn handle_event(
46//! &self,
47//! envelope: &EventEnvelope,
48//! ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
49//! match envelope.event_type.as_str() {
50//! "pull_request" => {
51//! println!("Processing PR event: {}", envelope.event_id);
52//! // Add your PR processing logic here
53//! }
54//! "issues" => {
55//! println!("Processing issue event: {}", envelope.event_id);
56//! // Add your issue processing logic here
57//! }
58//! _ => {
59//! println!("Ignoring event type: {}", envelope.event_type);
60//! }
61//! }
62//! Ok(())
63//! }
64//! }
65//!
66//! # async fn example(secret_provider: Arc<dyn SecretProvider>) -> Result<(), Box<dyn std::error::Error>> {
67//! // Setup receiver with processor
68//! let processor = EventProcessor::new(ProcessorConfig::default());
69//! let mut receiver = WebhookReceiver::new(secret_provider, processor);
70//!
71//! // Register your handler
72//! receiver.add_handler(Arc::new(MyBotHandler)).await;
73//!
74//! // Process incoming webhook (typically called from HTTP server)
75//! let headers = HashMap::from([
76//! ("x-github-event".to_string(), "pull_request".to_string()),
77//! ("x-github-delivery".to_string(), "12345-67890".to_string()),
78//! ("x-hub-signature-256".to_string(), "sha256=abc...".to_string()),
79//! ]);
80//! let body = bytes::Bytes::from_static(b"{\"action\":\"opened\",\"number\":1}");
81//! let request = WebhookRequest::new(headers, body);
82//!
83//! let response = receiver.receive_webhook(request).await;
84//! println!("Response status: {}", response.status_code());
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! ## Multiple Handlers
90//!
91//! ```rust,no_run
92//! use github_bot_sdk::webhook::{WebhookHandler, WebhookReceiver};
93//! use github_bot_sdk::events::EventEnvelope;
94//! use async_trait::async_trait;
95//! use std::sync::Arc;
96//!
97//! // Define specialized handlers
98//! struct PullRequestHandler;
99//! struct IssueHandler;
100//! struct SecurityHandler;
101//!
102//! #[async_trait]
103//! impl WebhookHandler for PullRequestHandler {
104//! async fn handle_event(&self, envelope: &EventEnvelope) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
105//! if envelope.event_type == "pull_request" {
106//! println!("PR Handler: Processing {}", envelope.event_id);
107//! // PR-specific logic
108//! }
109//! Ok(())
110//! }
111//! }
112//!
113//! #[async_trait]
114//! impl WebhookHandler for IssueHandler {
115//! async fn handle_event(&self, envelope: &EventEnvelope) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
116//! if envelope.event_type == "issues" {
117//! println!("Issue Handler: Processing {}", envelope.event_id);
118//! // Issue-specific logic
119//! }
120//! Ok(())
121//! }
122//! }
123//!
124//! #[async_trait]
125//! impl WebhookHandler for SecurityHandler {
126//! async fn handle_event(&self, envelope: &EventEnvelope) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
127//! // Security monitoring across all event types
128//! println!("Security: Auditing event {}", envelope.event_id);
129//! Ok(())
130//! }
131//! }
132//!
133//! # async fn example(mut receiver: WebhookReceiver) -> Result<(), Box<dyn std::error::Error>> {
134//! // Register multiple handlers - all will be invoked concurrently
135//! receiver.add_handler(Arc::new(PullRequestHandler)).await;
136//! receiver.add_handler(Arc::new(IssueHandler)).await;
137//! receiver.add_handler(Arc::new(SecurityHandler)).await;
138//! # Ok(())
139//! # }
140//! ```
141//!
142//! ## HTTP Server Integration (Axum Example)
143//!
144//! ```rust,ignore
145//! use github_bot_sdk::webhook::{WebhookReceiver, WebhookRequest, WebhookResponse};
146//! use axum::{
147//! extract::State,
148//! http::{HeaderMap, StatusCode},
149//! response::{IntoResponse, Response},
150//! Json, Router,
151//! routing::post,
152//! };
153//! use bytes::Bytes;
154//! use std::sync::Arc;
155//!
156//! // Application state with receiver
157//! #[derive(Clone)]
158//! struct AppState {
159//! receiver: Arc<WebhookReceiver>,
160//! }
161//!
162//! // HTTP handler for webhook endpoint
163//! async fn handle_webhook(
164//! State(state): State<AppState>,
165//! headers: HeaderMap,
166//! body: Bytes,
167//! ) -> Response {
168//! // Convert HTTP headers to HashMap
169//! let header_map: std::collections::HashMap<String, String> = headers
170//! .iter()
171//! .map(|(k, v)| (k.as_str().to_lowercase(), v.to_str().unwrap_or("").to_string()))
172//! .collect();
173//!
174//! // Create webhook request
175//! let request = WebhookRequest::new(header_map, body);
176//!
177//! // Process webhook
178//! let response = state.receiver.receive_webhook(request).await;
179//!
180//! // Convert to HTTP response
181//! let status = StatusCode::from_u16(response.status_code()).unwrap();
182//! let message = response.message().to_string();
183//!
184//! (status, Json(serde_json::json!({
185//! "message": message
186//! }))).into_response()
187//! }
188//!
189//! # async fn example(receiver: Arc<WebhookReceiver>) -> Result<(), Box<dyn std::error::Error>> {
190//! let state = AppState { receiver };
191//!
192//! let app = Router::new()
193//! .route("/webhook", post(handle_webhook))
194//! .with_state(state);
195//!
196//! // Run server
197//! // let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
198//! // axum::serve(listener, app).await?;
199//! # Ok(())
200//! # }
201//! ```
202//!
203//! ## Direct Signature Validation
204//!
205//! If you need to validate signatures independently:
206//!
207//! ```rust,no_run
208//! use github_bot_sdk::webhook::SignatureValidator;
209//! use github_bot_sdk::auth::SecretProvider;
210//! use std::sync::Arc;
211//!
212//! # async fn example(secret_provider: Arc<dyn SecretProvider>) -> Result<(), Box<dyn std::error::Error>> {
213//! let validator = SignatureValidator::new(secret_provider);
214//!
215//! let payload = b"{\"action\":\"opened\",\"number\":1}";
216//! let signature = "sha256=5c4a..."; // From X-Hub-Signature-256 header
217//!
218//! let is_valid = validator.validate(payload, signature).await?;
219//! if is_valid {
220//! println!("Webhook signature is valid");
221//! } else {
222//! println!("Invalid webhook signature - possible tampering");
223//! }
224//! # Ok(())
225//! # }
226//! ```
227
228pub mod handler;
229pub mod receiver;
230pub mod validation;
231
232// Re-export main types
233pub use handler::WebhookHandler;
234pub use receiver::{WebhookReceiver, WebhookRequest, WebhookResponse};
235pub use validation::SignatureValidator;
236
237#[cfg(test)]
238#[path = "mod_tests.rs"]
239mod tests;