+1
-1
crates/jacquard-common/src/xrpc.rs
+1
-1
crates/jacquard-common/src/xrpc.rs
···
271
#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
272
pub trait XrpcClient: HttpClient {
273
/// Get the base URI for the client.
274
-
fn base_uri(&self) -> impl Future<Output = Url>;
275
276
/// Set the base URI for the client.
277
fn set_base_uri(&self, url: Url) -> impl Future<Output = ()> {
···
271
#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
272
pub trait XrpcClient: HttpClient {
273
/// Get the base URI for the client.
274
+
fn base_uri(&self) -> impl Future<Output = CowStr<'static>>;
275
276
/// Set the base URI for the client.
277
fn set_base_uri(&self, url: Url) -> impl Future<Output = ()> {
+8
-4
crates/jacquard-common/src/xrpc/subscription.rs
+8
-4
crates/jacquard-common/src/xrpc/subscription.rs
···
13
use std::marker::PhantomData;
14
use url::Url;
15
16
use crate::error::DecodeError;
17
use crate::stream::StreamError;
18
use crate::websocket::{WebSocketClient, WebSocketConnection, WsSink, WsStream};
···
792
#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
793
pub trait SubscriptionClient: WebSocketClient {
794
/// Get the base URI for the client.
795
-
fn base_uri(&self) -> impl Future<Output = Url>;
796
797
/// Get the subscription options for the client.
798
fn subscription_opts(&self) -> impl Future<Output = SubscriptionOptions<'_>> {
···
847
/// or when you want to handle auth manually via headers.
848
pub struct BasicSubscriptionClient<W: WebSocketClient> {
849
client: W,
850
-
base_uri: Url,
851
opts: SubscriptionOptions<'static>,
852
}
853
854
impl<W: WebSocketClient> BasicSubscriptionClient<W> {
855
/// Create a new basic subscription client with the given WebSocket client and base URI.
856
pub fn new(client: W, base_uri: Url) -> Self {
857
Self {
858
client,
859
-
base_uri,
860
opts: SubscriptionOptions::default(),
861
}
862
}
···
890
}
891
892
impl<W: WebSocketClient> SubscriptionClient for BasicSubscriptionClient<W> {
893
-
async fn base_uri(&self) -> Url {
894
self.base_uri.clone()
895
}
896
···
934
Self: Sync,
935
{
936
let base = self.base_uri().await;
937
self.subscription(base)
938
.with_options(opts)
939
.subscribe(params)
···
950
Sub: XrpcSubscription + Send + Sync,
951
{
952
let base = self.base_uri().await;
953
self.subscription(base)
954
.with_options(opts)
955
.subscribe(params)
···
13
use std::marker::PhantomData;
14
use url::Url;
15
16
+
use crate::cowstr::ToCowStr;
17
use crate::error::DecodeError;
18
use crate::stream::StreamError;
19
use crate::websocket::{WebSocketClient, WebSocketConnection, WsSink, WsStream};
···
793
#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
794
pub trait SubscriptionClient: WebSocketClient {
795
/// Get the base URI for the client.
796
+
fn base_uri(&self) -> impl Future<Output = CowStr<'static>>;
797
798
/// Get the subscription options for the client.
799
fn subscription_opts(&self) -> impl Future<Output = SubscriptionOptions<'_>> {
···
848
/// or when you want to handle auth manually via headers.
849
pub struct BasicSubscriptionClient<W: WebSocketClient> {
850
client: W,
851
+
base_uri: CowStr<'static>,
852
opts: SubscriptionOptions<'static>,
853
}
854
855
impl<W: WebSocketClient> BasicSubscriptionClient<W> {
856
/// Create a new basic subscription client with the given WebSocket client and base URI.
857
pub fn new(client: W, base_uri: Url) -> Self {
858
+
let base_uri = base_uri.as_str().trim_end_matches("/");
859
Self {
860
client,
861
+
base_uri: base_uri.to_cowstr().into_static(),
862
opts: SubscriptionOptions::default(),
863
}
864
}
···
892
}
893
894
impl<W: WebSocketClient> SubscriptionClient for BasicSubscriptionClient<W> {
895
+
async fn base_uri(&self) -> CowStr<'static> {
896
self.base_uri.clone()
897
}
898
···
936
Self: Sync,
937
{
938
let base = self.base_uri().await;
939
+
let base = Url::parse(&base).expect("Failed to parse base URL");
940
self.subscription(base)
941
.with_options(opts)
942
.subscribe(params)
···
953
Sub: XrpcSubscription + Send + Sync,
954
{
955
let base = self.base_uri().await;
956
+
let base = Url::parse(&base).expect("Failed to parse base URL");
957
self.subscription(base)
958
.with_options(opts)
959
.subscribe(params)
+50
-43
crates/jacquard-oauth/src/atproto.rs
+50
-43
crates/jacquard-oauth/src/atproto.rs
···
2
3
use crate::types::OAuthClientMetadata;
4
use crate::{keyset::Keyset, scopes::Scope};
5
-
use jacquard_common::CowStr;
6
use serde::{Deserialize, Serialize};
7
use smol_str::{SmolStr, ToSmolStr};
8
use thiserror::Error;
···
217
};
218
219
Ok(OAuthClientMetadata {
220
-
client_id: metadata.client_id,
221
-
client_uri: metadata.client_uri,
222
-
redirect_uris: metadata.redirect_uris,
223
token_endpoint_auth_method: Some(auth_method.into()),
224
grant_types: if keyset.is_some() {
225
Some(metadata.grant_types.into_iter().map(|v| v.into()).collect())
···
228
},
229
scope: Some(Scope::serialize_multiple(metadata.scopes.as_slice())),
230
dpop_bound_access_tokens: Some(true),
231
-
jwks_uri,
232
jwks,
233
token_endpoint_auth_signing_alg: if keyset.is_some() {
234
Some(CowStr::new_static("ES256"))
···
236
None
237
},
238
client_name: metadata.client_name,
239
-
logo_uri: metadata.logo_uri,
240
-
tos_uri: metadata.tos_uri,
241
-
privacy_policy_uri: metadata.privacy_policy_uri,
242
})
243
}
244
···
265
atproto_client_metadata(AtprotoClientMetadata::new_localhost(None, None), &None)
266
.unwrap(),
267
OAuthClientMetadata {
268
-
client_id: Url::from_str("http://localhost").unwrap(),
269
client_uri: None,
270
redirect_uris: vec![
271
-
Url::from_str("http://127.0.0.1/").unwrap(),
272
-
Url::from_str("http://[::1]/").unwrap(),
273
],
274
scope: Some(CowStr::new_static("atproto")),
275
grant_types: None,
···
289
#[test]
290
fn test_localhost_client_metadata_custom() {
291
assert_eq!(
292
-
atproto_client_metadata(AtprotoClientMetadata::new_localhost(
293
-
Some(vec![
294
-
Url::from_str("http://127.0.0.1/callback").unwrap(),
295
-
Url::from_str("http://[::1]/callback").unwrap(),
296
-
]),
297
-
Some(
298
-
vec![
299
Scope::Atproto,
300
Scope::Transition(TransitionScope::Generic),
301
Scope::parse("account:email").unwrap()
302
-
]
303
-
)
304
-
), &None)
305
.expect("failed to convert metadata"),
306
OAuthClientMetadata {
307
-
client_id: Url::from_str(
308
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric"
309
-
).unwrap(),
310
client_uri: None,
311
redirect_uris: vec![
312
-
Url::from_str("http://127.0.0.1/callback").unwrap(),
313
// TODO: fix this so that it respects IPv6
314
-
Url::from_str("http://127.0.0.1/callback").unwrap(),
315
],
316
-
scope: Some(CowStr::new_static("account:email atproto transition:generic")),
317
grant_types: None,
318
token_endpoint_auth_method: Some(AuthMethod::None.into()),
319
dpop_bound_access_tokens: Some(true),
···
334
{
335
let out = atproto_client_metadata(
336
AtprotoClientMetadata::new_localhost(
337
-
Some(vec![Url::from_str("https://127.0.0.1/").unwrap()]),
338
None,
339
),
340
&None,
···
343
assert_eq!(
344
out,
345
OAuthClientMetadata {
346
-
client_id: Url::from_str(
347
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F"
348
-
)
349
-
.unwrap(),
350
client_uri: None,
351
-
redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()],
352
scope: Some(CowStr::new_static("atproto")),
353
grant_types: None,
354
token_endpoint_auth_method: Some(AuthMethod::None.into()),
···
366
{
367
let out = atproto_client_metadata(
368
AtprotoClientMetadata::new_localhost(
369
-
Some(vec![Url::from_str("http://localhost:8000/").unwrap()]),
370
None,
371
),
372
&None,
···
375
assert_eq!(
376
out,
377
OAuthClientMetadata {
378
-
client_id: Url::from_str(
379
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2F"
380
-
)
381
-
.unwrap(),
382
client_uri: None,
383
-
redirect_uris: vec![Url::from_str("http://127.0.0.1:8000/").unwrap()],
384
scope: Some(CowStr::new_static("atproto")),
385
grant_types: None,
386
token_endpoint_auth_method: Some(AuthMethod::None.into()),
···
407
assert_eq!(
408
out,
409
OAuthClientMetadata {
410
-
client_id: Url::from_str(
411
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F"
412
-
)
413
-
.unwrap(),
414
client_uri: None,
415
-
redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()],
416
scope: Some(CowStr::new_static("atproto")),
417
grant_types: None,
418
token_endpoint_auth_method: Some(AuthMethod::None.into()),
···
465
atproto_client_metadata(metadata, &Some(keyset.clone()))
466
.expect("failed to convert metadata"),
467
OAuthClientMetadata {
468
-
client_id: Url::from_str("https://example.com/client_metadata.json").unwrap(),
469
-
client_uri: Some(Url::from_str("https://example.com").unwrap()),
470
-
redirect_uris: vec![Url::from_str("https://example.com/callback").unwrap()],
471
scope: Some(CowStr::new_static("atproto")),
472
grant_types: Some(vec![CowStr::new_static("authorization_code")]),
473
token_endpoint_auth_method: Some(AuthMethod::PrivateKeyJwt.into()),
···
2
3
use crate::types::OAuthClientMetadata;
4
use crate::{keyset::Keyset, scopes::Scope};
5
+
use jacquard_common::cowstr::ToCowStr;
6
+
use jacquard_common::{CowStr, IntoStatic};
7
use serde::{Deserialize, Serialize};
8
use smol_str::{SmolStr, ToSmolStr};
9
use thiserror::Error;
···
218
};
219
220
Ok(OAuthClientMetadata {
221
+
client_id: metadata.client_id.to_cowstr().into_static(),
222
+
client_uri: metadata.client_uri.map(|u| u.to_cowstr().into_static()),
223
+
redirect_uris: metadata
224
+
.redirect_uris
225
+
.iter()
226
+
.map(|u| u.to_cowstr().into_static())
227
+
.collect(),
228
token_endpoint_auth_method: Some(auth_method.into()),
229
grant_types: if keyset.is_some() {
230
Some(metadata.grant_types.into_iter().map(|v| v.into()).collect())
···
233
},
234
scope: Some(Scope::serialize_multiple(metadata.scopes.as_slice())),
235
dpop_bound_access_tokens: Some(true),
236
+
jwks_uri: jwks_uri.map(|u| u.to_cowstr().into_static()),
237
jwks,
238
token_endpoint_auth_signing_alg: if keyset.is_some() {
239
Some(CowStr::new_static("ES256"))
···
241
None
242
},
243
client_name: metadata.client_name,
244
+
logo_uri: metadata.logo_uri.map(|u| u.to_cowstr().into_static()),
245
+
tos_uri: metadata.tos_uri.map(|u| u.to_cowstr().into_static()),
246
+
privacy_policy_uri: metadata
247
+
.privacy_policy_uri
248
+
.map(|u| u.to_cowstr().into_static()),
249
})
250
}
251
···
272
atproto_client_metadata(AtprotoClientMetadata::new_localhost(None, None), &None)
273
.unwrap(),
274
OAuthClientMetadata {
275
+
client_id: CowStr::new_static("http://localhost"),
276
client_uri: None,
277
redirect_uris: vec![
278
+
CowStr::new_static("http://127.0.0.1/"),
279
+
CowStr::new_static("http://[::1]/"),
280
],
281
scope: Some(CowStr::new_static("atproto")),
282
grant_types: None,
···
296
#[test]
297
fn test_localhost_client_metadata_custom() {
298
assert_eq!(
299
+
atproto_client_metadata(
300
+
AtprotoClientMetadata::new_localhost(
301
+
Some(vec![
302
+
Url::parse("http://127.0.0.1/callback").unwrap(),
303
+
Url::parse("http://[::1]/callback").unwrap(),
304
+
]),
305
+
Some(vec![
306
Scope::Atproto,
307
Scope::Transition(TransitionScope::Generic),
308
Scope::parse("account:email").unwrap()
309
+
])
310
+
),
311
+
&None
312
+
)
313
.expect("failed to convert metadata"),
314
OAuthClientMetadata {
315
+
client_id: CowStr::new_static(
316
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric"
317
+
),
318
client_uri: None,
319
redirect_uris: vec![
320
+
CowStr::new_static("http://127.0.0.1/callback"),
321
// TODO: fix this so that it respects IPv6
322
+
CowStr::new_static("http://127.0.0.1/callback"),
323
],
324
+
scope: Some(CowStr::new_static(
325
+
"account:email atproto transition:generic"
326
+
)),
327
grant_types: None,
328
token_endpoint_auth_method: Some(AuthMethod::None.into()),
329
dpop_bound_access_tokens: Some(true),
···
344
{
345
let out = atproto_client_metadata(
346
AtprotoClientMetadata::new_localhost(
347
+
Some(vec![Url::from_str("https://127.0.0.1").unwrap()]),
348
None,
349
),
350
&None,
···
353
assert_eq!(
354
out,
355
OAuthClientMetadata {
356
+
client_id: CowStr::new_static(
357
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F"
358
+
),
359
client_uri: None,
360
+
redirect_uris: vec![CowStr::new_static("http://127.0.0.1")],
361
scope: Some(CowStr::new_static("atproto")),
362
grant_types: None,
363
token_endpoint_auth_method: Some(AuthMethod::None.into()),
···
375
{
376
let out = atproto_client_metadata(
377
AtprotoClientMetadata::new_localhost(
378
+
Some(vec![Url::from_str("http://localhost:8000").unwrap()]),
379
None,
380
),
381
&None,
···
384
assert_eq!(
385
out,
386
OAuthClientMetadata {
387
+
client_id: CowStr::new_static(
388
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2F"
389
+
),
390
client_uri: None,
391
+
redirect_uris: vec![CowStr::new_static("http://127.0.0.1:8000")],
392
scope: Some(CowStr::new_static("atproto")),
393
grant_types: None,
394
token_endpoint_auth_method: Some(AuthMethod::None.into()),
···
415
assert_eq!(
416
out,
417
OAuthClientMetadata {
418
+
client_id: CowStr::new_static(
419
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F"
420
+
),
421
client_uri: None,
422
+
redirect_uris: vec![CowStr::new_static("http://127.0.0.1/")],
423
scope: Some(CowStr::new_static("atproto")),
424
grant_types: None,
425
token_endpoint_auth_method: Some(AuthMethod::None.into()),
···
472
atproto_client_metadata(metadata, &Some(keyset.clone()))
473
.expect("failed to convert metadata"),
474
OAuthClientMetadata {
475
+
client_id: CowStr::new_static("https://example.com/client_metadata.json"),
476
+
client_uri: Some(CowStr::new_static("https://example.com")),
477
+
redirect_uris: vec![CowStr::new_static("https://example.com/callback")],
478
scope: Some(CowStr::new_static("atproto")),
479
grant_types: Some(vec![CowStr::new_static("authorization_code")]),
480
token_endpoint_auth_method: Some(AuthMethod::PrivateKeyJwt.into()),
+25
-23
crates/jacquard-oauth/src/client.rs
+25
-23
crates/jacquard-oauth/src/client.rs
···
11
};
12
use jacquard_common::{
13
AuthorizationToken, CowStr, IntoStatic,
14
error::{AuthError, ClientError, XrpcResult},
15
http_client::HttpClient,
16
types::{did::Did, string::Handle},
···
29
resolver::{DidDocResponse, IdentityError, IdentityResolver, ResolverOptions},
30
};
31
use jose_jwk::JwkSet;
32
use std::{future::Future, sync::Arc};
33
use tokio::sync::RwLock;
34
use url::Url;
···
40
{
41
pub registry: Arc<SessionRegistry<T, S>>,
42
pub options: RwLock<CallOptions<'static>>,
43
-
pub endpoint: RwLock<Option<Url>>,
44
pub client: Arc<T>,
45
}
46
···
192
193
#[derive(serde::Serialize)]
194
struct Parameters<'s> {
195
-
client_id: Url,
196
request_uri: CowStr<'s>,
197
}
198
Ok(metadata.server_metadata.authorization_endpoint.to_string()
199
+ "?"
200
+ &serde_html_form::to_string(Parameters {
201
-
client_id: metadata.client_metadata.client_id.clone(),
202
request_uri: auth_req_info.request_uri,
203
})
204
.unwrap())
···
218
219
let metadata = self
220
.client
221
-
.get_authorization_server_metadata(&auth_req_info.authserver_url)
222
.await?;
223
224
if let Some(iss) = params.iss {
···
262
let client_data = ClientSessionData {
263
account_did: token_set.sub.clone(),
264
session_id: auth_req_info.state,
265
-
host_url: Url::parse(&token_set.iss).expect("Failed to parse host URL"),
266
-
authserver_url: auth_req_info.authserver_url,
267
authserver_token_endpoint: auth_req_info.authserver_token_endpoint,
268
authserver_revocation_endpoint: auth_req_info.authserver_revocation_endpoint,
269
scopes,
···
347
S: ClientAuthStore + Send + Sync + 'static,
348
T: OAuthResolver + DpopExt + Send + Sync + 'static,
349
{
350
-
async fn base_uri(&self) -> Url {
351
-
self.endpoint.read().await.clone().unwrap_or(
352
-
Url::parse("https://public.api.bsky.app").expect("public appview should be valid url"),
353
-
)
354
}
355
356
async fn opts(&self) -> CallOptions<'_> {
···
364
365
async fn set_base_uri(&self, url: Url) {
366
let mut guard = self.endpoint.write().await;
367
-
*guard = Some(url);
368
}
369
370
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
···
387
{
388
let base_uri = self.base_uri().await;
389
self.client
390
-
.xrpc(base_uri.clone())
391
.with_options(opts.clone())
392
.send(&request)
393
.await
···
470
(data.account_did.clone(), data.session_id.clone())
471
}
472
473
-
pub async fn endpoint(&self) -> Url {
474
self.data.read().await.host_url.clone()
475
}
476
···
574
T: OAuthResolver + DpopExt + XrpcExt + Send + Sync + 'static,
575
W: Send + Sync,
576
{
577
-
async fn base_uri(&self) -> Url {
578
self.data.read().await.host_url.clone()
579
}
580
···
589
590
async fn set_base_uri(&self, url: Url) {
591
let mut guard = self.data.write().await;
592
-
guard.host_url = url;
593
}
594
595
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
···
614
opts.auth = Some(self.access_token().await);
615
let guard = self.data.read().await;
616
let mut dpop = guard.dpop_data.clone();
617
let http_response = self
618
.client
619
.dpop_call(&mut dpop)
···
718
use jacquard_common::StreamError;
719
720
let base_uri = <Self as XrpcClient>::base_uri(self).await;
721
let mut opts = self.options.read().await.clone();
722
opts.auth = Some(self.access_token().await);
723
let http_request = build_http_request(&base_uri, &request, &opts)
···
773
let mut opts = self.options.read().await.clone();
774
opts.auth = Some(self.access_token().await);
775
776
-
let mut url = base_uri;
777
let mut path = url.path().trim_end_matches('/').to_owned();
778
path.push_str("/xrpc/");
779
path.push_str(<Str::Request as jacquard_common::xrpc::XrpcRequest>::NSID);
···
919
T: OAuthResolver + Send + Sync + 'static,
920
W: WebSocketClient + Send + Sync,
921
{
922
-
async fn base_uri(&self) -> Url {
923
-
#[cfg(not(target_arch = "wasm32"))]
924
-
if tokio::runtime::Handle::try_current().is_ok() {
925
-
return tokio::task::block_in_place(|| self.data.blocking_read().host_url.clone());
926
-
}
927
-
928
-
self.data.blocking_read().host_url.clone()
929
}
930
931
async fn subscription_opts(&self) -> jacquard_common::xrpc::SubscriptionOptions<'_> {
···
961
{
962
use jacquard_common::xrpc::SubscriptionExt;
963
let base = self.base_uri().await;
964
self.subscription(base)
965
.with_options(opts)
966
.subscribe(params)
···
11
};
12
use jacquard_common::{
13
AuthorizationToken, CowStr, IntoStatic,
14
+
cowstr::ToCowStr,
15
error::{AuthError, ClientError, XrpcResult},
16
http_client::HttpClient,
17
types::{did::Did, string::Handle},
···
30
resolver::{DidDocResponse, IdentityError, IdentityResolver, ResolverOptions},
31
};
32
use jose_jwk::JwkSet;
33
+
use smol_str::ToSmolStr;
34
use std::{future::Future, sync::Arc};
35
use tokio::sync::RwLock;
36
use url::Url;
···
42
{
43
pub registry: Arc<SessionRegistry<T, S>>,
44
pub options: RwLock<CallOptions<'static>>,
45
+
pub endpoint: RwLock<Option<CowStr<'static>>>,
46
pub client: Arc<T>,
47
}
48
···
194
195
#[derive(serde::Serialize)]
196
struct Parameters<'s> {
197
+
client_id: CowStr<'s>,
198
request_uri: CowStr<'s>,
199
}
200
Ok(metadata.server_metadata.authorization_endpoint.to_string()
201
+ "?"
202
+ &serde_html_form::to_string(Parameters {
203
+
client_id: metadata.client_metadata.client_id,
204
request_uri: auth_req_info.request_uri,
205
})
206
.unwrap())
···
220
221
let metadata = self
222
.client
223
+
.get_authorization_server_metadata(&auth_req_info.authserver_url.to_cowstr())
224
.await?;
225
226
if let Some(iss) = params.iss {
···
264
let client_data = ClientSessionData {
265
account_did: token_set.sub.clone(),
266
session_id: auth_req_info.state,
267
+
host_url: token_set.iss.clone(),
268
+
authserver_url: auth_req_info.authserver_url.to_cowstr(),
269
authserver_token_endpoint: auth_req_info.authserver_token_endpoint,
270
authserver_revocation_endpoint: auth_req_info.authserver_revocation_endpoint,
271
scopes,
···
349
S: ClientAuthStore + Send + Sync + 'static,
350
T: OAuthResolver + DpopExt + Send + Sync + 'static,
351
{
352
+
async fn base_uri(&self) -> CowStr<'static> {
353
+
self.endpoint
354
+
.read()
355
+
.await
356
+
.clone()
357
+
.unwrap_or(CowStr::new_static("https://public.api.bsky.app"))
358
}
359
360
async fn opts(&self) -> CallOptions<'_> {
···
368
369
async fn set_base_uri(&self, url: Url) {
370
let mut guard = self.endpoint.write().await;
371
+
*guard = Some(url.to_cowstr().into_static());
372
}
373
374
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
···
391
{
392
let base_uri = self.base_uri().await;
393
self.client
394
+
.xrpc(Url::parse(&base_uri).map_err(|e| ClientError::encode(e.to_smolstr()))?)
395
.with_options(opts.clone())
396
.send(&request)
397
.await
···
474
(data.account_did.clone(), data.session_id.clone())
475
}
476
477
+
pub async fn endpoint(&self) -> CowStr<'static> {
478
self.data.read().await.host_url.clone()
479
}
480
···
578
T: OAuthResolver + DpopExt + XrpcExt + Send + Sync + 'static,
579
W: Send + Sync,
580
{
581
+
async fn base_uri(&self) -> CowStr<'static> {
582
self.data.read().await.host_url.clone()
583
}
584
···
593
594
async fn set_base_uri(&self, url: Url) {
595
let mut guard = self.data.write().await;
596
+
guard.host_url = url.to_cowstr().into_static();
597
}
598
599
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
···
618
opts.auth = Some(self.access_token().await);
619
let guard = self.data.read().await;
620
let mut dpop = guard.dpop_data.clone();
621
+
let base_uri = Url::parse(&base_uri).map_err(|e| ClientError::transport(e))?;
622
let http_response = self
623
.client
624
.dpop_call(&mut dpop)
···
723
use jacquard_common::StreamError;
724
725
let base_uri = <Self as XrpcClient>::base_uri(self).await;
726
+
let base_uri = Url::parse(&base_uri).map_err(|e| StreamError::protocol(e.to_string()))?;
727
let mut opts = self.options.read().await.clone();
728
opts.auth = Some(self.access_token().await);
729
let http_request = build_http_request(&base_uri, &request, &opts)
···
779
let mut opts = self.options.read().await.clone();
780
opts.auth = Some(self.access_token().await);
781
782
+
let mut url = Url::parse(&base_uri).map_err(|e| StreamError::encode(e))?;
783
let mut path = url.path().trim_end_matches('/').to_owned();
784
path.push_str("/xrpc/");
785
path.push_str(<Str::Request as jacquard_common::xrpc::XrpcRequest>::NSID);
···
925
T: OAuthResolver + Send + Sync + 'static,
926
W: WebSocketClient + Send + Sync,
927
{
928
+
async fn base_uri(&self) -> CowStr<'static> {
929
+
self.data.read().await.host_url.clone()
930
}
931
932
async fn subscription_opts(&self) -> jacquard_common::xrpc::SubscriptionOptions<'_> {
···
962
{
963
use jacquard_common::xrpc::SubscriptionExt;
964
let base = self.base_uri().await;
965
+
let base = Url::parse(&base).expect("Failed to parse base URL");
966
self.subscription(base)
967
.with_options(opts)
968
.subscribe(params)
+10
-13
crates/jacquard-oauth/src/request.rs
+10
-13
crates/jacquard-oauth/src/request.rs
···
861
use crate::types::{OAuthAuthorizationServerMetadata, OAuthClientMetadata};
862
use bytes::Bytes;
863
use http::{Response as HttpResponse, StatusCode};
864
-
use jacquard_common::http_client::HttpClient;
865
use jacquard_identity::resolver::IdentityResolver;
866
use std::sync::Arc;
867
use tokio::sync::Mutex;
···
895
async fn resolve_handle(
896
&self,
897
_handle: &jacquard_common::types::string::Handle<'_>,
898
-
) -> std::result::Result<
899
-
jacquard_common::types::string::Did<'static>,
900
-
jacquard_identity::resolver::IdentityError,
901
-
> {
902
-
Ok(jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap())
903
}
904
async fn resolve_did_doc(
905
&self,
906
-
_did: &jacquard_common::types::string::Did<'_>,
907
) -> std::result::Result<
908
jacquard_identity::resolver::DidDocResponse,
909
jacquard_identity::resolver::IdentityError,
···
938
OAuthMetadata {
939
server_metadata: server,
940
client_metadata: OAuthClientMetadata {
941
-
client_id: url::Url::parse("https://client").unwrap(),
942
client_uri: None,
943
-
redirect_uris: vec![url::Url::parse("https://client/cb").unwrap()],
944
scope: Some(CowStr::from("atproto")),
945
grant_types: None,
946
token_endpoint_auth_method: Some(CowStr::from("none")),
···
976
let client = MockClient::default();
977
let meta = base_metadata();
978
let session = ClientSessionData {
979
-
account_did: jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap(),
980
session_id: CowStr::from("state"),
981
-
host_url: url::Url::parse("https://pds").unwrap(),
982
-
authserver_url: url::Url::parse("https://issuer").unwrap(),
983
authserver_token_endpoint: CowStr::from("https://issuer/token"),
984
authserver_revocation_endpoint: None,
985
scopes: vec![],
···
990
},
991
token_set: crate::types::TokenSet {
992
iss: CowStr::from("https://issuer"),
993
-
sub: jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap(),
994
aud: CowStr::from("https://pds"),
995
scope: None,
996
refresh_token: None,
···
861
use crate::types::{OAuthAuthorizationServerMetadata, OAuthClientMetadata};
862
use bytes::Bytes;
863
use http::{Response as HttpResponse, StatusCode};
864
+
use jacquard_common::{http_client::HttpClient, types::string::Did};
865
use jacquard_identity::resolver::IdentityResolver;
866
use std::sync::Arc;
867
use tokio::sync::Mutex;
···
895
async fn resolve_handle(
896
&self,
897
_handle: &jacquard_common::types::string::Handle<'_>,
898
+
) -> std::result::Result<Did<'static>, jacquard_identity::resolver::IdentityError> {
899
+
Ok(Did::new_static("did:plc:alice").unwrap())
900
}
901
async fn resolve_did_doc(
902
&self,
903
+
_did: &Did<'_>,
904
) -> std::result::Result<
905
jacquard_identity::resolver::DidDocResponse,
906
jacquard_identity::resolver::IdentityError,
···
935
OAuthMetadata {
936
server_metadata: server,
937
client_metadata: OAuthClientMetadata {
938
+
client_id: CowStr::new_static("https://client"),
939
client_uri: None,
940
+
redirect_uris: vec![CowStr::new_static("https://client/cb")],
941
scope: Some(CowStr::from("atproto")),
942
grant_types: None,
943
token_endpoint_auth_method: Some(CowStr::from("none")),
···
973
let client = MockClient::default();
974
let meta = base_metadata();
975
let session = ClientSessionData {
976
+
account_did: Did::new_static("did:plc:alice").unwrap(),
977
session_id: CowStr::from("state"),
978
+
host_url: CowStr::new_static("https://pds"),
979
+
authserver_url: CowStr::new_static("https://issuer"),
980
authserver_token_endpoint: CowStr::from("https://issuer/token"),
981
authserver_revocation_endpoint: None,
982
scopes: vec![],
···
987
},
988
token_set: crate::types::TokenSet {
989
iss: CowStr::from("https://issuer"),
990
+
sub: Did::new_static("did:plc:alice").unwrap(),
991
aud: CowStr::from("https://pds"),
992
scope: None,
993
refresh_token: None,
+35
-29
crates/jacquard-oauth/src/resolver.rs
+35
-29
crates/jacquard-oauth/src/resolver.rs
···
400
// when the user forgot their handle, or when the handle does not
401
// resolve to a DID)
402
Ok(if input.starts_with("https://") {
403
let url = Url::parse(input).map_err(|_| ResolverError::not_found())?;
404
-
(resolver.resolve_from_service(&url).await?, None)
405
} else {
406
let (metadata, identity) = resolver.resolve_from_identity(input).await?;
407
(metadata, Some(identity))
···
431
#[cfg(not(target_arch = "wasm32"))]
432
async fn resolve_from_service_impl<T: OAuthResolver + Sync + ?Sized>(
433
resolver: &T,
434
-
input: &Url,
435
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
436
// Assume first that input is a PDS URL (as required by ATPROTO)
437
if let Ok(metadata) = resolver.get_resource_server_metadata(input).await {
···
444
#[cfg(target_arch = "wasm32")]
445
async fn resolve_from_service_impl<T: OAuthResolver + ?Sized>(
446
resolver: &T,
447
-
input: &Url,
448
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
449
// Assume first that input is a PDS URL (as required by ATPROTO)
450
if let Ok(metadata) = resolver.get_resource_server_metadata(input).await {
···
466
.map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?;
467
let identity = resolver.resolve_ident_owned(&actor).await?;
468
if let Some(pds) = &identity.pds_endpoint() {
469
-
let metadata = resolver.get_resource_server_metadata(pds).await?;
470
Ok((metadata, identity))
471
} else {
472
Err(ResolverError::did_document("Did doc lacking pds"))
···
495
#[cfg(not(target_arch = "wasm32"))]
496
async fn get_authorization_server_metadata_impl<T: HttpClient + Sync + ?Sized>(
497
client: &T,
498
-
issuer: &Url,
499
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
500
let mut md = resolve_authorization_server(client, issuer).await?;
501
-
// Normalize issuer string to the input URL representation to avoid slash quirks
502
-
md.issuer = jacquard_common::CowStr::from(issuer.as_str()).into_static();
503
Ok(md)
504
}
505
506
#[cfg(target_arch = "wasm32")]
507
async fn get_authorization_server_metadata_impl<T: HttpClient + ?Sized>(
508
client: &T,
509
-
issuer: &Url,
510
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
511
let mut md = resolve_authorization_server(client, issuer).await?;
512
-
// Normalize issuer string to the input URL representation to avoid slash quirks
513
-
md.issuer = jacquard_common::CowStr::from(issuer.as_str()).into_static();
514
Ok(md)
515
}
516
517
#[cfg(not(target_arch = "wasm32"))]
518
async fn get_resource_server_metadata_impl<T: OAuthResolver + Sync + ?Sized>(
519
resolver: &T,
520
-
pds: &Url,
521
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
522
let rs_metadata = resolve_protected_resource_info(resolver, pds).await?;
523
// ATPROTO requires one, and only one, authorization server entry
···
575
#[cfg(target_arch = "wasm32")]
576
async fn get_resource_server_metadata_impl<T: OAuthResolver + ?Sized>(
577
resolver: &T,
578
-
pds: &Url,
579
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
580
let rs_metadata = resolve_protected_resource_info(resolver, pds).await?;
581
// ATPROTO requires one, and only one, authorization server entry
···
685
#[cfg(not(target_arch = "wasm32"))]
686
fn resolve_from_service(
687
&self,
688
-
input: &Url,
689
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send
690
where
691
Self: Sync,
···
696
#[cfg(target_arch = "wasm32")]
697
fn resolve_from_service(
698
&self,
699
-
input: &Url,
700
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> {
701
resolve_from_service_impl(self, input)
702
}
···
733
#[cfg(not(target_arch = "wasm32"))]
734
fn get_authorization_server_metadata(
735
&self,
736
-
issuer: &Url,
737
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send
738
where
739
Self: Sync,
···
744
#[cfg(target_arch = "wasm32")]
745
fn get_authorization_server_metadata(
746
&self,
747
-
issuer: &Url,
748
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> {
749
get_authorization_server_metadata_impl(self, issuer)
750
}
···
752
#[cfg(not(target_arch = "wasm32"))]
753
fn get_resource_server_metadata(
754
&self,
755
-
pds: &Url,
756
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send
757
where
758
Self: Sync,
···
763
#[cfg(target_arch = "wasm32")]
764
fn get_resource_server_metadata(
765
&self,
766
-
pds: &Url,
767
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> {
768
get_resource_server_metadata_impl(self, pds)
769
}
···
771
772
pub async fn resolve_authorization_server<T: HttpClient + ?Sized>(
773
client: &T,
774
-
server: &Url,
775
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
776
-
let url = server
777
-
.join("/.well-known/oauth-authorization-server")
778
-
.map_err(|e| ResolverError::transport(e))?;
779
780
let req = Request::builder()
781
-
.uri(url.to_string())
782
.body(Vec::new())
783
.map_err(|e| ResolverError::transport(e))?;
784
let res = client
···
804
805
pub async fn resolve_protected_resource_info<T: HttpClient + ?Sized>(
806
client: &T,
807
-
server: &Url,
808
) -> Result<OAuthProtectedResourceMetadata<'static>> {
809
-
let url = server
810
-
.join("/.well-known/oauth-protected-resource")
811
-
.map_err(|e| ResolverError::transport(e))?;
812
813
let req = Request::builder()
814
.uri(url.to_string())
···
873
.body(Vec::new())
874
.unwrap(),
875
);
876
-
let issuer = url::Url::parse("https://issuer").unwrap();
877
let err = super::resolve_authorization_server(&client, &issuer)
878
.await
879
.unwrap_err();
···
892
.body(b"{not json}".to_vec())
893
.unwrap(),
894
);
895
-
let issuer = url::Url::parse("https://issuer").unwrap();
896
let err = super::resolve_authorization_server(&client, &issuer)
897
.await
898
.unwrap_err();
···
400
// when the user forgot their handle, or when the handle does not
401
// resolve to a DID)
402
Ok(if input.starts_with("https://") {
403
+
use jacquard_common::cowstr::ToCowStr;
404
+
405
let url = Url::parse(input).map_err(|_| ResolverError::not_found())?;
406
+
(resolver.resolve_from_service(&url.to_cowstr()).await?, None)
407
} else {
408
let (metadata, identity) = resolver.resolve_from_identity(input).await?;
409
(metadata, Some(identity))
···
433
#[cfg(not(target_arch = "wasm32"))]
434
async fn resolve_from_service_impl<T: OAuthResolver + Sync + ?Sized>(
435
resolver: &T,
436
+
input: &CowStr<'_>,
437
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
438
// Assume first that input is a PDS URL (as required by ATPROTO)
439
if let Ok(metadata) = resolver.get_resource_server_metadata(input).await {
···
446
#[cfg(target_arch = "wasm32")]
447
async fn resolve_from_service_impl<T: OAuthResolver + ?Sized>(
448
resolver: &T,
449
+
input: &CowStr<'_>,
450
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
451
// Assume first that input is a PDS URL (as required by ATPROTO)
452
if let Ok(metadata) = resolver.get_resource_server_metadata(input).await {
···
468
.map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?;
469
let identity = resolver.resolve_ident_owned(&actor).await?;
470
if let Some(pds) = &identity.pds_endpoint() {
471
+
use jacquard_common::cowstr::ToCowStr;
472
+
473
+
let metadata = resolver
474
+
.get_resource_server_metadata(&pds.to_cowstr())
475
+
.await?;
476
Ok((metadata, identity))
477
} else {
478
Err(ResolverError::did_document("Did doc lacking pds"))
···
501
#[cfg(not(target_arch = "wasm32"))]
502
async fn get_authorization_server_metadata_impl<T: HttpClient + Sync + ?Sized>(
503
client: &T,
504
+
issuer: &CowStr<'_>,
505
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
506
let mut md = resolve_authorization_server(client, issuer).await?;
507
+
md.issuer = issuer.clone().into_static();
508
Ok(md)
509
}
510
511
#[cfg(target_arch = "wasm32")]
512
async fn get_authorization_server_metadata_impl<T: HttpClient + ?Sized>(
513
client: &T,
514
+
issuer: &CowStr<'_>,
515
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
516
let mut md = resolve_authorization_server(client, issuer).await?;
517
+
md.issuer = issuer.into_static();
518
Ok(md)
519
}
520
521
#[cfg(not(target_arch = "wasm32"))]
522
async fn get_resource_server_metadata_impl<T: OAuthResolver + Sync + ?Sized>(
523
resolver: &T,
524
+
pds: &CowStr<'_>,
525
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
526
let rs_metadata = resolve_protected_resource_info(resolver, pds).await?;
527
// ATPROTO requires one, and only one, authorization server entry
···
579
#[cfg(target_arch = "wasm32")]
580
async fn get_resource_server_metadata_impl<T: OAuthResolver + ?Sized>(
581
resolver: &T,
582
+
pds: &CowStr<'_>,
583
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
584
let rs_metadata = resolve_protected_resource_info(resolver, pds).await?;
585
// ATPROTO requires one, and only one, authorization server entry
···
689
#[cfg(not(target_arch = "wasm32"))]
690
fn resolve_from_service(
691
&self,
692
+
input: &CowStr<'_>,
693
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send
694
where
695
Self: Sync,
···
700
#[cfg(target_arch = "wasm32")]
701
fn resolve_from_service(
702
&self,
703
+
input: &CowStr<'_>,
704
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> {
705
resolve_from_service_impl(self, input)
706
}
···
737
#[cfg(not(target_arch = "wasm32"))]
738
fn get_authorization_server_metadata(
739
&self,
740
+
issuer: &CowStr<'_>,
741
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send
742
where
743
Self: Sync,
···
748
#[cfg(target_arch = "wasm32")]
749
fn get_authorization_server_metadata(
750
&self,
751
+
issuer: &CowStr<'_>,
752
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> {
753
get_authorization_server_metadata_impl(self, issuer)
754
}
···
756
#[cfg(not(target_arch = "wasm32"))]
757
fn get_resource_server_metadata(
758
&self,
759
+
pds: &CowStr<'_>,
760
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send
761
where
762
Self: Sync,
···
767
#[cfg(target_arch = "wasm32")]
768
fn get_resource_server_metadata(
769
&self,
770
+
pds: &CowStr<'_>,
771
) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> {
772
get_resource_server_metadata_impl(self, pds)
773
}
···
775
776
pub async fn resolve_authorization_server<T: HttpClient + ?Sized>(
777
client: &T,
778
+
server: &CowStr<'_>,
779
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
780
+
let url = format!(
781
+
"{}/.well-known/oauth-authorization-server",
782
+
server.trim_end_matches("/")
783
+
);
784
785
let req = Request::builder()
786
+
.uri(url)
787
.body(Vec::new())
788
.map_err(|e| ResolverError::transport(e))?;
789
let res = client
···
809
810
pub async fn resolve_protected_resource_info<T: HttpClient + ?Sized>(
811
client: &T,
812
+
server: &CowStr<'_>,
813
) -> Result<OAuthProtectedResourceMetadata<'static>> {
814
+
let url = format!(
815
+
"{}/.well-known/oauth-protected-resource",
816
+
server.trim_end_matches("/")
817
+
);
818
819
let req = Request::builder()
820
.uri(url.to_string())
···
879
.body(Vec::new())
880
.unwrap(),
881
);
882
+
let issuer = CowStr::new_static("https://issuer");
883
let err = super::resolve_authorization_server(&client, &issuer)
884
.await
885
.unwrap_err();
···
898
.body(b"{not json}".to_vec())
899
.unwrap(),
900
);
901
+
let issuer = CowStr::new_static("https://issuer");
902
let err = super::resolve_authorization_server(&client, &issuer)
903
.await
904
.unwrap_err();
+4
-4
crates/jacquard-oauth/src/session.rs
+4
-4
crates/jacquard-oauth/src/session.rs
···
43
pub session_id: CowStr<'s>,
44
45
// Base URL of the "resource server" (eg, PDS). Should include scheme, hostname, port; no path or auth info.
46
-
pub host_url: Url,
47
48
// Base URL of the "auth server" (eg, PDS or entryway). Should include scheme, hostname, port; no path or auth info.
49
-
pub authserver_url: Url,
50
51
// Full token endpoint
52
pub authserver_token_endpoint: CowStr<'s>,
···
70
71
fn into_static(self) -> Self::Output {
72
ClientSessionData {
73
-
authserver_url: self.authserver_url,
74
authserver_token_endpoint: self.authserver_token_endpoint.into_static(),
75
authserver_revocation_endpoint: self
76
.authserver_revocation_endpoint
···
80
token_set: self.token_set.into_static(),
81
account_did: self.account_did.into_static(),
82
session_id: self.session_id.into_static(),
83
-
host_url: self.host_url,
84
}
85
}
86
}
···
43
pub session_id: CowStr<'s>,
44
45
// Base URL of the "resource server" (eg, PDS). Should include scheme, hostname, port; no path or auth info.
46
+
pub host_url: CowStr<'s>,
47
48
// Base URL of the "auth server" (eg, PDS or entryway). Should include scheme, hostname, port; no path or auth info.
49
+
pub authserver_url: CowStr<'s>,
50
51
// Full token endpoint
52
pub authserver_token_endpoint: CowStr<'s>,
···
70
71
fn into_static(self) -> Self::Output {
72
ClientSessionData {
73
+
authserver_url: self.authserver_url.into_static(),
74
authserver_token_endpoint: self.authserver_token_endpoint.into_static(),
75
authserver_revocation_endpoint: self
76
.authserver_revocation_endpoint
···
80
token_set: self.token_set.into_static(),
81
account_did: self.account_did.into_static(),
82
session_id: self.session_id.into_static(),
83
+
host_url: self.host_url.into_static(),
84
}
85
}
86
}
+14
-14
crates/jacquard-oauth/src/types/client_metadata.rs
+14
-14
crates/jacquard-oauth/src/types/client_metadata.rs
···
6
7
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
8
pub struct OAuthClientMetadata<'c> {
9
-
pub client_id: Url,
10
#[serde(skip_serializing_if = "Option::is_none")]
11
-
pub client_uri: Option<Url>,
12
-
pub redirect_uris: Vec<Url>,
13
#[serde(skip_serializing_if = "Option::is_none")]
14
#[serde(borrow)]
15
pub scope: Option<CowStr<'c>>,
···
22
pub dpop_bound_access_tokens: Option<bool>,
23
// https://datatracker.ietf.org/doc/html/rfc7591#section-2
24
#[serde(skip_serializing_if = "Option::is_none")]
25
-
pub jwks_uri: Option<Url>,
26
#[serde(skip_serializing_if = "Option::is_none")]
27
pub jwks: Option<JwkSet>,
28
// https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
···
31
#[serde(skip_serializing_if = "Option::is_none")]
32
pub client_name: Option<SmolStr>,
33
#[serde(skip_serializing_if = "Option::is_none")]
34
-
pub logo_uri: Option<Url>,
35
#[serde(skip_serializing_if = "Option::is_none")]
36
-
pub tos_uri: Option<Url>,
37
#[serde(skip_serializing_if = "Option::is_none")]
38
-
pub privacy_policy_uri: Option<Url>,
39
}
40
41
impl OAuthClientMetadata<'_> {}
···
45
46
fn into_static(self) -> Self::Output {
47
OAuthClientMetadata {
48
-
client_id: self.client_id,
49
-
client_uri: self.client_uri,
50
-
redirect_uris: self.redirect_uris,
51
scope: self.scope.map(|scope| scope.into_static()),
52
grant_types: self.grant_types.map(|types| types.into_static()),
53
token_endpoint_auth_method: self
54
.token_endpoint_auth_method
55
.map(|method| method.into_static()),
56
dpop_bound_access_tokens: self.dpop_bound_access_tokens,
57
-
jwks_uri: self.jwks_uri,
58
jwks: self.jwks,
59
token_endpoint_auth_signing_alg: self
60
.token_endpoint_auth_signing_alg
61
.map(|alg| alg.into_static()),
62
client_name: self.client_name,
63
-
logo_uri: self.logo_uri,
64
-
tos_uri: self.tos_uri,
65
-
privacy_policy_uri: self.privacy_policy_uri,
66
}
67
}
68
}
···
6
7
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
8
pub struct OAuthClientMetadata<'c> {
9
+
pub client_id: CowStr<'c>,
10
#[serde(skip_serializing_if = "Option::is_none")]
11
+
pub client_uri: Option<CowStr<'c>>,
12
+
pub redirect_uris: Vec<CowStr<'c>>,
13
#[serde(skip_serializing_if = "Option::is_none")]
14
#[serde(borrow)]
15
pub scope: Option<CowStr<'c>>,
···
22
pub dpop_bound_access_tokens: Option<bool>,
23
// https://datatracker.ietf.org/doc/html/rfc7591#section-2
24
#[serde(skip_serializing_if = "Option::is_none")]
25
+
pub jwks_uri: Option<CowStr<'c>>,
26
#[serde(skip_serializing_if = "Option::is_none")]
27
pub jwks: Option<JwkSet>,
28
// https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
···
31
#[serde(skip_serializing_if = "Option::is_none")]
32
pub client_name: Option<SmolStr>,
33
#[serde(skip_serializing_if = "Option::is_none")]
34
+
pub logo_uri: Option<CowStr<'c>>,
35
#[serde(skip_serializing_if = "Option::is_none")]
36
+
pub tos_uri: Option<CowStr<'c>>,
37
#[serde(skip_serializing_if = "Option::is_none")]
38
+
pub privacy_policy_uri: Option<CowStr<'c>>,
39
}
40
41
impl OAuthClientMetadata<'_> {}
···
45
46
fn into_static(self) -> Self::Output {
47
OAuthClientMetadata {
48
+
client_id: self.client_id.into_static(),
49
+
client_uri: self.client_uri.into_static(),
50
+
redirect_uris: self.redirect_uris.into_static(),
51
scope: self.scope.map(|scope| scope.into_static()),
52
grant_types: self.grant_types.map(|types| types.into_static()),
53
token_endpoint_auth_method: self
54
.token_endpoint_auth_method
55
.map(|method| method.into_static()),
56
dpop_bound_access_tokens: self.dpop_bound_access_tokens,
57
+
jwks_uri: self.jwks_uri.into_static(),
58
jwks: self.jwks,
59
token_endpoint_auth_signing_alg: self
60
.token_endpoint_auth_signing_alg
61
.map(|alg| alg.into_static()),
62
client_name: self.client_name,
63
+
logo_uri: self.logo_uri.into_static(),
64
+
tos_uri: self.tos_uri.into_static(),
65
+
privacy_policy_uri: self.privacy_policy_uri.into_static(),
66
}
67
}
68
}
+2
-2
crates/jacquard-oauth/src/types/metadata.rs
+2
-2
crates/jacquard-oauth/src/types/metadata.rs
···
56
pub struct OAuthProtectedResourceMetadata<'s> {
57
#[serde(borrow)]
58
pub resource: CowStr<'s>,
59
-
pub authorization_servers: Option<Vec<Url>>,
60
pub jwks_uri: Option<CowStr<'s>>,
61
pub scopes_supported: Vec<CowStr<'s>>,
62
pub bearer_methods_supported: Option<Vec<CowStr<'s>>>,
···
71
fn into_static(self) -> Self::Output {
72
OAuthProtectedResourceMetadata {
73
resource: self.resource.into_static(),
74
-
authorization_servers: self.authorization_servers,
75
jwks_uri: self.jwks_uri.map(|v| v.into_static()),
76
scopes_supported: self.scopes_supported.into_static(),
77
bearer_methods_supported: self.bearer_methods_supported.map(|v| v.into_static()),
···
56
pub struct OAuthProtectedResourceMetadata<'s> {
57
#[serde(borrow)]
58
pub resource: CowStr<'s>,
59
+
pub authorization_servers: Option<Vec<CowStr<'s>>>,
60
pub jwks_uri: Option<CowStr<'s>>,
61
pub scopes_supported: Vec<CowStr<'s>>,
62
pub bearer_methods_supported: Option<Vec<CowStr<'s>>>,
···
71
fn into_static(self) -> Self::Output {
72
OAuthProtectedResourceMetadata {
73
resource: self.resource.into_static(),
74
+
authorization_servers: self.authorization_servers.into_static(),
75
jwks_uri: self.jwks_uri.map(|v| v.into_static()),
76
scopes_supported: self.scopes_supported.into_static(),
77
bearer_methods_supported: self.bearer_methods_supported.map(|v| v.into_static()),
+18
-14
crates/jacquard/src/client.rs
+18
-14
crates/jacquard/src/client.rs
···
40
},
41
server::{create_session::CreateSessionOutput, refresh_session::RefreshSessionOutput},
42
};
43
use jacquard_common::error::XrpcResult;
44
pub use jacquard_common::error::{ClientError, XrpcResult as ClientResult};
45
use jacquard_common::http_client::HttpClient;
···
96
fn session_info(&self)
97
-> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>>;
98
/// Current base endpoint.
99
-
fn endpoint(&self) -> impl Future<Output = url::Url>;
100
/// Override per-session call options.
101
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()>;
102
/// Refresh the session and return a fresh AuthorizationToken.
···
154
}
155
pub struct UnauthenticatedSession<T> {
156
resolver: Arc<T>,
157
-
endpoint: Arc<RwLock<Option<Url>>>,
158
options: Arc<RwLock<CallOptions<'static>>>,
159
}
160
···
213
T: Sync + Send,
214
{
215
#[doc = " Get the base URI for the client."]
216
-
fn base_uri(&self) -> impl Future<Output = Url> + Send {
217
async move {
218
-
self.endpoint.read().await.clone().unwrap_or(
219
-
Url::parse("https://public.bsky.app").expect("public appview should be valid url"),
220
-
)
221
}
222
}
223
···
249
{
250
async move {
251
let base_uri = self.base_uri().await;
252
self.resolver
253
.xrpc(base_uri.clone())
254
.with_options(opts.clone())
···
295
fn set_base_uri(&self, url: Url) -> impl Future<Output = ()> + Send {
296
async move {
297
let mut guard = self.endpoint.write().await;
298
-
*guard = Some(url);
299
}
300
}
301
···
326
async { None } // no session
327
}
328
329
-
fn endpoint(&self) -> impl Future<Output = Url> {
330
async { self.base_uri().await }
331
}
332
···
537
}
538
539
/// Get current endpoint.
540
-
pub async fn endpoint(&self) -> url::Url {
541
self.inner.endpoint().await
542
}
543
···
1199
.map(|(did, sid)| (did, Some(sid)))
1200
}
1201
}
1202
-
fn endpoint(&self) -> impl Future<Output = url::Url> {
1203
async move { CredentialSession::<S, T, W>::endpoint(self).await }
1204
}
1205
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> {
···
1231
Some((did.into_static(), Some(sid.into_static())))
1232
}
1233
}
1234
-
fn endpoint(&self) -> impl Future<Output = url::Url> {
1235
async { self.endpoint().await }
1236
}
1237
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> {
···
1260
) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> {
1261
async { None }
1262
}
1263
-
fn endpoint(&self) -> impl Future<Output = url::Url> {
1264
async { self.base_uri().await }
1265
}
1266
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> {
···
1368
}
1369
1370
impl<A: AgentSession> XrpcClient for Agent<A> {
1371
-
async fn base_uri(&self) -> url::Url {
1372
self.inner.base_uri().await
1373
}
1374
fn opts(&self) -> impl Future<Output = CallOptions<'_>> {
···
1513
async { self.info().await }
1514
}
1515
1516
-
fn endpoint(&self) -> impl Future<Output = url::Url> {
1517
async { self.endpoint().await }
1518
}
1519
···
40
},
41
server::{create_session::CreateSessionOutput, refresh_session::RefreshSessionOutput},
42
};
43
+
use jacquard_common::cowstr::ToCowStr;
44
use jacquard_common::error::XrpcResult;
45
pub use jacquard_common::error::{ClientError, XrpcResult as ClientResult};
46
use jacquard_common::http_client::HttpClient;
···
97
fn session_info(&self)
98
-> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>>;
99
/// Current base endpoint.
100
+
fn endpoint(&self) -> impl Future<Output = CowStr<'static>>;
101
/// Override per-session call options.
102
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()>;
103
/// Refresh the session and return a fresh AuthorizationToken.
···
155
}
156
pub struct UnauthenticatedSession<T> {
157
resolver: Arc<T>,
158
+
endpoint: Arc<RwLock<Option<CowStr<'static>>>>,
159
options: Arc<RwLock<CallOptions<'static>>>,
160
}
161
···
214
T: Sync + Send,
215
{
216
#[doc = " Get the base URI for the client."]
217
+
fn base_uri(&self) -> impl Future<Output = CowStr<'static>> + Send {
218
async move {
219
+
self.endpoint
220
+
.read()
221
+
.await
222
+
.clone()
223
+
.unwrap_or(CowStr::new_static("https://public.bsky.app"))
224
}
225
}
226
···
252
{
253
async move {
254
let base_uri = self.base_uri().await;
255
+
let base_uri = Url::parse(&base_uri).expect("base_uri should be valid url");
256
self.resolver
257
.xrpc(base_uri.clone())
258
.with_options(opts.clone())
···
299
fn set_base_uri(&self, url: Url) -> impl Future<Output = ()> + Send {
300
async move {
301
let mut guard = self.endpoint.write().await;
302
+
*guard = Some(url.to_cowstr().into_static());
303
}
304
}
305
···
330
async { None } // no session
331
}
332
333
+
fn endpoint(&self) -> impl Future<Output = CowStr<'static>> {
334
async { self.base_uri().await }
335
}
336
···
541
}
542
543
/// Get current endpoint.
544
+
pub async fn endpoint(&self) -> CowStr<'static> {
545
self.inner.endpoint().await
546
}
547
···
1203
.map(|(did, sid)| (did, Some(sid)))
1204
}
1205
}
1206
+
fn endpoint(&self) -> impl Future<Output = CowStr<'static>> {
1207
async move { CredentialSession::<S, T, W>::endpoint(self).await }
1208
}
1209
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> {
···
1235
Some((did.into_static(), Some(sid.into_static())))
1236
}
1237
}
1238
+
fn endpoint(&self) -> impl Future<Output = CowStr<'static>> {
1239
async { self.endpoint().await }
1240
}
1241
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> {
···
1264
) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> {
1265
async { None }
1266
}
1267
+
fn endpoint(&self) -> impl Future<Output = CowStr<'static>> {
1268
async { self.base_uri().await }
1269
}
1270
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> {
···
1372
}
1373
1374
impl<A: AgentSession> XrpcClient for Agent<A> {
1375
+
async fn base_uri(&self) -> CowStr<'static> {
1376
self.inner.base_uri().await
1377
}
1378
fn opts(&self) -> impl Future<Output = CallOptions<'_>> {
···
1517
async { self.info().await }
1518
}
1519
1520
+
fn endpoint(&self) -> impl Future<Output = CowStr<'static>> {
1521
async { self.endpoint().await }
1522
}
1523
+50
-20
crates/jacquard/src/client/credential_session.rs
+50
-20
crates/jacquard/src/client/credential_session.rs
···
5
};
6
use jacquard_common::{
7
AuthorizationToken, CowStr, IntoStatic,
8
error::{AuthError, ClientError, XrpcResult},
9
http_client::HttpClient,
10
session::SessionStore,
···
13
CallOptions, Response, XrpcClient, XrpcError, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse,
14
},
15
};
16
use tokio::sync::RwLock;
17
use url::Url;
18
···
48
/// Active session key, if any.
49
pub key: RwLock<Option<SessionKey>>,
50
/// Current base endpoint (PDS); defaults to public appview when unset.
51
-
pub endpoint: RwLock<Option<Url>>,
52
}
53
54
impl<S, T> CredentialSession<S, T, ()>
···
112
}
113
114
/// Current base endpoint. Defaults to the public appview when unset.
115
-
pub async fn endpoint(&self) -> Url {
116
-
self.endpoint.read().await.clone().unwrap_or(
117
-
Url::parse("https://public.bsky.app").expect("public appview should be valid url"),
118
-
)
119
}
120
121
/// Override the current base endpoint.
122
pub async fn set_endpoint(&self, endpoint: Url) {
123
-
*self.endpoint.write().await = Some(endpoint);
124
}
125
126
/// Current access token (Bearer), if logged in.
···
153
.ok_or_else(|| ClientError::auth(AuthError::NotAuthenticated))?;
154
let session = self.store.get(&key).await;
155
let endpoint = self.endpoint().await;
156
let mut opts = self.options.read().await.clone();
157
opts.auth = session.map(|s| AuthorizationToken::Bearer(s.refresh_jwt));
158
let response = self
···
277
}
278
// Activate
279
*self.key.write().await = Some(key);
280
-
*self.endpoint.write().await = Some(pds);
281
282
Ok(session)
283
}
···
318
319
// Activate
320
*self.key.write().await = Some(key.clone());
321
-
*self.endpoint.write().await = Some(pds);
322
// ensure store has the session (no-op if it existed)
323
self.store
324
.set((sess.did.clone(), session_id.into_static()), sess)
···
326
if let Some(file_store) =
327
(&*self.store as &dyn Any).downcast_ref::<crate::client::token::FileAuthStore>()
328
{
329
-
let _ = file_store.set_atp_pds(&key, &self.endpoint().await);
330
}
331
Ok(())
332
}
···
360
})?
361
});
362
*self.key.write().await = Some(key.clone());
363
-
*self.endpoint.write().await = Some(pds);
364
if let Some(file_store) =
365
(&*self.store as &dyn Any).downcast_ref::<crate::client::token::FileAuthStore>()
366
{
367
-
let _ = file_store.set_atp_pds(&key, &self.endpoint().await);
368
}
369
Ok(())
370
}
···
402
T: HttpClient + XrpcExt + Send + Sync + 'static,
403
W: Send + Sync,
404
{
405
-
async fn base_uri(&self) -> Url {
406
-
self.endpoint.read().await.clone().unwrap_or(
407
-
Url::parse("https://public.bsky.app").expect("public appview should be valid url"),
408
-
)
409
}
410
411
async fn opts(&self) -> CallOptions<'_> {
···
419
420
async fn set_base_uri(&self, url: Url) {
421
let mut guard = self.endpoint.write().await;
422
-
*guard = Some(url);
423
}
424
425
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
···
441
<R as XrpcRequest>::Response: Send + Sync,
442
{
443
let base_uri = self.base_uri().await;
444
let auth = self.access_token().await;
445
opts.auth = auth;
446
let resp = self
···
546
use jacquard_common::{StreamError, xrpc::build_http_request};
547
548
let base_uri = <Self as XrpcClient>::base_uri(self).await;
549
let mut opts = self.options.read().await.clone();
550
opts.auth = self.access_token().await;
551
···
599
use n0_future::TryStreamExt;
600
601
let base_uri = self.base_uri().await;
602
let mut opts = self.options.read().await.clone();
603
opts.auth = self.access_token().await;
604
···
782
T: Send + Sync + 'static,
783
W: WebSocketClient + Send + Sync,
784
{
785
-
async fn base_uri(&self) -> Url {
786
-
self.endpoint.read().await.clone().unwrap_or(
787
-
Url::parse("https://public.bsky.app").expect("public appview should be valid url"),
788
-
)
789
}
790
791
async fn subscription_opts(&self) -> jacquard_common::xrpc::SubscriptionOptions<'_> {
···
822
{
823
use jacquard_common::xrpc::SubscriptionExt;
824
let base = self.base_uri().await;
825
self.subscription(base)
826
.with_options(opts)
827
.subscribe(params)
···
5
};
6
use jacquard_common::{
7
AuthorizationToken, CowStr, IntoStatic,
8
+
cowstr::ToCowStr,
9
error::{AuthError, ClientError, XrpcResult},
10
http_client::HttpClient,
11
session::SessionStore,
···
14
CallOptions, Response, XrpcClient, XrpcError, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse,
15
},
16
};
17
+
use smol_str::ToSmolStr;
18
use tokio::sync::RwLock;
19
use url::Url;
20
···
50
/// Active session key, if any.
51
pub key: RwLock<Option<SessionKey>>,
52
/// Current base endpoint (PDS); defaults to public appview when unset.
53
+
pub endpoint: RwLock<Option<CowStr<'static>>>,
54
}
55
56
impl<S, T> CredentialSession<S, T, ()>
···
114
}
115
116
/// Current base endpoint. Defaults to the public appview when unset.
117
+
pub async fn endpoint(&self) -> CowStr<'static> {
118
+
self.endpoint
119
+
.read()
120
+
.await
121
+
.clone()
122
+
.unwrap_or(CowStr::new_static("https://public.bsky.app"))
123
}
124
125
/// Override the current base endpoint.
126
pub async fn set_endpoint(&self, endpoint: Url) {
127
+
*self.endpoint.write().await = Some(endpoint.to_cowstr().into_static());
128
}
129
130
/// Current access token (Bearer), if logged in.
···
157
.ok_or_else(|| ClientError::auth(AuthError::NotAuthenticated))?;
158
let session = self.store.get(&key).await;
159
let endpoint = self.endpoint().await;
160
+
let endpoint = Url::parse(endpoint.as_str()).map_err(|_| {
161
+
ClientError::auth(AuthError::NotAuthenticated)
162
+
.with_help("ensure endpoint is valid")
163
+
.with_url("com.atproto.server.refreshSession")
164
+
})?;
165
let mut opts = self.options.read().await.clone();
166
opts.auth = session.map(|s| AuthorizationToken::Bearer(s.refresh_jwt));
167
let response = self
···
286
}
287
// Activate
288
*self.key.write().await = Some(key);
289
+
*self.endpoint.write().await = Some(pds.to_cowstr().into_static());
290
291
Ok(session)
292
}
···
327
328
// Activate
329
*self.key.write().await = Some(key.clone());
330
+
*self.endpoint.write().await = Some(pds.to_cowstr().into_static());
331
// ensure store has the session (no-op if it existed)
332
self.store
333
.set((sess.did.clone(), session_id.into_static()), sess)
···
335
if let Some(file_store) =
336
(&*self.store as &dyn Any).downcast_ref::<crate::client::token::FileAuthStore>()
337
{
338
+
let _ = file_store.set_atp_pds(
339
+
&key,
340
+
&Url::parse(&self.endpoint().await).map_err(|e| {
341
+
ClientError::invalid_request("invalid PDS endpoint")
342
+
.with_help(format!("Failed to parse PDS endpoint: {}", e))
343
+
})?,
344
+
);
345
}
346
Ok(())
347
}
···
375
})?
376
});
377
*self.key.write().await = Some(key.clone());
378
+
*self.endpoint.write().await = Some(pds.to_cowstr().into_static());
379
if let Some(file_store) =
380
(&*self.store as &dyn Any).downcast_ref::<crate::client::token::FileAuthStore>()
381
{
382
+
let _ = file_store.set_atp_pds(
383
+
&key,
384
+
&Url::parse(&self.endpoint().await).map_err(|e| {
385
+
ClientError::invalid_request("invalid PDS endpoint")
386
+
.with_help(format!("Failed to parse PDS endpoint: {}", e))
387
+
})?,
388
+
);
389
}
390
Ok(())
391
}
···
423
T: HttpClient + XrpcExt + Send + Sync + 'static,
424
W: Send + Sync,
425
{
426
+
async fn base_uri(&self) -> CowStr<'static> {
427
+
self.endpoint
428
+
.read()
429
+
.await
430
+
.clone()
431
+
.unwrap_or(CowStr::new_static("https://public.bsky.app"))
432
}
433
434
async fn opts(&self) -> CallOptions<'_> {
···
442
443
async fn set_base_uri(&self, url: Url) {
444
let mut guard = self.endpoint.write().await;
445
+
*guard = Some(url.to_cowstr().into_static());
446
}
447
448
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
···
464
<R as XrpcRequest>::Response: Send + Sync,
465
{
466
let base_uri = self.base_uri().await;
467
+
let base_uri =
468
+
Url::parse(&base_uri).map_err(|e| ClientError::invalid_request(e.to_smolstr()))?;
469
let auth = self.access_token().await;
470
opts.auth = auth;
471
let resp = self
···
571
use jacquard_common::{StreamError, xrpc::build_http_request};
572
573
let base_uri = <Self as XrpcClient>::base_uri(self).await;
574
+
let base_uri = Url::parse(&base_uri).map_err(|e| StreamError::encode(e))?;
575
let mut opts = self.options.read().await.clone();
576
opts.auth = self.access_token().await;
577
···
625
use n0_future::TryStreamExt;
626
627
let base_uri = self.base_uri().await;
628
+
let base_uri = Url::parse(&base_uri).map_err(|e| StreamError::encode(e))?;
629
let mut opts = self.options.read().await.clone();
630
opts.auth = self.access_token().await;
631
···
809
T: Send + Sync + 'static,
810
W: WebSocketClient + Send + Sync,
811
{
812
+
async fn base_uri(&self) -> CowStr<'static> {
813
+
self.endpoint
814
+
.read()
815
+
.await
816
+
.clone()
817
+
.unwrap_or(CowStr::new_static("https://public.bsky.app"))
818
}
819
820
async fn subscription_opts(&self) -> jacquard_common::xrpc::SubscriptionOptions<'_> {
···
851
{
852
use jacquard_common::xrpc::SubscriptionExt;
853
let base = self.base_uri().await;
854
+
let base = Url::parse(&base).expect("base uri should be valid url");
855
self.subscription(base)
856
.with_options(opts)
857
.subscribe(params)
+6
-6
crates/jacquard/src/client/token.rs
+6
-6
crates/jacquard/src/client/token.rs
···
48
session_id: String,
49
50
/// Base URL of the resource server (PDS)
51
-
host_url: Url,
52
53
/// Base URL of the authorization server (PDS or entryway)
54
-
authserver_url: Url,
55
56
/// Full token endpoint URL
57
authserver_token_endpoint: String,
···
95
OAuthSession {
96
account_did: data.account_did.to_string(),
97
session_id: data.session_id.to_string(),
98
-
host_url: data.host_url,
99
-
authserver_url: data.authserver_url,
100
authserver_token_endpoint: data.authserver_token_endpoint.to_string(),
101
authserver_revocation_endpoint: data
102
.authserver_revocation_endpoint
···
122
ClientSessionData {
123
account_did: session.account_did.into(),
124
session_id: session.session_id.to_cowstr(),
125
-
host_url: session.host_url,
126
-
authserver_url: session.authserver_url,
127
authserver_token_endpoint: session.authserver_token_endpoint.to_cowstr(),
128
authserver_revocation_endpoint: session
129
.authserver_revocation_endpoint
···
48
session_id: String,
49
50
/// Base URL of the resource server (PDS)
51
+
host_url: String,
52
53
/// Base URL of the authorization server (PDS or entryway)
54
+
authserver_url: String,
55
56
/// Full token endpoint URL
57
authserver_token_endpoint: String,
···
95
OAuthSession {
96
account_did: data.account_did.to_string(),
97
session_id: data.session_id.to_string(),
98
+
host_url: data.host_url.to_string(),
99
+
authserver_url: data.authserver_url.to_string(),
100
authserver_token_endpoint: data.authserver_token_endpoint.to_string(),
101
authserver_revocation_endpoint: data
102
.authserver_revocation_endpoint
···
122
ClientSessionData {
123
account_did: session.account_did.into(),
124
session_id: session.session_id.to_cowstr(),
125
+
host_url: session.host_url.to_cowstr(),
126
+
authserver_url: session.authserver_url.to_cowstr(),
127
authserver_token_endpoint: session.authserver_token_endpoint.to_cowstr(),
128
authserver_revocation_endpoint: session
129
.authserver_revocation_endpoint
+9
-9
crates/jacquard/tests/oauth_auto_refresh.rs
+9
-9
crates/jacquard/tests/oauth_auto_refresh.rs
···
3
4
use bytes::Bytes;
5
use http::{HeaderValue, Method, Response as HttpResponse, StatusCode};
6
-
use jacquard::IntoStatic;
7
use jacquard::client::Agent;
8
use jacquard::types::did::Did;
9
use jacquard::xrpc::XrpcClient;
10
use jacquard_common::http_client::HttpClient;
11
use jacquard_oauth::atproto::AtprotoClientMetadata;
12
use jacquard_oauth::client::OAuthSession;
···
84
impl OAuthResolver for MockClient {
85
async fn get_authorization_server_metadata(
86
&self,
87
-
issuer: &url::Url,
88
) -> Result<OAuthAuthorizationServerMetadata<'static>, jacquard_oauth::resolver::ResolverError>
89
{
90
// Return minimal metadata with supported auth method "none" and DPoP support
···
103
104
async fn get_resource_server_metadata(
105
&self,
106
-
_pds: &url::Url,
107
) -> Result<OAuthAuthorizationServerMetadata<'static>, jacquard_oauth::resolver::ResolverError>
108
{
109
// Return metadata pointing to the same issuer as above
···
217
let session_data = ClientSessionData {
218
account_did: Did::new_static("did:plc:alice").unwrap(),
219
session_id: jacquard::CowStr::from("state"),
220
-
host_url: url::Url::parse("https://pds").unwrap(),
221
-
authserver_url: url::Url::parse("https://issuer").unwrap(),
222
authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"),
223
authserver_revocation_endpoint: None,
224
scopes: vec![Scope::Atproto],
···
246
let data_store = ClientSessionData {
247
account_did: Did::new_static("did:plc:alice").unwrap(),
248
session_id: jacquard::CowStr::from("state"),
249
-
host_url: url::Url::parse("https://pds").unwrap(),
250
-
authserver_url: url::Url::parse("https://issuer").unwrap(),
251
authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"),
252
authserver_revocation_endpoint: None,
253
scopes: vec![Scope::Atproto],
···
348
let session_data = ClientSessionData {
349
account_did: Did::new_static("did:plc:alice").unwrap(),
350
session_id: jacquard::CowStr::from("state"),
351
-
host_url: url::Url::parse("https://pds").unwrap(),
352
-
authserver_url: url::Url::parse("https://issuer").unwrap(),
353
authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"),
354
authserver_revocation_endpoint: None,
355
scopes: vec![Scope::Atproto],
···
3
4
use bytes::Bytes;
5
use http::{HeaderValue, Method, Response as HttpResponse, StatusCode};
6
use jacquard::client::Agent;
7
use jacquard::types::did::Did;
8
use jacquard::xrpc::XrpcClient;
9
+
use jacquard::{CowStr, IntoStatic};
10
use jacquard_common::http_client::HttpClient;
11
use jacquard_oauth::atproto::AtprotoClientMetadata;
12
use jacquard_oauth::client::OAuthSession;
···
84
impl OAuthResolver for MockClient {
85
async fn get_authorization_server_metadata(
86
&self,
87
+
issuer: &CowStr<'_>,
88
) -> Result<OAuthAuthorizationServerMetadata<'static>, jacquard_oauth::resolver::ResolverError>
89
{
90
// Return minimal metadata with supported auth method "none" and DPoP support
···
103
104
async fn get_resource_server_metadata(
105
&self,
106
+
_pds: &CowStr<'_>,
107
) -> Result<OAuthAuthorizationServerMetadata<'static>, jacquard_oauth::resolver::ResolverError>
108
{
109
// Return metadata pointing to the same issuer as above
···
217
let session_data = ClientSessionData {
218
account_did: Did::new_static("did:plc:alice").unwrap(),
219
session_id: jacquard::CowStr::from("state"),
220
+
host_url: CowStr::new_static("https://pds"),
221
+
authserver_url: CowStr::new_static("https://issuer"),
222
authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"),
223
authserver_revocation_endpoint: None,
224
scopes: vec![Scope::Atproto],
···
246
let data_store = ClientSessionData {
247
account_did: Did::new_static("did:plc:alice").unwrap(),
248
session_id: jacquard::CowStr::from("state"),
249
+
host_url: CowStr::new_static("https://pds"),
250
+
authserver_url: CowStr::new_static("https://issuer"),
251
authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"),
252
authserver_revocation_endpoint: None,
253
scopes: vec![Scope::Atproto],
···
348
let session_data = ClientSessionData {
349
account_did: Did::new_static("did:plc:alice").unwrap(),
350
session_id: jacquard::CowStr::from("state"),
351
+
host_url: CowStr::new_static("https://pds"),
352
+
authserver_url: CowStr::new_static("https://issuer"),
353
authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"),
354
authserver_revocation_endpoint: None,
355
scopes: vec![Scope::Atproto],
+4
-4
crates/jacquard/tests/oauth_flow.rs
+4
-4
crates/jacquard/tests/oauth_flow.rs
···
3
4
use bytes::Bytes;
5
use http::{Response as HttpResponse, StatusCode};
6
-
use jacquard::IntoStatic;
7
use jacquard::client::Agent;
8
use jacquard::xrpc::XrpcClient;
9
use jacquard_common::http_client::HttpClient;
10
use jacquard_oauth::atproto::AtprotoClientMetadata;
11
use jacquard_oauth::authstore::ClientAuthStore;
···
115
}
116
async fn get_authorization_server_metadata(
117
&self,
118
-
issuer: &url::Url,
119
) -> Result<
120
jacquard_oauth::types::OAuthAuthorizationServerMetadata<'static>,
121
jacquard_oauth::resolver::ResolverError,
···
134
135
async fn get_resource_server_metadata(
136
&self,
137
-
_pds: &url::Url,
138
) -> Result<
139
jacquard_oauth::types::OAuthAuthorizationServerMetadata<'static>,
140
jacquard_oauth::resolver::ResolverError,
···
243
// Construct authorization URL as OAuthClient::start_auth would do
244
#[derive(serde::Serialize)]
245
struct Parameters<'s> {
246
-
client_id: url::Url,
247
request_uri: jacquard::CowStr<'s>,
248
}
249
let auth_url = format!(
···
3
4
use bytes::Bytes;
5
use http::{Response as HttpResponse, StatusCode};
6
use jacquard::client::Agent;
7
use jacquard::xrpc::XrpcClient;
8
+
use jacquard::{CowStr, IntoStatic};
9
use jacquard_common::http_client::HttpClient;
10
use jacquard_oauth::atproto::AtprotoClientMetadata;
11
use jacquard_oauth::authstore::ClientAuthStore;
···
115
}
116
async fn get_authorization_server_metadata(
117
&self,
118
+
issuer: &CowStr<'_>,
119
) -> Result<
120
jacquard_oauth::types::OAuthAuthorizationServerMetadata<'static>,
121
jacquard_oauth::resolver::ResolverError,
···
134
135
async fn get_resource_server_metadata(
136
&self,
137
+
_pds: &CowStr<'_>,
138
) -> Result<
139
jacquard_oauth::types::OAuthAuthorizationServerMetadata<'static>,
140
jacquard_oauth::resolver::ResolverError,
···
243
// Construct authorization URL as OAuthClient::start_auth would do
244
#[derive(serde::Serialize)]
245
struct Parameters<'s> {
246
+
client_id: CowStr<'s>,
247
request_uri: jacquard::CowStr<'s>,
248
}
249
let auth_url = format!(