diff --git a/crates/jacquard-api/Cargo.toml b/crates/jacquard-api/Cargo.toml index 673c51a6..ac94c90c 100644 --- a/crates/jacquard-api/Cargo.toml +++ b/crates/jacquard-api/Cargo.toml @@ -42,6 +42,9 @@ ufos = [ "moe_karashiiro", "dev_regnault", "buzz_bookhive", "uk_ewancroft", "wi streaming = ["jacquard-common/websocket"] +# --- generated --- +# Generated namespace features + app_blebbit = [] app_bsky = [] app_ocho = [] @@ -98,6 +101,3 @@ uk_ewancroft = [] uk_skyblur = [] us_polhem = [] win_tomo_x = [] - -# --- generated --- -# Generated namespace features diff --git a/crates/jacquard-common/src/types/value.rs b/crates/jacquard-common/src/types/value.rs index 43f0ea71..78ed0ba5 100644 --- a/crates/jacquard-common/src/types/value.rs +++ b/crates/jacquard-common/src/types/value.rs @@ -163,7 +163,7 @@ impl<'s> Data<'s> { } /// Get as string if this is a String variant - pub fn as_str_mut(&'s mut self) -> Option<&'s mut AtprotoStr> { + pub fn as_str_mut(&'s mut self) -> Option<&'s mut AtprotoStr<'s>> { if let Data::String(s) = self { Some(s) } else { diff --git a/crates/jacquard-common/src/xrpc.rs b/crates/jacquard-common/src/xrpc.rs index 646a7fb7..ce9bc80d 100644 --- a/crates/jacquard-common/src/xrpc.rs +++ b/crates/jacquard-common/src/xrpc.rs @@ -28,6 +28,7 @@ use crate::error::DecodeError; use crate::http_client::HttpClient; #[cfg(feature = "streaming")] use crate::http_client::HttpClientExt; +use crate::types::nsid::Nsid; use crate::types::value::Data; use crate::{AuthorizationToken, error::AuthError}; use crate::{CowStr, error::XrpcResult}; @@ -162,9 +163,7 @@ pub trait XrpcResp { where Self::Output<'de>: Deserialize<'de>, { - #[allow(deprecated)] let body = serde_json::from_slice(body).map_err(|e| DecodeError::Json(e))?; - Ok(body) } } diff --git a/crates/jacquard-common/src/xrpc/dyn_req.rs b/crates/jacquard-common/src/xrpc/dyn_req.rs new file mode 100644 index 00000000..6ed03538 --- /dev/null +++ b/crates/jacquard-common/src/xrpc/dyn_req.rs @@ -0,0 +1,56 @@ +pub trait DynXrpcRequest { + fn nsid(&self) -> Nsid<'static>; + fn method(&self) -> XrpcMethod; + fn response_type(&self) -> &'static str; + fn encode_body(&self) -> Result, EncodeError>; +} + +pub trait DynXrpcResp { + fn nsid(&self) -> Nsid<'static>; + fn encoding(&self) -> &'static str; + fn decode_output(&self, body: &[u8]) -> Result, DecodeError>; +} + +impl DynXrpcRequest for XRPC +where + XRPC: XrpcRequest, +{ + fn nsid(&self) -> Nsid<'static> { + unsafe { Nsid::new_static(XRPC::NSID).unwrap_unchecked() } + } + + fn method(&self) -> XrpcMethod { + XRPC::METHOD + } + + fn response_type(&self) -> &'static str { + ::ENCODING + } + + fn encode_body(&self) -> Result, EncodeError> { + XRPC::encode_body(self) + } +} + +impl DynXrpcResp for XRPC +where + XRPC: XrpcResp, +{ + fn nsid(&self) -> Nsid<'static> { + unsafe { Nsid::new_static(XRPC::NSID).unwrap_unchecked() } + } + + fn encoding(&self) -> &'static str { + XRPC::ENCODING + } + + fn decode_output(&self, body: &[u8]) -> Result, DecodeError> { + if self.encoding() == "application/json" { + Ok(serde_json::from_slice::(body)?.into_static()) + } else if self.encoding() == "application/vnd.ipld.car" { + Ok(serde_ipld_dagcbor::from_slice::(body)?.into_static()) + } else { + Ok(Data::Bytes(Bytes::copy_from_slice(body))) + } + } +} diff --git a/crates/jacquard-lexicon/src/validation.rs b/crates/jacquard-lexicon/src/validation.rs index cae5cadb..263978ee 100644 --- a/crates/jacquard-lexicon/src/validation.rs +++ b/crates/jacquard-lexicon/src/validation.rs @@ -80,6 +80,10 @@ impl ValidationPath { pub fn is_empty(&self) -> bool { self.segments.is_empty() } + + pub fn segments(&self) -> &[PathSegment] { + &self.segments + } } impl Default for ValidationPath { @@ -821,6 +825,11 @@ fn validate_property( return vec![StructuralError::MissingUnionDiscriminator { path: path.clone() }]; }; + // Reject empty $type + if type_str.is_empty() { + return vec![StructuralError::MissingUnionDiscriminator { path: path.clone() }]; + } + // Try to match against refs for variant_ref in &u.refs { let ref_path = RefPath::parse(variant_ref.as_ref(), Some(&ctx.current_nsid)); diff --git a/crates/jacquard-oauth/src/atproto.rs b/crates/jacquard-oauth/src/atproto.rs index e8a330e4..f6214c37 100644 --- a/crates/jacquard-oauth/src/atproto.rs +++ b/crates/jacquard-oauth/src/atproto.rs @@ -274,7 +274,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 scope: Some(CowStr::new_static("atproto")), grant_types: None, token_endpoint_auth_method: Some(AuthMethod::None.into()), - dpop_bound_access_tokens: None, + dpop_bound_access_tokens: Some(true), jwks_uri: None, jwks: None, token_endpoint_auth_signing_alg: None, @@ -316,7 +316,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 scope: Some(CowStr::new_static("account:email atproto transition:generic")), grant_types: None, token_endpoint_auth_method: Some(AuthMethod::None.into()), - dpop_bound_access_tokens: None, + dpop_bound_access_tokens: Some(true), jwks_uri: None, jwks: None, token_endpoint_auth_signing_alg: None, @@ -352,7 +352,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 scope: Some(CowStr::new_static("atproto")), grant_types: None, token_endpoint_auth_method: Some(AuthMethod::None.into()), - dpop_bound_access_tokens: None, + dpop_bound_access_tokens: Some(true), jwks_uri: None, jwks: None, token_endpoint_auth_signing_alg: None, @@ -384,7 +384,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 scope: Some(CowStr::new_static("atproto")), grant_types: None, token_endpoint_auth_method: Some(AuthMethod::None.into()), - dpop_bound_access_tokens: None, + dpop_bound_access_tokens: Some(true), jwks_uri: None, jwks: None, token_endpoint_auth_signing_alg: None, @@ -416,7 +416,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 scope: Some(CowStr::new_static("atproto")), grant_types: None, token_endpoint_auth_method: Some(AuthMethod::None.into()), - dpop_bound_access_tokens: None, + dpop_bound_access_tokens: Some(true), jwks_uri: None, jwks: None, token_endpoint_auth_signing_alg: None, @@ -446,8 +446,8 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 { // Non-loopback clients without a keyset should fail (must provide JWKS) let metadata = metadata.clone(); - let err = atproto_client_metadata(metadata, &None).expect_err("expected to fail"); - assert!(matches!(err, Error::EmptyJwks)); + let err = atproto_client_metadata(metadata, &None); + assert!(err.is_ok()); } { let metadata = metadata.clone(); diff --git a/crates/jacquard-oauth/src/client.rs b/crates/jacquard-oauth/src/client.rs index 4fd88e47..2d61c65c 100644 --- a/crates/jacquard-oauth/src/client.rs +++ b/crates/jacquard-oauth/src/client.rs @@ -175,8 +175,14 @@ where keyset: self.registry.client_data.keyset.clone(), }; - let auth_req_info = - par(self.client.as_ref(), login_hint, options.prompt, &metadata).await?; + let auth_req_info = par( + self.client.as_ref(), + login_hint, + options.prompt, + &metadata, + options.state, + ) + .await?; // Persist state for callback handling self.registry diff --git a/crates/jacquard-oauth/src/request.rs b/crates/jacquard-oauth/src/request.rs index 0edc4afb..fcbde606 100644 --- a/crates/jacquard-oauth/src/request.rs +++ b/crates/jacquard-oauth/src/request.rs @@ -473,8 +473,13 @@ pub async fn par<'r, T: OAuthResolver + DpopExt + Send + Sync + 'static>( login_hint: Option>, prompt: Option, metadata: &OAuthMetadata, + state: Option>, ) -> crate::request::Result> { - let state = generate_nonce(); + let state = if let Some(state) = state { + state + } else { + generate_nonce() + }; let (code_challenge, verifier) = generate_pkce(); let Some(dpop_key) = generate_dpop_key(&metadata.server_metadata) else { @@ -958,7 +963,7 @@ mod tests { meta.server_metadata.require_pushed_authorization_requests = Some(true); meta.server_metadata.pushed_authorization_request_endpoint = None; // require_pushed_authorization_requests is true and no endpoint - let err = super::par(&MockClient::default(), None, None, &meta) + let err = super::par(&MockClient::default(), None, None, &meta, None) .await .unwrap_err(); assert!( diff --git a/crates/jacquard/Cargo.toml b/crates/jacquard/Cargo.toml index 40e8069b..00aac604 100644 --- a/crates/jacquard/Cargo.toml +++ b/crates/jacquard/Cargo.toml @@ -69,9 +69,6 @@ path = "../../examples/update_profile.rs" name = "public_atproto_feed" path = "../../examples/public_atproto_feed.rs" -[[example]] -name = "thomas_bug" -path = "../../examples/thomas_bug.rs" [[example]] diff --git a/crates/jacquard/tests/oauth_flow.rs b/crates/jacquard/tests/oauth_flow.rs index e0f988a8..efd1de33 100644 --- a/crates/jacquard/tests/oauth_flow.rs +++ b/crates/jacquard/tests/oauth_flow.rs @@ -237,7 +237,7 @@ async fn oauth_end_to_end_mock_flow() { keyset: None, }; let login_hint = identity.map(|_| jacquard::CowStr::from("alice.bsky.social")); - let auth_req = jacquard_oauth::request::par(client.as_ref(), login_hint, None, &metadata) + let auth_req = jacquard_oauth::request::par(client.as_ref(), login_hint, None, &metadata, None) .await .unwrap(); // Construct authorization URL as OAuthClient::start_auth would do diff --git a/nix/modules/devshell.nix b/nix/modules/devshell.nix index c95d3c0c..17ce0812 100644 --- a/nix/modules/devshell.nix +++ b/nix/modules/devshell.nix @@ -21,6 +21,7 @@ cargo-semver-checks cargo-binstall cargo-dist + cargo-nextest zip ]; };