diff --git a/crates/jacquard-api/Cargo.toml b/crates/jacquard-api/Cargo.toml index d9916bcf..38800a9e 100644 --- a/crates/jacquard-api/Cargo.toml +++ b/crates/jacquard-api/Cargo.toml @@ -30,7 +30,7 @@ non_snake_case = "allow" unused_imports = "allow" [features] -default = [ "minimal", "std", "streaming"] +default = [ "minimal", "std"] std = ["serde/std", "jacquard-common/std"] minimal = [ "com_atproto", "com_bad_example"] bluesky = [ "com_atproto", "app_bsky", "chat_bsky", "tools_ozone", "minimal"] diff --git a/crates/jacquard-axum/src/did_web.rs b/crates/jacquard-axum/src/did_web.rs index 0bdc17c9..95904a34 100644 --- a/crates/jacquard-axum/src/did_web.rs +++ b/crates/jacquard-axum/src/did_web.rs @@ -39,6 +39,7 @@ use axum::{ response::IntoResponse, routing::get, }; +use jacquard::deps::smol_str::SmolStr; use jacquard_common::types::did_doc::DidDocument; /// Create a router that serves a DID document at `/.well-known/did.json` @@ -58,7 +59,7 @@ use jacquard_common::types::did_doc::DidDocument; /// .merge(did_web_router(did_doc)); /// # } /// ``` -pub fn did_web_router(did_doc: DidDocument<'static>) -> Router { +pub fn did_web_router(did_doc: DidDocument) -> Router { Router::new().route( "/.well-known/did.json", get(move || async move { diff --git a/crates/jacquard-axum/src/lib.rs b/crates/jacquard-axum/src/lib.rs index 3da7eaaf..455d98cc 100644 --- a/crates/jacquard-axum/src/lib.rs +++ b/crates/jacquard-axum/src/lib.rs @@ -57,7 +57,7 @@ use axum::{ response::{IntoResponse, Response}, }; use jacquard::{ - IntoStatic, + BosStr, IntoStatic, xrpc::{XrpcEndpoint, XrpcError, XrpcMethod, XrpcRequest}, }; use serde_json::json; @@ -66,13 +66,14 @@ use serde_json::json; /// /// Deserializes incoming requests based on the endpoint's method type (Query or Procedure) /// and returns the owned (`'static`) request type ready for handler logic. -pub struct ExtractXrpc(pub E::Request<'static>); +pub struct ExtractXrpc(pub E::Request); -impl FromRequest for ExtractXrpc +impl FromRequest for ExtractXrpc where - S: Send + Sync, + S: BosStr + Send + Sync, + B: BosStr, R: XrpcEndpoint, - for<'a> R::Request<'a>: IntoStatic>, + R::Request: IntoStatic>, { type Rejection = Response; @@ -102,8 +103,8 @@ where XrpcMethod::Query => { if let Some(path_query) = req.uri().path_and_query() { let query = path_query.query().unwrap_or(""); - let value: R::Request<'_> = - serde_html_form::from_str::>(query).map_err(|e| { + let value: R::Request = + serde_html_form::from_str::>(query).map_err(|e| { ( StatusCode::BAD_REQUEST, Json(json!({ diff --git a/crates/jacquard-axum/src/service_auth.rs b/crates/jacquard-axum/src/service_auth.rs index f4f0b049..6caa9a63 100644 --- a/crates/jacquard-axum/src/service_auth.rs +++ b/crates/jacquard-axum/src/service_auth.rs @@ -66,7 +66,7 @@ pub trait ServiceAuth { type Resolver: IdentityResolver; /// Get the service DID (expected audience) - fn service_did(&self) -> &Did<'_>; + fn service_did(&self) -> Did<&str>; /// Get a reference to the identity resolver fn resolver(&self) -> &Self::Resolver; @@ -81,7 +81,7 @@ pub trait ServiceAuth { /// by the `ExtractServiceAuth` extractor. pub struct ServiceAuthConfig { /// The DID of your service (the expected audience) - service_did: Did<'static>, + service_did: Did, /// Identity resolver for fetching DID documents resolver: Arc, /// Whether to require the `lxm` (method binding) field @@ -103,7 +103,7 @@ impl ServiceAuthConfig { /// /// This enables `lxm` (method binding). If you need backward compatibility, /// use `ServiceAuthConfig::new_legacy()` - pub fn new(service_did: Did<'static>, resolver: R) -> Self { + pub fn new(service_did: Did, resolver: R) -> Self { Self { service_did, resolver: Arc::new(resolver), @@ -114,7 +114,7 @@ impl ServiceAuthConfig { /// Create a new service auth config. /// /// `lxm` (method binding) is disabled for backwards compatibility - pub fn new_legacy(service_did: Did<'static>, resolver: R) -> Self { + pub fn new_legacy(service_did: Did, resolver: R) -> Self { Self { service_did, resolver: Arc::new(resolver), @@ -132,8 +132,8 @@ impl ServiceAuthConfig { } /// Get the service DID. - pub fn service_did(&self) -> &Did<'static> { - &self.service_did + pub fn service_did(&self) -> Did<&str> { + self.service_did.borrow() } /// Get a reference to the identity resolver. @@ -145,8 +145,8 @@ impl ServiceAuthConfig { impl ServiceAuth for ServiceAuthConfig { type Resolver = R; - fn service_did(&self) -> &Did<'_> { - &self.service_did + fn service_did(&self) -> Did<&str> { + self.service_did.borrow() } fn resolver(&self) -> &Self::Resolver { @@ -165,29 +165,29 @@ impl ServiceAuth for ServiceAuthConfig { #[derive(Debug, Clone, jacquard_derive::IntoStatic)] pub struct VerifiedServiceAuth<'a> { /// The authenticated user's DID (from `iss` claim) - did: Did<'a>, + did: Did, /// The audience (should match your service DID) - aud: Did<'a>, + aud: Did, /// The lexicon method NSID, if present - lxm: Option>, + lxm: Option, /// JWT ID (nonce), if present jti: Option>, } impl<'a> VerifiedServiceAuth<'a> { /// Get the authenticated user's DID. - pub fn did(&self) -> &Did<'a> { - &self.did + pub fn did(&self) -> Did<&str> { + self.did.borrow() } /// Get the audience (your service DID). - pub fn aud(&self) -> &Did<'a> { - &self.aud + pub fn aud(&self) -> Did<&str> { + self.aud.borrow() } /// Get the lexicon method NSID, if present. - pub fn lxm(&self) -> Option<&Nsid<'a>> { - self.lxm.as_ref() + pub fn lxm(&self) -> Option> { + self.lxm.as_ref().map(|l| l.borrow()) } /// Get the JWT ID (nonce), if present. @@ -310,14 +310,14 @@ pub enum ServiceAuthError { /// DID resolution failed #[error("failed to resolve DID {did}: {source}")] DidResolutionFailed { - did: Did<'static>, + did: Did, #[source] source: Box, }, /// No valid signing key found in DID document #[error("no valid signing key found in DID document for {0}")] - NoSigningKey(Did<'static>), + NoSigningKey(Did), /// Method binding required but missing #[error("lxm (method binding) is required but missing from token")] @@ -445,7 +445,7 @@ where service_auth::verify_signature(&parsed, &signing_key)?; // Now validate claims (audience, expiration, etc.) - claims.validate(state.service_did())?; + claims.validate(&state.service_did())?; // Check method binding if required if state.require_lxm() && claims.lxm.is_none() { @@ -549,7 +549,7 @@ where /// /// This looks for a key with type "atproto" or the first available key /// if no atproto-specific key is found. -fn extract_signing_key(methods: &[VerificationMethod]) -> Option { +fn extract_signing_key(methods: &[VerificationMethod]) -> Option { // First try to find an atproto-specific key let atproto_method = methods .iter() diff --git a/crates/jacquard-common/src/lib.rs b/crates/jacquard-common/src/lib.rs index 4f82312b..4f430847 100644 --- a/crates/jacquard-common/src/lib.rs +++ b/crates/jacquard-common/src/lib.rs @@ -212,9 +212,9 @@ pub type Lazy = std::sync::LazyLock; #[cfg(not(feature = "std"))] pub use spin::Lazy; +pub use bos::{BorrowOrShare, Bos, BosStr, DefaultStr, FromStaticStr}; pub use cowstr::CowStr; pub use into_static::IntoStatic; -pub use bos::{Bos, BorrowOrShare, BosStr, DefaultStr, FromStaticStr}; /// A copy-on-write immutable string type that uses [`smol_str::SmolStr`] for /// the "owned" variant. @@ -266,16 +266,20 @@ pub use types::value::*; /// Authorization token types for XRPC requests. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum AuthorizationToken<'s> { +pub enum AuthorizationToken { /// Bearer token (access JWT, refresh JWT to refresh the session) - Bearer(CowStr<'s>), + Bearer(S), /// DPoP token (proof-of-possession) for OAuth - Dpop(CowStr<'s>), + Dpop(S), } -impl<'s> IntoStatic for AuthorizationToken<'s> { - type Output = AuthorizationToken<'static>; - fn into_static(self) -> AuthorizationToken<'static> { +impl IntoStatic for AuthorizationToken +where + S: IntoStatic, + S::Output: BosStr, +{ + type Output = AuthorizationToken; + fn into_static(self) -> AuthorizationToken { match self { AuthorizationToken::Bearer(token) => AuthorizationToken::Bearer(token.into_static()), AuthorizationToken::Dpop(token) => AuthorizationToken::Dpop(token.into_static()), diff --git a/crates/jacquard-common/src/service_auth.rs b/crates/jacquard-common/src/service_auth.rs index 61380baa..462588d0 100644 --- a/crates/jacquard-common/src/service_auth.rs +++ b/crates/jacquard-common/src/service_auth.rs @@ -144,8 +144,8 @@ pub struct ServiceAuthClaims<'a> { pub jti: Option>, /// Lexicon method NSID (method binding) - #[serde(borrow, skip_serializing_if = "Option::is_none")] - pub lxm: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub lxm: Option, } impl<'a> IntoStatic for ServiceAuthClaims<'a> { @@ -169,7 +169,7 @@ impl<'a> ServiceAuthClaims<'a> { /// Checks: /// - Audience matches expected DID /// - Token is not expired - pub fn validate(&self, expected_aud: &Did) -> Result<(), ServiceAuthError> { + pub fn validate(&self, expected_aud: &Did<&str>) -> Result<(), ServiceAuthError> { // Check audience if self.aud.as_str() != expected_aud.as_str() { return Err(ServiceAuthError::AudienceMismatch { @@ -456,10 +456,10 @@ mod tests { lxm: None, }; - let expected_aud = Did::new_static("did:web:example.com").unwrap(); + let expected_aud = Did::new("did:web:example.com").unwrap(); assert!(claims.validate(&expected_aud).is_ok()); - let wrong_aud = Did::new_static("did:web:wrong.com").unwrap(); + let wrong_aud = Did::new("did:web:wrong.com").unwrap(); assert!(matches!( claims.validate(&wrong_aud), Err(ServiceAuthError::AudienceMismatch { .. }) diff --git a/crates/jacquard-common/src/types/did.rs b/crates/jacquard-common/src/types/did.rs index cb764dba..6075be03 100644 --- a/crates/jacquard-common/src/types/did.rs +++ b/crates/jacquard-common/src/types/did.rs @@ -164,6 +164,15 @@ impl> Did { pub fn convert + From>(self) -> Did { Did(B::from(self.0)) } + + /// Borrow as a `Did<&str>`, analogous to `Uri::borrow()`. + pub fn borrow(&self) -> Did<&str> + where + S: AsRef, + { + // SAFETY: self is already validated. + unsafe { Did::unchecked(self.0.as_ref()) } + } } impl + FromStr> FromStr for Did { diff --git a/crates/jacquard-common/src/types/handle.rs b/crates/jacquard-common/src/types/handle.rs index 2e7a7f60..2bf465f5 100644 --- a/crates/jacquard-common/src/types/handle.rs +++ b/crates/jacquard-common/src/types/handle.rs @@ -95,6 +95,15 @@ impl> Handle { pub unsafe fn unchecked(handle: S) -> Self { Handle(handle) } + + /// Borrow as a `Handle<&str>`, analogous to `Uri::borrow()`. + pub fn borrow(&self) -> Handle<&str> + where + S: AsRef, + { + // SAFETY: self is already validated. + unsafe { Handle::unchecked(self.0.as_ref()) } + } } // --------------------------------------------------------------------------- @@ -359,11 +368,15 @@ mod tests { assert!(Handle::<&str>::new("@alice.test").is_err()); assert!(Handle::<&str>::new("at://alice.test").is_err()); assert_eq!( - Handle::::new_owned("@alice.test").unwrap().as_str(), + Handle::::new_owned("@alice.test") + .unwrap() + .as_str(), "alice.test" ); assert_eq!( - Handle::::new_owned("at://alice.test").unwrap().as_str(), + Handle::::new_owned("at://alice.test") + .unwrap() + .as_str(), "alice.test" ); assert_eq!( diff --git a/crates/jacquard-common/src/types/nsid.rs b/crates/jacquard-common/src/types/nsid.rs index fd454e0d..c6d1f86f 100644 --- a/crates/jacquard-common/src/types/nsid.rs +++ b/crates/jacquard-common/src/types/nsid.rs @@ -72,6 +72,15 @@ impl> Nsid { pub unsafe fn unchecked(nsid: S) -> Self { Nsid(nsid) } + + /// Borrow as an `Nsid<&str>`, analogous to `Uri::borrow()`. + pub fn borrow(&self) -> Nsid<&str> + where + S: AsRef, + { + // SAFETY: self is already validated. + unsafe { Nsid::unchecked(self.0.as_ref()) } + } } impl + AsRef> Nsid { diff --git a/crates/jacquard-common/src/types/recordkey.rs b/crates/jacquard-common/src/types/recordkey.rs index 6ab998c0..6c49456f 100644 --- a/crates/jacquard-common/src/types/recordkey.rs +++ b/crates/jacquard-common/src/types/recordkey.rs @@ -105,6 +105,13 @@ impl RecordKey { } } +impl + AsRef> RecordKey> { + /// Borrow as a `RecordKey>`, analogous to `Uri::borrow()`. + pub fn borrow(&self) -> RecordKey> { + RecordKey(self.0.borrow()) + } +} + /// AT Protocol record key (generic "any" type) /// /// Record keys uniquely identify records within a collection. This is the catch-all @@ -164,6 +171,15 @@ impl> Rkey { pub unsafe fn unchecked(rkey: S) -> Self { Rkey(rkey) } + + /// Borrow as an `Rkey<&str>`, analogous to `Uri::borrow()`. + pub fn borrow(&self) -> Rkey<&str> + where + S: AsRef, + { + // SAFETY: self is already validated. + unsafe { Rkey::unchecked(self.0.as_ref()) } + } } impl + AsRef> Rkey { diff --git a/crates/jacquard-common/src/types/value.rs b/crates/jacquard-common/src/types/value.rs index 4fd1cf08..21352d23 100644 --- a/crates/jacquard-common/src/types/value.rs +++ b/crates/jacquard-common/src/types/value.rs @@ -4,7 +4,6 @@ use crate::{ }; use alloc::boxed::Box; use alloc::collections::BTreeMap; -use alloc::string::ToString; use alloc::vec::Vec; use bytes::Bytes; use core::convert::Infallible; @@ -874,15 +873,11 @@ where /// # Ok(()) /// # } /// ``` -pub fn to_data<'s, T, S>(value: &T) -> Result, convert::ConversionError> +pub fn to_data<'s, T>(value: &T) -> Result, serde_impl::RawDataSerializerError> where T: serde::Serialize, - S: Bos + AsRef + serde::Serialize + From>, { - let raw = to_raw_data(value).map_err(|e| convert::ConversionError::InvalidRawData { - message: e.to_string(), - })?; - raw.try_into() + value.serialize(serde_impl::DataSerializer) } /// Parse and traverse a path through nested Data structures diff --git a/crates/jacquard-common/src/types/value/serde_impl.rs b/crates/jacquard-common/src/types/value/serde_impl.rs index 7beb1826..6a362d1c 100644 --- a/crates/jacquard-common/src/types/value/serde_impl.rs +++ b/crates/jacquard-common/src/types/value/serde_impl.rs @@ -1630,6 +1630,342 @@ impl serde::ser::Error for RawDataSerializerError { } } +/// Serializer that produces RawData values +pub struct DataSerializer; + +impl serde::Serializer for DataSerializer { + type Ok = Data; + type Error = RawDataSerializerError; + + type SerializeSeq = DataSeqSerializer; + type SerializeTuple = DataSeqSerializer; + type SerializeTupleStruct = DataSeqSerializer; + type SerializeTupleVariant = DataSeqSerializer; + type SerializeMap = DataMapSerializer; + type SerializeStruct = DataMapSerializer; + type SerializeStructVariant = DataMapSerializer; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Data::Boolean(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(Data::Integer(v as i64)) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(Data::Integer(v as i64)) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(Data::Integer(v as i64)) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Data::Integer(v as i64)) + } + + fn serialize_u8(self, v: u8) -> Result { + Ok(Data::Integer(v as i64)) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(Data::Integer(v as i64)) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(Data::Integer(v as i64)) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Data::Integer((v as i128 % (i64::MAX as i128)) as i64)) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(Data::InvalidNumber(SmolStr::from(v.to_string()))) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Data::InvalidNumber(SmolStr::from(v.to_string()))) + } + + fn serialize_char(self, v: char) -> Result { + Ok(Data::String(AtprotoStr::String(v.to_smolstr()))) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Data::String(parse_string(v).convert())) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(Data::Bytes(Bytes::copy_from_slice(v))) + } + + fn serialize_none(self) -> Result { + Ok(Data::Null) + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(Data::Null) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Ok(Data::Null) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + Ok(Data::String(AtprotoStr::String(SmolStr::new_static( + variant, + )))) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + let mut map = BTreeMap::new(); + map.insert(variant.to_smolstr(), value.serialize(DataSerializer)?); + Ok(Data::Object(map.into())) + } + + fn serialize_seq(self, len: Option) -> Result { + Ok(DataSeqSerializer { + items: Vec::with_capacity(len.unwrap_or(0)), + }) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(DataMapSerializer { + map: BTreeMap::new(), + next_key: None, + }) + } + + fn serialize_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_map(Some(len)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + len: usize, + ) -> Result { + self.serialize_map(Some(len)) + } +} + +/// Sequence serializer accumulator +pub struct RawDataSeqSerializer { + items: Vec>, +} + +impl serde::ser::SerializeSeq for RawDataSeqSerializer { + type Ok = RawData<'static>; + type Error = RawDataSerializerError; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + self.items.push(value.serialize(RawDataSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(RawData::Array(self.items)) + } +} + +impl serde::ser::SerializeTuple for RawDataSeqSerializer { + type Ok = RawData<'static>; + type Error = RawDataSerializerError; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl serde::ser::SerializeTupleStruct for RawDataSeqSerializer { + type Ok = RawData<'static>; + type Error = RawDataSerializerError; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl serde::ser::SerializeTupleVariant for RawDataSeqSerializer { + type Ok = RawData<'static>; + type Error = RawDataSerializerError; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +/// Map serializer accumulator +pub struct RawDataMapSerializer { + map: BTreeMap>, + next_key: Option, +} + +impl serde::ser::SerializeMap for RawDataMapSerializer { + type Ok = RawData<'static>; + type Error = RawDataSerializerError; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + let key_data = key.serialize(RawDataSerializer)?; + match key_data { + RawData::String(s) => { + self.next_key = Some(s.to_smolstr()); + Ok(()) + } + _ => Err(RawDataSerializerError::Message( + "map keys must be strings".to_string(), + )), + } + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + let key = self + .next_key + .take() + .ok_or_else(|| RawDataSerializerError::Message("missing key".to_string()))?; + self.map.insert(key, value.serialize(RawDataSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(RawData::Object(self.map)) + } +} + +impl serde::ser::SerializeStruct for RawDataMapSerializer { + type Ok = RawData<'static>; + type Error = RawDataSerializerError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + self.map + .insert(key.to_smolstr(), value.serialize(RawDataSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(RawData::Object(self.map)) + } +} + +impl serde::ser::SerializeStructVariant for RawDataMapSerializer { + type Ok = RawData<'static>; + type Error = RawDataSerializerError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + serde::ser::SerializeStruct::serialize_field(self, key, value) + } + + fn end(self) -> Result { + serde::ser::SerializeStruct::end(self) + } +} + /// Serializer that produces RawData values pub struct RawDataSerializer; @@ -1814,29 +2150,29 @@ impl serde::Serializer for RawDataSerializer { } /// Sequence serializer accumulator -pub struct RawDataSeqSerializer { - items: Vec>, +pub struct DataSeqSerializer { + items: Vec>, } -impl serde::ser::SerializeSeq for RawDataSeqSerializer { - type Ok = RawData<'static>; +impl serde::ser::SerializeSeq for DataSeqSerializer { + type Ok = Data; type Error = RawDataSerializerError; fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> where T: Serialize, { - self.items.push(value.serialize(RawDataSerializer)?); + self.items.push(value.serialize(DataSerializer)?); Ok(()) } fn end(self) -> Result { - Ok(RawData::Array(self.items)) + Ok(Data::Array(self.items.into())) } } -impl serde::ser::SerializeTuple for RawDataSeqSerializer { - type Ok = RawData<'static>; +impl serde::ser::SerializeTuple for DataSeqSerializer { + type Ok = Data; type Error = RawDataSerializerError; fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> @@ -1851,8 +2187,8 @@ impl serde::ser::SerializeTuple for RawDataSeqSerializer { } } -impl serde::ser::SerializeTupleStruct for RawDataSeqSerializer { - type Ok = RawData<'static>; +impl serde::ser::SerializeTupleStruct for DataSeqSerializer { + type Ok = Data; type Error = RawDataSerializerError; fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> @@ -1867,8 +2203,8 @@ impl serde::ser::SerializeTupleStruct for RawDataSeqSerializer { } } -impl serde::ser::SerializeTupleVariant for RawDataSeqSerializer { - type Ok = RawData<'static>; +impl serde::ser::SerializeTupleVariant for DataSeqSerializer { + type Ok = Data; type Error = RawDataSerializerError; fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> @@ -1884,13 +2220,13 @@ impl serde::ser::SerializeTupleVariant for RawDataSeqSerializer { } /// Map serializer accumulator -pub struct RawDataMapSerializer { - map: BTreeMap>, +pub struct DataMapSerializer { + map: BTreeMap>, next_key: Option, } -impl serde::ser::SerializeMap for RawDataMapSerializer { - type Ok = RawData<'static>; +impl serde::ser::SerializeMap for DataMapSerializer { + type Ok = Data; type Error = RawDataSerializerError; fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> @@ -1917,17 +2253,17 @@ impl serde::ser::SerializeMap for RawDataMapSerializer { .next_key .take() .ok_or_else(|| RawDataSerializerError::Message("missing key".to_string()))?; - self.map.insert(key, value.serialize(RawDataSerializer)?); + self.map.insert(key, value.serialize(DataSerializer)?); Ok(()) } fn end(self) -> Result { - Ok(RawData::Object(self.map)) + Ok(Data::Object(self.map.into())) } } -impl serde::ser::SerializeStruct for RawDataMapSerializer { - type Ok = RawData<'static>; +impl serde::ser::SerializeStruct for DataMapSerializer { + type Ok = Data; type Error = RawDataSerializerError; fn serialize_field( @@ -1939,17 +2275,17 @@ impl serde::ser::SerializeStruct for RawDataMapSerializer { T: Serialize, { self.map - .insert(key.to_smolstr(), value.serialize(RawDataSerializer)?); + .insert(key.to_smolstr(), value.serialize(DataSerializer)?); Ok(()) } fn end(self) -> Result { - Ok(RawData::Object(self.map)) + Ok(Data::Object(self.map.into())) } } -impl serde::ser::SerializeStructVariant for RawDataMapSerializer { - type Ok = RawData<'static>; +impl serde::ser::SerializeStructVariant for DataMapSerializer { + type Ok = Data; type Error = RawDataSerializerError; fn serialize_field( diff --git a/crates/jacquard-common/src/types/value/tests.rs b/crates/jacquard-common/src/types/value/tests.rs index ee413166..8f363d0d 100644 --- a/crates/jacquard-common/src/types/value/tests.rs +++ b/crates/jacquard-common/src/types/value/tests.rs @@ -2,7 +2,7 @@ use crate::cowstr::ToCowStr; use super::*; use core::str::FromStr; -use std::string::String; +use std::string::{String, ToString}; /// Canonicalize JSON by sorting object keys recursively fn canonicalize_json(value: &serde_json::Value) -> serde_json::Value { diff --git a/crates/jacquard-common/src/xrpc.rs b/crates/jacquard-common/src/xrpc.rs index 88476234..32d89bb8 100644 --- a/crates/jacquard-common/src/xrpc.rs +++ b/crates/jacquard-common/src/xrpc.rs @@ -248,7 +248,7 @@ impl Error for GenericError {} #[derive(Debug, Default, Clone)] pub struct CallOptions<'a> { /// Optional Authorization to apply (`Bearer` or `DPoP`). - pub auth: Option>, + pub auth: Option>, /// `atproto-proxy` header value. pub atproto_proxy: Option>, /// `atproto-accept-labelers` header values. @@ -397,20 +397,19 @@ pub trait XrpcStreamingClient: XrpcClient + HttpClientExt { /// Stream an XRPC procedure call and its response #[cfg(not(target_arch = "wasm32"))] - fn stream( + fn stream( &self, - stream: XrpcProcedureSend>, + stream: XrpcProcedureSend>, ) -> impl Future< Output = Result< - XrpcResponseStream< - <::Response as XrpcStreamResp>::Frame<'static>, - >, + XrpcResponseStream<<::Response as XrpcStreamResp>::Frame>, StreamError, >, > where + B: BosStr + 'static, S: XrpcProcedureStream + 'static, - <::Response as XrpcStreamResp>::Frame<'static>: XrpcStreamResp, + <::Response as XrpcStreamResp>::Frame: XrpcStreamResp, Self: Sync; /// Stream an XRPC procedure call and its response @@ -460,7 +459,7 @@ pub struct XrpcCall<'a, C: HttpClient> { impl<'a, C: HttpClient> XrpcCall<'a, C> { /// Apply Authorization to this call. - pub fn auth(mut self, token: AuthorizationToken<'a>) -> Self { + pub fn auth(mut self, token: AuthorizationToken) -> Self { self.opts.auth = Some(token); self } @@ -662,9 +661,9 @@ where if let Some(token) = &opts.auth { let hv = match token { AuthorizationToken::Bearer(t) => { - HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) + HeaderValue::from_str(&format!("Bearer {}", t.as_str())) } - AuthorizationToken::Dpop(t) => HeaderValue::from_str(&format!("DPoP {}", t.as_ref())), + AuthorizationToken::Dpop(t) => HeaderValue::from_str(&format!("DPoP {}", t.as_str())), } .map_err(|e| ClientError::invalid_request(format!("Invalid authorization token: {}", e)))?; builder = builder.header(Header::Authorization, hv); @@ -1044,13 +1043,14 @@ impl<'a, C: HttpClient + HttpClientExt> XrpcCall<'a, C> { /// /// Useful for streaming upload of large payloads, or for "pipe-through" operations /// where you are processing a large payload. - pub async fn stream( + pub async fn stream( self, - stream: XrpcProcedureSend>, - ) -> Result::Frame<'static>>, StreamError> + stream: XrpcProcedureSend>, + ) -> Result::Frame>, StreamError> where S: XrpcProcedureStream + 'static, - <::Response as XrpcStreamResp>::Frame<'static>: XrpcStreamResp, + B: BosStr + 'static, + <::Response as XrpcStreamResp>::Frame: XrpcStreamResp, { use alloc::boxed::Box; use futures::TryStreamExt; @@ -1064,10 +1064,10 @@ impl<'a, C: HttpClient + HttpClientExt> XrpcCall<'a, C> { if let Some(token) = &self.opts.auth { let hv = match token { AuthorizationToken::Bearer(t) => { - HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) + HeaderValue::from_str(&format!("Bearer {}", t.as_str())) } AuthorizationToken::Dpop(t) => { - HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) + HeaderValue::from_str(&format!("DPoP {}", t.as_str())) } } .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; @@ -1108,8 +1108,8 @@ impl<'a, C: HttpClient + HttpClientExt> XrpcCall<'a, C> { let (parts, body) = resp.into_parts(); Ok(XrpcResponseStream::< - <::Response as XrpcStreamResp>::Frame<'static>, - >::from_typed_parts(parts, body)) + <::Response as XrpcStreamResp>::Frame, + >::from_typed_parts::(parts, body)) } } diff --git a/crates/jacquard-common/src/xrpc/streaming.rs b/crates/jacquard-common/src/xrpc/streaming.rs index fa0775f0..d4367112 100644 --- a/crates/jacquard-common/src/xrpc/streaming.rs +++ b/crates/jacquard-common/src/xrpc/streaming.rs @@ -1,6 +1,6 @@ //! Streaming support for XRPC requests and responses -use crate::{IntoStatic, StreamError, stream::ByteStream, xrpc::XrpcRequest}; +use crate::{BosStr, StreamError, stream::ByteStream, xrpc::XrpcRequest}; use alloc::boxed::Box; use bytes::Bytes; use core::{marker::PhantomData, pin::Pin}; @@ -28,7 +28,7 @@ pub trait XrpcProcedureStream { const ENCODING: &'static str; /// Frame type for this streaming procedure - type Frame<'de>; + type Frame; /// Associated request type type Request: XrpcRequest; @@ -39,9 +39,9 @@ pub trait XrpcProcedureStream { /// Encode a frame into bytes for transmission. /// /// Default implementation uses DAG-CBOR encoding. - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result + fn encode_frame(data: Self::Frame) -> Result where - Self::Frame<'de>: Serialize, + Self::Frame: Serialize, { Ok(Bytes::from_owner( serde_ipld_dagcbor::to_vec(&data).map_err(StreamError::encode)?, @@ -51,9 +51,9 @@ pub trait XrpcProcedureStream { /// Decode the request body for procedures. /// /// Default implementation deserializes from CBOR. Override for non-CBOR encodings. - fn decode_frame<'de>(frame: &'de [u8]) -> Result, StreamError> + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result, StreamError> where - Self::Frame<'de>: Deserialize<'de>, + Self::Frame: Deserialize<'de>, { Ok(serde_ipld_dagcbor::from_slice(frame).map_err(StreamError::decode)?) } @@ -70,14 +70,14 @@ pub trait XrpcStreamResp { const ENCODING: &'static str; /// Response output type - type Frame<'de>: IntoStatic; + type Frame; /// Encode a frame into bytes for transmission. /// /// Default implementation uses DAG-CBOR encoding. - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result + fn encode_frame(data: Self::Frame) -> Result where - Self::Frame<'de>: Serialize, + Self::Frame: Serialize, { Ok(Bytes::from_owner( serde_ipld_dagcbor::to_vec(&data).map_err(StreamError::encode)?, @@ -89,9 +89,9 @@ pub trait XrpcStreamResp { /// Default implementation deserializes from CBOR. Override for non-CBOR encodings. /// /// TODO: make this handle when frames are fragmented? - fn decode_frame<'de>(frame: &'de [u8]) -> Result, StreamError> + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result, StreamError> where - Self::Frame<'de>: Deserialize<'de>, + Self::Frame: Deserialize<'de>, { Ok(serde_ipld_dagcbor::from_slice(frame).map_err(StreamError::decode)?) } @@ -147,14 +147,14 @@ pub async fn upload_stream(file: impl AsRef) -> Result( - s: Boxed>, -) -> XrpcProcedureSend> +pub fn encode_stream( + s: Boxed>, +) -> XrpcProcedureSend> where -

