reasonkit_web/stripe/
config.rs1use std::env;
6use std::time::Duration;
7
8use crate::stripe::error::{StripeWebhookError, StripeWebhookResult};
9
10#[derive(Debug, Clone)]
12pub struct StripeWebhookConfig {
13 webhook_secret: String,
16
17 pub max_timestamp_age: Duration,
20
21 pub max_clock_drift: Duration,
23
24 pub idempotency_ttl: Duration,
26
27 pub idempotency_max_entries: usize,
29
30 pub processing_timeout: Duration,
32
33 pub max_retries: u32,
35
36 pub retry_base_delay: Duration,
38
39 pub log_payloads: bool,
41}
42
43impl StripeWebhookConfig {
44 pub fn from_env() -> StripeWebhookResult<Self> {
59 let webhook_secret =
61 env::var("STRIPE_WEBHOOK_SECRET").map_err(|_| StripeWebhookError::MissingSecret)?;
62
63 Self::validate_secret(&webhook_secret)?;
64
65 let max_timestamp_age = env::var("STRIPE_WEBHOOK_MAX_AGE")
66 .ok()
67 .and_then(|v| v.parse::<u64>().ok())
68 .map(Duration::from_secs)
69 .unwrap_or(Duration::from_secs(300)); let idempotency_ttl = env::var("STRIPE_WEBHOOK_IDEMPOTENCY_TTL")
72 .ok()
73 .and_then(|v| v.parse::<u64>().ok())
74 .map(Duration::from_secs)
75 .unwrap_or(Duration::from_secs(86400)); let processing_timeout = env::var("STRIPE_WEBHOOK_PROCESSING_TIMEOUT")
78 .ok()
79 .and_then(|v| v.parse::<u64>().ok())
80 .map(Duration::from_secs)
81 .unwrap_or(Duration::from_secs(30));
82
83 let max_retries = env::var("STRIPE_WEBHOOK_MAX_RETRIES")
84 .ok()
85 .and_then(|v| v.parse::<u32>().ok())
86 .unwrap_or(3);
87
88 let log_payloads = env::var("STRIPE_WEBHOOK_LOG_PAYLOADS")
89 .map(|v| v.to_lowercase() == "true")
90 .unwrap_or(false);
91
92 Ok(Self {
93 webhook_secret,
94 max_timestamp_age,
95 max_clock_drift: Duration::from_secs(60), idempotency_ttl,
97 idempotency_max_entries: 100_000,
98 processing_timeout,
99 max_retries,
100 retry_base_delay: Duration::from_secs(1),
101 log_payloads,
102 })
103 }
104
105 #[cfg(test)]
107 pub fn test_config() -> Self {
108 Self {
109 webhook_secret: "whsec_test_secret_for_unit_tests_only_12345".to_string(),
110 max_timestamp_age: Duration::from_secs(300),
111 max_clock_drift: Duration::from_secs(60),
112 idempotency_ttl: Duration::from_secs(3600),
113 idempotency_max_entries: 1000,
114 processing_timeout: Duration::from_secs(5),
115 max_retries: 3,
116 retry_base_delay: Duration::from_millis(100),
117 log_payloads: true, }
119 }
120
121 fn validate_secret(secret: &str) -> StripeWebhookResult<()> {
123 if secret.is_empty() {
124 return Err(StripeWebhookError::InvalidSecretFormat(
125 "Secret cannot be empty".to_string(),
126 ));
127 }
128
129 if !secret.starts_with("whsec_") {
131 tracing::warn!("STRIPE_WEBHOOK_SECRET does not start with 'whsec_' - may be invalid");
132 }
133
134 if secret.len() < 20 {
136 return Err(StripeWebhookError::InvalidSecretFormat(
137 "Secret too short (minimum 20 characters)".to_string(),
138 ));
139 }
140
141 Ok(())
142 }
143
144 pub(crate) fn webhook_secret(&self) -> &str {
150 &self.webhook_secret
151 }
152
153 pub fn retry_delay(&self, attempt: u32) -> Duration {
155 let base = self.retry_base_delay.as_millis() as u64;
156 let delay = base.saturating_mul(2u64.saturating_pow(attempt));
157
158 let jitter = delay / 10 + (rand::random::<u64>() % (delay / 10 + 1));
160 Duration::from_millis(delay.saturating_add(jitter).min(30_000)) }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_config_validation() {
170 assert!(
172 StripeWebhookConfig::validate_secret("whsec_test_secret_12345678901234567890").is_ok()
173 );
174
175 assert!(matches!(
177 StripeWebhookConfig::validate_secret(""),
178 Err(StripeWebhookError::InvalidSecretFormat(_))
179 ));
180
181 assert!(matches!(
183 StripeWebhookConfig::validate_secret("short"),
184 Err(StripeWebhookError::InvalidSecretFormat(_))
185 ));
186 }
187
188 #[test]
189 fn test_retry_delay() {
190 let config = StripeWebhookConfig::test_config();
191
192 let delay0 = config.retry_delay(0);
193 let delay1 = config.retry_delay(1);
194 let delay2 = config.retry_delay(2);
195
196 assert!(delay1 > delay0);
198 assert!(delay2 > delay1);
199
200 let delay_max = config.retry_delay(20);
202 assert!(delay_max <= Duration::from_secs(30));
203 }
204}