use std::time::Duration;
pub const DEFAULT_REPLAY_WINDOW_SECS: u64 = 300;
pub const REPLAY_TIMESTAMP_HEADER: &str = "x-webhook-timestamp";
#[derive(Debug, Clone)]
pub struct SignatureConfig {
pub algorithm: SignatureAlgorithm,
pub header_name: &'static str,
pub secret_env: &'static str,
pub replay_window_secs: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SignatureAlgorithm {
HmacSha256,
StripeWebhooks,
HmacSha256Base64,
Ed25519,
}
impl SignatureAlgorithm {
pub fn prefix(&self) -> &'static str {
match self {
Self::HmacSha256 => "sha256=",
Self::StripeWebhooks | Self::HmacSha256Base64 | Self::Ed25519 => "",
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum IdempotencySource {
Header(&'static str),
Body(&'static str),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct IdempotencyConfig {
pub source: IdempotencySource,
pub ttl: Duration,
pub processing_timeout: Duration,
}
impl IdempotencyConfig {
pub fn new(source: IdempotencySource) -> Self {
Self {
source,
ttl: Duration::from_secs(24 * 60 * 60),
processing_timeout: Duration::from_secs(5 * 60),
}
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
self
}
pub fn with_processing_timeout(mut self, timeout: Duration) -> Self {
self.processing_timeout = timeout;
self
}
}
pub struct WebhookSignature;
impl WebhookSignature {
pub const fn hmac_sha256(header: &'static str, secret_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::HmacSha256,
header_name: header,
secret_env,
replay_window_secs: DEFAULT_REPLAY_WINDOW_SECS,
}
}
pub const fn stripe_webhooks(secret_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::StripeWebhooks,
header_name: "stripe-signature",
secret_env,
replay_window_secs: DEFAULT_REPLAY_WINDOW_SECS,
}
}
pub const fn shopify_webhooks(secret_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::HmacSha256Base64,
header_name: "x-shopify-hmac-sha256",
secret_env,
replay_window_secs: DEFAULT_REPLAY_WINDOW_SECS,
}
}
pub const fn ed25519(header: &'static str, public_key_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::Ed25519,
header_name: header,
secret_env: public_key_env,
replay_window_secs: DEFAULT_REPLAY_WINDOW_SECS,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
use super::*;
#[test]
fn test_signature_config_creation() {
let config = WebhookSignature::hmac_sha256("X-Hub-Signature-256", "GITHUB_SECRET");
assert_eq!(config.algorithm, SignatureAlgorithm::HmacSha256);
assert_eq!(config.header_name, "X-Hub-Signature-256");
assert_eq!(config.secret_env, "GITHUB_SECRET");
}
#[test]
fn test_algorithm_prefix() {
assert_eq!(SignatureAlgorithm::HmacSha256.prefix(), "sha256=");
assert_eq!(SignatureAlgorithm::StripeWebhooks.prefix(), "");
assert_eq!(SignatureAlgorithm::Ed25519.prefix(), "");
}
#[test]
fn test_idempotency_config_default_ttl() {
let config = IdempotencyConfig::new(IdempotencySource::Header("X-Id"));
assert_eq!(config.ttl, Duration::from_secs(24 * 60 * 60));
}
}