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