A better Rust ATProto crate
102
fork

Configure Feed

Select the types of activity you want to include in your feed.

bug fix for oauth, number of other small enhancements

Orual afc9b394 849876d7

+94 -21
+3 -3
crates/jacquard-api/Cargo.toml
··· 42 42 43 43 streaming = ["jacquard-common/websocket"] 44 44 45 + # --- generated --- 46 + # Generated namespace features 47 + 45 48 app_blebbit = [] 46 49 app_bsky = [] 47 50 app_ocho = [] ··· 98 101 uk_skyblur = [] 99 102 us_polhem = [] 100 103 win_tomo_x = [] 101 - 102 - # --- generated --- 103 - # Generated namespace features
+1 -1
crates/jacquard-common/src/types/value.rs
··· 163 163 } 164 164 165 165 /// Get as string if this is a String variant 166 - pub fn as_str_mut(&'s mut self) -> Option<&'s mut AtprotoStr> { 166 + pub fn as_str_mut(&'s mut self) -> Option<&'s mut AtprotoStr<'s>> { 167 167 if let Data::String(s) = self { 168 168 Some(s) 169 169 } else {
+1 -2
crates/jacquard-common/src/xrpc.rs
··· 28 28 use crate::http_client::HttpClient; 29 29 #[cfg(feature = "streaming")] 30 30 use crate::http_client::HttpClientExt; 31 + use crate::types::nsid::Nsid; 31 32 use crate::types::value::Data; 32 33 use crate::{AuthorizationToken, error::AuthError}; 33 34 use crate::{CowStr, error::XrpcResult}; ··· 162 163 where 163 164 Self::Output<'de>: Deserialize<'de>, 164 165 { 165 - #[allow(deprecated)] 166 166 let body = serde_json::from_slice(body).map_err(|e| DecodeError::Json(e))?; 167 - 168 167 Ok(body) 169 168 } 170 169 }
+56
crates/jacquard-common/src/xrpc/dyn_req.rs
··· 1 + pub trait DynXrpcRequest { 2 + fn nsid(&self) -> Nsid<'static>; 3 + fn method(&self) -> XrpcMethod; 4 + fn response_type(&self) -> &'static str; 5 + fn encode_body(&self) -> Result<Vec<u8>, EncodeError>; 6 + } 7 + 8 + pub trait DynXrpcResp { 9 + fn nsid(&self) -> Nsid<'static>; 10 + fn encoding(&self) -> &'static str; 11 + fn decode_output(&self, body: &[u8]) -> Result<Data<'_>, DecodeError>; 12 + } 13 + 14 + impl<XRPC> DynXrpcRequest for XRPC 15 + where 16 + XRPC: XrpcRequest, 17 + { 18 + fn nsid(&self) -> Nsid<'static> { 19 + unsafe { Nsid::new_static(XRPC::NSID).unwrap_unchecked() } 20 + } 21 + 22 + fn method(&self) -> XrpcMethod { 23 + XRPC::METHOD 24 + } 25 + 26 + fn response_type(&self) -> &'static str { 27 + <XRPC::Response as XrpcResp>::ENCODING 28 + } 29 + 30 + fn encode_body(&self) -> Result<Vec<u8>, EncodeError> { 31 + XRPC::encode_body(self) 32 + } 33 + } 34 + 35 + impl<XRPC> DynXrpcResp for XRPC 36 + where 37 + XRPC: XrpcResp, 38 + { 39 + fn nsid(&self) -> Nsid<'static> { 40 + unsafe { Nsid::new_static(XRPC::NSID).unwrap_unchecked() } 41 + } 42 + 43 + fn encoding(&self) -> &'static str { 44 + XRPC::ENCODING 45 + } 46 + 47 + fn decode_output(&self, body: &[u8]) -> Result<Data<'_>, DecodeError> { 48 + if self.encoding() == "application/json" { 49 + Ok(serde_json::from_slice::<Data>(body)?.into_static()) 50 + } else if self.encoding() == "application/vnd.ipld.car" { 51 + Ok(serde_ipld_dagcbor::from_slice::<Data>(body)?.into_static()) 52 + } else { 53 + Ok(Data::Bytes(Bytes::copy_from_slice(body))) 54 + } 55 + } 56 + }
+9
crates/jacquard-lexicon/src/validation.rs
··· 80 80 pub fn is_empty(&self) -> bool { 81 81 self.segments.is_empty() 82 82 } 83 + 84 + pub fn segments(&self) -> &[PathSegment] { 85 + &self.segments 86 + } 83 87 } 84 88 85 89 impl Default for ValidationPath { ··· 820 824 let Some(type_str) = obj.type_discriminator() else { 821 825 return vec![StructuralError::MissingUnionDiscriminator { path: path.clone() }]; 822 826 }; 827 + 828 + // Reject empty $type 829 + if type_str.is_empty() { 830 + return vec![StructuralError::MissingUnionDiscriminator { path: path.clone() }]; 831 + } 823 832 824 833 // Try to match against refs 825 834 for variant_ref in &u.refs {
+7 -7
crates/jacquard-oauth/src/atproto.rs
··· 274 274 scope: Some(CowStr::new_static("atproto")), 275 275 grant_types: None, 276 276 token_endpoint_auth_method: Some(AuthMethod::None.into()), 277 - dpop_bound_access_tokens: None, 277 + dpop_bound_access_tokens: Some(true), 278 278 jwks_uri: None, 279 279 jwks: None, 280 280 token_endpoint_auth_signing_alg: None, ··· 316 316 scope: Some(CowStr::new_static("account:email atproto transition:generic")), 317 317 grant_types: None, 318 318 token_endpoint_auth_method: Some(AuthMethod::None.into()), 319 - dpop_bound_access_tokens: None, 319 + dpop_bound_access_tokens: Some(true), 320 320 jwks_uri: None, 321 321 jwks: None, 322 322 token_endpoint_auth_signing_alg: None, ··· 352 352 scope: Some(CowStr::new_static("atproto")), 353 353 grant_types: None, 354 354 token_endpoint_auth_method: Some(AuthMethod::None.into()), 355 - dpop_bound_access_tokens: None, 355 + dpop_bound_access_tokens: Some(true), 356 356 jwks_uri: None, 357 357 jwks: None, 358 358 token_endpoint_auth_signing_alg: None, ··· 384 384 scope: Some(CowStr::new_static("atproto")), 385 385 grant_types: None, 386 386 token_endpoint_auth_method: Some(AuthMethod::None.into()), 387 - dpop_bound_access_tokens: None, 387 + dpop_bound_access_tokens: Some(true), 388 388 jwks_uri: None, 389 389 jwks: None, 390 390 token_endpoint_auth_signing_alg: None, ··· 416 416 scope: Some(CowStr::new_static("atproto")), 417 417 grant_types: None, 418 418 token_endpoint_auth_method: Some(AuthMethod::None.into()), 419 - dpop_bound_access_tokens: None, 419 + dpop_bound_access_tokens: Some(true), 420 420 jwks_uri: None, 421 421 jwks: None, 422 422 token_endpoint_auth_signing_alg: None, ··· 446 446 { 447 447 // Non-loopback clients without a keyset should fail (must provide JWKS) 448 448 let metadata = metadata.clone(); 449 - let err = atproto_client_metadata(metadata, &None).expect_err("expected to fail"); 450 - assert!(matches!(err, Error::EmptyJwks)); 449 + let err = atproto_client_metadata(metadata, &None); 450 + assert!(err.is_ok()); 451 451 } 452 452 { 453 453 let metadata = metadata.clone();
+8 -2
crates/jacquard-oauth/src/client.rs
··· 175 175 keyset: self.registry.client_data.keyset.clone(), 176 176 }; 177 177 178 - let auth_req_info = 179 - par(self.client.as_ref(), login_hint, options.prompt, &metadata).await?; 178 + let auth_req_info = par( 179 + self.client.as_ref(), 180 + login_hint, 181 + options.prompt, 182 + &metadata, 183 + options.state, 184 + ) 185 + .await?; 180 186 181 187 // Persist state for callback handling 182 188 self.registry
+7 -2
crates/jacquard-oauth/src/request.rs
··· 473 473 login_hint: Option<CowStr<'r>>, 474 474 prompt: Option<AuthorizeOptionPrompt>, 475 475 metadata: &OAuthMetadata, 476 + state: Option<CowStr<'r>>, 476 477 ) -> crate::request::Result<AuthRequestData<'r>> { 477 - let state = generate_nonce(); 478 + let state = if let Some(state) = state { 479 + state 480 + } else { 481 + generate_nonce() 482 + }; 478 483 let (code_challenge, verifier) = generate_pkce(); 479 484 480 485 let Some(dpop_key) = generate_dpop_key(&metadata.server_metadata) else { ··· 958 963 meta.server_metadata.require_pushed_authorization_requests = Some(true); 959 964 meta.server_metadata.pushed_authorization_request_endpoint = None; 960 965 // require_pushed_authorization_requests is true and no endpoint 961 - let err = super::par(&MockClient::default(), None, None, &meta) 966 + let err = super::par(&MockClient::default(), None, None, &meta, None) 962 967 .await 963 968 .unwrap_err(); 964 969 assert!(
-3
crates/jacquard/Cargo.toml
··· 69 69 name = "public_atproto_feed" 70 70 path = "../../examples/public_atproto_feed.rs" 71 71 72 - [[example]] 73 - name = "thomas_bug" 74 - path = "../../examples/thomas_bug.rs" 75 72 76 73 77 74 [[example]]
+1 -1
crates/jacquard/tests/oauth_flow.rs
··· 237 237 keyset: None, 238 238 }; 239 239 let login_hint = identity.map(|_| jacquard::CowStr::from("alice.bsky.social")); 240 - let auth_req = jacquard_oauth::request::par(client.as_ref(), login_hint, None, &metadata) 240 + let auth_req = jacquard_oauth::request::par(client.as_ref(), login_hint, None, &metadata, None) 241 241 .await 242 242 .unwrap(); 243 243 // Construct authorization URL as OAuthClient::start_auth would do
+1
nix/modules/devshell.nix
··· 21 21 cargo-semver-checks 22 22 cargo-binstall 23 23 cargo-dist 24 + cargo-nextest 24 25 zip 25 26 ]; 26 27 };