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)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_signature_config_creation() {
139 let config = WebhookSignature::hmac_sha256("X-Hub-Signature-256", "GITHUB_SECRET");
140 assert_eq!(config.algorithm, SignatureAlgorithm::HmacSha256);
141 assert_eq!(config.header_name, "X-Hub-Signature-256");
142 assert_eq!(config.secret_env, "GITHUB_SECRET");
143 }
144
145 #[test]
146 fn test_algorithm_prefix() {
147 assert_eq!(SignatureAlgorithm::HmacSha256.prefix(), "sha256=");
148 assert_eq!(SignatureAlgorithm::HmacSha1.prefix(), "sha1=");
149 assert_eq!(SignatureAlgorithm::HmacSha512.prefix(), "sha512=");
150 }
151
152 #[test]
153 fn test_idempotency_source_parsing() {
154 let header = IdempotencySource::parse("header:X-Request-Id");
155 assert!(matches!(
156 header,
157 Some(IdempotencySource::Header("X-Request-Id"))
158 ));
159
160 let body = IdempotencySource::parse("body:$.id");
161 assert!(matches!(body, Some(IdempotencySource::Body("$.id"))));
162
163 let invalid = IdempotencySource::parse("invalid");
164 assert!(invalid.is_none());
165 }
166
167 #[test]
168 fn test_idempotency_config_default_ttl() {
169 let config = IdempotencyConfig::new(IdempotencySource::Header("X-Id"));
170 assert_eq!(config.ttl, Duration::from_secs(24 * 60 * 60));
171 }
172}