A better Rust ATProto crate

moved identity resolution stuff into its own crate

Orual 102d6666 36ef115b

Changed files
+134 -81
crates
+38 -3
Cargo.lock
···
"bon",
"bytes",
"clap",
-
"hickory-resolver",
"http",
"jacquard-api",
"jacquard-common",
"jacquard-derive",
+
"jacquard-identity",
"jacquard-oauth",
"jose-jwk",
"miette",
···
"cid",
"ed25519-dalek",
"enum_dispatch",
-
"hickory-resolver",
"http",
"ipld-core",
"k256",
···
"smol_str",
"thiserror 2.0.17",
"tokio",
+
"trait-variant",
"url",
···
[[package]]
+
name = "jacquard-identity"
+
version = "0.2.0"
+
dependencies = [
+
"async-trait",
+
"bon",
+
"bytes",
+
"hickory-resolver",
+
"http",
+
"jacquard-api",
+
"jacquard-common",
+
"miette",
+
"percent-encoding",
+
"reqwest",
+
"serde",
+
"serde_html_form",
+
"serde_json",
+
"thiserror 2.0.17",
+
"tokio",
+
"url",
+
"urlencoding",
+
]
+
+
[[package]]
name = "jacquard-lexicon"
version = "0.2.0"
dependencies = [
···
dependencies = [
"async-trait",
"base64 0.22.1",
-
"bon",
"chrono",
"dashmap",
"elliptic-curve",
"http",
"jacquard-common",
+
"jacquard-identity",
"jose-jwa",
"jose-jwk",
"miette",
···
"smol_str",
"thiserror 2.0.17",
"tokio",
+
"trait-variant",
"url",
"uuid",
···
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
+
]
+
+
[[package]]
+
name = "trait-variant"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn 2.0.106",
[[package]]
+6
Cargo.toml
···
miette = "7.6"
thiserror = "2.0"
+
# trait stuff
+
trait-variant = "0.1.2"
+
+
+
bon = "3.8.0"
+
# Data types
bytes = "1.10"
smol_str = { version = "0.3", features = ["serde"] }
+1 -2
crates/jacquard-common/Cargo.toml
···
async-trait = "0.1"
tokio = { version = "1", features = ["sync"] }
reqwest = { workspace = true, optional = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] }
-
hickory-resolver = { version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"], optional = true }
serde_ipld_dagcbor.workspace = true
+
trait-variant.workspace = true
[features]
default = []
-
dns = ["dep:hickory-resolver"]
crypto = []
crypto-ed25519 = ["crypto", "dep:ed25519-dalek"]
crypto-k256 = ["crypto", "dep:k256"]
+2 -2
crates/jacquard-common/src/cowstr.rs
···
-
use serde::{Deserialize, Serialize, de::DeserializeOwned};
-
use smol_str::{SmolStr, ToSmolStr};
+
use serde::{Deserialize, Serialize};
+
use smol_str::SmolStr;
use std::{
borrow::Cow,
fmt,
+12 -12
crates/jacquard-common/src/ident_resolver.rs crates/jacquard-identity/src/resolver.rs
···
use std::collections::BTreeMap;
use std::str::FromStr;
-
use crate::error::TransportError;
-
use crate::types::did_doc::Service;
-
use crate::types::ident::AtIdentifier;
-
use crate::types::string::AtprotoStr;
-
use crate::types::uri::Uri;
-
use crate::types::value::Data;
-
use crate::{CowStr, IntoStatic};
use bon::Builder;
use bytes::Bytes;
use http::StatusCode;
+
use jacquard_common::error::TransportError;
+
use jacquard_common::types::did::Did;
+
use jacquard_common::types::did_doc::{DidDocument, Service};
+
use jacquard_common::types::ident::AtIdentifier;
+
use jacquard_common::types::string::{AtprotoStr, Handle};
+
use jacquard_common::types::uri::Uri;
+
use jacquard_common::types::value::{AtDataError, Data};
+
use jacquard_common::{CowStr, IntoStatic};
use miette::Diagnostic;
use thiserror::Error;
use url::Url;
-
use crate::types::did_doc::DidDocument;
-
use crate::types::string::{Did, Handle};
-
use crate::types::value::AtDataError;
/// Errors that can occur during identity resolution.
///
/// Note: when validating a fetched DID document against a requested DID, a
···
/// mismatch). Use `into_owned()` to parse into an owned document.
#[derive(Clone)]
pub struct DidDocResponse {
+
#[allow(missing_docs)]
pub buffer: Bytes,
+
#[allow(missing_docs)]
pub status: StatusCode,
/// Optional DID we intended to resolve; used for validation helpers
pub requested: Option<Did<'static>>,
···
#[serde(borrow)]
pub handle: Handle<'a>,
#[serde(borrow)]
-
pub pds: crate::CowStr<'a>,
+
pub pds: CowStr<'a>,
#[serde(borrow, rename = "signingKey", alias = "signing_key")]
-
pub signing_key: crate::CowStr<'a>,
+
pub signing_key: CowStr<'a>,
}
/// Handle → DID fallback step.
-1
crates/jacquard-common/src/lib.rs
···
pub mod error;
/// HTTP client abstraction used by jacquard crates.
pub mod http_client;
-
pub mod ident_resolver;
pub mod macros;
/// Generic session storage traits and utilities.
pub mod session;
+35
crates/jacquard-identity/Cargo.toml
···
+
[package]
+
name = "jacquard-identity"
+
edition.workspace = true
+
version.workspace = true
+
authors.workspace = true
+
repository.workspace = true
+
keywords.workspace = true
+
categories.workspace = true
+
readme.workspace = true
+
exclude.workspace = true
+
homepage.workspace = true
+
license.workspace = true
+
description.workspace = true
+
+
[features]
+
dns = ["dep:hickory-resolver"]
+
+
[dependencies]
+
async-trait = "0.1.89"
+
bon.workspace = true
+
bytes.workspace = true
+
jacquard-common = { version = "0.2", path = "../jacquard-common" }
+
percent-encoding = "2.3.2"
+
reqwest.workspace = true
+
url.workspace = true
+
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] }
+
hickory-resolver = { optional = true, version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"]}
+
serde.workspace = true
+
serde_json.workspace = true
+
thiserror.workspace = true
+
miette.workspace = true
+
http.workspace = true
+
jacquard-api = { version = "0.2.0", path = "../jacquard-api" }
+
serde_html_form.workspace = true
+
urlencoding = "2.1.3"
+3 -1
crates/jacquard-oauth/Cargo.toml
···
async-trait = "0.1.89"
dashmap = "6.1.0"
tokio = { version = "1.47.1", features = ["sync"] }
-
bon = "3.8.0"
+
reqwest.workspace = true
+
trait-variant.workspace = true
+
jacquard-identity = { version = "0.2.0", path = "../jacquard-identity" }
+1 -1
crates/jacquard-oauth/src/atproto.rs
···
use jacquard_common::CowStr;
use serde::{Deserialize, Serialize};
use thiserror::Error;
-
use url::{Host, Url};
+
use url::Url;
#[derive(Error, Debug)]
pub enum Error {
-13
crates/jacquard-oauth/src/dpop.rs
···
{
DpopCall::client(self, data_source)
}
-
-
async fn wrap_with_dpop<'r, D>(
-
&'r self,
-
is_to_auth_server: bool,
-
data_source: &'r mut D,
-
request: Request<Vec<u8>>,
-
) -> Result<Response<Vec<u8>>>
-
where
-
Self: Sized,
-
D: DpopDataSource,
-
{
-
wrap_request_with_dpop(self, data_source, is_to_auth_server, request).await
-
}
}
pub struct DpopCall<'r, C: HttpClient, D: DpopDataSource> {
+13 -17
crates/jacquard-oauth/src/request.rs
···
-
use chrono::{DateTime, FixedOffset, TimeDelta, Utc};
+
use chrono::{TimeDelta, Utc};
use http::{Method, Request, StatusCode};
use jacquard_common::{
CowStr, IntoStatic,
cowstr::ToCowStr,
http_client::HttpClient,
-
ident_resolver::{IdentityError, IdentityResolver},
session::SessionStoreError,
types::{
did::Did,
string::{AtStrError, Datetime},
},
};
-
use jose_jwk::Key;
-
use serde::{Serialize, de::DeserializeOwned};
+
use jacquard_identity::resolver::IdentityError;
+
use serde::Serialize;
use serde_json::Value;
use smol_str::ToSmolStr;
-
use std::sync::Arc;
use thiserror::Error;
-
use url::Url;
use crate::{
FALLBACK_ALG,
-
atproto::{AtprotoClientMetadata, atproto_client_metadata},
-
dpop::{DpopClient, DpopExt},
+
atproto::atproto_client_metadata,
+
dpop::DpopExt,
jose::jwt::{RegisteredClaims, RegisteredClaimsAud},
keyset::Keyset,
resolver::OAuthResolver,
···
}
}
+
#[inline]
fn endpoint_for_req<'a, 'r>(
server_metadata: &'r OAuthAuthorizationServerMetadata<'a>,
request: &'r OAuthRequest,
···
}
}
-
fn build_oauth_req_body<'a, S>(
-
client_assertions: ClientAssertions<'a>,
-
parameters: S,
-
) -> Result<String>
+
#[inline]
+
fn build_oauth_req_body<'a, S>(client_assertions: ClientAuth<'a>, parameters: S) -> Result<String>
where
S: Serialize,
{
···
}
#[derive(Debug, Clone, Default)]
-
pub struct ClientAssertions<'a> {
+
pub struct ClientAuth<'a> {
client_id: CowStr<'a>,
assertion_type: Option<CowStr<'a>>, // either none or `CLIENT_ASSERTION_TYPE_JWT_BEARER`
assertion: Option<CowStr<'a>>,
}
-
impl<'s> ClientAssertions<'s> {
+
impl<'s> ClientAuth<'s> {
pub fn new_id(client_id: CowStr<'s>) -> Self {
Self {
client_id,
···
keyset: Option<&Keyset>,
server_metadata: &OAuthAuthorizationServerMetadata<'a>,
client_metadata: &OAuthClientMetadata<'a>,
-
) -> Result<ClientAssertions<'a>> {
+
) -> Result<ClientAuth<'a>> {
let method_supported = server_metadata
.token_endpoint_auth_methods_supported
.as_ref();
···
.unwrap_or(vec![FALLBACK_ALG.into()]);
algs.sort_by(compare_algos);
let iat = Utc::now().timestamp();
-
return Ok(ClientAssertions {
+
return Ok(ClientAuth {
client_id: client_id.clone(),
assertion_type: Some(CowStr::new_static(CLIENT_ASSERTION_TYPE_JWT_BEARER)),
assertion: Some(
···
.as_ref()
.is_some_and(|v| v.contains(&CowStr::new_static("none"))) =>
{
-
return Ok(ClientAssertions::new_id(client_id));
+
return Ok(ClientAuth::new_id(client_id));
}
_ => {}
}
+1 -5
crates/jacquard-oauth/src/resolver.rs
···
use crate::types::{OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata};
use http::{Request, StatusCode};
use jacquard_common::IntoStatic;
-
use jacquard_common::ident_resolver::{IdentityError, IdentityResolver};
use jacquard_common::types::did_doc::DidDocument;
use jacquard_common::types::ident::AtIdentifier;
use jacquard_common::{http_client::HttpClient, types::did::Did};
-
use sha2::digest::const_oid::Arc;
+
use jacquard_identity::resolver::{IdentityError, IdentityResolver};
use url::Url;
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
···
Ok(as_metadata)
}
}
-
-
#[async_trait::async_trait]
-
impl<T: OAuthResolver + Sync + Send> OAuthResolver for std::sync::Arc<T> {}
pub async fn resolve_authorization_server<T: HttpClient + ?Sized>(
client: &T,
+2 -1
crates/jacquard-oauth/src/session.rs
···
return Ok(session);
}
}
-
let metadata = OAuthMetadata::new(&self.client, &self.client_data, &session).await?;
+
let metadata =
+
OAuthMetadata::new(self.client.as_ref(), &self.client_data, &session).await?;
session = refresh(self.client.as_ref(), session, &metadata).await?;
self.store.upsert_session(session.clone()).await?;
+1 -2
crates/jacquard-oauth/src/utils.rs
···
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use elliptic_curve::SecretKey;
-
use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr};
+
use jacquard_common::CowStr;
use jose_jwk::{Key, crypto};
use rand::{CryptoRng, RngCore, rngs::ThreadRng};
use sha2::{Digest, Sha256};
-
use smol_str::ToSmolStr;
use std::cmp::Ordering;
use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata};
+2 -2
crates/jacquard/Cargo.toml
···
derive = ["dep:jacquard-derive"]
api = ["jacquard-api/com_atproto"]
api_all = ["api", "jacquard-api/app_bsky", "jacquard-api/chat_bsky", "jacquard-api/tools_ozone"]
-
dns = ["dep:hickory-resolver", "jacquard-common/dns"]
+
dns = ["jacquard-identity/dns"]
fancy = ["miette/fancy"]
loopback = ["dep:rouille"]
···
serde_json.workspace = true
thiserror.workspace = true
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] }
-
hickory-resolver = { version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"], optional = true }
url.workspace = true
smol_str.workspace = true
percent-encoding = "2"
···
p256 = { version = "0.13", features = ["ecdsa"] }
rand_core = "0.6"
rouille = { version = "3.6.2", optional = true }
+
jacquard-identity = { version = "0.2.0", path = "../jacquard-identity" }
+3 -5
crates/jacquard/src/client.rs
···
pub use token::FileTokenStore;
use url::Url;
-
use p256::SecretKey;
-
// Note: Stateless and stateful XRPC clients are implemented in xrpc_call.rs and at_client.rs
pub(crate) const NSID_REFRESH_SESSION: &str = "com.atproto.server.refreshSession";
···
#[derive(Debug, Clone)]
pub enum AuthSession {
AppPassword(AtpSession),
-
OAuth(jacquard_oauth::session::OauthSession<'static>),
+
OAuth(jacquard_oauth::session::ClientSessionData<'static>),
}
impl AuthSession {
···
}
}
-
impl From<jacquard_oauth::session::OauthSession<'static>> for AuthSession {
-
fn from(session: jacquard_oauth::session::OauthSession<'static>) -> Self {
+
impl From<jacquard_oauth::session::ClientSessionData<'static>> for AuthSession {
+
fn from(session: jacquard_oauth::session::ClientSessionData<'static>) -> Self {
AuthSession::OAuth(session)
}
}
+1 -1
crates/jacquard/src/client/at_client.rs
···
use jacquard_common::types::xrpc::{XrpcRequest, build_http_request};
-
use crate::client::{AtpSession, AuthSession, FileTokenStore, NSID_REFRESH_SESSION};
+
use crate::client::{AtpSession, AuthSession, NSID_REFRESH_SESSION};
/// Per-call overrides when sending via `AtClient`.
#[derive(Debug, Default, Clone)]
+11 -10
crates/jacquard/src/identity.rs crates/jacquard-identity/src/lib.rs
···
//! and optionally validate the document `id` against the requested DID.
// use crate::CowStr; // not currently needed directly here
+
pub mod resolver;
+
use crate::resolver::{
+
DidDocResponse, DidStep, HandleStep, IdentityError, IdentityResolver, MiniDoc, PlcSource,
+
ResolverOptions,
+
};
use bytes::Bytes;
-
use jacquard_common::IntoStatic;
+
use jacquard_api::com_atproto::identity::resolve_did;
+
use jacquard_api::com_atproto::identity::resolve_handle::ResolveHandle;
use jacquard_common::error::TransportError;
use jacquard_common::http_client::HttpClient;
-
use jacquard_common::ident_resolver::{
-
DidDocResponse, DidStep, HandleStep, IdentityError, IdentityResolver, MiniDoc, PlcSource,
-
ResolverOptions,
-
};
+
use jacquard_common::types::did::Did;
+
use jacquard_common::types::did_doc::DidDocument;
+
use jacquard_common::types::ident::AtIdentifier;
use jacquard_common::types::xrpc::XrpcExt;
+
use jacquard_common::{IntoStatic, types::string::Handle};
use percent_encoding::percent_decode_str;
use reqwest::StatusCode;
use url::{ParseError, Url};
-
-
use crate::api::com_atproto::identity::{resolve_did, resolve_handle::ResolveHandle};
-
use crate::types::did_doc::DidDocument;
-
use crate::types::ident::AtIdentifier;
-
use crate::types::string::{Did, Handle};
#[cfg(feature = "dns")]
use hickory_resolver::{TokioAsyncResolver, config::ResolverConfig};
+1 -2
crates/jacquard/src/lib.rs
···
/// if enabled, reexport the attribute macros
pub use jacquard_derive::*;
-
/// Identity resolution helpers (DIDs, handles, PDS endpoints)
-
pub mod identity;
+
pub use jacquard_identity as identity;
+1 -1
crates/jacquard/src/main.rs
···
use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
use jacquard::api::com_atproto::server::create_session::CreateSession;
use jacquard::client::{AtpSession, AuthSession, BasicClient};
-
use jacquard::ident_resolver::IdentityResolver;
+
use jacquard::identity::resolver::IdentityResolver;
use jacquard::identity::slingshot_resolver_default;
use jacquard::types::string::Handle;
use miette::IntoDiagnostic;