tangled
alpha
login
or
join now
alephcubed.com
/
jacquard
forked from
nonbinary.computer/jacquard
0
fork
atom
A better Rust ATProto crate
0
fork
atom
overview
issues
pulls
pipelines
bug fix for oauth, number of other small enhancements
Orual
3 months ago
afc9b394
849876d7
+94
-21
11 changed files
expand all
collapse all
unified
split
crates
jacquard
Cargo.toml
tests
oauth_flow.rs
jacquard-api
Cargo.toml
jacquard-common
src
types
value.rs
xrpc
dyn_req.rs
xrpc.rs
jacquard-lexicon
src
validation.rs
jacquard-oauth
src
atproto.rs
client.rs
request.rs
nix
modules
devshell.nix
+3
-3
crates/jacquard-api/Cargo.toml
···
42
42
43
43
streaming = ["jacquard-common/websocket"]
44
44
45
45
+
# --- generated ---
46
46
+
# Generated namespace features
47
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
101
-
102
102
-
# --- generated ---
103
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
166
-
pub fn as_str_mut(&'s mut self) -> Option<&'s mut AtprotoStr> {
166
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
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
165
-
#[allow(deprecated)]
166
166
let body = serde_json::from_slice(body).map_err(|e| DecodeError::Json(e))?;
167
167
-
168
167
Ok(body)
169
168
}
170
169
}
+56
crates/jacquard-common/src/xrpc/dyn_req.rs
···
1
1
+
pub trait DynXrpcRequest {
2
2
+
fn nsid(&self) -> Nsid<'static>;
3
3
+
fn method(&self) -> XrpcMethod;
4
4
+
fn response_type(&self) -> &'static str;
5
5
+
fn encode_body(&self) -> Result<Vec<u8>, EncodeError>;
6
6
+
}
7
7
+
8
8
+
pub trait DynXrpcResp {
9
9
+
fn nsid(&self) -> Nsid<'static>;
10
10
+
fn encoding(&self) -> &'static str;
11
11
+
fn decode_output(&self, body: &[u8]) -> Result<Data<'_>, DecodeError>;
12
12
+
}
13
13
+
14
14
+
impl<XRPC> DynXrpcRequest for XRPC
15
15
+
where
16
16
+
XRPC: XrpcRequest,
17
17
+
{
18
18
+
fn nsid(&self) -> Nsid<'static> {
19
19
+
unsafe { Nsid::new_static(XRPC::NSID).unwrap_unchecked() }
20
20
+
}
21
21
+
22
22
+
fn method(&self) -> XrpcMethod {
23
23
+
XRPC::METHOD
24
24
+
}
25
25
+
26
26
+
fn response_type(&self) -> &'static str {
27
27
+
<XRPC::Response as XrpcResp>::ENCODING
28
28
+
}
29
29
+
30
30
+
fn encode_body(&self) -> Result<Vec<u8>, EncodeError> {
31
31
+
XRPC::encode_body(self)
32
32
+
}
33
33
+
}
34
34
+
35
35
+
impl<XRPC> DynXrpcResp for XRPC
36
36
+
where
37
37
+
XRPC: XrpcResp,
38
38
+
{
39
39
+
fn nsid(&self) -> Nsid<'static> {
40
40
+
unsafe { Nsid::new_static(XRPC::NSID).unwrap_unchecked() }
41
41
+
}
42
42
+
43
43
+
fn encoding(&self) -> &'static str {
44
44
+
XRPC::ENCODING
45
45
+
}
46
46
+
47
47
+
fn decode_output(&self, body: &[u8]) -> Result<Data<'_>, DecodeError> {
48
48
+
if self.encoding() == "application/json" {
49
49
+
Ok(serde_json::from_slice::<Data>(body)?.into_static())
50
50
+
} else if self.encoding() == "application/vnd.ipld.car" {
51
51
+
Ok(serde_ipld_dagcbor::from_slice::<Data>(body)?.into_static())
52
52
+
} else {
53
53
+
Ok(Data::Bytes(Bytes::copy_from_slice(body)))
54
54
+
}
55
55
+
}
56
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
83
+
84
84
+
pub fn segments(&self) -> &[PathSegment] {
85
85
+
&self.segments
86
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
827
+
828
828
+
// Reject empty $type
829
829
+
if type_str.is_empty() {
830
830
+
return vec![StructuralError::MissingUnionDiscriminator { path: path.clone() }];
831
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
277
-
dpop_bound_access_tokens: None,
277
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
319
-
dpop_bound_access_tokens: None,
319
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
355
-
dpop_bound_access_tokens: None,
355
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
387
-
dpop_bound_access_tokens: None,
387
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
419
-
dpop_bound_access_tokens: None,
419
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
449
-
let err = atproto_client_metadata(metadata, &None).expect_err("expected to fail");
450
450
-
assert!(matches!(err, Error::EmptyJwks));
449
449
+
let err = atproto_client_metadata(metadata, &None);
450
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
178
-
let auth_req_info =
179
179
-
par(self.client.as_ref(), login_hint, options.prompt, &metadata).await?;
178
178
+
let auth_req_info = par(
179
179
+
self.client.as_ref(),
180
180
+
login_hint,
181
181
+
options.prompt,
182
182
+
&metadata,
183
183
+
options.state,
184
184
+
)
185
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
476
+
state: Option<CowStr<'r>>,
476
477
) -> crate::request::Result<AuthRequestData<'r>> {
477
477
-
let state = generate_nonce();
478
478
+
let state = if let Some(state) = state {
479
479
+
state
480
480
+
} else {
481
481
+
generate_nonce()
482
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
961
-
let err = super::par(&MockClient::default(), None, None, &meta)
966
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
72
-
[[example]]
73
73
-
name = "thomas_bug"
74
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
240
-
let auth_req = jacquard_oauth::request::par(client.as_ref(), login_hint, None, &metadata)
240
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
24
+
cargo-nextest
24
25
zip
25
26
];
26
27
};