use std::time::Duration;
#[derive(Debug, Clone)]
pub struct SignatureConfig {
pub algorithm: SignatureAlgorithm,
pub header_name: &'static str,
pub secret_env: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureAlgorithm {
HmacSha256,
HmacSha1,
HmacSha512,
StandardWebhooks,
StripeWebhooks,
HmacSha256Base64,
Ed25519,
}
impl SignatureAlgorithm {
pub fn prefix(&self) -> &'static str {
match self {
Self::HmacSha256 => "sha256=",
Self::HmacSha1 => "sha1=",
Self::HmacSha512 => "sha512=",
Self::StandardWebhooks => "v1,",
Self::StripeWebhooks | Self::HmacSha256Base64 | Self::Ed25519 => "",
}
}
}
#[derive(Debug, Clone)]
pub enum IdempotencySource {
Header(&'static str),
Body(&'static str),
}
impl IdempotencySource {
pub fn parse(s: &str) -> Option<Self> {
let (prefix, value) = s.split_once(':')?;
match prefix {
"header" => Some(Self::Header(Box::leak(value.to_string().into_boxed_str()))),
"body" => Some(Self::Body(Box::leak(value.to_string().into_boxed_str()))),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct IdempotencyConfig {
pub source: IdempotencySource,
pub ttl: Duration,
}
impl IdempotencyConfig {
pub fn new(source: IdempotencySource) -> Self {
Self {
source,
ttl: Duration::from_secs(24 * 60 * 60), }
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
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,
}
}
pub const fn hmac_sha1(header: &'static str, secret_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::HmacSha1,
header_name: header,
secret_env,
}
}
pub const fn hmac_sha512(header: &'static str, secret_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::HmacSha512,
header_name: header,
secret_env,
}
}
pub const fn standard_webhooks(secret_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::StandardWebhooks,
header_name: "webhook-signature",
secret_env,
}
}
pub const fn stripe_webhooks(secret_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::StripeWebhooks,
header_name: "stripe-signature",
secret_env,
}
}
pub const fn shopify_webhooks(secret_env: &'static str) -> SignatureConfig {
SignatureConfig {
algorithm: SignatureAlgorithm::HmacSha256Base64,
header_name: "x-shopify-hmac-sha256",
secret_env,
}
}
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,
}
}
}
#[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::HmacSha1.prefix(), "sha1=");
assert_eq!(SignatureAlgorithm::HmacSha512.prefix(), "sha512=");
}
#[test]
fn test_idempotency_source_parsing() {
let header = IdempotencySource::parse("header:X-Request-Id");
assert!(matches!(
header,
Some(IdempotencySource::Header("X-Request-Id"))
));
let body = IdempotencySource::parse("body:$.id");
assert!(matches!(body, Some(IdempotencySource::Body("$.id"))));
let invalid = IdempotencySource::parse("invalid");
assert!(invalid.is_none());
}
#[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));
}
}