+38
-3
Cargo.lock
+38
-3
Cargo.lock
···
1539
1539
"bon",
1540
1540
"bytes",
1541
1541
"clap",
1542
-
"hickory-resolver",
1543
1542
"http",
1544
1543
"jacquard-api",
1545
1544
"jacquard-common",
1546
1545
"jacquard-derive",
1546
+
"jacquard-identity",
1547
1547
"jacquard-oauth",
1548
1548
"jose-jwk",
1549
1549
"miette",
···
1588
1588
"cid",
1589
1589
"ed25519-dalek",
1590
1590
"enum_dispatch",
1591
-
"hickory-resolver",
1592
1591
"http",
1593
1592
"ipld-core",
1594
1593
"k256",
···
1610
1609
"smol_str",
1611
1610
"thiserror 2.0.17",
1612
1611
"tokio",
1612
+
"trait-variant",
1613
1613
"url",
1614
1614
]
1615
1615
···
1631
1631
]
1632
1632
1633
1633
[[package]]
1634
+
name = "jacquard-identity"
1635
+
version = "0.2.0"
1636
+
dependencies = [
1637
+
"async-trait",
1638
+
"bon",
1639
+
"bytes",
1640
+
"hickory-resolver",
1641
+
"http",
1642
+
"jacquard-api",
1643
+
"jacquard-common",
1644
+
"miette",
1645
+
"percent-encoding",
1646
+
"reqwest",
1647
+
"serde",
1648
+
"serde_html_form",
1649
+
"serde_json",
1650
+
"thiserror 2.0.17",
1651
+
"tokio",
1652
+
"url",
1653
+
"urlencoding",
1654
+
]
1655
+
1656
+
[[package]]
1634
1657
name = "jacquard-lexicon"
1635
1658
version = "0.2.0"
1636
1659
dependencies = [
···
1657
1680
dependencies = [
1658
1681
"async-trait",
1659
1682
"base64 0.22.1",
1660
-
"bon",
1661
1683
"chrono",
1662
1684
"dashmap",
1663
1685
"elliptic-curve",
1664
1686
"http",
1665
1687
"jacquard-common",
1688
+
"jacquard-identity",
1666
1689
"jose-jwa",
1667
1690
"jose-jwk",
1668
1691
"miette",
···
1678
1701
"smol_str",
1679
1702
"thiserror 2.0.17",
1680
1703
"tokio",
1704
+
"trait-variant",
1681
1705
"url",
1682
1706
"uuid",
1683
1707
]
···
3344
3368
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
3345
3369
dependencies = [
3346
3370
"once_cell",
3371
+
]
3372
+
3373
+
[[package]]
3374
+
name = "trait-variant"
3375
+
version = "0.1.2"
3376
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3377
+
checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
3378
+
dependencies = [
3379
+
"proc-macro2",
3380
+
"quote",
3381
+
"syn 2.0.106",
3347
3382
]
3348
3383
3349
3384
[[package]]
+6
Cargo.toml
+6
Cargo.toml
+1
-2
crates/jacquard-common/Cargo.toml
+1
-2
crates/jacquard-common/Cargo.toml
···
39
39
async-trait = "0.1"
40
40
tokio = { version = "1", features = ["sync"] }
41
41
reqwest = { workspace = true, optional = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] }
42
-
hickory-resolver = { version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"], optional = true }
43
42
serde_ipld_dagcbor.workspace = true
43
+
trait-variant.workspace = true
44
44
45
45
[features]
46
46
default = []
47
-
dns = ["dep:hickory-resolver"]
48
47
crypto = []
49
48
crypto-ed25519 = ["crypto", "dep:ed25519-dalek"]
50
49
crypto-k256 = ["crypto", "dep:k256"]
+2
-2
crates/jacquard-common/src/cowstr.rs
+2
-2
crates/jacquard-common/src/cowstr.rs
+12
-12
crates/jacquard-common/src/ident_resolver.rs
crates/jacquard-identity/src/resolver.rs
+12
-12
crates/jacquard-common/src/ident_resolver.rs
crates/jacquard-identity/src/resolver.rs
···
12
12
use std::collections::BTreeMap;
13
13
use std::str::FromStr;
14
14
15
-
use crate::error::TransportError;
16
-
use crate::types::did_doc::Service;
17
-
use crate::types::ident::AtIdentifier;
18
-
use crate::types::string::AtprotoStr;
19
-
use crate::types::uri::Uri;
20
-
use crate::types::value::Data;
21
-
use crate::{CowStr, IntoStatic};
22
15
use bon::Builder;
23
16
use bytes::Bytes;
24
17
use http::StatusCode;
18
+
use jacquard_common::error::TransportError;
19
+
use jacquard_common::types::did::Did;
20
+
use jacquard_common::types::did_doc::{DidDocument, Service};
21
+
use jacquard_common::types::ident::AtIdentifier;
22
+
use jacquard_common::types::string::{AtprotoStr, Handle};
23
+
use jacquard_common::types::uri::Uri;
24
+
use jacquard_common::types::value::{AtDataError, Data};
25
+
use jacquard_common::{CowStr, IntoStatic};
25
26
use miette::Diagnostic;
26
27
use thiserror::Error;
27
28
use url::Url;
28
29
29
-
use crate::types::did_doc::DidDocument;
30
-
use crate::types::string::{Did, Handle};
31
-
use crate::types::value::AtDataError;
32
30
/// Errors that can occur during identity resolution.
33
31
///
34
32
/// Note: when validating a fetched DID document against a requested DID, a
···
114
112
/// mismatch). Use `into_owned()` to parse into an owned document.
115
113
#[derive(Clone)]
116
114
pub struct DidDocResponse {
115
+
#[allow(missing_docs)]
117
116
pub buffer: Bytes,
117
+
#[allow(missing_docs)]
118
118
pub status: StatusCode,
119
119
/// Optional DID we intended to resolve; used for validation helpers
120
120
pub requested: Option<Did<'static>>,
···
205
205
#[serde(borrow)]
206
206
pub handle: Handle<'a>,
207
207
#[serde(borrow)]
208
-
pub pds: crate::CowStr<'a>,
208
+
pub pds: CowStr<'a>,
209
209
#[serde(borrow, rename = "signingKey", alias = "signing_key")]
210
-
pub signing_key: crate::CowStr<'a>,
210
+
pub signing_key: CowStr<'a>,
211
211
}
212
212
213
213
/// Handle → DID fallback step.
-1
crates/jacquard-common/src/lib.rs
-1
crates/jacquard-common/src/lib.rs
+35
crates/jacquard-identity/Cargo.toml
+35
crates/jacquard-identity/Cargo.toml
···
1
+
[package]
2
+
name = "jacquard-identity"
3
+
edition.workspace = true
4
+
version.workspace = true
5
+
authors.workspace = true
6
+
repository.workspace = true
7
+
keywords.workspace = true
8
+
categories.workspace = true
9
+
readme.workspace = true
10
+
exclude.workspace = true
11
+
homepage.workspace = true
12
+
license.workspace = true
13
+
description.workspace = true
14
+
15
+
[features]
16
+
dns = ["dep:hickory-resolver"]
17
+
18
+
[dependencies]
19
+
async-trait = "0.1.89"
20
+
bon.workspace = true
21
+
bytes.workspace = true
22
+
jacquard-common = { version = "0.2", path = "../jacquard-common" }
23
+
percent-encoding = "2.3.2"
24
+
reqwest.workspace = true
25
+
url.workspace = true
26
+
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] }
27
+
hickory-resolver = { optional = true, version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"]}
28
+
serde.workspace = true
29
+
serde_json.workspace = true
30
+
thiserror.workspace = true
31
+
miette.workspace = true
32
+
http.workspace = true
33
+
jacquard-api = { version = "0.2.0", path = "../jacquard-api" }
34
+
serde_html_form.workspace = true
35
+
urlencoding = "2.1.3"
+3
-1
crates/jacquard-oauth/Cargo.toml
+3
-1
crates/jacquard-oauth/Cargo.toml
+1
-1
crates/jacquard-oauth/src/atproto.rs
+1
-1
crates/jacquard-oauth/src/atproto.rs
-13
crates/jacquard-oauth/src/dpop.rs
-13
crates/jacquard-oauth/src/dpop.rs
···
63
63
{
64
64
DpopCall::client(self, data_source)
65
65
}
66
-
67
-
async fn wrap_with_dpop<'r, D>(
68
-
&'r self,
69
-
is_to_auth_server: bool,
70
-
data_source: &'r mut D,
71
-
request: Request<Vec<u8>>,
72
-
) -> Result<Response<Vec<u8>>>
73
-
where
74
-
Self: Sized,
75
-
D: DpopDataSource,
76
-
{
77
-
wrap_request_with_dpop(self, data_source, is_to_auth_server, request).await
78
-
}
79
66
}
80
67
81
68
pub struct DpopCall<'r, C: HttpClient, D: DpopDataSource> {
+13
-17
crates/jacquard-oauth/src/request.rs
+13
-17
crates/jacquard-oauth/src/request.rs
···
1
-
use chrono::{DateTime, FixedOffset, TimeDelta, Utc};
1
+
use chrono::{TimeDelta, Utc};
2
2
use http::{Method, Request, StatusCode};
3
3
use jacquard_common::{
4
4
CowStr, IntoStatic,
5
5
cowstr::ToCowStr,
6
6
http_client::HttpClient,
7
-
ident_resolver::{IdentityError, IdentityResolver},
8
7
session::SessionStoreError,
9
8
types::{
10
9
did::Did,
11
10
string::{AtStrError, Datetime},
12
11
},
13
12
};
14
-
use jose_jwk::Key;
15
-
use serde::{Serialize, de::DeserializeOwned};
13
+
use jacquard_identity::resolver::IdentityError;
14
+
use serde::Serialize;
16
15
use serde_json::Value;
17
16
use smol_str::ToSmolStr;
18
-
use std::sync::Arc;
19
17
use thiserror::Error;
20
-
use url::Url;
21
18
22
19
use crate::{
23
20
FALLBACK_ALG,
24
-
atproto::{AtprotoClientMetadata, atproto_client_metadata},
25
-
dpop::{DpopClient, DpopExt},
21
+
atproto::atproto_client_metadata,
22
+
dpop::DpopExt,
26
23
jose::jwt::{RegisteredClaims, RegisteredClaimsAud},
27
24
keyset::Keyset,
28
25
resolver::OAuthResolver,
···
424
421
}
425
422
}
426
423
424
+
#[inline]
427
425
fn endpoint_for_req<'a, 'r>(
428
426
server_metadata: &'r OAuthAuthorizationServerMetadata<'a>,
429
427
request: &'r OAuthRequest,
···
438
436
}
439
437
}
440
438
441
-
fn build_oauth_req_body<'a, S>(
442
-
client_assertions: ClientAssertions<'a>,
443
-
parameters: S,
444
-
) -> Result<String>
439
+
#[inline]
440
+
fn build_oauth_req_body<'a, S>(client_assertions: ClientAuth<'a>, parameters: S) -> Result<String>
445
441
where
446
442
S: Serialize,
447
443
{
···
454
450
}
455
451
456
452
#[derive(Debug, Clone, Default)]
457
-
pub struct ClientAssertions<'a> {
453
+
pub struct ClientAuth<'a> {
458
454
client_id: CowStr<'a>,
459
455
assertion_type: Option<CowStr<'a>>, // either none or `CLIENT_ASSERTION_TYPE_JWT_BEARER`
460
456
assertion: Option<CowStr<'a>>,
461
457
}
462
458
463
-
impl<'s> ClientAssertions<'s> {
459
+
impl<'s> ClientAuth<'s> {
464
460
pub fn new_id(client_id: CowStr<'s>) -> Self {
465
461
Self {
466
462
client_id,
···
474
470
keyset: Option<&Keyset>,
475
471
server_metadata: &OAuthAuthorizationServerMetadata<'a>,
476
472
client_metadata: &OAuthClientMetadata<'a>,
477
-
) -> Result<ClientAssertions<'a>> {
473
+
) -> Result<ClientAuth<'a>> {
478
474
let method_supported = server_metadata
479
475
.token_endpoint_auth_methods_supported
480
476
.as_ref();
···
494
490
.unwrap_or(vec![FALLBACK_ALG.into()]);
495
491
algs.sort_by(compare_algos);
496
492
let iat = Utc::now().timestamp();
497
-
return Ok(ClientAssertions {
493
+
return Ok(ClientAuth {
498
494
client_id: client_id.clone(),
499
495
assertion_type: Some(CowStr::new_static(CLIENT_ASSERTION_TYPE_JWT_BEARER)),
500
496
assertion: Some(
···
526
522
.as_ref()
527
523
.is_some_and(|v| v.contains(&CowStr::new_static("none"))) =>
528
524
{
529
-
return Ok(ClientAssertions::new_id(client_id));
525
+
return Ok(ClientAuth::new_id(client_id));
530
526
}
531
527
_ => {}
532
528
}
+1
-5
crates/jacquard-oauth/src/resolver.rs
+1
-5
crates/jacquard-oauth/src/resolver.rs
···
1
1
use crate::types::{OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata};
2
2
use http::{Request, StatusCode};
3
3
use jacquard_common::IntoStatic;
4
-
use jacquard_common::ident_resolver::{IdentityError, IdentityResolver};
5
4
use jacquard_common::types::did_doc::DidDocument;
6
5
use jacquard_common::types::ident::AtIdentifier;
7
6
use jacquard_common::{http_client::HttpClient, types::did::Did};
8
-
use sha2::digest::const_oid::Arc;
7
+
use jacquard_identity::resolver::{IdentityError, IdentityResolver};
9
8
use url::Url;
10
9
11
10
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
···
160
159
Ok(as_metadata)
161
160
}
162
161
}
163
-
164
-
#[async_trait::async_trait]
165
-
impl<T: OAuthResolver + Sync + Send> OAuthResolver for std::sync::Arc<T> {}
166
162
167
163
pub async fn resolve_authorization_server<T: HttpClient + ?Sized>(
168
164
client: &T,
+2
-1
crates/jacquard-oauth/src/session.rs
+2
-1
crates/jacquard-oauth/src/session.rs
···
308
308
return Ok(session);
309
309
}
310
310
}
311
-
let metadata = OAuthMetadata::new(&self.client, &self.client_data, &session).await?;
311
+
let metadata =
312
+
OAuthMetadata::new(self.client.as_ref(), &self.client_data, &session).await?;
312
313
session = refresh(self.client.as_ref(), session, &metadata).await?;
313
314
self.store.upsert_session(session.clone()).await?;
314
315
+1
-2
crates/jacquard-oauth/src/utils.rs
+1
-2
crates/jacquard-oauth/src/utils.rs
···
1
1
use base64::Engine;
2
2
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3
3
use elliptic_curve::SecretKey;
4
-
use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr};
4
+
use jacquard_common::CowStr;
5
5
use jose_jwk::{Key, crypto};
6
6
use rand::{CryptoRng, RngCore, rngs::ThreadRng};
7
7
use sha2::{Digest, Sha256};
8
-
use smol_str::ToSmolStr;
9
8
use std::cmp::Ordering;
10
9
11
10
use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata};
+2
-2
crates/jacquard/Cargo.toml
+2
-2
crates/jacquard/Cargo.toml
···
16
16
derive = ["dep:jacquard-derive"]
17
17
api = ["jacquard-api/com_atproto"]
18
18
api_all = ["api", "jacquard-api/app_bsky", "jacquard-api/chat_bsky", "jacquard-api/tools_ozone"]
19
-
dns = ["dep:hickory-resolver", "jacquard-common/dns"]
19
+
dns = ["jacquard-identity/dns"]
20
20
fancy = ["miette/fancy"]
21
21
loopback = ["dep:rouille"]
22
22
···
47
47
serde_json.workspace = true
48
48
thiserror.workspace = true
49
49
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] }
50
-
hickory-resolver = { version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"], optional = true }
51
50
url.workspace = true
52
51
smol_str.workspace = true
53
52
percent-encoding = "2"
···
56
55
p256 = { version = "0.13", features = ["ecdsa"] }
57
56
rand_core = "0.6"
58
57
rouille = { version = "3.6.2", optional = true }
58
+
jacquard-identity = { version = "0.2.0", path = "../jacquard-identity" }
+3
-5
crates/jacquard/src/client.rs
+3
-5
crates/jacquard/src/client.rs
···
21
21
pub use token::FileTokenStore;
22
22
use url::Url;
23
23
24
-
use p256::SecretKey;
25
-
26
24
// Note: Stateless and stateful XRPC clients are implemented in xrpc_call.rs and at_client.rs
27
25
28
26
pub(crate) const NSID_REFRESH_SESSION: &str = "com.atproto.server.refreshSession";
···
133
131
#[derive(Debug, Clone)]
134
132
pub enum AuthSession {
135
133
AppPassword(AtpSession),
136
-
OAuth(jacquard_oauth::session::OauthSession<'static>),
134
+
OAuth(jacquard_oauth::session::ClientSessionData<'static>),
137
135
}
138
136
139
137
impl AuthSession {
···
187
185
}
188
186
}
189
187
190
-
impl From<jacquard_oauth::session::OauthSession<'static>> for AuthSession {
191
-
fn from(session: jacquard_oauth::session::OauthSession<'static>) -> Self {
188
+
impl From<jacquard_oauth::session::ClientSessionData<'static>> for AuthSession {
189
+
fn from(session: jacquard_oauth::session::ClientSessionData<'static>) -> Self {
192
190
AuthSession::OAuth(session)
193
191
}
194
192
}
+1
-1
crates/jacquard/src/client/at_client.rs
+1
-1
crates/jacquard/src/client/at_client.rs
···
13
13
14
14
use jacquard_common::types::xrpc::{XrpcRequest, build_http_request};
15
15
16
-
use crate::client::{AtpSession, AuthSession, FileTokenStore, NSID_REFRESH_SESSION};
16
+
use crate::client::{AtpSession, AuthSession, NSID_REFRESH_SESSION};
17
17
18
18
/// Per-call overrides when sending via `AtClient`.
19
19
#[derive(Debug, Default, Clone)]
+11
-10
crates/jacquard/src/identity.rs
crates/jacquard-identity/src/lib.rs
+11
-10
crates/jacquard/src/identity.rs
crates/jacquard-identity/src/lib.rs
···
12
12
//! and optionally validate the document `id` against the requested DID.
13
13
14
14
// use crate::CowStr; // not currently needed directly here
15
+
pub mod resolver;
15
16
17
+
use crate::resolver::{
18
+
DidDocResponse, DidStep, HandleStep, IdentityError, IdentityResolver, MiniDoc, PlcSource,
19
+
ResolverOptions,
20
+
};
16
21
use bytes::Bytes;
17
-
use jacquard_common::IntoStatic;
22
+
use jacquard_api::com_atproto::identity::resolve_did;
23
+
use jacquard_api::com_atproto::identity::resolve_handle::ResolveHandle;
18
24
use jacquard_common::error::TransportError;
19
25
use jacquard_common::http_client::HttpClient;
20
-
use jacquard_common::ident_resolver::{
21
-
DidDocResponse, DidStep, HandleStep, IdentityError, IdentityResolver, MiniDoc, PlcSource,
22
-
ResolverOptions,
23
-
};
26
+
use jacquard_common::types::did::Did;
27
+
use jacquard_common::types::did_doc::DidDocument;
28
+
use jacquard_common::types::ident::AtIdentifier;
24
29
use jacquard_common::types::xrpc::XrpcExt;
30
+
use jacquard_common::{IntoStatic, types::string::Handle};
25
31
use percent_encoding::percent_decode_str;
26
32
use reqwest::StatusCode;
27
33
use url::{ParseError, Url};
28
-
29
-
use crate::api::com_atproto::identity::{resolve_did, resolve_handle::ResolveHandle};
30
-
use crate::types::did_doc::DidDocument;
31
-
use crate::types::ident::AtIdentifier;
32
-
use crate::types::string::{Did, Handle};
33
34
34
35
#[cfg(feature = "dns")]
35
36
use hickory_resolver::{TokioAsyncResolver, config::ResolverConfig};
+1
-2
crates/jacquard/src/lib.rs
+1
-2
crates/jacquard/src/lib.rs
+1
-1
crates/jacquard/src/main.rs
+1
-1
crates/jacquard/src/main.rs
···
3
3
use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
4
4
use jacquard::api::com_atproto::server::create_session::CreateSession;
5
5
use jacquard::client::{AtpSession, AuthSession, BasicClient};
6
-
use jacquard::ident_resolver::IdentityResolver;
6
+
use jacquard::identity::resolver::IdentityResolver;
7
7
use jacquard::identity::slingshot_resolver_default;
8
8
use jacquard::types::string::Handle;
9
9
use miette::IntoDiagnostic;