-1
Cargo.lock
-1
Cargo.lock
-1
Cargo.toml
-1
Cargo.toml
+1
-1
crates/jacquard-common/Cargo.toml
+1
-1
crates/jacquard-common/Cargo.toml
-1
crates/jacquard-oauth/Cargo.toml
-1
crates/jacquard-oauth/Cargo.toml
+30
-21
crates/jacquard-oauth/src/atproto.rs
+30
-21
crates/jacquard-oauth/src/atproto.rs
···
152
#[derive(serde::Serialize)]
153
struct Parameters<'a> {
154
#[serde(skip_serializing_if = "Option::is_none")]
155
-
redirect_uri: Option<Vec<Url>>,
156
#[serde(skip_serializing_if = "Option::is_none")]
157
scope: Option<CowStr<'a>>,
158
}
159
let query = serde_html_form::to_string(Parameters {
160
-
redirect_uri: redirect_uris.clone(),
161
scope: scopes
162
.as_ref()
163
.map(|s| Scope::serialize_multiple(s.as_slice())),
164
})
165
.ok();
166
-
let mut client_id = String::from("http://localhost");
167
if let Some(query) = query
168
&& !query.is_empty()
169
{
···
173
client_id: Url::parse(&client_id).unwrap(),
174
client_uri: None,
175
redirect_uris: redirect_uris.unwrap_or(vec![
176
-
Url::from_str("http://127.0.0.1/").unwrap(),
177
-
Url::from_str("http://[::1]/").unwrap(),
178
]),
179
grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
180
scopes: scopes.unwrap_or(vec![Scope::Atproto]),
···
216
} else {
217
(AuthMethod::None, None, None)
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"))
···
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,
···
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![
···
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")],
···
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")],
···
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()),
···
152
#[derive(serde::Serialize)]
153
struct Parameters<'a> {
154
#[serde(skip_serializing_if = "Option::is_none")]
155
+
redirect_uri: Option<Vec<CowStr<'a>>>,
156
#[serde(skip_serializing_if = "Option::is_none")]
157
scope: Option<CowStr<'a>>,
158
}
159
let query = serde_html_form::to_string(Parameters {
160
+
redirect_uri: redirect_uris.as_ref().map(|u| {
161
+
u.iter()
162
+
.map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static())
163
+
.collect()
164
+
}),
165
scope: scopes
166
.as_ref()
167
.map(|s| Scope::serialize_multiple(s.as_slice())),
168
})
169
.ok();
170
+
let mut client_id = String::from("http://localhost/");
171
if let Some(query) = query
172
&& !query.is_empty()
173
{
···
177
client_id: Url::parse(&client_id).unwrap(),
178
client_uri: None,
179
redirect_uris: redirect_uris.unwrap_or(vec![
180
+
Url::from_str("http://127.0.0.1").unwrap(),
181
+
Url::from_str("http://[::1]").unwrap(),
182
]),
183
grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
184
scopes: scopes.unwrap_or(vec![Scope::Atproto]),
···
220
} else {
221
(AuthMethod::None, None, None)
222
};
223
+
let client_id = metadata.client_id.as_str().trim_end_matches("/");
224
+
let client_uri = metadata
225
+
.client_uri
226
+
.map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static());
227
+
let redirect_uris = metadata
228
+
.redirect_uris
229
+
.iter()
230
+
.map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static())
231
+
.collect();
232
+
let jwks_uri = jwks_uri.map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static());
233
Ok(OAuthClientMetadata {
234
+
client_id: client_id.to_cowstr().into_static(),
235
+
client_uri,
236
+
redirect_uris,
237
token_endpoint_auth_method: Some(auth_method.into()),
238
grant_types: if keyset.is_some() {
239
Some(metadata.grant_types.into_iter().map(|v| v.into()).collect())
···
242
},
243
scope: Some(Scope::serialize_multiple(metadata.scopes.as_slice())),
244
dpop_bound_access_tokens: Some(true),
245
+
jwks_uri,
246
jwks,
247
token_endpoint_auth_signing_alg: if keyset.is_some() {
248
Some(CowStr::new_static("ES256"))
···
284
client_id: CowStr::new_static("http://localhost"),
285
client_uri: None,
286
redirect_uris: vec![
287
+
CowStr::new_static("http://127.0.0.1"),
288
+
CowStr::new_static("http://[::1]"),
289
],
290
scope: Some(CowStr::new_static("atproto")),
291
grant_types: None,
···
322
.expect("failed to convert metadata"),
323
OAuthClientMetadata {
324
client_id: CowStr::new_static(
325
+
"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"
326
),
327
client_uri: None,
328
redirect_uris: vec![
···
363
out,
364
OAuthClientMetadata {
365
client_id: CowStr::new_static(
366
+
"http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1"
367
),
368
client_uri: None,
369
redirect_uris: vec![CowStr::new_static("http://127.0.0.1")],
···
394
out,
395
OAuthClientMetadata {
396
client_id: CowStr::new_static(
397
+
"http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000"
398
),
399
client_uri: None,
400
redirect_uris: vec![CowStr::new_static("http://127.0.0.1:8000")],
···
425
out,
426
OAuthClientMetadata {
427
client_id: CowStr::new_static(
428
+
"http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1"
429
),
430
client_uri: None,
431
+
redirect_uris: vec![CowStr::new_static("http://127.0.0.1")],
432
scope: Some(CowStr::new_static("atproto")),
433
grant_types: None,
434
token_endpoint_auth_method: Some(AuthMethod::None.into()),
+1
-1
crates/jacquard-oauth/src/client.rs
+1
-1
crates/jacquard-oauth/src/client.rs
+6
-3
crates/jacquard-oauth/src/resolver.rs
+6
-3
crates/jacquard-oauth/src/resolver.rs
···
5
use http::{Request, StatusCode};
6
use jacquard_common::CowStr;
7
use jacquard_common::IntoStatic;
8
use jacquard_common::types::did_doc::DidDocument;
9
use jacquard_common::types::ident::AtIdentifier;
10
use jacquard_common::{http_client::HttpClient, types::did::Did};
···
423
// resolve to a DID)
424
Ok(if input.starts_with("https://") {
425
let url = Url::parse(input).map_err(|_| ResolverError::not_found())?;
426
-
(resolver.resolve_from_service(&url).await?, None)
427
} else {
428
let (metadata, identity) = resolver.resolve_from_identity(input).await?;
429
(metadata, Some(identity))
···
491
.map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?;
492
let identity = resolver.resolve_ident_owned(&actor).await?;
493
if let Some(pds) = &identity.pds_endpoint() {
494
-
let metadata = resolver.get_resource_server_metadata(pds).await?;
495
Ok((metadata, identity))
496
} else {
497
Err(ResolverError::did_document("Did doc lacking pds"))
···
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
···
5
use http::{Request, StatusCode};
6
use jacquard_common::CowStr;
7
use jacquard_common::IntoStatic;
8
+
use jacquard_common::cowstr::ToCowStr;
9
use jacquard_common::types::did_doc::DidDocument;
10
use jacquard_common::types::ident::AtIdentifier;
11
use jacquard_common::{http_client::HttpClient, types::did::Did};
···
424
// resolve to a DID)
425
Ok(if input.starts_with("https://") {
426
let url = Url::parse(input).map_err(|_| ResolverError::not_found())?;
427
+
(resolver.resolve_from_service(&url.to_cowstr()).await?, None)
428
} else {
429
let (metadata, identity) = resolver.resolve_from_identity(input).await?;
430
(metadata, Some(identity))
···
492
.map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?;
493
let identity = resolver.resolve_ident_owned(&actor).await?;
494
if let Some(pds) = &identity.pds_endpoint() {
495
+
let metadata = resolver
496
+
.get_resource_server_metadata(&pds.to_cowstr())
497
+
.await?;
498
Ok((metadata, identity))
499
} else {
500
Err(ResolverError::did_document("Did doc lacking pds"))
···
517
issuer: &CowStr<'_>,
518
) -> Result<OAuthAuthorizationServerMetadata<'static>> {
519
let mut md = resolve_authorization_server(client, issuer).await?;
520
+
md.issuer = issuer.clone().into_static();
521
Ok(md)
522
}
523
+1
-1
crates/jacquard/Cargo.toml
+1
-1
crates/jacquard/Cargo.toml
+3
-2
crates/jacquard/src/client.rs
+3
-2
crates/jacquard/src/client.rs
···
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())
259
.send(&request)
260
.await
···
287
{
288
async move {
289
let base_uri = self.base_uri().await;
290
self.resolver
291
-
.xrpc(base_uri.clone())
292
.with_options(opts.clone())
293
.send(&request)
294
.await
···
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)
258
.with_options(opts.clone())
259
.send(&request)
260
.await
···
287
{
288
async move {
289
let base_uri = self.base_uri().await;
290
+
let base_uri = Url::parse(&base_uri).expect("base_uri should be valid url");
291
self.resolver
292
+
.xrpc(base_uri)
293
.with_options(opts.clone())
294
.send(&request)
295
.await
+3
-2
crates/jacquard/src/client/credential_session.rs
+3
-2
crates/jacquard/src/client/credential_session.rs
···
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
}
···
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>>
···
286
}
287
// Activate
288
*self.key.write().await = Some(key);
289
+
*self.endpoint.write().await =
290
+
Some(pds.as_str().trim_end_matches("/").to_cowstr().into_static());
291
292
Ok(session)
293
}
···
443
444
async fn set_base_uri(&self, url: Url) {
445
let mut guard = self.endpoint.write().await;
446
+
*guard = Some(url.as_str().trim_end_matches("/").to_cowstr().into_static());
447
}
448
449
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
+2
-5
crates/jacquard/tests/credential_session.rs
+2
-5
crates/jacquard/tests/credential_session.rs
···
169
let session = CredentialSession::new(store.clone(), client.clone());
170
171
// Before login, default endpoint should be public appview
172
-
assert_eq!(
173
-
session.endpoint().await.as_str(),
174
-
"https://public.bsky.app/"
175
-
);
176
177
// Login using handle; resolves to PDS and persists session
178
session
···
187
.expect("login ok");
188
189
// Endpoint switches to PDS
190
-
assert_eq!(session.endpoint().await.as_str(), "https://pds/");
191
192
// Send a request that will first 401 (ExpiredToken), then refresh, then succeed
193
let resp = session
···
169
let session = CredentialSession::new(store.clone(), client.clone());
170
171
// Before login, default endpoint should be public appview
172
+
assert_eq!(session.endpoint().await.as_str(), "https://public.bsky.app");
173
174
// Login using handle; resolves to PDS and persists session
175
session
···
184
.expect("login ok");
185
186
// Endpoint switches to PDS
187
+
assert_eq!(session.endpoint().await.as_str(), "https://pds");
188
189
// Send a request that will first 401 (ExpiredToken), then refresh, then succeed
190
let resp = session