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> {
52 let (prefix, value) = s.split_once(':')?;
53 match prefix {
56 "header" => Some(Self::Header(Box::leak(value.to_string().into_boxed_str()))),
57 "body" => Some(Self::Body(Box::leak(value.to_string().into_boxed_str()))),
58 _ => None,
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct IdempotencyConfig {
66 pub source: IdempotencySource,
68 pub ttl: Duration,
70}
71
72impl IdempotencyConfig {
73 pub fn new(source: IdempotencySource) -> Self {
75 Self {
76 source,
77 ttl: Duration::from_secs(24 * 60 * 60), }
79 }
80
81 pub fn with_ttl(mut self, ttl: Duration) -> Self {
83 self.ttl = ttl;
84 self
85 }
86}
87
88pub struct WebhookSignature;
97
98impl WebhookSignature {
99 pub const fn hmac_sha256(header: &'static str, secret_env: &'static str) -> SignatureConfig {
105 SignatureConfig {
106 algorithm: SignatureAlgorithm::HmacSha256,
107 header_name: header,
108 secret_env,
109 }
110 }
111
112 pub const fn hmac_sha1(header: &'static str, secret_env: &'static str) -> SignatureConfig {
118 SignatureConfig {
119 algorithm: SignatureAlgorithm::HmacSha1,
120 header_name: header,
121 secret_env,
122 }
123 }
124
125 pub const fn hmac_sha512(header: &'static str, secret_env: &'static str) -> SignatureConfig {
131 SignatureConfig {
132 algorithm: SignatureAlgorithm::HmacSha512,
133 header_name: header,
134 secret_env,
135 }
136 }
137}
138
139#[cfg(test)]
140#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_signature_config_creation() {
146 let config = WebhookSignature::hmac_sha256("X-Hub-Signature-256", "GITHUB_SECRET");
147 assert_eq!(config.algorithm, SignatureAlgorithm::HmacSha256);
148 assert_eq!(config.header_name, "X-Hub-Signature-256");
149 assert_eq!(config.secret_env, "GITHUB_SECRET");
150 }
151
152 #[test]
153 fn test_algorithm_prefix() {
154 assert_eq!(SignatureAlgorithm::HmacSha256.prefix(), "sha256=");
155 assert_eq!(SignatureAlgorithm::HmacSha1.prefix(), "sha1=");
156 assert_eq!(SignatureAlgorithm::HmacSha512.prefix(), "sha512=");
157 }
158
159 #[test]
160 fn test_idempotency_source_parsing() {
161 let header = IdempotencySource::parse("header:X-Request-Id");
162 assert!(matches!(
163 header,
164 Some(IdempotencySource::Header("X-Request-Id"))
165 ));
166
167 let body = IdempotencySource::parse("body:$.id");
168 assert!(matches!(body, Some(IdempotencySource::Body("$.id"))));
169
170 let invalid = IdempotencySource::parse("invalid");
171 assert!(invalid.is_none());
172 }
173
174 #[test]
175 fn test_idempotency_config_default_ttl() {
176 let config = IdempotencyConfig::new(IdempotencySource::Header("X-Id"));
177 assert_eq!(config.ttl, Duration::from_secs(24 * 60 * 60));
178 }
179}