Skip to main content

busbar_sf_auth/
error.rs

1//! Error types for sf-auth.
2//!
3//! Error messages are designed to avoid exposing sensitive credential data.
4
5/// Result type alias for sf-auth operations.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Error type for sf-auth operations.
9///
10/// Error messages are sanitized to prevent accidental credential exposure.
11#[derive(Debug, thiserror::Error)]
12#[error("{kind}")]
13pub struct Error {
14    /// The kind of error that occurred.
15    pub kind: ErrorKind,
16    /// Optional source error.
17    #[source]
18    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
19}
20
21impl Error {
22    /// Create a new error with the given kind.
23    pub fn new(kind: ErrorKind) -> Self {
24        Self { kind, source: None }
25    }
26
27    /// Create a new error with the given kind and source.
28    pub fn with_source(
29        kind: ErrorKind,
30        source: impl std::error::Error + Send + Sync + 'static,
31    ) -> Self {
32        Self {
33            kind,
34            source: Some(Box::new(source)),
35        }
36    }
37}
38
39/// The kind of error that occurred.
40///
41/// Error messages avoid including credential values.
42#[derive(Debug, thiserror::Error)]
43pub enum ErrorKind {
44    /// OAuth error response from Salesforce.
45    #[error("OAuth error: {error} - {description}")]
46    OAuth { error: String, description: String },
47
48    /// Token expired.
49    #[error("Token expired")]
50    TokenExpired,
51
52    /// Token invalid.
53    #[error("Token invalid: {0}")]
54    TokenInvalid(String),
55
56    /// JWT signing error.
57    #[error("JWT error: {0}")]
58    Jwt(String),
59
60    /// Invalid credentials configuration.
61    #[error("Invalid credentials: {0}")]
62    InvalidCredentials(String),
63
64    /// HTTP error during authentication.
65    #[error("HTTP error: {0}")]
66    Http(String),
67
68    /// IO error.
69    #[error("IO error: {0}")]
70    Io(String),
71
72    /// JSON error.
73    #[error("JSON error: {0}")]
74    Json(String),
75
76    /// Serialization error.
77    #[error("Serialization error: {0}")]
78    Serialization(String),
79
80    /// Environment variable not set.
81    #[error("Environment variable not set: {0}")]
82    EnvVar(String),
83
84    /// SFDX CLI error.
85    #[error("SFDX CLI error: {0}")]
86    SfdxCli(String),
87
88    /// Configuration error.
89    #[error("Configuration error: {0}")]
90    Config(String),
91
92    /// Invalid input provided.
93    #[error("Invalid input: {0}")]
94    InvalidInput(String),
95
96    /// Other error.
97    #[error("{0}")]
98    Other(String),
99}
100
101impl From<reqwest::Error> for Error {
102    fn from(err: reqwest::Error) -> Self {
103        // Sanitize the error message to avoid exposing URLs with tokens
104        let message = err.to_string();
105        let sanitized = if message.contains("access_token") || message.contains("token=") {
106            "HTTP request failed (details redacted for security)".to_string()
107        } else {
108            message
109        };
110        Error::with_source(ErrorKind::Http(sanitized), err)
111    }
112}
113
114impl From<serde_json::Error> for Error {
115    fn from(err: serde_json::Error) -> Self {
116        Error::with_source(ErrorKind::Json(err.to_string()), err)
117    }
118}
119
120impl From<serde_urlencoded::ser::Error> for Error {
121    fn from(err: serde_urlencoded::ser::Error) -> Self {
122        Error::with_source(ErrorKind::Serialization(err.to_string()), err)
123    }
124}
125
126impl From<std::io::Error> for Error {
127    fn from(err: std::io::Error) -> Self {
128        Error::with_source(ErrorKind::Io(err.to_string()), err)
129    }
130}
131
132impl From<std::env::VarError> for Error {
133    fn from(err: std::env::VarError) -> Self {
134        Error::with_source(ErrorKind::EnvVar(err.to_string()), err)
135    }
136}
137
138impl From<jsonwebtoken::errors::Error> for Error {
139    fn from(err: jsonwebtoken::errors::Error) -> Self {
140        Error::with_source(ErrorKind::Jwt(err.to_string()), err)
141    }
142}
143
144impl From<busbar_sf_client::Error> for Error {
145    fn from(err: busbar_sf_client::Error) -> Self {
146        // Sanitize any potential credential exposure
147        let message = err.to_string();
148        let sanitized = if message.contains("Bearer") || message.contains("token") {
149            "Client error (details redacted for security)".to_string()
150        } else {
151            message
152        };
153        Error::with_source(ErrorKind::Http(sanitized), err)
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_error_kind_display() {
163        let err = ErrorKind::TokenExpired;
164        assert_eq!(err.to_string(), "Token expired");
165
166        let err = ErrorKind::OAuth {
167            error: "invalid_grant".to_string(),
168            description: "expired access/refresh token".to_string(),
169        };
170        assert_eq!(
171            err.to_string(),
172            "OAuth error: invalid_grant - expired access/refresh token"
173        );
174    }
175
176    #[test]
177    fn test_error_messages_dont_contain_credentials() {
178        // Ensure common error patterns don't leak credentials
179        let err = Error::new(ErrorKind::TokenInvalid("validation failed".to_string()));
180        let msg = err.to_string();
181        assert!(!msg.contains("Bearer"));
182        assert!(!msg.contains("00D")); // Salesforce org ID prefix
183    }
184}