forge_core/webhook/
signature.rs1use std::time::Duration;
2
3#[derive(Debug, Clone)]
5pub struct SignatureConfig {
6 pub algorithm: SignatureAlgorithm,
8 pub header_name: &'static str,
10 pub secret_env: &'static str,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SignatureAlgorithm {
17 HmacSha256,
19 HmacSha1,
21 HmacSha512,
23}
24
25impl SignatureAlgorithm {
26 pub fn prefix(&self) -> &'static str {
28 match self {
29 Self::HmacSha256 => "sha256=",
30 Self::HmacSha1 => "sha1=",
31 Self::HmacSha512 => "sha512=",
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub enum IdempotencySource {
39 Header(&'static str),
41 Body(&'static str),
43}
44
45impl IdempotencySource {
46 pub fn parse(s: &str) -> Option<Self> {
48 let (prefix, value) = s.split_once(':')?;
49 match prefix {
50 "header" => Some(Self::Header(Box::leak(value.to_string().into_boxed_str()))),
51 "body" => Some(Self::Body(Box::leak(value.to_string().into_boxed_str()))),
52 _ => None,
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct IdempotencyConfig {
60 pub source: IdempotencySource,
62 pub ttl: Duration,
64}
65
66impl IdempotencyConfig {
67 pub fn new(source: IdempotencySource) -> Self {
69 Self {
70 source,
71 ttl: Duration::from_secs(24 * 60 * 60), }
73 }
74
75 pub fn with_ttl(mut self, ttl: Duration) -> Self {
77 self.ttl = ttl;
78 self
79 }
80}
81
82pub struct WebhookSignature;
91
92impl WebhookSignature {
93 pub const fn hmac_sha256(header: &'static str, secret_env: &'static str) -> SignatureConfig {
99 SignatureConfig {
100 algorithm: SignatureAlgorithm::HmacSha256,
101 header_name: header,
102 secret_env,
103 }
104 }
105
106 pub const fn hmac_sha1(header: &'static str, secret_env: &'static str) -> SignatureConfig {
112 SignatureConfig {
113 algorithm: SignatureAlgorithm::HmacSha1,
114 header_name: header,
115 secret_env,
116 }
117 }
118
119 pub const fn hmac_sha512(header: &'static str, secret_env: &'static str) -> SignatureConfig {
125 SignatureConfig {
126 algorithm: SignatureAlgorithm::HmacSha512,
127 header_name: header,
128 secret_env,
129 }
130 }
131}
132
133#[cfg(test)]
134#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_signature_config_creation() {
140 let config = WebhookSignature::hmac_sha256("X-Hub-Signature-256", "GITHUB_SECRET");
141 assert_eq!(config.algorithm, SignatureAlgorithm::HmacSha256);
142 assert_eq!(config.header_name, "X-Hub-Signature-256");
143 assert_eq!(config.secret_env, "GITHUB_SECRET");
144 }
145
146 #[test]
147 fn test_algorithm_prefix() {
148 assert_eq!(SignatureAlgorithm::HmacSha256.prefix(), "sha256=");
149 assert_eq!(SignatureAlgorithm::HmacSha1.prefix(), "sha1=");
150 assert_eq!(SignatureAlgorithm::HmacSha512.prefix(), "sha512=");
151 }
152
153 #[test]
154 fn test_idempotency_source_parsing() {
155 let header = IdempotencySource::parse("header:X-Request-Id");
156 assert!(matches!(
157 header,
158 Some(IdempotencySource::Header("X-Request-Id"))
159 ));
160
161 let body = IdempotencySource::parse("body:$.id");
162 assert!(matches!(body, Some(IdempotencySource::Body("$.id"))));
163
164 let invalid = IdempotencySource::parse("invalid");
165 assert!(invalid.is_none());
166 }
167
168 #[test]
169 fn test_idempotency_config_default_ttl() {
170 let config = IdempotencyConfig::new(IdempotencySource::Header("X-Id"));
171 assert_eq!(config.ttl, Duration::from_secs(24 * 60 * 60));
172 }
173}