use chrono::{DateTime, Utc};
use thiserror::Error;
use crate::auth::InstallationId;
#[derive(Debug, Error)]
pub enum AuthError {
#[error("Invalid GitHub App credentials")]
InvalidCredentials,
#[error("Installation {installation_id} not found or access denied")]
InstallationNotFound { installation_id: InstallationId },
#[error("Installation token expired")]
TokenExpired,
#[error("Insufficient permissions for operation: {permission}")]
InsufficientPermissions { permission: String },
#[error("Invalid private key: {message}")]
InvalidPrivateKey { message: String },
#[error("JWT generation failed: {message}")]
JwtGenerationFailed { message: String },
#[error("Token generation failed: {message}")]
TokenGenerationFailed { message: String },
#[error("Token exchange failed for installation {installation_id}: {message}")]
TokenExchangeFailed {
installation_id: InstallationId,
message: String,
},
#[error("GitHub API error: {status} - {message}")]
GitHubApiError { status: u16, message: String },
#[error("JWT signing failed: {0}")]
SigningError(#[from] SigningError),
#[error("Secret retrieval failed: {0}")]
SecretError(#[from] SecretError),
#[error("Token cache error: {0}")]
CacheError(#[from] CacheError),
#[error("Network error: {0}")]
NetworkError(String),
#[error("API error: {0}")]
ApiError(#[from] ApiError),
}
impl AuthError {
pub fn is_transient(&self) -> bool {
match self {
Self::InvalidCredentials => false,
Self::InstallationNotFound { .. } => false,
Self::TokenExpired => true, Self::InsufficientPermissions { .. } => false,
Self::InvalidPrivateKey { .. } => false,
Self::JwtGenerationFailed { .. } => false,
Self::TokenGenerationFailed { .. } => false,
Self::TokenExchangeFailed { .. } => false,
Self::GitHubApiError { status, .. } => *status >= 500 || *status == 429,
Self::SigningError(_) => false,
Self::SecretError(e) => e.is_transient(),
Self::CacheError(_) => true, Self::NetworkError(_) => true,
Self::ApiError(e) => e.is_transient(),
}
}
pub fn should_retry(&self) -> bool {
self.is_transient()
}
pub fn retry_after(&self) -> Option<chrono::Duration> {
match self {
Self::GitHubApiError { status, .. } if *status == 429 => {
Some(chrono::Duration::minutes(1))
}
Self::NetworkError(_) => Some(chrono::Duration::seconds(5)),
_ => None,
}
}
}
#[derive(Debug, Error)]
pub enum SecretError {
#[error("Secret not found: {key}")]
NotFound { key: String },
#[error("Access denied to secret: {key}")]
AccessDenied { key: String },
#[error("Secret provider unavailable: {0}")]
ProviderUnavailable(String),
#[error("Invalid secret format: {key}")]
InvalidFormat { key: String },
}
impl SecretError {
pub fn is_transient(&self) -> bool {
matches!(self, Self::ProviderUnavailable(_))
}
}
#[derive(Debug, Error)]
pub enum CacheError {
#[error("Cache operation failed: {message}")]
OperationFailed { message: String },
#[error("Cache unavailable: {message}")]
Unavailable { message: String },
#[error("Serialization failed: {0}")]
Serialization(#[from] serde_json::Error),
}
#[derive(Debug, Error)]
pub enum SigningError {
#[error("Invalid private key: {message}")]
InvalidKey { message: String },
#[error("Signing operation failed: {message}")]
SigningFailed { message: String },
#[error("Token encoding failed: {message}")]
EncodingFailed { message: String },
}
#[derive(Debug, Error)]
pub enum ApiError {
#[error("HTTP error: {status} - {message}")]
HttpError { status: u16, message: String },
#[error("Rate limit exceeded. Reset at: {reset_at}")]
RateLimitExceeded { reset_at: DateTime<Utc> },
#[error("Secondary rate limit exceeded (abuse detection). Retry after 60+ seconds")]
SecondaryRateLimit,
#[error("Request timeout")]
Timeout,
#[error("Invalid request: {message}")]
InvalidRequest { message: String },
#[error("Authentication failed")]
AuthenticationFailed,
#[error("Authorization failed")]
AuthorizationFailed,
#[error("Resource not found")]
NotFound,
#[error("JSON parsing error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("HTTP client error: {0}")]
HttpClientError(#[from] reqwest::Error),
#[error("Configuration error: {message}")]
Configuration { message: String },
#[error("Token generation failed: {message}")]
TokenGenerationFailed { message: String },
#[error("Token exchange failed: {message}")]
TokenExchangeFailed { message: String },
#[error("GraphQL error: {message}")]
GraphQlError { message: String },
}
impl ApiError {
pub fn is_transient(&self) -> bool {
match self {
Self::HttpError { status, .. } => *status >= 500 || *status == 429,
Self::RateLimitExceeded { .. } => true,
Self::SecondaryRateLimit => true, Self::Timeout => true,
Self::InvalidRequest { .. } => false,
Self::AuthenticationFailed => false,
Self::AuthorizationFailed => false,
Self::NotFound => false,
Self::JsonError(_) => false,
Self::HttpClientError(_) => true, Self::Configuration { .. } => false, Self::TokenGenerationFailed { .. } => false, Self::TokenExchangeFailed { .. } => false, Self::GraphQlError { .. } => false, }
}
}
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("Required field missing: {field}")]
Required { field: String },
#[error("Invalid format for {field}: {message}")]
InvalidFormat { field: String, message: String },
#[error("Value out of range for {field}: {message}")]
OutOfRange { field: String, message: String },
#[error("Invalid signature format: {message}")]
InvalidSignatureFormat { message: String },
#[error("HMAC computation failed: {message}")]
HmacError { message: String },
}
#[derive(Debug, Error)]
pub enum EventError {
#[error("Invalid event payload: {message}")]
InvalidPayload { message: String },
#[error("Unsupported event type: {event_type}")]
UnsupportedEventType { event_type: String },
#[error("Signature validation failed")]
InvalidSignature,
#[error("Missing required field: {field}")]
MissingField { field: String },
#[error("Payload too large: {size} bytes (max: {max})")]
PayloadTooLarge { size: usize, max: usize },
#[error("JSON parsing error: {0}")]
JsonParsing(#[from] serde_json::Error),
#[error("Secret provider error: {0}")]
SecretProvider(#[source] Box<dyn std::error::Error + Send + Sync>),
}
impl EventError {
pub fn is_transient(&self) -> bool {
match self {
Self::InvalidPayload { .. } => false,
Self::UnsupportedEventType { .. } => false,
Self::InvalidSignature => false,
Self::MissingField { .. } => false,
Self::PayloadTooLarge { .. } => false,
Self::JsonParsing(_) => false,
Self::SecretProvider(_) => true, }
}
}
#[cfg(test)]
#[path = "error_tests.rs"]
mod tests;