1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! ATProto authentication via OAuth 2.0 + DPoP.
//!
//! As of 0.3.0 this crate no longer supports legacy App Password auth. All
//! authenticated calls against a user's PDS go through a
//! [`proto_blue_oauth::OAuthSession`], which automatically attaches DPoP
//! proofs and rotates per-origin nonces.
//!
//! # Runtime requirement
//!
//! Every async function in this module uses `reqwest` (via proto-blue-oauth),
//! which **requires a Tokio runtime** on native targets. Calling them from a
//! Bevy system directly will panic with `"must be called from within a Tokio
//! runtime"`. On WASM the browser's `fetch` backend is used and no runtime is
//! required.
//!
//! The recommended pattern is to spawn a detached Tokio task from a Bevy
//! `IoTaskPool` task, or to call them from inside a
//! `tokio::runtime::Runtime::block_on` block.
use Arc;
use crateSymbiosError;
use *;
use ;
use Deserialize;
/// Response from `com.atproto.server.getServiceAuth`.
/// An authenticated ATProto session, backed by OAuth 2.0 + DPoP.
///
/// Inserted as a Bevy [`Resource`] after the host app finishes the OAuth
/// authorization-code exchange. Holds the user's identity (DID + handle), the
/// PDS base URL discovered during auth, and a shared [`OAuthSession`] that
/// stamps every outgoing request with a DPoP proof bound to the user's
/// private key.
///
/// Unlike the 0.2 App-Password-backed struct, this type is *not*
/// `Serialize`/`Deserialize`: the DPoP private key lives inside the
/// `OAuthSession` and must not be persisted by naive disk dumps. Host
/// applications that want to persist sessions should use
/// [`proto_blue_oauth::AuthState`] / [`proto_blue_oauth::TokenSet`] directly
/// and rebuild an `OAuthSession` on resume.
/// Request a service auth JWT from the user's PDS, targeting a specific
/// audience DID.
///
/// Service auth tokens are signed with the user's `#atproto` signing key
/// (held by the PDS on behalf of the user). Third-party services such as
/// relay servers verify them by resolving the user's DID document and
/// checking the signature against the `#atproto` verification key.
///
/// This is the correct token type for authenticating to relay servers: the
/// OAuth access token is DPoP-bound and cannot be handed off to a different
/// service.
///
/// # Arguments
///
/// - `aud` — The DID of the target service (e.g. `"did:web:relay.example.com"`).
pub async
/// Best-effort logout: revoke the session's tokens at the OAuth provider's
/// revocation endpoint (RFC 7009).
///
/// Refresh-then-access ordering is intentional: per RFC 7009 §2.1, revoking a
/// refresh token typically also invalidates every access token derived from
/// it on the same authorization grant, so revoking it first means even if the
/// access-token call fails the refresh token is already dead and the access
/// token will time out on its own short TTL. Both calls are attempted; the
/// first error short-circuits and is returned, leaving any later token still
/// live until expiry — consistent with RFC 7009's "implementation-specific
/// best effort" guidance.
///
/// # Local state
///
/// This function only handles the *server-side* revocation. The host
/// application is still responsible for clearing local state — calling
/// [`crate::signaller::TokenSource::set`]`(None)`, removing the
/// [`AtprotoSession`] resource, and dropping any persisted session blobs.
/// Crucially, **clear local state regardless of whether this returns
/// `Ok` or `Err`**: a failed revocation is a network glitch, not a reason
/// to leave the user partly logged in.
///
/// # Arguments
///
/// - `session` — the active [`AtprotoSession`] whose tokens should be revoked.
/// - `oauth_client` — the [`OAuthClient`] used to acquire `session` (carries
/// the registered client_id required by the revocation request).
/// - `server_metadata` — the OAuth server metadata for the user's PDS,
/// discovered during the original auth flow. Supplies `revocation_endpoint`.
///
/// `oauth_client` and `server_metadata` are taken as parameters rather than
/// pulled off [`AtprotoSession`] to avoid bloating that resource with auth
/// machinery the rest of the crate never reads. Host applications typically
/// keep them together with the session in their own login bundle (see e.g.
/// `symbios-overlands`'s `OauthRefreshCtx`).
///
/// # Runtime
///
/// Like [`get_service_auth`], requires a Tokio runtime on native targets.
///
/// # Example
///
/// ```rust,no_run
/// # use bevy_symbios_multiuser::auth::{AtprotoSession, logout};
/// # use bevy_symbios_multiuser::signaller::TokenSource;
/// # use proto_blue_oauth::{OAuthClient, OAuthServerMetadata};
/// # async fn run(
/// # session: AtprotoSession,
/// # oauth_client: OAuthClient,
/// # server_metadata: OAuthServerMetadata,
/// # token_source: TokenSource,
/// # ) {
/// // Best-effort revoke. Clear local state whether or not this succeeds.
/// if let Err(e) = logout(&session, &oauth_client, &server_metadata).await {
/// tracing::warn!(%e, "token revocation failed; clearing local state anyway");
/// }
/// token_source.set(None);
/// // …then drop the AtprotoSession resource and any persisted session blob.
/// # }
/// ```
pub async
/// Minimal percent-encoder for the `aud` query parameter. `did:` identifiers
/// contain `:` which is reserved in a query value (RFC 3986 §3.4); encoding it
/// keeps the URL well-formed.