1pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, thiserror::Error)]
12#[error("{kind}")]
13pub struct Error {
14 pub kind: ErrorKind,
16 #[source]
18 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
19}
20
21impl Error {
22 pub fn new(kind: ErrorKind) -> Self {
24 Self { kind, source: None }
25 }
26
27 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#[derive(Debug, thiserror::Error)]
43pub enum ErrorKind {
44 #[error("OAuth error: {error} - {description}")]
46 OAuth { error: String, description: String },
47
48 #[error("Token expired")]
50 TokenExpired,
51
52 #[error("Token invalid: {0}")]
54 TokenInvalid(String),
55
56 #[error("JWT error: {0}")]
58 Jwt(String),
59
60 #[error("Invalid credentials: {0}")]
62 InvalidCredentials(String),
63
64 #[error("HTTP error: {0}")]
66 Http(String),
67
68 #[error("IO error: {0}")]
70 Io(String),
71
72 #[error("JSON error: {0}")]
74 Json(String),
75
76 #[error("Serialization error: {0}")]
78 Serialization(String),
79
80 #[error("Environment variable not set: {0}")]
82 EnvVar(String),
83
84 #[error("SFDX CLI error: {0}")]
86 SfdxCli(String),
87
88 #[error("Configuration error: {0}")]
90 Config(String),
91
92 #[error("Invalid input: {0}")]
94 InvalidInput(String),
95
96 #[error("{0}")]
98 Other(String),
99}
100
101impl From<reqwest::Error> for Error {
102 fn from(err: reqwest::Error) -> Self {
103 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 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 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")); }
184}