::Frame<'static>: Serialize, +

::Frame: Serialize + 'static, { let stream = - s.map(|f| P::encode_frame(f).map(|b| XrpcStreamFrame::new_typed::>(b))); + s.map(|f| P::encode_frame(f).map(|b| XrpcStreamFrame::new_typed::>(b))); XrpcProcedureSend(Box::pin(stream)) } @@ -208,23 +208,23 @@ impl XrpcResponseStream { impl XrpcResponseStream { /// Create a typed response stream from a `StreamingResponse` - pub fn from_stream(StreamingResponse { parts, body }: StreamingResponse) -> Self { + pub fn from_stream(StreamingResponse { parts, body }: StreamingResponse) -> Self { Self { parts, body: Box::pin( body.into_inner() - .map_ok(|b| XrpcStreamFrame::new_typed::>(b)), + .map_ok(|b| XrpcStreamFrame::new_typed::>(b)), ), } } /// Create a typed response stream from parts and body - pub fn from_typed_parts(parts: http::response::Parts, body: ByteStream) -> Self { + pub fn from_typed_parts(parts: http::response::Parts, body: ByteStream) -> Self { Self { parts, body: Box::pin( body.into_inner() - .map_ok(|b| XrpcStreamFrame::new_typed::>(b)), + .map_ok(|b| XrpcStreamFrame::new_typed::>(b)), ), } } diff --git a/crates/jacquard-oauth/src/client.rs b/crates/jacquard-oauth/src/client.rs index 05f76ef6..e8bcbb2d 100644 --- a/crates/jacquard-oauth/src/client.rs +++ b/crates/jacquard-oauth/src/client.rs @@ -564,24 +564,22 @@ where /// /// The token may be stale if it has expired; use [`OAuthSession::refresh`] or /// rely on the automatic refresh performed by `send_with_opts` to obtain a fresh one. - pub async fn access_token(&self) -> AuthorizationToken<'static> { - AuthorizationToken::Dpop(CowStr::Owned( - self.data.read().await.token_set.access_token.clone(), - )) + pub async fn access_token(&self) -> AuthorizationToken { + AuthorizationToken::Dpop(self.data.read().await.token_set.access_token.clone()) } /// Return the current refresh token for this session, if one is present. /// /// Not all authorization servers issue refresh tokens. When `None` is returned, /// the session cannot be silently renewed and the user must re-authenticate. - pub async fn refresh_token(&self) -> Option> { + pub async fn refresh_token(&self) -> Option> { self.data .read() .await .token_set .refresh_token .clone() - .map(|t| AuthorizationToken::Dpop(CowStr::Owned(t))) + .map(|t| AuthorizationToken::Dpop(t)) } /// Derive an unauthenticated [`OAuthClient`] that shares the same registry and resolver. @@ -653,15 +651,14 @@ where /// The actual token exchange is serialized per `(DID, session_id)` pair via a `Mutex` inside /// the registry, so concurrent refresh attempts will not result in duplicate token exchanges. #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))] - pub async fn refresh(&self) -> Result> { + pub async fn refresh(&self) -> Result> { // Read identifiers without holding the lock across await let (did, sid) = { let data = self.data.read().await; (data.account_did.clone(), data.session_id.clone()) }; let refreshed = self.registry.as_ref().get(&did, &sid, true).await?; - let token = - AuthorizationToken::Dpop(CowStr::Owned(refreshed.token_set.access_token.clone())); + let token = AuthorizationToken::Dpop(refreshed.token_set.access_token.clone()); // Write back updated session *self.data.write().await = refreshed.clone().into_static(); // Store in the registry @@ -899,18 +896,19 @@ where } } - async fn stream( + async fn stream( &self, - stream: jacquard_common::xrpc::streaming::XrpcProcedureSend>, + stream: jacquard_common::xrpc::streaming::XrpcProcedureSend>, ) -> core::result::Result< jacquard_common::xrpc::streaming::XrpcResponseStream< - <::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>, + <::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame, >, jacquard_common::StreamError, > where Str: jacquard_common::xrpc::streaming::XrpcProcedureStream + 'static, - <::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::streaming::XrpcStreamResp, + <::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame: jacquard_common::xrpc::streaming::XrpcStreamResp, + B: BosStr + 'static, { use jacquard_common::StreamError; use n0_future::TryStreamExt; @@ -929,10 +927,10 @@ where use jacquard_common::AuthorizationToken; let hv = match token { AuthorizationToken::Bearer(t) => { - http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) + http::HeaderValue::from_str(&format!("Bearer {}", t.as_str())) } AuthorizationToken::Dpop(t) => { - http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) + http::HeaderValue::from_str(&format!("DPoP {}", t.as_str())) } } .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; @@ -977,7 +975,7 @@ where Ok(response) => { let (resp_parts, resp_body) = response.into_parts(); Ok( - jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts( + jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts::( resp_parts, resp_body, ), ) @@ -1071,8 +1069,8 @@ where let mut opts = jacquard_common::xrpc::SubscriptionOptions::default(); let token = self.access_token().await; let auth_value = match token { - AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_ref()), - AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_ref()), + AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_str()), + AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_str()), }; opts.headers .push((CowStr::from("Authorization"), CowStr::from(auth_value))); diff --git a/crates/jacquard/Cargo.toml b/crates/jacquard/Cargo.toml index 17d6817d..197a4e79 100644 --- a/crates/jacquard/Cargo.toml +++ b/crates/jacquard/Cargo.toml @@ -12,7 +12,7 @@ exclude.workspace = true license.workspace = true [features] -default = ["api_full", "dns", "loopback", "derive", "cache"] +default = ["api_full", "dns", "loopback", "derive", "cache", "websocket"] derive = ["dep:jacquard-derive"] # Minimal API bindings api = ["jacquard-api/minimal"] diff --git a/crates/jacquard/src/client.rs b/crates/jacquard/src/client.rs index 604ef933..05a353fe 100644 --- a/crates/jacquard/src/client.rs +++ b/crates/jacquard/src/client.rs @@ -53,6 +53,8 @@ use jacquard_common::http_client::HttpClient; pub use jacquard_common::session::{MemorySessionStore, SessionStore, SessionStoreError}; use jacquard_common::types::blob::{Blob, MimeType}; use jacquard_common::types::collection::Collection; +#[cfg(feature = "api")] +use jacquard_common::types::ident::AtIdentifier; use jacquard_common::types::recordkey::{RecordKey, Rkey}; use jacquard_common::types::string::AtUri; #[cfg(feature = "api")] @@ -63,7 +65,7 @@ use jacquard_common::xrpc::{ }; use jacquard_common::{AuthorizationToken, xrpc}; use jacquard_common::{ - CowStr, IntoStatic, + BosStr, CowStr, IntoStatic, types::string::{Did, Handle}, }; use jacquard_identity::resolver::{ @@ -76,6 +78,9 @@ use jacquard_oauth::dpop::DpopExt; use jacquard_oauth::resolver::OAuthResolver; use serde::Serialize; #[cfg(feature = "api")] +use serde::de::DeserializeOwned; +use smol_str::SmolStr; +#[cfg(feature = "api")] use std::marker::Send; use std::option::Option; use std::sync::Arc; @@ -99,14 +104,13 @@ pub trait AgentSession: XrpcClient + HttpClient + Send + Sync { /// Identify the kind of session. fn session_kind(&self) -> AgentKind; /// Return current DID and an optional session id (always Some for OAuth). - fn session_info(&self) - -> impl Future, Option>)>>; + fn session_info(&self) -> impl Future)>>; /// Current base endpoint. fn endpoint(&self) -> impl Future>; /// Override per-session call options. fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future; /// Refresh the session and return a fresh AuthorizationToken. - fn refresh(&self) -> impl Future>>; + fn refresh(&self) -> impl Future>>; } /// Alias for an agent over a credential (app‑password) session. @@ -139,7 +143,7 @@ impl BasicClient { /// # async fn main() -> Result<(), Box> { /// let client = BasicClient::unauthenticated(); /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5abc").unwrap(); - /// let response = client.get_record::>(&uri).await?; + /// let response = client.get_record::(&uri).await?; /// # Ok(()) /// # } /// ``` @@ -238,7 +242,7 @@ where #[cfg(not(target_arch = "wasm32"))] fn send(&self, request: R) -> impl Future>> + Send where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, Self: Sync, { @@ -256,14 +260,14 @@ where opts: CallOptions<'_>, ) -> impl Future>> + Send where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, Self: Sync, { async move { let base_uri = self.base_uri().await; self.resolver - .xrpc(base_uri) + .xrpc(base_uri.borrow()) .with_options(opts.clone()) .send(&request) .await @@ -274,7 +278,7 @@ where #[cfg(target_arch = "wasm32")] fn send(&self, request: R) -> impl Future>> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, { async move { @@ -291,13 +295,13 @@ where opts: CallOptions<'_>, ) -> impl Future>> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, { async move { let base_uri = self.base_uri().await; self.resolver - .xrpc(base_uri) + .xrpc(base_uri.borrow()) .with_options(opts.clone()) .send(&request) .await @@ -334,9 +338,7 @@ where AgentKind::AppPassword } - fn session_info( - &self, - ) -> impl Future, Option>)>> { + fn session_info(&self) -> impl Future)>> { async { None } // no session } @@ -352,7 +354,7 @@ where } #[doc = " Refresh the session and return a fresh AuthorizationToken."] - fn refresh(&self) -> impl Future>> + Send { + fn refresh(&self) -> impl Future>> + Send { async { Err(ClientError::auth( jacquard_common::error::AuthError::NotAuthenticated, @@ -369,10 +371,10 @@ impl IdentityResolver for UnauthenticatedSession #[doc = " Resolve handle"] #[cfg(not(target_arch = "wasm32"))] - fn resolve_handle( + fn resolve_handle( &self, - handle: &Handle<'_>, - ) -> impl Future, IdentityError>> + Send + handle: &Handle, + ) -> impl Future> + Send where Self: Sync, { @@ -381,29 +383,30 @@ impl IdentityResolver for UnauthenticatedSession #[doc = " Resolve DID document"] #[cfg(not(target_arch = "wasm32"))] - fn resolve_did_doc( + fn resolve_did_doc( &self, - did: &Did<'_>, + did: &Did, ) -> impl Future> + Send where Self: Sync, { self.resolver.resolve_did_doc(did) } + #[doc = " Resolve handle"] #[cfg(target_arch = "wasm32")] - fn resolve_handle( + fn resolve_handle( &self, - handle: &Handle<'_>, - ) -> impl Future, IdentityError>> { + handle: &Handle, + ) -> impl Future> { self.resolver.resolve_handle(handle) } #[doc = " Resolve DID document"] #[cfg(target_arch = "wasm32")] - fn resolve_did_doc( + fn resolve_did_doc( &self, - did: &Did<'_>, + did: &Did, ) -> impl Future> { self.resolver.resolve_did_doc(did) } @@ -479,49 +482,43 @@ impl Default for MemoryCredentialSession { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AtpSession { /// Access token (JWT) used for authenticated requests - #[serde(borrow)] - pub access_jwt: CowStr<'static>, + pub access_jwt: SmolStr, /// Refresh token (JWT) used to obtain new access tokens - pub refresh_jwt: CowStr<'static>, + pub refresh_jwt: SmolStr, /// User's DID (Decentralized Identifier) - pub did: Did<'static>, + pub did: Did, /// User's handle (e.g., "alice.bsky.social") - pub handle: Handle<'static>, + pub handle: Handle, } impl IntoStatic for AtpSession { type Output = Self; fn into_static(self) -> Self { - Self { - access_jwt: self.access_jwt.into_static(), - refresh_jwt: self.refresh_jwt.into_static(), - did: self.did.into_static(), - handle: self.handle.into_static(), - } + self } } #[cfg(feature = "api")] -impl From> for AtpSession { - fn from(output: CreateSessionOutput<'_>) -> Self { +impl From for AtpSession { + fn from(output: CreateSessionOutput) -> Self { Self { - access_jwt: output.access_jwt.into_static(), - refresh_jwt: output.refresh_jwt.into_static(), - did: output.did.into_static(), - handle: output.handle.into_static(), + access_jwt: output.access_jwt, + refresh_jwt: output.refresh_jwt, + did: output.did, + handle: output.handle, } } } #[cfg(feature = "api")] -impl From> for AtpSession { - fn from(output: RefreshSessionOutput<'_>) -> Self { +impl From for AtpSession { + fn from(output: RefreshSessionOutput) -> Self { Self { - access_jwt: output.access_jwt.into_static(), - refresh_jwt: output.refresh_jwt.into_static(), - did: output.did.into_static(), - handle: output.handle.into_static(), + access_jwt: output.access_jwt, + refresh_jwt: output.refresh_jwt, + did: output.did, + handle: output.handle, } } } @@ -548,7 +545,7 @@ impl Agent { } /// Return session info if available. - pub async fn info(&self) -> Option<(Did<'static>, Option>)> { + pub async fn info(&self) -> Option<(Did, Option)> { self.inner.session_info().await } @@ -563,27 +560,27 @@ impl Agent { } /// Refresh the session and return a fresh token. - pub async fn refresh(&self) -> ClientResult> { + pub async fn refresh(&self) -> ClientResult> { self.inner.refresh().await } } -/// Output type for a collection record retrieval operation -pub type CollectionOutput<'a, R> = <::Record as XrpcResp>::Output<'a>; +/// Output type for a collection record retrieval operation (SmolStr-backed, as returned by `into_output()`) +pub type CollectionOutput = <::Record as XrpcResp>::Output; /// Error type for a collection record retrieval operation -pub type CollectionErr<'a, R> = <::Record as XrpcResp>::Err<'a>; +pub type CollectionErr = <::Record as XrpcResp>::Err; /// Response type for the get request of a vec update operation pub type VecGetResponse = <::GetRequest as XrpcRequest>::Response; /// Response type for the put request of a vec update operation pub type VecPutResponse = <::PutRequest as XrpcRequest>::Response; -type CollectionError<'a, R> = <::Record as XrpcResp>::Err<'a>; +type CollectionError = <::Record as XrpcResp>::Err; -type VecUpdateGetError<'a, U> = - <<::GetRequest as XrpcRequest>::Response as XrpcResp>::Err<'a>; +type VecUpdateGetError = + <<::GetRequest as XrpcRequest>::Response as XrpcResp>::Err; -type VecUpdatePutError<'a, U> = - <<::PutRequest as XrpcRequest>::Response as XrpcResp>::Err<'a>; +type VecUpdatePutError = + <<::PutRequest as XrpcRequest>::Response as XrpcResp>::Err; /// Extension trait providing convenience methods for common repository operations. /// @@ -621,7 +618,7 @@ type VecUpdatePutError<'a, U> = /// let output = agent.create_record(post, None).await?; /// /// // Read it back -/// let response = agent.get_record::(&output.uri).await?; +/// let response = agent.get_record::(&output.uri).await?; /// let record = response.parse()?; /// println!("Post: {}", record.value.text); /// # Ok(()) @@ -665,8 +662,8 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { fn create_record( &self, record: R, - rkey: Option>>, - ) -> impl Future>> + rkey: Option>, + ) -> impl Future> where R: Collection + serde::Serialize, { @@ -688,9 +685,9 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { let request = CreateRecord::new() .repo(AtIdentifier::Did(did)) - .collection(R::nsid()) + .collection(R::nsid().into_static()) .record(data) - .maybe_rkey(rkey) + .rkey(rkey.map(|k| k.clone())) .build(); #[cfg(feature = "tracing")] @@ -725,7 +722,7 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { /// # async fn main() -> Result<(), Box> { /// # let agent: BasicClient = todo!(); /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5bqm7lepk2c").unwrap(); - /// let response = agent.get_record::(&uri).await?; + /// let response = agent.get_record::(&uri).await?; /// let output = response.parse()?; // PostGetRecordOutput<'_> borrowing from buffer /// println!("Post text: {}", output.value.text); /// @@ -734,12 +731,13 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { /// # Ok(()) /// # } /// ``` - fn get_record( + fn get_record( &self, - uri: &AtUri<'_>, + uri: &AtUri, ) -> impl Future>> where R: Collection, + S: BosStr + Sync, { async move { #[cfg(feature = "tracing")] @@ -766,33 +764,35 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { #[cfg(feature = "tracing")] _span.exit(); - // Resolve authority (DID or handle) to get DID and PDS - use jacquard_common::types::ident::AtIdentifier; + // Resolve authority (DID or handle) to get DID and PDS. let (repo_did, pds_url) = match uri.authority() { AtIdentifier::Did(did) => { - let pds = self.pds_for_did(did).await.map_err(|e| { + let pds = self.pds_for_did(&did).await.map_err(|e| { ClientError::from(e) .with_context("DID document resolution failed during record retrieval") })?; - (did.clone(), pds) + (did.into_static(), pds) + } + AtIdentifier::Handle(handle) => { + self.pds_for_handle(&handle).await.map_err(|e| { + ClientError::from(e) + .with_context("handle resolution failed during record retrieval") + })? } - AtIdentifier::Handle(handle) => self.pds_for_handle(handle).await.map_err(|e| { - ClientError::from(e) - .with_context("handle resolution failed during record retrieval") - })?, }; - // Make stateless XRPC call to that PDS (no auth required for public records) + // Make stateless XRPC call to that PDS (no auth required for public records). + // All fields use SmolStr backing to satisfy the builder's single S type parameter. use jacquard_api::com_atproto::repo::get_record::GetRecord; let request = GetRecord::new() - .repo(AtIdentifier::Did(repo_did)) - .collection(R::nsid()) - .rkey(rkey.clone()) + .repo(AtIdentifier::Did(repo_did.clone())) + .collection(R::nsid().into_static()) + .rkey(rkey.into_static()) .build(); let response: Response = { let http_request = - xrpc::build_http_request(&pds_url, &request, &self.opts().await)?; + xrpc::build_http_request(&pds_url.borrow(), &request, &self.opts().await)?; let http_response = self .send_http(http_request) @@ -808,10 +808,13 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { /// Untyped, freeform record fetcher. /// Hits - fn fetch_record_slingshot( + fn fetch_record_slingshot( &self, - uri: &AtUri<'_>, - ) -> impl Future>> { + uri: &AtUri, + ) -> impl Future> + where + S: BosStr + Sync, + { async move { #[cfg(feature = "tracing")] let _span = tracing::debug_span!("fetch_record_slingshot", uri = %uri).entered(); @@ -829,7 +832,7 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { let request = GetRecord::new() .repo(uri.authority().clone()) .collection(collection.clone()) - .rkey(rkey.clone()) + .rkey(RecordKey(rkey.clone())) .build(); #[cfg(feature = "tracing")] @@ -838,8 +841,7 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { let response: Response = { let http_request = xrpc::build_http_request( &Uri::parse("https://slingshot.microcosm.blue") - .expect("slingshot url is valid") - .to_owned(), + .expect("slingshot url is valid"), &request, &self.opts().await, )?; @@ -864,20 +866,21 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { /// /// Takes an at:// URI annotated with the collection type, which be constructed with `R::uri(uri)` /// where `R` is the type of record you want (e.g. `app_bsky::feed::post::Post::uri(uri)` for Bluesky posts). - fn fetch_record( + fn fetch_record( &self, - uri: &RecordUri<'_, R>, - ) -> impl Future>> + uri: &RecordUri, + ) -> impl Future>> where R: Collection, - for<'a> CollectionOutput<'a, R>: IntoStatic>, - for<'a> CollectionErr<'a, R>: IntoStatic> + Send + Sync, + S: BosStr + Sync, + CollectionOutput: serde::de::DeserializeOwned, + CollectionError: Send + Sync + 'static, { let uri = uri.as_uri(); async move { use smol_str::format_smolstr; - let response = self.get_record::(uri).await?; + let response = self.get_record::(uri).await?; let response: Response = response.transmute(); let output = response.into_output().map_err(|e| match e { XrpcError::Auth(auth) => AgentError::from(auth), @@ -888,7 +891,7 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { None, ) .with_details(format_smolstr!("{:?}", typed)), - // Note for future orual: the above was done this way due to GAT lifetime inference constraints.. + // Note: typed error formatted as Debug since CollectionErr is not Display. e => AgentError::xrpc(e), })?; Ok(output) @@ -914,39 +917,38 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { /// # let agent: BasicClient = todo!(); /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.actor.profile/self").unwrap(); /// // Update profile record in-place - /// agent.update_record::(&uri, |profile| { + /// agent.update_record::(&uri, |profile| { /// profile.display_name = Some(CowStr::from("New Name")); /// profile.description = Some(CowStr::from("Updated bio")); /// }).await?; /// # Ok(()) /// # } /// ``` - fn update_record( + fn update_record( &self, - uri: &AtUri<'_>, + uri: &AtUri, f: impl FnOnce(&mut R), - ) -> impl Future>> + ) -> impl Future> where R: Collection + Serialize, - R: for<'a> From>, - for<'a> as IntoStatic>::Output: - IntoStatic + std::error::Error + Send + Sync, - for<'a> CollectionError<'a, R>: Send + Sync + std::error::Error + IntoStatic, + R: From>, + CollectionOutput: serde::de::DeserializeOwned, + CollectionError: Send + Sync + std::error::Error + 'static, + S: BosStr + Sync, { async move { - // Fetch the record - Response where R::Record::Output<'de> = R<'de> - let response = self.get_record::(uri).await?; + // Fetch the record - Response where R::Record::Output = R + let response = self.get_record::(uri).await?; #[cfg(feature = "tracing")] let _span = tracing::debug_span!("update_record", collection = %R::nsid(), uri = %uri) .entered(); - // Parse to get R<'_> borrowing from response buffer + // Parse to get the record, borrowing from the response buffer. + // Err is now a plain owned type; no into_static() needed. let record = response.parse().map_err(|e| match e { XrpcError::Auth(auth) => AgentError::from(auth), - XrpcError::Xrpc(typed) => { - AgentError::sub_operation("parse record", typed.into_static()) - } + XrpcError::Xrpc(typed) => AgentError::sub_operation("parse record", typed), e => AgentError::xrpc(e), })?; @@ -957,17 +959,20 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { f(&mut owned); // Put it back - let rkey = uri - .rkey() - .ok_or_else(|| { - use jacquard_common::types::string::AtStrError; - AgentError::sub_operation( - "extract rkey", - AtStrError::missing("at-uri-scheme", &uri, "rkey"), - ) - })? - .clone() - .into_static(); + // Convert the borrowed Rkey<&str> to an owned Rkey, then wrap in RecordKey. + // The Rkey is already validated (extracted from a valid AtUri), so direct + // construction is safe. + let rkey = RecordKey( + uri.rkey() + .ok_or_else(|| { + use jacquard_common::types::string::AtStrError; + AgentError::sub_operation( + "extract rkey", + AtStrError::missing("at-uri-scheme", &uri, "rkey"), + ) + })? + .convert::(), + ); #[cfg(feature = "tracing")] _span.exit(); @@ -981,12 +986,12 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { /// The repo is automatically filled from the session info. fn delete_record( &self, - rkey: RecordKey>, - ) -> impl Future>> + rkey: RecordKey, + ) -> impl Future> where - R: Collection, + R: Collection + Serialize, { - async { + async move { let (did, _) = self .session_info() .await @@ -998,9 +1003,9 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { use jacquard_common::types::ident::AtIdentifier; let request = DeleteRecord::new() - .repo(AtIdentifier::Did(did)) - .collection(R::nsid()) - .rkey(rkey) + .repo(AtIdentifier::Did(did.clone())) + .collection(R::nsid().into_static()) + .rkey(rkey.into_static()) .build(); #[cfg(feature = "tracing")] @@ -1024,9 +1029,9 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { /// The repo is automatically filled from the session info. fn put_record( &self, - rkey: RecordKey>, + rkey: RecordKey, record: R, - ) -> impl Future>> + ) -> impl Future> where R: Collection + serde::Serialize, { @@ -1047,9 +1052,9 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { to_data(&record).map_err(|e| AgentError::sub_operation("serialize record", e))?; let request = PutRecord::new() - .repo(AtIdentifier::Did(did)) - .collection(R::nsid()) - .rkey(rkey) + .repo(AtIdentifier::Did(did.clone())) + .collection(R::nsid().into_static()) + .rkey(rkey.into_static()) .record(data) .build(); @@ -1091,8 +1096,8 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { fn upload_blob( &self, data: impl Into, - mime_type: MimeType<'_>, - ) -> impl Future>> { + mime_type: MimeType<&str>, + ) -> impl Future> { async move { #[cfg(feature = "tracing")] let _span = tracing::debug_span!("upload_blob", mime_type = %mime_type).entered(); @@ -1121,7 +1126,8 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { XrpcError::Xrpc(typed) => AgentError::sub_operation("upload blob", typed), e => AgentError::xrpc(e), })?; - Ok(output.blob.blob().clone().into_static()) + // Blob is now SmolStr-backed (owned), so no into_static() needed. + Ok(output.blob.blob().clone()) } } @@ -1138,36 +1144,32 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { /// prefs.retain(|p| !matches!(p, Preference::Hidden(_))); /// }).await?; /// ``` - fn update_vec( + fn update_vec<'a, U>( &self, modify: impl FnOnce(&mut Vec<::Item>), ) -> impl Future>>> where U: VecUpdate, - ::PutRequest: Send + Sync, - ::GetRequest: Send + Sync, + ::PutRequest: Send + Sync + Serialize, + ::GetRequest: Send + Sync + Serialize, VecGetResponse: Send + Sync, VecPutResponse: Send + Sync, - for<'a> VecUpdateGetError<'a, U>: Send + Sync + std::error::Error + IntoStatic, - for<'a> VecUpdatePutError<'a, U>: Send + Sync + std::error::Error + IntoStatic, - for<'a> as IntoStatic>::Output: - Send + Sync + std::error::Error + IntoStatic + 'static, - for<'a> as IntoStatic>::Output: - Send + Sync + std::error::Error + IntoStatic + 'static, + as XrpcResp>::Output: DeserializeOwned, + as XrpcResp>::Output: DeserializeOwned, + VecUpdateGetError: Send + Sync + std::error::Error + 'static, + VecUpdatePutError: Send + Sync + std::error::Error + 'static, { async { // Fetch current data let get_request = U::build_get(); let response = self.send(get_request).await?; - let output = response.parse().map_err(|e| match e { + let output = response.into_output().map_err(|e| match e { XrpcError::Auth(auth) => AgentError::from(auth), - XrpcError::Xrpc(typed) => { - AgentError::sub_operation("update vec", typed.into_static()) - } + XrpcError::Xrpc(typed) => AgentError::sub_operation("update vec", typed), e => AgentError::xrpc(e), })?; - // Extract vec (converts to owned via IntoStatic) + // Extract vec let mut items = U::extract_vec(output); // Apply modification @@ -1198,16 +1200,14 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { ) -> impl Future>>> where U: VecUpdate, - ::PutRequest: Send + Sync, - ::GetRequest: Send + Sync, + ::PutRequest: Send + Sync + Serialize, + ::GetRequest: Send + Sync + Serialize, VecGetResponse: Send + Sync, VecPutResponse: Send + Sync, - for<'a> VecUpdateGetError<'a, U>: Send + Sync + std::error::Error + IntoStatic, - for<'a> VecUpdatePutError<'a, U>: Send + Sync + std::error::Error + IntoStatic, - for<'a> as IntoStatic>::Output: - Send + Sync + std::error::Error + IntoStatic + 'static, - for<'a> as IntoStatic>::Output: - Send + Sync + std::error::Error + IntoStatic + 'static, + as XrpcResp>::Output: DeserializeOwned, + as XrpcResp>::Output: DeserializeOwned, + VecUpdateGetError: Send + Sync + std::error::Error + 'static, + VecUpdatePutError: Send + Sync + std::error::Error + 'static, { async { self.update_vec::(|vec| { @@ -1234,12 +1234,11 @@ where fn session_kind(&self) -> AgentKind { AgentKind::AppPassword } - fn session_info( - &self, - ) -> impl Future, Option>)>> { + fn session_info(&self) -> impl Future)>> { async move { CredentialSession::::session_info(self) .await + // Convert the SmolStr session id to CowStr<'static>. .map(|key| (key.0, Some(key.1))) } } @@ -1249,7 +1248,7 @@ where fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future { async move { CredentialSession::::set_options(self, opts).await } } - fn refresh(&self) -> impl Future>> { + fn refresh(&self) -> impl Future>> { async move { Ok(CredentialSession::::refresh(self) .await? @@ -1267,12 +1266,11 @@ where fn session_kind(&self) -> AgentKind { AgentKind::OAuth } - fn session_info( - &self, - ) -> impl Future, Option>)>> { + fn session_info(&self) -> impl Future)>> { async { let (did, sid) = OAuthSession::::session_info(self).await; - Some((did.into_static(), Some(sid.into_static()))) + // did is already Did; convert SmolStr sid to CowStr<'static>. + Some((did, Some(sid))) } } fn endpoint(&self) -> impl Future> { @@ -1281,7 +1279,7 @@ where fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future { async { self.set_options(opts).await } } - fn refresh(&self) -> impl Future>> { + fn refresh(&self) -> impl Future>> { async { self.refresh() .await @@ -1299,9 +1297,7 @@ where fn session_kind(&self) -> AgentKind { AgentKind::OAuth } - fn session_info( - &self, - ) -> impl Future, Option>)>> { + fn session_info(&self) -> impl Future)>> { async { None } } fn endpoint(&self) -> impl Future> { @@ -1310,7 +1306,7 @@ where fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future { async { self.set_opts(opts).await } } - fn refresh(&self) -> impl Future>> { + fn refresh(&self) -> impl Future>> { async { Err(ClientError::auth( jacquard_common::error::AuthError::NotAuthenticated, @@ -1431,7 +1427,7 @@ impl XrpcClient for Agent { request: R, ) -> impl Future::Response>>> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, { async move { self.inner.send(request).await } @@ -1443,7 +1439,7 @@ impl XrpcClient for Agent { opts: CallOptions<'_>, ) -> XrpcResult::Response>> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, { self.inner.send_with_opts(request, opts).await @@ -1466,7 +1462,7 @@ where >, > + Send where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, Self: Sync, { @@ -1484,28 +1480,29 @@ where >, > where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, { self.inner.download(request) } #[cfg(not(target_arch = "wasm32"))] - fn stream( + fn stream( &self, - stream: jacquard_common::xrpc::XrpcProcedureSend>, + stream: jacquard_common::xrpc::XrpcProcedureSend>, ) -> impl Future< Output = core::result::Result< - jacquard_common::xrpc::XrpcResponseStream<<::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>>, + jacquard_common::xrpc::XrpcResponseStream<<::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame>, jacquard_common::StreamError, >, > where + B: BosStr + 'static, S: jacquard_common::xrpc::XrpcProcedureStream + 'static, - <::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::XrpcStreamResp, + <::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame: jacquard_common::xrpc::XrpcStreamResp, Self: Sync, { - self.inner.stream::(stream) + self.inner.stream::(stream) } #[cfg(target_arch = "wasm32")] @@ -1531,16 +1528,16 @@ impl IdentityResolver for Agent { self.inner.options() } - fn resolve_handle( + fn resolve_handle( &self, - handle: &Handle<'_>, - ) -> impl Future, IdentityError>> { + handle: &Handle, + ) -> impl Future> { async { self.inner.resolve_handle(handle).await } } - fn resolve_did_doc( + fn resolve_did_doc( &self, - did: &Did<'_>, + did: &Did, ) -> impl Future> { async { self.inner.resolve_did_doc(did).await } } @@ -1551,9 +1548,7 @@ impl AgentSession for Agent { self.kind() } - fn session_info( - &self, - ) -> impl Future, Option>)>> { + fn session_info(&self) -> impl Future)>> { async { self.info().await } } @@ -1565,7 +1560,7 @@ impl AgentSession for Agent { async { self.set_options(opts).await } } - fn refresh(&self) -> impl Future>> { + fn refresh(&self) -> impl Future>> { async { self.refresh().await } } } diff --git a/crates/jacquard/src/client/credential_session.rs b/crates/jacquard/src/client/credential_session.rs index ed4fec7b..a95f7da1 100644 --- a/crates/jacquard/src/client/credential_session.rs +++ b/crates/jacquard/src/client/credential_session.rs @@ -5,15 +5,17 @@ use jacquard_api::com_atproto::server::{ }; use jacquard_common::{ AuthorizationToken, CowStr, IntoStatic, + bos::BosStr, deps::fluent_uri::Uri, error::{AuthError, ClientError, XrpcResult}, http_client::HttpClient, session::SessionStore, types::{did::Did, string::Handle}, - xrpc::{ - CallOptions, Response, XrpcClient, XrpcError, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse, - }, + xrpc::{CallOptions, Response, XrpcClient, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse}, }; +#[cfg(feature = "streaming")] +use serde::Serialize; +use smol_str::SmolStr; use tokio::sync::RwLock; use crate::client::AtpSession; @@ -29,7 +31,7 @@ use jacquard_common::xrpc::XrpcSubscription; /// Storage key for app‑password sessions: `(account DID, session id)`. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SessionKey(pub Did<'static>, pub CowStr<'static>); +pub struct SessionKey(pub Did, pub SmolStr); /// Stateful client for app‑password based sessions. /// @@ -128,14 +130,14 @@ where } /// Current access token (Bearer), if logged in. - pub async fn access_token(&self) -> Option> { + pub async fn access_token(&self) -> Option { let key = self.key.read().await.clone()?; let session = self.store.get(&key).await; session.map(|session| AuthorizationToken::Bearer(session.access_jwt)) } /// Current refresh token (Bearer), if logged in. - pub async fn refresh_token(&self) -> Option> { + pub async fn refresh_token(&self) -> Option { let key = self.key.read().await.clone()?; let session = self.store.get(&key).await; session.map(|session| AuthorizationToken::Bearer(session.refresh_jwt)) @@ -148,7 +150,7 @@ where T: HttpClient, { /// Refresh the active session by calling `com.atproto.server.refreshSession`. - pub async fn refresh(&self) -> std::result::Result, ClientError> { + pub async fn refresh(&self) -> std::result::Result { let key = self .key .read() @@ -161,7 +163,7 @@ where opts.auth = session.map(|s| AuthorizationToken::Bearer(s.refresh_jwt)); let response = self .client - .xrpc(endpoint) + .xrpc(endpoint.borrow()) .with_options(opts) .send(&RefreshSession) .await?; @@ -227,10 +229,13 @@ where let resp = self.client.resolve_did_doc(&did).await.map_err(|e| { ClientError::from(e).with_context("DID document resolution failed during login") })?; - resp.into_owned()?.pds_endpoint().ok_or_else(|| { - ClientError::invalid_request("missing PDS endpoint") - .with_help("DID document must include a PDS service endpoint") - })? + resp.into_owned()? + .pds_endpoint() + .map(|u| u.to_owned()) + .ok_or_else(|| { + ClientError::invalid_request("missing PDS endpoint") + .with_help("DID document must include a PDS service endpoint") + })? } else if identifier.as_ref().contains("@") && !identifier.as_ref().starts_with("@") { // we're going to assume its an email pds.ok_or_else(|| { @@ -250,10 +255,13 @@ where let resp = self.client.resolve_did_doc(&did).await.map_err(|e| { ClientError::from(e).with_context("DID document resolution failed during login") })?; - resp.into_owned()?.pds_endpoint().ok_or_else(|| { - ClientError::invalid_request("missing PDS endpoint") - .with_help("DID document must include a PDS service endpoint") - })? + resp.into_owned()? + .pds_endpoint() + .map(|u| u.to_owned()) + .ok_or_else(|| { + ClientError::invalid_request("missing PDS endpoint") + .with_help("DID document must include a PDS service endpoint") + })? }; // Build and send createSession @@ -267,7 +275,7 @@ where let resp = self .client - .xrpc(pds.clone()) + .xrpc(pds.borrow()) .with_options(self.options.read().await.clone()) .send(&req) .await?; @@ -279,7 +287,7 @@ where let session = AtpSession::from(out); let sid = session_id.unwrap_or_else(|| CowStr::new_static("session")); - let key = SessionKey(session.did.clone(), sid.into_static()); + let key = SessionKey(session.did.clone().convert::(), SmolStr::from(sid)); self.store .set(key.clone(), session.clone()) .await @@ -301,7 +309,7 @@ where /// Restore a previously persisted app-password session and set base endpoint. pub async fn restore( &self, - did: Did<'_>, + did: Did, session_id: CowStr<'_>, ) -> std::result::Result<(), ClientError> where @@ -312,7 +320,7 @@ where tracing::info_span!("credential_session_restore", did = %did, session_id = %session_id) .entered(); - let key = SessionKey(did.clone().into_static(), session_id.clone().into_static()); + let key = SessionKey(did.clone(), SmolStr::from(session_id.clone())); let Some(sess) = self.store.get(&key).await else { return Err(ClientError::auth(AuthError::NotAuthenticated)); }; @@ -326,10 +334,13 @@ where } .unwrap_or({ let resp = self.client.resolve_did_doc(&did).await?; - resp.into_owned()?.pds_endpoint().ok_or_else(|| { - ClientError::invalid_request("missing PDS endpoint") - .with_help("DID document must include a PDS service endpoint") - })? + resp.into_owned()? + .pds_endpoint() + .map(|u| u.to_owned()) + .ok_or_else(|| { + ClientError::invalid_request("missing PDS endpoint") + .with_help("DID document must include a PDS service endpoint") + })? }); // Activate @@ -338,7 +349,13 @@ where *self.endpoint.write().await = Some(pds_uri.clone()); // ensure store has the session (no-op if it existed) self.store - .set(SessionKey(sess.did.clone(), session_id.into_static()), sess) + .set( + SessionKey( + sess.did.clone().convert::(), + SmolStr::from(session_id), + ), + sess, + ) .await?; if let Some(file_store) = (&*self.store as &dyn Any).downcast_ref::() @@ -351,13 +368,13 @@ where /// Switch to a different stored session (and refresh endpoint/PDS). pub async fn switch_session( &self, - did: Did<'_>, + did: Did, session_id: CowStr<'_>, ) -> std::result::Result<(), ClientError> where S: Any + 'static, { - let key = SessionKey(did.clone().into_static(), session_id.into_static()); + let key = SessionKey(did.clone(), SmolStr::from(session_id)); if self.store.get(&key).await.is_none() { return Err(ClientError::auth(AuthError::NotAuthenticated)); } @@ -371,10 +388,13 @@ where } .unwrap_or({ let resp = self.client.resolve_did_doc(&did).await?; - resp.into_owned()?.pds_endpoint().ok_or_else(|| { - ClientError::invalid_request("missing PDS endpoint") - .with_help("DID document must include a PDS service endpoint") - })? + resp.into_owned()? + .pds_endpoint() + .map(|u| u.to_owned()) + .ok_or_else(|| { + ClientError::invalid_request("missing PDS endpoint") + .with_help("DID document must include a PDS service endpoint") + })? }); *self.key.write().await = Some(key.clone()); let pds_uri = jacquard_common::xrpc::normalize_base_uri(pds); @@ -445,7 +465,7 @@ where async fn send(&self, request: R) -> XrpcResult> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync, { let opts = self.options.read().await.clone(); @@ -458,7 +478,7 @@ where mut opts: CallOptions<'_>, ) -> XrpcResult> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync, { let base_uri = self.base_uri().await; @@ -466,7 +486,7 @@ where opts.auth = auth; let resp = self .client - .xrpc(base_uri.clone()) + .xrpc(base_uri.borrow()) .with_options(opts.clone()) .send(&request) .await; @@ -475,7 +495,7 @@ where let auth = self.refresh().await?; opts.auth = Some(auth); self.client - .xrpc(base_uri) + .xrpc(base_uri.borrow()) .with_options(opts) .send(&request) .await @@ -496,10 +516,7 @@ fn is_expired(response: &XrpcResult>) -> bool { { true } - Ok(resp) => match resp.parse() { - Err(XrpcError::Auth(AuthError::TokenExpired)) => true, - _ => false, - }, + Ok(_) => false, _ => false, } } @@ -561,7 +578,7 @@ where request: R, ) -> core::result::Result where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + Serialize, ::Response: Send + Sync, { use jacquard_common::{StreamError, xrpc::build_http_request}; @@ -570,7 +587,7 @@ where let mut opts = self.options.read().await.clone(); opts.auth = self.access_token().await; - let http_request = build_http_request(&base_uri, &request, &opts) + let http_request = build_http_request(&base_uri.borrow(), &request, &opts) .map_err(|e| StreamError::protocol(e.to_string()))?; let response = self @@ -588,7 +605,7 @@ where let auth = self.refresh().await.map_err(StreamError::transport)?; opts.auth = Some(auth); - let http_request = build_http_request(&base_uri, &request, &opts) + let http_request = build_http_request(&base_uri.borrow(), &request, &opts) .map_err(|e| StreamError::protocol(e.to_string()))?; let response = self @@ -603,18 +620,19 @@ where } } - async fn stream( + async fn stream( &self, - stream: jacquard_common::xrpc::streaming::XrpcProcedureSend>, + stream: jacquard_common::xrpc::streaming::XrpcProcedureSend>, ) -> core::result::Result< jacquard_common::xrpc::streaming::XrpcResponseStream< - <::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>, + <::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame, >, jacquard_common::StreamError, > where + B: BosStr + 'static, Str: jacquard_common::xrpc::streaming::XrpcProcedureStream + 'static, - <::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::streaming::XrpcStreamResp, + <::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame: jacquard_common::xrpc::streaming::XrpcStreamResp, { use jacquard_common::StreamError; use n0_future::TryStreamExt; @@ -633,10 +651,10 @@ where use jacquard_common::AuthorizationToken; let hv = match token { AuthorizationToken::Bearer(t) => { - http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) + http::HeaderValue::from_str(&format!("Bearer {}", t.as_str())) } AuthorizationToken::Dpop(t) => { - http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) + http::HeaderValue::from_str(&format!("DPoP {}", t.as_str())) } } .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; @@ -692,10 +710,10 @@ where use jacquard_common::AuthorizationToken; let hv = match token { AuthorizationToken::Bearer(t) => { - http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) + http::HeaderValue::from_str(&format!("Bearer {}", t.as_str())) } AuthorizationToken::Dpop(t) => { - http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) + http::HeaderValue::from_str(&format!("DPoP {}", t.as_str())) } } .map_err(|e| { @@ -733,13 +751,13 @@ where .map_err(StreamError::transport)?; let (resp_parts, resp_body) = response.into_parts(); Ok( - jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts( + jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts::( resp_parts, resp_body, ), ) } else { Ok( - jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts( + jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts::( resp_parts, resp_body, ), ) @@ -757,16 +775,40 @@ where self.client.options() } - fn resolve_handle( + #[cfg(not(target_arch = "wasm32"))] + fn resolve_handle( &self, - handle: &Handle<'_>, - ) -> impl Future, IdentityError>> { + handle: &Handle, + ) -> impl Future> + where + Self: Sync, + { + async { self.client.resolve_handle(handle).await } + } + + #[cfg(target_arch = "wasm32")] + fn resolve_handle( + &self, + handle: &Handle, + ) -> impl Future> { async { self.client.resolve_handle(handle).await } } - fn resolve_did_doc( + #[cfg(not(target_arch = "wasm32"))] + fn resolve_did_doc( + &self, + did: &Did, + ) -> impl Future> + where + Self: Sync, + { + async { self.client.resolve_did_doc(did).await } + } + + #[cfg(target_arch = "wasm32")] + fn resolve_did_doc( &self, - did: &Did<'_>, + did: &Did, ) -> impl Future> { async { self.client.resolve_did_doc(did).await } } @@ -813,8 +855,8 @@ where let mut opts = jacquard_common::xrpc::SubscriptionOptions::default(); if let Some(token) = self.access_token().await { let auth_value = match token { - AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_ref()), - AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_ref()), + AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_str()), + AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_str()), }; opts.headers .push((CowStr::from("Authorization"), CowStr::from(auth_value))); @@ -827,7 +869,7 @@ where params: &Sub, ) -> Result, Self::Error> where - Sub: XrpcSubscription + Send + Sync, + Sub: XrpcSubscription + Send + Sync + serde::Serialize, { let opts = self.subscription_opts().await; self.subscribe_with_opts(params, opts).await @@ -839,7 +881,7 @@ where opts: jacquard_common::xrpc::SubscriptionOptions<'_>, ) -> Result, Self::Error> where - Sub: XrpcSubscription + Send + Sync, + Sub: XrpcSubscription + Send + Sync + serde::Serialize, { use jacquard_common::xrpc::SubscriptionExt; let base = self.base_uri().await; diff --git a/crates/jacquard/src/client/error.rs b/crates/jacquard/src/client/error.rs index 87738a79..911973f6 100644 --- a/crates/jacquard/src/client/error.rs +++ b/crates/jacquard/src/client/error.rs @@ -22,7 +22,7 @@ pub struct AgentError { url: Option, details: Option, location: Option, - xrpc: Option>, + xrpc: Option, } impl std::fmt::Display for AgentError { @@ -78,11 +78,11 @@ pub enum AgentErrorKind { #[diagnostic(code(jacquard::agent::record_operation))] RecordOperation { /// The repository DID - repo: Did<'static>, + repo: Did, /// The collection NSID - collection: Nsid<'static>, + collection: Nsid, /// The record key - rkey: RecordKey>, + rkey: RecordKey, }, /// Multi-step operation failed at sub-step (e.g., get failed in update_record) @@ -206,37 +206,27 @@ impl AgentError { /// Add XRPC error data to this error for observability pub fn with_xrpc(mut self, xrpc: XrpcError) -> Self where - E: std::error::Error + jacquard_common::IntoStatic + serde::Serialize, + E: std::error::Error + serde::Serialize, { use jacquard_common::types::value::to_data; - // Attempt to serialize XrpcError to Data for observability - if let Ok(data) = to_data(&xrpc) { - self.xrpc = Some(data.into_static()); + // Attempt to serialize XrpcError to Data for observability. + if let Ok(data) = to_data::<_>(&xrpc) { + self.xrpc = Some(data); } self } - /// Create an XRPC error with attached error data for observability + /// Create an XRPC error with attached error data for observability. pub fn xrpc(error: XrpcError) -> Self where - E: std::error::Error + jacquard_common::IntoStatic + serde::Serialize + Send + Sync, - ::Output: IntoStatic + std::error::Error + Send + Sync, + E: std::error::Error + serde::Serialize + Send + Sync + 'static, { use jacquard_common::types::value::to_data; - // Attempt to serialize XrpcError to Data for observability - if let Ok(data) = to_data(&error) { - let mut error = Self::new( - AgentErrorKind::XrpcError, - Some(Box::new(error.into_static())), - ); - error.xrpc = Some(data.into_static()); - error - } else { - Self::new( - AgentErrorKind::XrpcError, - Some(Box::new(error.into_static())), - ) - } + // Attempt to serialize XrpcError to Data for observability. + let xrpc = to_data::<_>(&error).ok(); + let mut err = Self::new(AgentErrorKind::XrpcError, Some(Box::new(error))); + err.xrpc = xrpc; + err } // Constructors @@ -259,9 +249,9 @@ impl AgentError { /// Create a record operation error pub fn record_operation( - repo: Did<'static>, - collection: Nsid<'static>, - rkey: RecordKey>, + repo: Did, + collection: Nsid, + rkey: RecordKey, source: impl std::error::Error + Send + Sync + 'static, ) -> Self { Self::new( diff --git a/crates/jacquard/src/client/token.rs b/crates/jacquard/src/client/token.rs index 50d5c229..9f792585 100644 --- a/crates/jacquard/src/client/token.rs +++ b/crates/jacquard/src/client/token.rs @@ -1,5 +1,3 @@ -use jacquard_common::IntoStatic; -use jacquard_common::cowstr::ToCowStr; use jacquard_common::deps::fluent_uri::Uri; use jacquard_common::session::{FileTokenStore, SessionStore, SessionStoreError}; use jacquard_common::types::string::{Datetime, Did}; @@ -9,6 +7,7 @@ use jacquard_oauth::types::OAuthTokenType; use jose_jwk::Key; use serde::{Deserialize, Serialize}; use serde_json::Value; +use smol_str::SmolStr; /// On-disk session records for app-password and OAuth flows, sharing a single JSON map. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -90,66 +89,77 @@ pub struct OAuthSession { pub expires_at: Option, } -impl From> for OAuthSession { - fn from(data: ClientSessionData<'_>) -> Self { +impl From> for OAuthSession { + fn from(data: ClientSessionData) -> Self { OAuthSession { - account_did: data.account_did.to_string(), - session_id: data.session_id.to_string(), + account_did: AsRef::::as_ref(&data.account_did).to_owned(), + session_id: AsRef::::as_ref(&data.session_id).to_owned(), host_url: data.host_url.clone(), - authserver_url: data.authserver_url.to_string(), - authserver_token_endpoint: data.authserver_token_endpoint.to_string(), + authserver_url: AsRef::::as_ref(&data.authserver_url).to_owned(), + authserver_token_endpoint: AsRef::::as_ref(&data.authserver_token_endpoint) + .to_owned(), authserver_revocation_endpoint: data .authserver_revocation_endpoint - .map(|s| s.to_string()), - scopes: data.scopes.into_iter().map(|s| s.to_string()).collect(), + .map(|s| AsRef::::as_ref(&s).to_owned()), + scopes: data + .scopes + .into_iter() + .map(|s| String::from(s.to_string_normalized())) + .collect(), dpop_key: data.dpop_data.dpop_key, - dpop_authserver_nonce: data.dpop_data.dpop_authserver_nonce.to_string(), - dpop_host_nonce: data.dpop_data.dpop_host_nonce.to_string(), - iss: data.token_set.iss.to_string(), - sub: data.token_set.sub.to_string(), - aud: data.token_set.aud.to_string(), - scope: data.token_set.scope.map(|s| s.to_string()), - refresh_token: data.token_set.refresh_token.map(|s| s.to_string()), - access_token: data.token_set.access_token.to_string(), + dpop_authserver_nonce: AsRef::::as_ref(&data.dpop_data.dpop_authserver_nonce) + .to_owned(), + dpop_host_nonce: AsRef::::as_ref(&data.dpop_data.dpop_host_nonce).to_owned(), + iss: AsRef::::as_ref(&data.token_set.iss).to_owned(), + sub: AsRef::::as_ref(&data.token_set.sub).to_owned(), + aud: AsRef::::as_ref(&data.token_set.aud).to_owned(), + scope: data + .token_set + .scope + .map(|s| AsRef::::as_ref(&s).to_owned()), + refresh_token: data + .token_set + .refresh_token + .map(|s| AsRef::::as_ref(&s).to_owned()), + access_token: AsRef::::as_ref(&data.token_set.access_token).to_owned(), token_type: data.token_set.token_type, expires_at: data.token_set.expires_at, } } } -impl From for ClientSessionData<'_> { +impl From for ClientSessionData { fn from(session: OAuthSession) -> Self { ClientSessionData { - account_did: session.account_did.into(), - session_id: session.session_id.to_cowstr(), + account_did: Did::new_owned(session.account_did).expect("stored DID should be valid"), + session_id: SmolStr::from(session.session_id), host_url: session.host_url, - authserver_url: session.authserver_url.to_cowstr(), - authserver_token_endpoint: session.authserver_token_endpoint.to_cowstr(), + authserver_url: SmolStr::from(session.authserver_url), + authserver_token_endpoint: SmolStr::from(session.authserver_token_endpoint), authserver_revocation_endpoint: session .authserver_revocation_endpoint - .map(|s| s.to_cowstr().into_static()), + .map(SmolStr::from), scopes: session .scopes .into_iter() - .map(|s| Scope::parse(&s).unwrap().into_static()) + .map(|s| Scope::parse(&s).unwrap()) .collect(), dpop_data: DpopClientData { dpop_key: session.dpop_key, - dpop_authserver_nonce: session.dpop_authserver_nonce.to_cowstr(), - dpop_host_nonce: session.dpop_host_nonce.to_cowstr(), + dpop_authserver_nonce: SmolStr::from(session.dpop_authserver_nonce), + dpop_host_nonce: SmolStr::from(session.dpop_host_nonce), }, token_set: jacquard_oauth::types::TokenSet { - iss: session.iss.into(), - sub: session.sub.into(), - aud: session.aud.into(), - scope: session.scope.map(|s| s.into()), - refresh_token: session.refresh_token.map(|s| s.into()), - access_token: session.access_token.into(), + iss: SmolStr::from(session.iss), + sub: Did::new_owned(session.sub).expect("stored DID should be valid"), + aud: SmolStr::from(session.aud), + scope: session.scope.map(SmolStr::from), + refresh_token: session.refresh_token.map(SmolStr::from), + access_token: SmolStr::from(session.access_token), token_type: session.token_type, expires_at: session.expires_at, }, } - .into_static() } } @@ -189,52 +199,59 @@ pub struct OAuthState { pub dpop_authserver_nonce: Option, } -impl TryFrom> for OAuthState { +impl TryFrom> for OAuthState { type Error = jacquard_common::deps::fluent_uri::ParseError; - fn try_from(value: AuthRequestData) -> Result { + fn try_from(value: AuthRequestData) -> Result { Ok(OAuthState { - authserver_url: Uri::parse(value.authserver_url.as_str())?.to_owned(), - account_did: value.account_did.map(|s| s.to_string()), - scopes: value.scopes.into_iter().map(|s| s.to_string()).collect(), - request_uri: value.request_uri.to_string(), - authserver_token_endpoint: value.authserver_token_endpoint.to_string(), + authserver_url: Uri::parse(value.authserver_url.as_ref())?.to_owned(), + account_did: value + .account_did + .map(|s| AsRef::::as_ref(&s).to_owned()), + scopes: value + .scopes + .into_iter() + .map(|s| String::from(s.to_string_normalized())) + .collect(), + request_uri: AsRef::::as_ref(&value.request_uri).to_owned(), + authserver_token_endpoint: AsRef::::as_ref(&value.authserver_token_endpoint) + .to_owned(), authserver_revocation_endpoint: value .authserver_revocation_endpoint - .map(|s| s.to_string()), - pkce_verifier: value.pkce_verifier.to_string(), + .map(|s| AsRef::::as_ref(&s).to_owned()), + pkce_verifier: AsRef::::as_ref(&value.pkce_verifier).to_owned(), dpop_key: value.dpop_data.dpop_key, - dpop_authserver_nonce: value.dpop_data.dpop_authserver_nonce.map(|s| s.to_string()), - state: value.state.to_string(), + dpop_authserver_nonce: value + .dpop_data + .dpop_authserver_nonce + .map(|s| AsRef::::as_ref(&s).to_owned()), + state: AsRef::::as_ref(&value.state).to_owned(), }) } } -impl From for AuthRequestData<'_> { +impl From for AuthRequestData { fn from(value: OAuthState) -> Self { AuthRequestData { - authserver_url: value.authserver_url.as_str().into(), - state: value.state.to_cowstr(), - account_did: value.account_did.map(|s| Did::from(s).into_static()), - authserver_revocation_endpoint: value - .authserver_revocation_endpoint - .map(|s| s.to_cowstr().into_static()), + authserver_url: SmolStr::from(value.authserver_url.as_str()), + state: SmolStr::from(value.state), + account_did: value + .account_did + .map(|s| Did::new_owned(s).expect("stored DID should be valid")), + authserver_revocation_endpoint: value.authserver_revocation_endpoint.map(SmolStr::from), scopes: value .scopes .into_iter() - .map(|s| Scope::parse(&s).unwrap().into_static()) + .map(|s| Scope::parse(&s).unwrap()) .collect(), - request_uri: value.request_uri.to_cowstr(), - authserver_token_endpoint: value.authserver_token_endpoint.to_cowstr(), - pkce_verifier: value.pkce_verifier.to_cowstr(), + request_uri: SmolStr::from(value.request_uri), + authserver_token_endpoint: SmolStr::from(value.authserver_token_endpoint), + pkce_verifier: SmolStr::from(value.pkce_verifier), dpop_data: DpopReqData { dpop_key: value.dpop_key, - dpop_authserver_nonce: value - .dpop_authserver_nonce - .map(|s| s.to_cowstr().into_static()), + dpop_authserver_nonce: value.dpop_authserver_nonce.map(SmolStr::from), }, } - .into_static() } } @@ -263,11 +280,11 @@ impl FileAuthStore { } impl jacquard_oauth::authstore::ClientAuthStore for FileAuthStore { - async fn get_session( + async fn get_session( &self, - did: &Did<'_>, + did: &Did, session_id: &str, - ) -> Result>, SessionStoreError> { + ) -> Result, SessionStoreError> { let key = format!("{}_{}", did, session_id); if let StoredSession::OAuth(session) = self .0 @@ -281,10 +298,7 @@ impl jacquard_oauth::authstore::ClientAuthStore for FileAuthStore { } } - async fn upsert_session( - &self, - session: ClientSessionData<'_>, - ) -> Result<(), SessionStoreError> { + async fn upsert_session(&self, session: ClientSessionData) -> Result<(), SessionStoreError> { let key = format!("{}_{}", session.account_did, session.session_id); self.0 .set(key, StoredSession::OAuth(session.into())) @@ -292,9 +306,9 @@ impl jacquard_oauth::authstore::ClientAuthStore for FileAuthStore { Ok(()) } - async fn delete_session( + async fn delete_session( &self, - did: &Did<'_>, + did: &Did, session_id: &str, ) -> Result<(), SessionStoreError> { let key = format!("{}_{}", did, session_id); @@ -314,7 +328,7 @@ impl jacquard_oauth::authstore::ClientAuthStore for FileAuthStore { async fn get_auth_req_info( &self, state: &str, - ) -> Result>, SessionStoreError> { + ) -> Result, SessionStoreError> { let key = format!("authreq_{}", state); if let StoredSession::OAuthState(auth_req) = self .0 @@ -330,7 +344,7 @@ impl jacquard_oauth::authstore::ClientAuthStore for FileAuthStore { async fn save_auth_req_info( &self, - auth_req_info: &AuthRequestData<'_>, + auth_req_info: &AuthRequestData, ) -> Result<(), SessionStoreError> { let key = format!("authreq_{}", auth_req_info.state); let state = auth_req_info.clone().try_into().map_err( @@ -501,7 +515,7 @@ mod tests { let restored = jacquard_common::session::SessionStore::get(&store, &key) .await .unwrap(); - assert_eq!(restored.access_jwt.as_ref(), "a"); + assert_eq!(restored.access_jwt.as_str(), "a"); // clean up let _ = fs::remove_file(&path); } diff --git a/crates/jacquard/src/client/vec_update.rs b/crates/jacquard/src/client/vec_update.rs index dfbe63cf..557d6423 100644 --- a/crates/jacquard/src/client/vec_update.rs +++ b/crates/jacquard/src/client/vec_update.rs @@ -52,9 +52,9 @@ pub trait VecUpdate { /// Build the get request fn build_get() -> Self::GetRequest; - /// Extract the vec from the get response output - fn extract_vec<'s>( - output: <::Response as XrpcResp>::Output<'s>, + /// Extract the vec from the get response output (always owned/DefaultStr-backed). + fn extract_vec( + output: <::Response as XrpcResp>::Output, ) -> Vec; /// Build the put request from the modified vec diff --git a/crates/jacquard/src/client/vec_update/preferences.rs b/crates/jacquard/src/client/vec_update/preferences.rs index 178387ad..c7d7eba4 100644 --- a/crates/jacquard/src/client/vec_update/preferences.rs +++ b/crates/jacquard/src/client/vec_update/preferences.rs @@ -1,7 +1,6 @@ use jacquard_api::app_bsky::actor::PreferencesItem; use jacquard_api::app_bsky::actor::get_preferences::{GetPreferences, GetPreferencesOutput}; use jacquard_api::app_bsky::actor::put_preferences::PutPreferences; -use jacquard_common::IntoStatic; /// VecUpdate implementation for Bluesky actor preferences. /// @@ -35,28 +34,22 @@ pub struct PreferencesUpdate; impl super::VecUpdate for PreferencesUpdate { type GetRequest = GetPreferences; - type PutRequest = PutPreferences<'static>; - type Item = PreferencesItem<'static>; + type PutRequest = PutPreferences; + type Item = PreferencesItem; fn build_get() -> Self::GetRequest { GetPreferences } - fn extract_vec<'s>( - output: GetPreferencesOutput<'s>, - ) -> Vec<::Output> { - output - .preferences - .into_iter() - .map(|p| p.into_static()) - .collect() + fn extract_vec(output: GetPreferencesOutput) -> Vec { + output.preferences } - fn build_put(items: Vec<::Output>) -> Self::PutRequest { + fn build_put(items: Vec) -> Self::PutRequest { PutPreferences::new().preferences(items).build() } - fn matches<'s>(a: &'s Self::Item, b: &'s Self::Item) -> bool { + fn matches(a: &Self::Item, b: &Self::Item) -> bool { // Match preferences by enum variant discriminant std::mem::discriminant(a) == std::mem::discriminant(b) } diff --git a/crates/jacquard/src/moderation.rs b/crates/jacquard/src/moderation.rs index feed5947..819d315b 100644 --- a/crates/jacquard/src/moderation.rs +++ b/crates/jacquard/src/moderation.rs @@ -25,7 +25,7 @@ //! ```ignore //! # use jacquard::moderation::*; //! # use jacquard_api::app_bsky::feed::PostView; -//! # fn example(post: &PostView<'_>, prefs: &ModerationPrefs<'_>, defs: &LabelerDefs<'_>) { +//! # fn example(post: &PostView, prefs: &ModerationPrefs, defs: &LabelerDefs) { //! let decision = moderate(post, prefs, defs, &[]); //! if decision.filter { //! // hide the post diff --git a/crates/jacquard/src/moderation/decision.rs b/crates/jacquard/src/moderation/decision.rs index 7d60f079..1e746a85 100644 --- a/crates/jacquard/src/moderation/decision.rs +++ b/crates/jacquard/src/moderation/decision.rs @@ -3,8 +3,9 @@ use super::{ ModerationPrefs, }; use jacquard_api::com_atproto::label::{Label, LabelValue}; -use jacquard_common::IntoStatic; +use jacquard_common::bos::BosStr; use jacquard_common::types::string::{Datetime, Did}; +use smol_str::SmolStr; /// Apply moderation logic to a single piece of content /// @@ -16,18 +17,18 @@ use jacquard_common::types::string::{Datetime, Did}; /// ```ignore /// # use jacquard::moderation::*; /// # use jacquard_api::app_bsky::feed::PostView; -/// # fn example(post: &PostView<'_>, prefs: &ModerationPrefs<'_>, defs: &LabelerDefs<'_>) { +/// # fn example(post: &PostView, prefs: &ModerationPrefs, defs: &LabelerDefs) { /// let decision = moderate(post, prefs, defs, &[]); /// if decision.filter { /// println!("This post should be hidden"); /// } /// # } /// ``` -pub fn moderate<'a, T: Labeled<'a>>( - item: &'a T, - prefs: &ModerationPrefs<'_>, - defs: &LabelerDefs<'_>, - accepted_labelers: &[Did<'_>], +pub fn moderate>( + item: &T, + prefs: &ModerationPrefs, + defs: &LabelerDefs, + accepted_labelers: &[Did], ) -> ModerationDecision { let mut decision = ModerationDecision::none(); let now = Datetime::now(); @@ -41,15 +42,21 @@ pub fn moderate<'a, T: Labeled<'a>>( } } - // Skip labels from untrusted labelers (if acceptance list is provided) - if !accepted_labelers.is_empty() && !accepted_labelers.contains(&label.src) { + // Skip labels from untrusted labelers (if acceptance list is provided). + // Compare by string since label.src may use a different backing type. + if !accepted_labelers.is_empty() + && !accepted_labelers + .iter() + .any(|d| d.as_ref() == label.src.as_ref()) + { continue; } // Handle negation labels (remove previous causes) if label.neg.unwrap_or(false) { decision.causes.retain(|cause| { - !(cause.label.as_str() == label.val.as_ref() && cause.source == label.src) + !(cause.label.as_str() == label.val.as_ref() + && cause.source.as_ref() == label.src.as_ref()) }); continue; } @@ -60,18 +67,18 @@ pub fn moderate<'a, T: Labeled<'a>>( // Process self-labels if let Some(self_labels) = item.self_labels() { for self_label in self_labels.values { - // Self-labels don't have a source DID, so we'll use a placeholder approach - // In practice, self-labels are usually just used for adult content marking + // Self-labels don't have a source DID, so we'll use a placeholder approach. + // In practice, self-labels are usually just used for adult content marking. // Check user preference for this label let pref = prefs .labels .iter() - .find(|(k, _)| k.as_ref() == self_label.val.as_ref()) + .find(|(k, _)| k.as_str() == self_label.val.as_ref()) .map(|(_, v)| v); // For self-labels, we generally respect them as warnings/info - // unless user has explicitly set a preference + // unless user has explicitly set a preference. match pref { Some(LabelPref::Hide) => { decision.filter = true; @@ -94,29 +101,32 @@ pub fn moderate<'a, T: Labeled<'a>>( } /// Apply a single label to a moderation decision -fn apply_label( - label: &Label<'_>, - prefs: &ModerationPrefs<'_>, - defs: &LabelerDefs<'_>, +fn apply_label( + label: &Label, + prefs: &ModerationPrefs, + defs: &LabelerDefs, decision: &mut ModerationDecision, ) { let label_val = label.val.as_ref(); - // Get user preference (per-labeler override first, then global) + // Get user preference (per-labeler override first, then global). + // Use string comparison since label.src may use a different backing type than + // the Did keys in prefs.labelers. let pref = prefs .labelers - .get(&label.src) - .and_then(|labeler_prefs| { + .iter() + .find(|(k, _)| k.as_str() == label.src.as_ref()) + .and_then(|(_, labeler_prefs)| { labeler_prefs .iter() - .find(|(k, _)| k.as_ref() == label_val) + .find(|(k, _)| k.as_str() == label_val) .map(|(_, v)| v) }) .or_else(|| { prefs .labels .iter() - .find(|(k, _)| k.as_ref() == label_val) + .find(|(k, _)| k.as_str() == label_val) .map(|(_, v)| v) }); @@ -129,8 +139,9 @@ fn apply_label( decision.filter = true; decision.no_override = true; decision.causes.push(LabelCause { - label: LabelValue::from(label_val).into_static(), - source: label.src.clone().into_static(), + label: LabelValue::from_value(SmolStr::new(label_val)), + source: Did::new_owned(label.src.as_ref()) + .expect("label.src must be a valid DID"), target: determine_target(label), }); return; @@ -142,8 +153,9 @@ fn apply_label( Some(LabelPref::Hide) => { decision.filter = true; decision.causes.push(LabelCause { - label: LabelValue::from(label_val).into_static(), - source: label.src.clone().into_static(), + label: LabelValue::from_value(SmolStr::new(label_val)), + source: Did::new_owned(label.src.as_ref()) + .expect("label.src must be a valid DID"), target: determine_target(label), }); } @@ -161,9 +173,9 @@ fn apply_label( } /// Apply warning-level moderation based on label definition -fn apply_warning( - label: &Label<'_>, - def: Option<&jacquard_api::com_atproto::label::LabelValueDefinition<'_>>, +fn apply_warning( + label: &Label, + def: Option<&jacquard_api::com_atproto::label::LabelValueDefinition>, decision: &mut ModerationDecision, ) { let label_val = label.val.as_ref(); @@ -203,16 +215,16 @@ fn apply_warning( } decision.causes.push(LabelCause { - label: LabelValue::from(label_val).into_static(), - source: label.src.clone().into_static(), + label: LabelValue::from_value(SmolStr::new(label_val)), + source: Did::new_owned(label.src.as_ref()).expect("label.src must be a valid DID"), target: determine_target(label), }); } /// Apply default moderation when user has no preference -fn apply_default( - label: &Label<'_>, - def: Option<&jacquard_api::com_atproto::label::LabelValueDefinition<'_>>, +fn apply_default( + label: &Label, + def: Option<&jacquard_api::com_atproto::label::LabelValueDefinition>, decision: &mut ModerationDecision, ) { let label_val = label.val.as_ref(); @@ -224,8 +236,9 @@ fn apply_default( "hide" => { decision.filter = true; decision.causes.push(LabelCause { - label: LabelValue::from(label_val).into_static(), - source: label.src.clone().into_static(), + label: LabelValue::from_value(SmolStr::new(label_val)), + source: Did::new_owned(label.src.as_ref()) + .expect("label.src must be a valid DID"), target: determine_target(label), }); return; @@ -247,8 +260,9 @@ fn apply_default( decision.filter = true; decision.no_override = true; decision.causes.push(LabelCause { - label: LabelValue::from(label_val).into_static(), - source: label.src.clone().into_static(), + label: LabelValue::from_value(SmolStr::new(label_val)), + source: Did::new_owned(label.src.as_ref()) + .expect("label.src must be a valid DID"), target: determine_target(label), }); } @@ -267,8 +281,9 @@ fn apply_default( "porn" | "nsfl" => { decision.filter = true; decision.causes.push(LabelCause { - label: LabelValue::from(label_val).into_static(), - source: label.src.clone().into_static(), + label: LabelValue::from_value(SmolStr::new(label_val)), + source: Did::new_owned(label.src.as_ref()) + .expect("label.src must be a valid DID"), target: determine_target(label), }); } @@ -279,8 +294,9 @@ fn apply_default( // Unknown label - default to informational decision.inform = true; decision.causes.push(LabelCause { - label: LabelValue::from(label_val).into_static(), - source: label.src.clone().into_static(), + label: LabelValue::from_value(SmolStr::new(label_val)), + source: Did::new_owned(label.src.as_ref()) + .expect("label.src must be a valid DID"), target: determine_target(label), }); } @@ -289,15 +305,13 @@ fn apply_default( } /// Determine whether a label targets an account or content -fn determine_target(label: &Label<'_>) -> LabelTarget { +fn determine_target(label: &Label) -> LabelTarget { // Try to parse as a DID - this handles both: // - Bare DIDs: did:plc:xyz // - at:// URIs with only DID authority: at://did:plc:xyz // If it parses successfully, it's account-level. // If it fails, it must be a full URI with collection/rkey, so content-level. - use jacquard_common::types::string::Did; - - if Did::new(label.uri.as_ref()).is_ok() { + if Did::::new_owned(label.uri.as_ref()).is_ok() { LabelTarget::Account } else { LabelTarget::Content @@ -313,7 +327,7 @@ fn determine_target(label: &Label<'_>) -> LabelTarget { /// ```ignore /// # use jacquard::moderation::*; /// # use jacquard_api::app_bsky::feed::PostView; -/// # fn example(posts: &[PostView<'_>], prefs: &ModerationPrefs<'_>, defs: &LabelerDefs<'_>) { +/// # fn example(posts: &[PostView], prefs: &ModerationPrefs, defs: &LabelerDefs) { /// let results = moderate_all(posts, prefs, defs, &[]); /// for (post, decision) in results { /// if decision.filter { @@ -322,11 +336,11 @@ fn determine_target(label: &Label<'_>) -> LabelTarget { /// } /// # } /// ``` -pub fn moderate_all<'a, T: Labeled<'a>>( +pub fn moderate_all<'a, S: BosStr, T: Labeled>( items: &'a [T], - prefs: &ModerationPrefs<'_>, - defs: &LabelerDefs<'_>, - accepted_labelers: &[Did<'_>], + prefs: &ModerationPrefs, + defs: &LabelerDefs, + accepted_labelers: &[Did], ) -> Vec<(&'a T, ModerationDecision)> { items .iter() @@ -338,26 +352,33 @@ pub fn moderate_all<'a, T: Labeled<'a>>( /// /// Provides convenience methods for filtering and mapping moderation decisions /// over collections. -pub trait ModerationIterExt<'a, T: Labeled<'a> + 'a>: Iterator + Sized { +pub trait ModerationIterExt<'a, S: BosStr, T: Labeled + 'a>: + Iterator + Sized +{ /// Map each item to a tuple of (item, decision) fn with_moderation( self, - prefs: &'a ModerationPrefs<'_>, - defs: &'a LabelerDefs<'_>, - accepted_labelers: &'a [Did<'_>], + prefs: &'a ModerationPrefs, + defs: &'a LabelerDefs, + accepted_labelers: &'a [Did], ) -> impl Iterator { - self.map(move |item| (item, moderate(item, prefs, defs, accepted_labelers))) + self.map(move |item| (item, moderate::(item, prefs, defs, accepted_labelers))) } /// Filter out items that should be hidden fn filter_moderated( self, - prefs: &'a ModerationPrefs<'_>, - defs: &'a LabelerDefs<'_>, - accepted_labelers: &'a [Did<'_>], + prefs: &'a ModerationPrefs, + defs: &'a LabelerDefs, + accepted_labelers: &'a [Did], ) -> impl Iterator { - self.filter(move |item| !moderate(*item, prefs, defs, accepted_labelers).filter) + self.filter(move |item| { + !moderate::(*item, prefs, defs, accepted_labelers).filter + }) } } -impl<'a, T: Labeled<'a> + 'a, I: Iterator> ModerationIterExt<'a, T> for I {} +impl<'a, S: BosStr, T: Labeled + 'a, I: Iterator> + ModerationIterExt<'a, S, T> for I +{ +} diff --git a/crates/jacquard/src/moderation/fetch.rs b/crates/jacquard/src/moderation/fetch.rs index ea23ab76..6ac03055 100644 --- a/crates/jacquard/src/moderation/fetch.rs +++ b/crates/jacquard/src/moderation/fetch.rs @@ -1,3 +1,5 @@ +use std::convert::From; + use super::LabelerDefs; use crate::client::{AgentError, AgentSessionExt, CollectionErr, CollectionOutput}; use crate::moderation::labeled::LabeledRecord; @@ -8,28 +10,28 @@ use jacquard_api::app_bsky::labeler::{ service::Service, }; use jacquard_api::com_atproto::label::{Label, query_labels::QueryLabels}; -use jacquard_common::cowstr::ToCowStr; +use jacquard_common::BosStr; +use jacquard_common::bos::DefaultStr; use jacquard_common::error::ClientError; use jacquard_common::types::collection::Collection; use jacquard_common::types::string::Did; use jacquard_common::types::uri::RecordUri; -use jacquard_common::xrpc::{XrpcClient, XrpcError}; -use jacquard_common::{CowStr, IntoStatic}; -use std::convert::From; +use jacquard_common::xrpc::{XrpcClient, XrpcError, XrpcResp}; +use smol_str::SmolStr; /// Fetch labeler definitions from Bluesky's AppView (or a compatible one) #[cfg(feature = "api_bluesky")] pub async fn fetch_labeler_defs( client: &(impl XrpcClient + Sync), - dids: Vec>, -) -> Result, ClientError> { + dids: Vec, +) -> Result { #[cfg(feature = "tracing")] let _span = tracing::debug_span!("fetch_labeler_defs", count = dids.len()).entered(); let request = GetServices::new().dids(dids).detailed(true).build(); let response = client.send(request).await?; - let output: GetServicesOutput<'static> = response.into_output().map_err(|e| match e { + let output: GetServicesOutput = response.into_output().map_err(|e| match e { XrpcError::Auth(auth) => ClientError::auth(auth), XrpcError::Generic(g) => ClientError::decode(g.to_string()), XrpcError::Decode(e) => ClientError::decode(format!("{:?}", e)), @@ -46,11 +48,8 @@ pub async fn fetch_labeler_defs( GetServicesOutputViewsItem::LabelerViewDetailed(detailed) => { if let Some(label_value_definitions) = &detailed.policies.label_value_definitions { defs.insert( - detailed.creator.did.clone().into_static(), - label_value_definitions - .iter() - .map(|d| d.clone().into_static()) - .collect(), + detailed.creator.did.clone(), + label_value_definitions.clone(), ); } } @@ -77,8 +76,8 @@ pub async fn fetch_labeler_defs( #[cfg(feature = "api_bluesky")] pub async fn fetch_labeler_defs_direct( client: &(impl AgentSessionExt + Sync), - dids: Vec>, -) -> Result, AgentError> { + dids: Vec, +) -> Result { #[cfg(feature = "tracing")] let _span = tracing::debug_span!("fetch_labeler_defs_direct", count = dids.len()).entered(); @@ -91,10 +90,10 @@ pub async fn fetch_labeler_defs_direct( })?; let output = client.fetch_record(&record_uri).await?; - let service: Service<'static> = output.value; + let service: Service = output.value; if let Some(label_value_definitions) = service.policies.label_value_definitions { - defs.insert(did.into_static(), label_value_definitions); + defs.insert(did, label_value_definitions); } } @@ -114,10 +113,10 @@ pub async fn fetch_labeler_defs_direct( /// on labelers to tail their output, and index them alongside the data your app cares about. pub async fn fetch_labels( client: &impl AgentSessionExt, - uri_patterns: Vec>, - sources: Vec>, - cursor: Option>, -) -> Result<(Vec>, Option>), AgentError> { + uri_patterns: Vec, + sources: Vec, + cursor: Option, +) -> Result<(Vec