A better Rust ATProto crate
1use jacquard_common::session::SessionStoreError;
2use miette::Diagnostic;
3#[cfg(feature = "scope-check")]
4use smol_str::SmolStr;
5
6use crate::request::RequestError;
7use crate::resolver::ResolverError;
8
9/// High-level errors emitted by OAuth helpers.
10#[derive(Debug, thiserror::Error, Diagnostic)]
11#[non_exhaustive]
12pub enum OAuthError {
13 /// An error occurred during identity or metadata resolution.
14 #[error(transparent)]
15 #[diagnostic(code(jacquard_oauth::resolver))]
16 Resolver(#[from] ResolverError),
17
18 /// An error occurred while making an OAuth HTTP request.
19 #[error(transparent)]
20 #[diagnostic(code(jacquard_oauth::request))]
21 Request(#[from] RequestError),
22
23 /// An error occurred reading or writing session state.
24 #[error(transparent)]
25 #[diagnostic(code(jacquard_oauth::storage))]
26 Storage(#[from] SessionStoreError),
27
28 /// An error occurred during DPoP proof generation or validation.
29 #[error(transparent)]
30 #[diagnostic(code(jacquard_oauth::dpop))]
31 Dpop(#[from] crate::dpop::DpopError),
32
33 /// An error occurred with the client's key set.
34 #[error(transparent)]
35 #[diagnostic(code(jacquard_oauth::keyset))]
36 Keyset(#[from] crate::keyset::Error),
37
38 /// An ATProto-specific OAuth error (e.g. scope validation, client ID).
39 #[error(transparent)]
40 #[diagnostic(code(jacquard_oauth::atproto))]
41 Atproto(#[from] crate::atproto::Error),
42
43 /// An error occurred managing or refreshing an OAuth session.
44 #[error(transparent)]
45 #[diagnostic(code(jacquard_oauth::session))]
46 Session(#[from] crate::session::Error),
47
48 /// A JSON serialization or deserialization error.
49 #[error(transparent)]
50 #[diagnostic(code(jacquard_oauth::serde_json))]
51 SerdeJson(#[from] serde_json::Error),
52
53 /// A URI parse error.
54 #[error(transparent)]
55 #[diagnostic(code(jacquard_oauth::url))]
56 Url(#[from] jacquard_common::deps::fluent_uri::ParseError),
57
58 /// A form (URL-encoded) serialization error.
59 #[error(transparent)]
60 #[diagnostic(code(jacquard_oauth::form))]
61 Form(#[from] serde_html_form::ser::Error),
62
63 /// Invalid OAuth helper input.
64 #[error("invalid OAuth request: {0}")]
65 #[diagnostic(code(jacquard_oauth::invalid_request))]
66 InvalidRequest(String),
67
68 /// An error validating an authorization callback.
69 #[error(transparent)]
70 #[diagnostic(code(jacquard_oauth::callback))]
71 Callback(#[from] CallbackError),
72
73 /// An error occurred checking request scope permissions.
74 #[cfg(feature = "scope-check")]
75 #[error(transparent)]
76 #[diagnostic(transparent)]
77 ScopeCheck(#[from] ScopeError),
78}
79
80/// Typed callback validation errors (redirect handling).
81#[derive(Debug, thiserror::Error, Diagnostic)]
82#[non_exhaustive]
83pub enum CallbackError {
84 /// The `state` parameter was absent from the authorization callback.
85 ///
86 /// State is required to prevent CSRF attacks per RFC 6749 §10.12.
87 #[error("missing state parameter in callback")]
88 #[diagnostic(code(jacquard_oauth::callback::missing_state))]
89 MissingState,
90 /// The `iss` (issuer) parameter was absent from the authorization callback.
91 ///
92 /// RFC 9207 requires `iss` to be present so that clients can reject
93 /// mix-up attacks from malicious authorization servers.
94 #[error("missing `iss` parameter")]
95 #[diagnostic(code(jacquard_oauth::callback::missing_iss))]
96 MissingIssuer,
97 /// The issuer in the callback did not match the expected authorization server.
98 #[error("issuer mismatch: expected {expected}, got {got}")]
99 #[diagnostic(code(jacquard_oauth::callback::issuer_mismatch))]
100 IssuerMismatch {
101 /// The issuer that was expected.
102 expected: String,
103 /// The issuer that was actually present in the callback.
104 got: String,
105 },
106 /// The authorization request timed out before a callback was received.
107 #[error("timeout")]
108 #[diagnostic(code(jacquard_oauth::callback::timeout))]
109 Timeout,
110 /// The local loopback callback server could not start or accept callbacks.
111 #[error("loopback callback server error: {0}")]
112 #[diagnostic(code(jacquard_oauth::callback::loopback_server))]
113 LoopbackServer(String),
114 /// An error occurred resolving permission sets during session creation.
115 #[cfg(feature = "scope-check")]
116 #[error("scope resolution failed: {detail}")]
117 #[diagnostic(code(jacquard_oauth::callback::scope_resolution))]
118 ScopeResolution {
119 /// Description of the resolution failure.
120 detail: String,
121 },
122}
123
124/// Error returned when a request's required scope is not covered by the session's granted scopes.
125#[cfg(feature = "scope-check")]
126#[derive(Debug, thiserror::Error, Diagnostic)]
127#[error("request to `{nsid}` not permitted: no granted scope covers this endpoint")]
128#[diagnostic(
129 code(jacquard_oauth::scope_check),
130 help("granted scopes: {granted}. The endpoint requires an `rpc:` scope covering `{nsid}`.")
131)]
132pub struct ScopeError {
133 /// The NSID of the XRPC method that was denied.
134 pub nsid: SmolStr,
135 /// Human-readable summary of the granted scopes for diagnostic output.
136 pub granted: SmolStr,
137}
138
139/// Convenience alias for `Result<T, OAuthError>`.
140pub type Result<T> = core::result::Result<T, OAuthError>;