1mod http;
9mod trust_registry;
10
11pub use http::HttpError;
12pub use trust_registry::TrustRegistryError;
13
14#[cfg(feature = "websocket")]
15mod websocket;
16use thiserror::Error;
17#[cfg(feature = "websocket")]
18pub use websocket::WebSocketError;
19
20#[derive(Debug, Error)]
53#[non_exhaustive]
54pub enum LastIDError {
55 #[error("Configuration error: {0}")]
57 Config(String),
58
59 #[error("HTTP request failed: {0}")]
61 Http(#[from] HttpError),
62
63 #[error("Trust registry error: {0}")]
65 TrustRegistry(#[from] TrustRegistryError),
66
67 #[error("Cryptographic operation failed: {0}")]
69 Crypto(String),
70
71 #[error("Invalid credential: {0}")]
73 InvalidCredential(String),
74
75 #[error("Request timeout after {0} seconds")]
77 Timeout(u64),
78
79 #[error("Policy validation failed: {0}")]
81 PolicyValidation(String),
82
83 #[error("Serialization error: {0}")]
85 Serialization(#[from] serde_json::Error),
86
87 #[error("IO error: {0}")]
89 Io(#[from] std::io::Error),
90
91 #[error("TOML parsing error: {0}")]
93 TomlParse(#[from] toml::de::Error),
94
95 #[cfg(feature = "websocket")]
97 #[error("WebSocket error: {0}")]
98 WebSocket(#[from] WebSocketError),
99
100 #[error("Invalid DID format: {0}")]
102 InvalidDid(String),
103
104 #[error("Client ID cannot be empty")]
106 EmptyClientId,
107
108 #[error("HTTP URLs are not allowed; use HTTPS (except localhost for dev): {0}")]
110 InsecureUrl(String),
111
112 #[error("Clock skew exceeded {tolerance_seconds}s tolerance")]
114 ClockSkewExceeded {
115 tolerance_seconds: u64,
117 },
118
119 #[error("Token binding mismatch: expected thumbprint {expected}, got {actual}")]
121 TokenBindingMismatch {
122 expected: String,
124 actual: String,
126 },
127}
128
129impl LastIDError {
130 #[must_use]
132 pub fn config(message: impl Into<String>) -> Self {
133 Self::Config(message.into())
134 }
135
136 #[must_use]
138 pub fn crypto(message: impl Into<String>) -> Self {
139 Self::Crypto(message.into())
140 }
141
142 #[must_use]
144 pub fn invalid_credential(message: impl Into<String>) -> Self {
145 Self::InvalidCredential(message.into())
146 }
147
148 #[must_use]
163 pub fn credential_parse(message: impl Into<String>) -> Self {
164 Self::InvalidCredential(message.into())
165 }
166
167 #[must_use]
169 pub fn policy_validation(message: impl Into<String>) -> Self {
170 Self::PolicyValidation(message.into())
171 }
172
173 #[must_use]
175 #[allow(clippy::missing_const_for_fn)] pub fn is_retryable(&self) -> bool {
177 match self {
178 Self::Http(e) => e.is_retryable(),
179 Self::TrustRegistry(e) => e.is_retryable(),
180 Self::Timeout(_) => true,
181 _ => false,
182 }
183 }
184
185 #[must_use]
212 #[allow(clippy::missing_const_for_fn)] pub fn suggested_retry_delay_ms(&self) -> Option<u64> {
214 match self {
215 Self::Http(e) => e.suggested_retry_delay_ms(),
216 Self::TrustRegistry(e) => e.suggested_retry_delay_ms(),
217 Self::Timeout(_) => Some(2000), _ => None,
219 }
220 }
221
222 #[must_use]
227 pub const fn category(&self) -> &'static str {
228 match self {
229 Self::Config(_) => "config",
230 Self::Http(_) => "http",
231 Self::TrustRegistry(_) => "trust_registry",
232 Self::Crypto(_) => "crypto",
233 Self::InvalidCredential(_) => "invalid_credential",
234 Self::Timeout(_) => "timeout",
235 Self::PolicyValidation(_) => "policy_validation",
236 Self::Serialization(_) => "serialization",
237 Self::Io(_) => "io",
238 Self::TomlParse(_) => "toml_parse",
239 #[cfg(feature = "websocket")]
240 Self::WebSocket(_) => "websocket",
241 Self::InvalidDid(_) => "invalid_did",
242 Self::EmptyClientId => "empty_client_id",
243 Self::InsecureUrl(_) => "insecure_url",
244 Self::ClockSkewExceeded { .. } => "clock_skew",
245 Self::TokenBindingMismatch { .. } => "token_binding",
246 }
247 }
248
249 #[must_use]
251 pub fn invalid_did(did: impl Into<String>) -> Self {
252 Self::InvalidDid(did.into())
253 }
254
255 #[must_use]
257 pub fn insecure_url(url: impl Into<String>) -> Self {
258 Self::InsecureUrl(url.into())
259 }
260
261 #[must_use]
263 pub const fn clock_skew_exceeded(tolerance_seconds: u64) -> Self {
264 Self::ClockSkewExceeded { tolerance_seconds }
265 }
266
267 #[must_use]
269 pub fn token_binding_mismatch(expected: impl Into<String>, actual: impl Into<String>) -> Self {
270 Self::TokenBindingMismatch {
271 expected: expected.into(),
272 actual: actual.into(),
273 }
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::trust_registry::IssuerStatus;
281
282 #[test]
283 fn test_http_error_is_retryable() {
284 assert!(HttpError::network("connection refused").is_retryable());
285 assert!(HttpError::Timeout.is_retryable());
286 assert!(HttpError::status(500, "internal error").is_retryable());
287 assert!(HttpError::status(503, "unavailable").is_retryable());
288 assert!(HttpError::rate_limited(60).is_retryable());
289
290 assert!(!HttpError::status(400, "bad request").is_retryable());
291 assert!(!HttpError::status(401, "unauthorized").is_retryable());
292 assert!(!HttpError::status(404, "not found").is_retryable());
293 }
294
295 #[test]
296 fn test_http_error_is_rate_limited() {
297 assert!(HttpError::rate_limited(60).is_rate_limited());
298 assert!(HttpError::status(429, "too many requests").is_rate_limited());
299 assert!(!HttpError::status(500, "error").is_rate_limited());
300 }
301
302 #[test]
303 fn test_lastid_error_is_retryable() {
304 assert!(LastIDError::Http(HttpError::Timeout).is_retryable());
305 assert!(LastIDError::Timeout(300).is_retryable());
306 assert!(!LastIDError::config("missing").is_retryable());
307 assert!(!LastIDError::crypto("invalid key").is_retryable());
308 }
309
310 #[test]
311 fn test_trust_registry_error_is_retryable() {
312 assert!(TrustRegistryError::Http(HttpError::Timeout).is_retryable());
313 assert!(!TrustRegistryError::issuer_not_found("did:test").is_retryable());
314 assert!(!TrustRegistryError::invalid_status(IssuerStatus::Suspended).is_retryable());
315 }
316
317 #[test]
318 fn test_http_error_suggested_retry_delay() {
319 assert_eq!(
321 HttpError::network("err").suggested_retry_delay_ms(),
322 Some(500)
323 );
324
325 assert_eq!(HttpError::Timeout.suggested_retry_delay_ms(), Some(1000));
327
328 assert_eq!(
330 HttpError::status(500, "error").suggested_retry_delay_ms(),
331 Some(2000)
332 );
333 assert_eq!(
334 HttpError::status(503, "error").suggested_retry_delay_ms(),
335 Some(2000)
336 );
337
338 assert_eq!(
340 HttpError::rate_limited(60).suggested_retry_delay_ms(),
341 Some(60_000)
342 );
343
344 assert_eq!(
346 HttpError::status(400, "error").suggested_retry_delay_ms(),
347 None
348 );
349 assert_eq!(
350 HttpError::status(401, "error").suggested_retry_delay_ms(),
351 None
352 );
353 assert_eq!(
354 HttpError::status(404, "error").suggested_retry_delay_ms(),
355 None
356 );
357 }
358
359 #[test]
360 fn test_lastid_error_suggested_retry_delay() {
361 assert_eq!(
363 LastIDError::Http(HttpError::Timeout).suggested_retry_delay_ms(),
364 Some(1000)
365 );
366
367 assert_eq!(
369 LastIDError::Timeout(300).suggested_retry_delay_ms(),
370 Some(2000)
371 );
372
373 assert_eq!(
375 LastIDError::config("error").suggested_retry_delay_ms(),
376 None
377 );
378 assert_eq!(
379 LastIDError::crypto("error").suggested_retry_delay_ms(),
380 None
381 );
382 }
383
384 #[test]
385 fn test_trust_registry_error_suggested_retry_delay() {
386 assert_eq!(
388 TrustRegistryError::Http(HttpError::Timeout).suggested_retry_delay_ms(),
389 Some(1000)
390 );
391
392 assert_eq!(
394 TrustRegistryError::issuer_not_found("did:test").suggested_retry_delay_ms(),
395 None
396 );
397 }
398
399 #[test]
400 fn test_http_error_status_code() {
401 assert_eq!(HttpError::status(404, "not found").status_code(), Some(404));
402 assert_eq!(HttpError::rate_limited(60).status_code(), Some(429));
403 assert_eq!(HttpError::network("error").status_code(), None);
404 assert_eq!(HttpError::Timeout.status_code(), None);
405 }
406
407 #[test]
408 fn test_error_categories() {
409 assert_eq!(LastIDError::config("error").category(), "config");
411 assert_eq!(LastIDError::Http(HttpError::Timeout).category(), "http");
412 assert_eq!(LastIDError::Timeout(60).category(), "timeout");
413
414 assert_eq!(HttpError::network("error").category(), "network");
416 assert_eq!(HttpError::Timeout.category(), "timeout");
417 assert_eq!(HttpError::rate_limited(60).category(), "rate_limited");
418 assert_eq!(HttpError::status(500, "error").category(), "server_error");
419 assert_eq!(HttpError::status(400, "error").category(), "client_error");
420
421 assert_eq!(
423 TrustRegistryError::issuer_not_found("did").category(),
424 "issuer_not_found"
425 );
426 assert_eq!(
427 TrustRegistryError::invalid_status(IssuerStatus::Suspended).category(),
428 "invalid_issuer_status"
429 );
430 }
431}