1use chrono::{DateTime, Utc};
7use thiserror::Error;
8
9use crate::auth::InstallationId;
10
11#[derive(Debug, Error)]
17pub enum AuthError {
18 #[error("Invalid GitHub App credentials")]
20 InvalidCredentials,
21
22 #[error("Installation {installation_id} not found or access denied")]
24 InstallationNotFound { installation_id: InstallationId },
25
26 #[error("Installation token expired")]
28 TokenExpired,
29
30 #[error("Insufficient permissions for operation: {permission}")]
32 InsufficientPermissions { permission: String },
33
34 #[error("Invalid private key: {message}")]
36 InvalidPrivateKey { message: String },
37
38 #[error("JWT generation failed: {message}")]
40 JwtGenerationFailed { message: String },
41
42 #[error("Token generation failed: {message}")]
44 TokenGenerationFailed { message: String },
45
46 #[error("Token exchange failed for installation {installation_id}: {message}")]
48 TokenExchangeFailed {
49 installation_id: InstallationId,
50 message: String,
51 },
52
53 #[error("GitHub API error: {status} - {message}")]
55 GitHubApiError { status: u16, message: String },
56
57 #[error("JWT signing failed: {0}")]
59 SigningError(#[from] SigningError),
60
61 #[error("Secret retrieval failed: {0}")]
63 SecretError(#[from] SecretError),
64
65 #[error("Token cache error: {0}")]
67 CacheError(#[from] CacheError),
68
69 #[error("Network error: {0}")]
71 NetworkError(String),
72
73 #[error("API error: {0}")]
75 ApiError(#[from] ApiError),
76}
77
78impl AuthError {
79 pub fn is_transient(&self) -> bool {
94 match self {
95 Self::InvalidCredentials => false,
96 Self::InstallationNotFound { .. } => false,
97 Self::TokenExpired => true, Self::InsufficientPermissions { .. } => false,
99 Self::InvalidPrivateKey { .. } => false,
100 Self::JwtGenerationFailed { .. } => false,
101 Self::TokenGenerationFailed { .. } => false,
102 Self::TokenExchangeFailed { .. } => false,
103 Self::GitHubApiError { status, .. } => *status >= 500 || *status == 429,
104 Self::SigningError(_) => false,
105 Self::SecretError(e) => e.is_transient(),
106 Self::CacheError(_) => true, Self::NetworkError(_) => true,
108 Self::ApiError(e) => e.is_transient(),
109 }
110 }
111
112 pub fn should_retry(&self) -> bool {
116 self.is_transient()
117 }
118
119 pub fn retry_after(&self) -> Option<chrono::Duration> {
124 match self {
125 Self::GitHubApiError { status, .. } if *status == 429 => {
126 Some(chrono::Duration::minutes(1))
127 }
128 Self::NetworkError(_) => Some(chrono::Duration::seconds(5)),
129 _ => None,
130 }
131 }
132}
133
134#[derive(Debug, Error)]
139pub enum SecretError {
140 #[error("Secret not found: {key}")]
142 NotFound { key: String },
143
144 #[error("Access denied to secret: {key}")]
146 AccessDenied { key: String },
147
148 #[error("Secret provider unavailable: {0}")]
150 ProviderUnavailable(String),
151
152 #[error("Invalid secret format: {key}")]
154 InvalidFormat { key: String },
155}
156
157impl SecretError {
158 pub fn is_transient(&self) -> bool {
162 matches!(self, Self::ProviderUnavailable(_))
163 }
164}
165
166#[derive(Debug, Error)]
170pub enum CacheError {
171 #[error("Cache operation failed: {message}")]
173 OperationFailed { message: String },
174
175 #[error("Cache unavailable: {message}")]
177 Unavailable { message: String },
178
179 #[error("Serialization failed: {0}")]
181 Serialization(#[from] serde_json::Error),
182}
183
184#[derive(Debug, Error)]
188pub enum SigningError {
189 #[error("Invalid private key: {message}")]
191 InvalidKey { message: String },
192
193 #[error("Signing operation failed: {message}")]
195 SigningFailed { message: String },
196
197 #[error("Token encoding failed: {message}")]
199 EncodingFailed { message: String },
200}
201
202#[derive(Debug, Error)]
207pub enum ApiError {
208 #[error("HTTP error: {status} - {message}")]
210 HttpError { status: u16, message: String },
211
212 #[error("Rate limit exceeded. Reset at: {reset_at}")]
214 RateLimitExceeded { reset_at: DateTime<Utc> },
215
216 #[error("Secondary rate limit exceeded (abuse detection). Retry after 60+ seconds")]
219 SecondaryRateLimit,
220
221 #[error("Request timeout")]
223 Timeout,
224
225 #[error("Invalid request: {message}")]
227 InvalidRequest { message: String },
228
229 #[error("Authentication failed")]
231 AuthenticationFailed,
232
233 #[error("Authorization failed")]
235 AuthorizationFailed,
236
237 #[error("Resource not found")]
239 NotFound,
240
241 #[error("JSON parsing error: {0}")]
243 JsonError(#[from] serde_json::Error),
244
245 #[error("HTTP client error: {0}")]
247 HttpClientError(#[from] reqwest::Error),
248
249 #[error("Configuration error: {message}")]
251 Configuration { message: String },
252
253 #[error("Token generation failed: {message}")]
255 TokenGenerationFailed { message: String },
256
257 #[error("Token exchange failed: {message}")]
259 TokenExchangeFailed { message: String },
260
261 #[error("GraphQL error: {message}")]
269 GraphQlError { message: String },
270}
271
272impl ApiError {
273 pub fn is_transient(&self) -> bool {
281 match self {
282 Self::HttpError { status, .. } => *status >= 500 || *status == 429,
283 Self::RateLimitExceeded { .. } => true,
284 Self::SecondaryRateLimit => true, Self::Timeout => true,
286 Self::InvalidRequest { .. } => false,
287 Self::AuthenticationFailed => false,
288 Self::AuthorizationFailed => false,
289 Self::NotFound => false,
290 Self::JsonError(_) => false,
291 Self::HttpClientError(_) => true, Self::Configuration { .. } => false, Self::TokenGenerationFailed { .. } => false, Self::TokenExchangeFailed { .. } => false, Self::GraphQlError { .. } => false, }
297 }
298}
299
300#[derive(Debug, Error)]
304pub enum ValidationError {
305 #[error("Required field missing: {field}")]
307 Required { field: String },
308
309 #[error("Invalid format for {field}: {message}")]
311 InvalidFormat { field: String, message: String },
312
313 #[error("Value out of range for {field}: {message}")]
315 OutOfRange { field: String, message: String },
316
317 #[error("Invalid signature format: {message}")]
319 InvalidSignatureFormat { message: String },
320
321 #[error("HMAC computation failed: {message}")]
323 HmacError { message: String },
324}
325
326#[derive(Debug, Error)]
331pub enum EventError {
332 #[error("Invalid event payload: {message}")]
334 InvalidPayload { message: String },
335
336 #[error("Unsupported event type: {event_type}")]
338 UnsupportedEventType { event_type: String },
339
340 #[error("Signature validation failed")]
342 InvalidSignature,
343
344 #[error("Missing required field: {field}")]
346 MissingField { field: String },
347
348 #[error("Payload too large: {size} bytes (max: {max})")]
350 PayloadTooLarge { size: usize, max: usize },
351
352 #[error("JSON parsing error: {0}")]
354 JsonParsing(#[from] serde_json::Error),
355
356 #[error("Secret provider error: {0}")]
358 SecretProvider(#[source] Box<dyn std::error::Error + Send + Sync>),
359}
360
361impl EventError {
362 pub fn is_transient(&self) -> bool {
364 match self {
365 Self::InvalidPayload { .. } => false,
366 Self::UnsupportedEventType { .. } => false,
367 Self::InvalidSignature => false,
368 Self::MissingField { .. } => false,
369 Self::PayloadTooLarge { .. } => false,
370 Self::JsonParsing(_) => false,
371 Self::SecretProvider(_) => true, }
373 }
374}
375
376#[cfg(test)]
377#[path = "error_tests.rs"]
378mod tests;