From 058fe3843501c41704823085b453214c0e3d3c68 Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 23 Mar 2026 10:43:17 -0400 Subject: [PATCH] repo and oauth migrated --- Cargo.lock | 2217 ++++++++++++++++- Cargo.toml | 2 +- crates/jacquard-common/src/types/did.rs | 18 +- crates/jacquard-common/src/types/nsid.rs | 18 +- crates/jacquard-common/src/xrpc.rs | 12 +- crates/jacquard-oauth/src/atproto.rs | 254 +- crates/jacquard-oauth/src/authstore.rs | 64 +- crates/jacquard-oauth/src/client.rs | 181 +- crates/jacquard-oauth/src/dpop.rs | 189 +- crates/jacquard-oauth/src/jose.rs | 7 +- crates/jacquard-oauth/src/jose/jws.rs | 60 +- crates/jacquard-oauth/src/jose/jwt.rs | 139 +- crates/jacquard-oauth/src/jose/signing.rs | 44 +- crates/jacquard-oauth/src/keyset.rs | 24 +- crates/jacquard-oauth/src/loopback.rs | 29 +- crates/jacquard-oauth/src/request.rs | 280 ++- crates/jacquard-oauth/src/resolver.rs | 195 +- crates/jacquard-oauth/src/scopes.rs | 745 ++++-- crates/jacquard-oauth/src/session.rs | 266 +- crates/jacquard-oauth/src/types.rs | 57 +- .../src/types/client_metadata.rs | 61 +- crates/jacquard-oauth/src/types/metadata.rs | 175 +- crates/jacquard-oauth/src/types/request.rs | 73 +- crates/jacquard-oauth/src/types/token.rs | 48 +- crates/jacquard-oauth/src/utils.rs | 32 +- crates/jacquard-repo/src/commit/firehose.rs | 95 +- crates/jacquard-repo/src/commit/mod.rs | 46 +- crates/jacquard-repo/src/commit/proof.rs | 15 +- crates/jacquard-repo/src/mst/diff.rs | 18 +- crates/jacquard-repo/src/mst/tree.rs | 21 +- crates/jacquard-repo/src/repo.rs | 208 +- .../jacquard-repo/tests/large_proof_tests.rs | 41 +- 32 files changed, 4114 insertions(+), 1520 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afb2077a5..ebe3b82f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,24 @@ dependencies = [ "gimli", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -32,6 +44,39 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -47,6 +92,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_colours" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14eec43e0298190790f41679fe69ef7a829d2a2ddd78c8c00339e84710e435fe" +dependencies = [ + "rgb", +] + [[package]] name = "anstream" version = "1.0.0" @@ -103,6 +157,50 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "async-compression" version = "0.4.41" @@ -147,6 +245,141 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec 0.7.6", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec 0.7.6", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec 0.7.6", +] + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "axum-test" +version = "18.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce2a8627e8d8851f894696b39f2b67807d6375c177361d376173ace306a21e2" +dependencies = [ + "anyhow", + "axum", + "bytes", + "bytesize", + "cookie", + "expect-json", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower", + "url", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -156,7 +389,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object", "rustc-demangle", "windows-link", @@ -193,6 +426,12 @@ dependencies = [ "match-lookup", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" @@ -205,12 +444,42 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -261,18 +530,77 @@ dependencies = [ "cfg_aliases", ] +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "buffer" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab7228d32b5d95be40adeba1d9461c8547b5dadf5f9cbfba09b6d578991df28" +dependencies = [ + "arrayvec 0.5.2", + "mac 0.0.2", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -282,6 +610,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + [[package]] name = "cast" version = "0.3.0" @@ -335,6 +669,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "ciborium" version = "0.2.2" @@ -373,7 +713,7 @@ dependencies = [ "multihash", "serde", "serde_bytes", - "unsigned-varint", + "unsigned-varint 0.8.0", ] [[package]] @@ -444,12 +784,28 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compression-codecs" version = "0.4.37" @@ -467,6 +823,18 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -479,6 +847,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "cordyceps" version = "0.3.4" @@ -486,7 +864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" dependencies = [ "loom", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -558,10 +936,51 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "crossbeam-deque" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "parking_lot", + "rustix 0.38.44", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] [[package]] name = "crunchy" @@ -604,6 +1023,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -691,6 +1111,16 @@ dependencies = [ "syn", ] +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", + "gzip-header", +] + [[package]] name = "der" version = "0.7.10" @@ -738,6 +1168,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -793,10 +1229,19 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", + "serde", "sha2", "subtle", + "zeroize", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -809,6 +1254,7 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", "pem-rfc7468", "pkcs8", "rand_core 0.6.4", @@ -817,6 +1263,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -829,6 +1284,32 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "embedded-io-adapters" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ccf22c3feffc79593914c0b4be9a2ed6b11e44cf1f84fd6b77d2ee92de0077" +dependencies = [ + "embedded-io 0.6.1", + "embedded-io-async", + "tokio", +] + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io 0.6.1", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -850,12 +1331,43 @@ dependencies = [ "syn", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -866,12 +1378,85 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "expect-json" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869f97f4abe8e78fc812a94ad6b721d72c4fb5532877c79610f2c238d7ccf6c4" +dependencies = [ + "chrono", + "email_address", + "expect-json-macros", + "num", + "regex", + "serde", + "serde_json", + "thiserror 2.0.18", + "typetag", + "uuid", +] + +[[package]] +name = "expect-json-macros" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6fdf550180a6c29a28cb9aac262dc0064c25735641d2317f670075e9a469d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -888,6 +1473,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -901,7 +1497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -936,6 +1532,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac 0.1.1", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.32" @@ -944,6 +1550,7 @@ checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -979,6 +1586,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1027,6 +1645,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1090,6 +1709,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.32.3" @@ -1102,6 +1731,34 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "group" version = "0.13.0" @@ -1113,6 +1770,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "gzip-header" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +dependencies = [ + "crc32fast", +] + [[package]] name = "h2" version = "0.4.13" @@ -1129,7 +1795,7 @@ dependencies = [ "slab", "tokio", "tokio-util", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1152,6 +1818,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1182,13 +1857,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", - "hash32", + "hash32 0.2.1", "rustc_version", "serde", "spin 0.9.8", "stable_deref_trait", ] +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -1201,6 +1886,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1227,7 +1918,7 @@ dependencies = [ "thiserror 1.0.69", "tinyvec", "tokio", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "url", ] @@ -1249,7 +1940,16 @@ dependencies = [ "smallvec", "thiserror 1.0.69", "tokio", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", ] [[package]] @@ -1261,6 +1961,20 @@ dependencies = [ "digest", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac 0.1.1", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "http" version = "1.4.0" @@ -1300,6 +2014,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -1314,6 +2034,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1345,7 +2066,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1360,7 +2081,7 @@ dependencies = [ "system-configuration", "tokio", "tower-service", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "windows-registry", ] @@ -1497,39 +2218,90 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "2.13.0" +name = "image" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ - "equivalent", - "hashbrown 0.16.1", + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff 0.11.3", + "zune-core", + "zune-jpeg", ] [[package]] -name = "inventory" -version = "0.3.22" +name = "image-webp" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ - "rustversion", + "byteorder-lite", + "quick-error 2.0.1", ] [[package]] -name = "ipconfig" -version = "0.3.2" +name = "imgref" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.10", - "widestring", - "windows-sys 0.48.0", - "winreg", -] +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] -name = "ipld-core" -version = "0.4.3" +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "inventory" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipld-core" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "090f624976d72f0b0bb71b86d58dc16c15e069193067cb3a3a09d655246cbbda" dependencies = [ @@ -1554,6 +2326,22 @@ dependencies = [ "serde", ] +[[package]] +name = "iroh-car" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f8cd4cb9aa083fba8b52e921764252d0b4dcb1cd6d120b809dbfe1106e81a" +dependencies = [ + "anyhow", + "cid", + "futures", + "serde", + "serde_ipld_dagcbor", + "thiserror 1.0.69", + "tokio", + "unsigned-varint 0.7.2", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -1566,12 +2354,55 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jacquard" +version = "0.11.0" +dependencies = [ + "bytes", + "clap", + "getrandom 0.2.17", + "gloo-storage", + "http", + "image", + "jacquard-api", + "jacquard-common", + "jacquard-derive", + "jacquard-identity", + "jacquard-oauth", + "jose-jwk", + "miette", + "n0-future", + "regex", + "regex-lite", + "reqwest", + "serde", + "serde_html_form", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "tiff 0.6.1", + "tokio", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "trait-variant", + "viuer", + "webpage", +] + [[package]] name = "jacquard-api" version = "0.11.1" @@ -1584,11 +2415,56 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "jacquard-axum" +version = "0.11.0" +dependencies = [ + "axum", + "axum-macros", + "axum-test", + "base64 0.22.1", + "bytes", + "chrono", + "jacquard", + "jacquard-common", + "jacquard-derive", + "jacquard-identity", + "k256", + "miette", + "multibase", + "rand 0.8.5", + "reqwest", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower", + "tower-http", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-subscriber 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "jacquard-codegen-tests" +version = "0.0.0" +dependencies = [ + "jacquard-common", + "jacquard-derive", + "jacquard-lexicon", + "miette", + "serde", + "serde_ipld_dagcbor", + "serde_json", + "smol_str", + "thiserror 2.0.18", +] + [[package]] name = "jacquard-common" version = "0.11.0" dependencies = [ - "base64", + "base64 0.22.1", "bon", "bytes", "chrono", @@ -1605,7 +2481,7 @@ dependencies = [ "http", "ipld-core", "k256", - "maitake-sync", + "maitake-sync 0.1.2", "miette", "multibase", "multihash", @@ -1633,7 +2509,7 @@ dependencies = [ "tokio", "tokio-tungstenite-wasm", "tokio-util", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "trait-variant", "unicode-segmentation", "zstd", @@ -1674,7 +2550,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "trait-variant", ] @@ -1733,6 +2609,121 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "jacquard-oauth" +version = "0.11.0" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "dashmap", + "ed25519-dalek", + "elliptic-curve", + "http", + "jacquard-common", + "jacquard-identity", + "jose-jwa", + "jose-jwk", + "k256", + "miette", + "n0-future", + "p256", + "p384", + "rand 0.8.5", + "rouille", + "serde", + "serde_html_form", + "serde_json", + "sha2", + "smol_str", + "thiserror 2.0.18", + "tokio", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "trait-variant", + "webbrowser", +] + +[[package]] +name = "jacquard-repo" +version = "0.11.0" +dependencies = [ + "anyhow", + "bytes", + "cid", + "ed25519-dalek", + "hex", + "iroh-car", + "jacquard-api", + "jacquard-common", + "jacquard-derive", + "k256", + "miette", + "multihash", + "n0-future", + "p256", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_ipld_dagcbor", + "serde_ipld_dagjson", + "serde_json", + "sha2", + "smol_str", + "tempfile", + "thiserror 2.0.18", + "tokio", + "trait-variant", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1743,6 +2734,48 @@ dependencies = [ "libc", ] +[[package]] +name = "jose-b64" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" +dependencies = [ + "base64ct", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "jose-jwa" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" +dependencies = [ + "serde", +] + +[[package]] +name = "jose-jwk" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" +dependencies = [ + "jose-b64", + "jose-jwa", + "p256", + "p384", + "rsa", + "serde", + "zeroize", +] + +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" + [[package]] name = "js-sys" version = "0.3.91" @@ -1762,7 +2795,9 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", + "once_cell", "sha2", + "signature", ] [[package]] @@ -1776,11 +2811,49 @@ dependencies = [ "winnow 0.6.24", ] +[[package]] +name = "lazy-collections" +version = "0.11.0" +dependencies = [ + "buffer", + "bytes", + "chrono", + "embedded-io 0.6.1", + "embedded-io-adapters", + "embedded-io-async", + "heapless 0.9.2", + "lock_api", + "loom", + "maitake-sync 0.2.2", + "managed", + "miette", + "n0-future", + "pin-project", + "postcard", + "proptest", + "serde", + "serde_bytes", + "tokio", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing 0.1.44 (git+https://github.com/tokio-rs/tracing)", + "tracing-subscriber 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-subscriber 0.3.23 (git+https://github.com/tokio-rs/tracing)", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" @@ -1788,18 +2861,46 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.3", +] + [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1836,8 +2937,17 @@ dependencies = [ "cfg-if", "generator", "scoped-tls", - "tracing", - "tracing-subscriber", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-subscriber 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", ] [[package]] @@ -1855,6 +2965,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1db08c0d0ddbb591e65f1da58d1cefccc94a2faa0c55bf979ce215a3e04d5e" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "maitake-sync" version = "0.1.2" @@ -1868,6 +2990,53 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "maitake-sync" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "748f86d9befd480b602c3bebc9ef30dbf2f3dfc8acc4a73d07b90f0117e6de3f" +dependencies = [ + "cordyceps", + "loom", + "mutex-traits", + "mycelium-bitfield", + "pin-project", + "portable-atomic", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "match-lookup" version = "0.1.2" @@ -1888,6 +3057,22 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1930,6 +3115,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mini-moka-wasm" version = "0.10.99" @@ -1958,6 +3153,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1979,6 +3184,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multibase" version = "0.9.2" @@ -1999,9 +3214,33 @@ checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" dependencies = [ "core2", "serde", - "unsigned-varint", + "unsigned-varint 0.8.0", ] +[[package]] +name = "multipart" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error 1.2.3", + "rand 0.8.5", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "mutex-traits" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3929f2b5633d29cf7b6624992e5f3c1e9334f1193423e12d17be4faf678cde3f" + [[package]] name = "mycelium-bitfield" version = "0.1.5" @@ -2029,6 +3268,33 @@ dependencies = [ "web-time", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2062,6 +3328,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -2077,6 +3359,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -2112,10 +3405,54 @@ dependencies = [ name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "autocfg", - "libm", + "bitflags", + "objc2", ] [[package]] @@ -2202,6 +3539,18 @@ dependencies = [ "sha2", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.1" @@ -2226,11 +3575,23 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2256,6 +3617,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -2320,6 +3691,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2336,6 +3718,25 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -2351,7 +3752,7 @@ dependencies = [ "cobs", "embedded-io 0.4.0", "embedded-io 0.6.1", - "heapless", + "heapless 0.7.17", "serde", ] @@ -2379,6 +3780,22 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2420,6 +3837,71 @@ dependencies = [ "yansi", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quinn" version = "0.11.9" @@ -2436,7 +3918,7 @@ dependencies = [ "socket2 0.6.3", "thiserror 2.0.18", "tokio", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "web-time", ] @@ -2457,7 +3939,7 @@ dependencies = [ "slab", "thiserror 2.0.18", "tinyvec", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "web-time", ] @@ -2471,7 +3953,7 @@ dependencies = [ "libc", "once_cell", "socket2 0.6.3", - "tracing", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "windows-sys 0.60.2", ] @@ -2549,6 +4031,85 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec 0.7.6", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2558,6 +4119,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -2619,7 +4189,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2657,6 +4227,15 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reserve-port" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "resolv-conf" version = "0.7.6" @@ -2673,6 +4252,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.14" @@ -2693,6 +4281,65 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" +[[package]] +name = "rouille" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921" +dependencies = [ + "base64 0.13.1", + "brotli", + "chrono", + "deflate", + "filetime", + "multipart", + "percent-encoding", + "rand 0.8.5", + "serde", + "serde_derive", + "serde_json", + "sha1_smol", + "threadpool", + "time", + "tiny_http", + "url", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "rand 0.9.2", + "thiserror 2.0.18", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -2714,6 +4361,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2723,7 +4383,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -2780,12 +4440,30 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -2929,6 +4607,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_ipld_dagjson" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d2d9d1f29999ee9a3d774fe2a5db4cc199da5178d0350f5e4482ea04252aee" +dependencies = [ + "ipld-core", + "serde", + "serde_json", +] + [[package]] name = "serde_json" version = "1.0.149" @@ -3001,7 +4690,7 @@ version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "serde_core", @@ -3033,6 +4722,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -3073,17 +4768,42 @@ dependencies = [ name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" dependencies = [ - "digest", - "rand_core 0.6.4", + "quote", ] [[package]] -name = "simd-adler32" -version = "0.3.8" +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" @@ -3170,6 +4890,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3276,10 +5021,21 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac 0.1.1", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3295,7 +5051,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix", + "rustix 1.1.4", "windows-sys 0.60.2", ] @@ -3358,6 +5114,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -3365,10 +5155,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", + "time-macros", ] [[package]] @@ -3377,6 +5171,28 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -3582,6 +5398,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3605,6 +5422,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3624,10 +5442,21 @@ name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" dependencies = [ "pin-project-lite", - "tracing-attributes", - "tracing-core", + "tracing-attributes 0.1.31 (git+https://github.com/tokio-rs/tracing)", + "tracing-core 0.1.36 (git+https://github.com/tokio-rs/tracing)", ] [[package]] @@ -3641,6 +5470,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -3651,6 +5490,14 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" +dependencies = [ + "valuable", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -3659,7 +5506,17 @@ checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", - "tracing-core", + "tracing-core 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" +dependencies = [ + "log", + "once_cell", + "tracing-core 0.1.36 (git+https://github.com/tokio-rs/tracing)", ] [[package]] @@ -3675,9 +5532,27 @@ dependencies = [ "sharded-slab", "smallvec", "thread_local", - "tracing", - "tracing-core", - "tracing-log", + "time", + "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing 0.1.44 (git+https://github.com/tokio-rs/tracing)", + "tracing-core 0.1.36 (git+https://github.com/tokio-rs/tracing)", + "tracing-log 0.2.0 (git+https://github.com/tokio-rs/tracing)", ] [[package]] @@ -3738,12 +5613,63 @@ dependencies = [ "utf-8", ] +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "typetag" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -3780,6 +5706,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + [[package]] name = "unsigned-varint" version = "0.8.0" @@ -3822,6 +5754,27 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3834,6 +5787,31 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "viuer" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae7c6870b98c838123f22cac9a594cbe2d74ea48d79271c08f8c9e680b40fac" +dependencies = [ + "ansi_colours", + "base64 0.22.1", + "console", + "crossterm", + "image", + "lazy_static", + "tempfile", + "termcolor", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -3999,6 +5977,34 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", +] + +[[package]] +name = "webpage" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" +dependencies = [ + "html5ever", + "markup5ever_rcdom", + "serde_json", + "url", +] + [[package]] name = "webpki-roots" version = "1.0.6" @@ -4008,12 +6014,34 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "widestring" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -4023,6 +6051,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -4111,6 +6145,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -4361,6 +6404,23 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac 0.1.1", + "markup5ever", +] + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yansi" version = "1.0.1" @@ -4436,6 +6496,9 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "serde", +] [[package]] name = "zerotrie" @@ -4503,3 +6566,27 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 3bcb7e699..c2ab19e49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/jacquard-common", "crates/jacquard-lexicon", "crates/jacquard-derive", "crates/jacquard-lexgen", "crates/jacquard-identity", "crates/jacquard-api"] +members = ["crates/*"] [workspace.package] diff --git a/crates/jacquard-common/src/types/did.rs b/crates/jacquard-common/src/types/did.rs index 91ca05abe..cb764dba6 100644 --- a/crates/jacquard-common/src/types/did.rs +++ b/crates/jacquard-common/src/types/did.rs @@ -166,23 +166,7 @@ impl> Did { } } -impl FromStr for Did { - type Err = AtStrError; - - fn from_str(s: &str) -> Result { - Self::new_owned(s) - } -} - -impl FromStr for Did> { - type Err = AtStrError; - - fn from_str(s: &str) -> Result { - Self::new_owned(s) - } -} - -impl FromStr for Did { +impl + FromStr> FromStr for Did { type Err = AtStrError; fn from_str(s: &str) -> Result { diff --git a/crates/jacquard-common/src/types/nsid.rs b/crates/jacquard-common/src/types/nsid.rs index 6daf51637..fd454e0df 100644 --- a/crates/jacquard-common/src/types/nsid.rs +++ b/crates/jacquard-common/src/types/nsid.rs @@ -138,23 +138,7 @@ impl> Nsid { } } -impl FromStr for Nsid { - type Err = AtStrError; - - fn from_str(s: &str) -> Result { - Self::new_owned(s) - } -} - -impl FromStr for Nsid> { - type Err = AtStrError; - - fn from_str(s: &str) -> Result { - Self::new_owned(s) - } -} - -impl FromStr for Nsid { +impl + FromStr> FromStr for Nsid { type Err = AtStrError; fn from_str(s: &str) -> Result { diff --git a/crates/jacquard-common/src/xrpc.rs b/crates/jacquard-common/src/xrpc.rs index 2b75c9e12..884762345 100644 --- a/crates/jacquard-common/src/xrpc.rs +++ b/crates/jacquard-common/src/xrpc.rs @@ -336,7 +336,7 @@ pub trait XrpcClient: HttpClient { #[cfg(not(target_arch = "wasm32"))] fn send(&self, request: R) -> impl Future>> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync, Self: Sync; @@ -344,7 +344,7 @@ pub trait XrpcClient: HttpClient { #[cfg(target_arch = "wasm32")] fn send(&self, request: R) -> impl Future>> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync; /// Send an XRPC request and parse the response @@ -355,7 +355,7 @@ pub trait XrpcClient: HttpClient { opts: CallOptions<'_>, ) -> impl Future>> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync, Self: Sync; @@ -367,7 +367,7 @@ pub trait XrpcClient: HttpClient { opts: CallOptions<'_>, ) -> impl Future>> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync; } @@ -381,7 +381,7 @@ pub trait XrpcStreamingClient: XrpcClient + HttpClientExt { request: R, ) -> impl Future> + Send where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync, Self: Sync; @@ -392,7 +392,7 @@ pub trait XrpcStreamingClient: XrpcClient + HttpClientExt { request: R, ) -> impl Future> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync; /// Stream an XRPC procedure call and its response diff --git a/crates/jacquard-oauth/src/atproto.rs b/crates/jacquard-oauth/src/atproto.rs index 948bc3f06..d1ec67ce5 100644 --- a/crates/jacquard-oauth/src/atproto.rs +++ b/crates/jacquard-oauth/src/atproto.rs @@ -1,10 +1,11 @@ +use std::str::FromStr; + use crate::types::OAuthClientMetadata; use crate::{keyset::Keyset, scopes::Scope}; -use jacquard_common::cowstr::ToCowStr; use jacquard_common::deps::fluent_uri::Uri; -use jacquard_common::{CowStr, IntoStatic}; +use jacquard_common::{BosStr, IntoStatic}; use serde::{Deserialize, Serialize}; -use smol_str::{SmolStr, ToSmolStr}; +use smol_str::SmolStr; use thiserror::Error; /// Errors that can occur when building AT Protocol OAuth client metadata. @@ -78,11 +79,20 @@ pub enum AuthMethod { PrivateKeyJwt, } -impl From for CowStr<'static> { +impl From for SmolStr { + fn from(value: AuthMethod) -> Self { + match value { + AuthMethod::None => SmolStr::new_static("none"), + AuthMethod::PrivateKeyJwt => SmolStr::new_static("private_key_jwt"), + } + } +} + +impl From for &'static str { fn from(value: AuthMethod) -> Self { match value { - AuthMethod::None => CowStr::new_static("none"), - AuthMethod::PrivateKeyJwt => CowStr::new_static("private_key_jwt"), + AuthMethod::None => "none", + AuthMethod::PrivateKeyJwt => "private_key_jwt", } } } @@ -97,11 +107,20 @@ pub enum GrantType { RefreshToken, } -impl From for CowStr<'static> { +impl From for SmolStr { fn from(value: GrantType) -> Self { match value { - GrantType::AuthorizationCode => CowStr::new_static("authorization_code"), - GrantType::RefreshToken => CowStr::new_static("refresh_token"), + GrantType::AuthorizationCode => SmolStr::new_static("authorization_code"), + GrantType::RefreshToken => SmolStr::new_static("refresh_token"), + } + } +} + +impl From for &'static str { + fn from(value: GrantType) -> Self { + match value { + GrantType::AuthorizationCode => "authorization_code", + GrantType::RefreshToken => "refresh_token", } } } @@ -113,7 +132,10 @@ impl From for CowStr<'static> { /// typed fields for URIs and scopes rather than raw strings. Use [`atproto_client_metadata`] /// to convert this into the wire format expected by OAuth servers. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct AtprotoClientMetadata<'m> { +pub struct AtprotoClientMetadata +where + ::Err: core::fmt::Debug, +{ /// The unique identifier for this client, typically the URL of its metadata document. pub client_id: Uri, /// The URI of the client's homepage or information page. @@ -123,12 +145,11 @@ pub struct AtprotoClientMetadata<'m> { /// The grant types this client will use. pub grant_types: Vec, /// The OAuth scopes this client requests; must include `atproto`. - #[serde(borrow)] - pub scopes: Vec>, + pub scopes: Vec>, /// URI pointing to the client's JWK Set; mutually exclusive with inline `jwks`. pub jwks_uri: Option>, /// Human-readable display name for the client. - pub client_name: Option, + pub client_name: Option, /// URI of the client's logo image. pub logo_uri: Option>, /// URI of the client's terms of service document. @@ -137,9 +158,15 @@ pub struct AtprotoClientMetadata<'m> { pub privacy_policy_uri: Option>, } -impl<'m> IntoStatic for AtprotoClientMetadata<'m> { - type Output = AtprotoClientMetadata<'static>; - fn into_static(self) -> AtprotoClientMetadata<'static> { +impl IntoStatic for AtprotoClientMetadata +where + S: BosStr + IntoStatic + Ord + FromStr, + ::Err: core::fmt::Debug, + S::Output: BosStr + FromStr + Ord, + ::Err: core::fmt::Debug, +{ + type Output = AtprotoClientMetadata; + fn into_static(self) -> AtprotoClientMetadata { AtprotoClientMetadata { client_id: self.client_id, client_uri: self.client_uri, @@ -147,7 +174,7 @@ impl<'m> IntoStatic for AtprotoClientMetadata<'m> { grant_types: self.grant_types, scopes: self.scopes.into_static(), jwks_uri: self.jwks_uri, - client_name: self.client_name, + client_name: self.client_name.into_static(), logo_uri: self.logo_uri, tos_uri: self.tos_uri, privacy_policy_uri: None, @@ -155,19 +182,25 @@ impl<'m> IntoStatic for AtprotoClientMetadata<'m> { } } -impl<'m> AtprotoClientMetadata<'m> { +impl AtprotoClientMetadata +where + S: BosStr + IntoStatic + Ord + FromStr, + ::Err: core::fmt::Debug, + S::Output: BosStr + FromStr + Ord, + ::Err: core::fmt::Debug, +{ /// Attach optional production branding fields to the metadata. /// /// Chainable builder method for setting display name, logo, and policy URLs after /// constructing the base metadata. pub fn with_prod_info( mut self, - client_name: &str, + client_name: S, logo_uri: Option>, tos_uri: Option>, privacy_policy_uri: Option>, ) -> Self { - self.client_name = Some(client_name.to_smolstr()); + self.client_name = Some(client_name); self.logo_uri = logo_uri; self.tos_uri = tos_uri; self.privacy_policy_uri = privacy_policy_uri; @@ -182,7 +215,10 @@ impl<'m> AtprotoClientMetadata<'m> { pub fn default_localhost() -> Self { Self::new_localhost( None, - Some(Scope::parse_multiple("atproto transition:generic").unwrap()), + Some(vec![ + Scope::Atproto, + Scope::Transition(crate::scopes::TransitionScope::Generic), + ]), ) } @@ -194,26 +230,26 @@ impl<'m> AtprotoClientMetadata<'m> { /// are used. pub fn new_localhost( redirect_uris: Option>>, - scopes: Option>>, - ) -> AtprotoClientMetadata<'static> { + scopes: Option>>, + ) -> AtprotoClientMetadata { // determine client_id #[derive(serde::Serialize)] - struct Parameters<'a> { + struct Parameters { #[serde(skip_serializing_if = "Option::is_none")] - redirect_uri: Option>>, + redirect_uri: Option>, #[serde(skip_serializing_if = "Option::is_none")] - scope: Option>, + scope: Option, } let redir_str = redirect_uris.as_ref().map(|uris| { uris.iter() - .map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()) + .map(|u| SmolStr::from(u.as_str().trim_end_matches("/"))) .collect() }); let query = serde_html_form::to_string(Parameters { redirect_uri: redir_str, scope: scopes .as_ref() - .map(|s| Scope::serialize_multiple(s.as_slice())), + .map(|s| SmolStr::from(Scope::serialize_multiple(s.as_slice()).as_str())), }) .ok(); let mut client_id = String::from("http://localhost/"); @@ -246,16 +282,20 @@ impl<'m> AtprotoClientMetadata<'m> { /// selects the appropriate `token_endpoint_auth_method` based on whether a keyset is provided, /// and serializes scopes and grant types into their string representations. Returns an error /// if any required field is missing or invalid. -pub fn atproto_client_metadata<'m>( - metadata: AtprotoClientMetadata<'m>, +pub fn atproto_client_metadata( + metadata: &AtprotoClientMetadata, keyset: &Option, -) -> Result> { +) -> Result> +where + S: BosStr + Ord + FromStr + Clone, + ::Err: core::fmt::Debug, +{ let is_loopback = metadata.client_id.scheme().as_str() == "http" && metadata.client_id.authority().map(|a| a.host()) == Some("localhost"); let application_type = if is_loopback { - Some(CowStr::new_static("native")) + Some(S::from_static("native")) } else { - Some(CowStr::new_static("web")) + Some(S::from_static("web")) }; if metadata.redirect_uris.is_empty() { return Err(Error::EmptyRedirectUris); @@ -272,55 +312,59 @@ pub fn atproto_client_metadata<'m>( } else { None }; - (AuthMethod::PrivateKeyJwt, metadata.jwks_uri, jwks) + (AuthMethod::PrivateKeyJwt, metadata.jwks_uri.as_ref(), jwks) } else { (AuthMethod::None, None, None) }; - let client_id = metadata - .client_id - .as_str() - .trim_end_matches("/") - .to_string(); + let client_id = metadata.client_id.as_str(); let client_uri = metadata .client_uri .as_ref() - .map(|u| u.as_str().trim_end_matches("/").to_string().into()); + .and_then(|u| S::from_str(u.as_str()).ok()); let redirect_uris = metadata .redirect_uris .iter() - .map(|u| u.as_str().trim_end_matches("/").to_string().into()) + .filter_map(|u| S::from_str(u.as_str()).ok()) .collect(); - let jwks_uri = jwks_uri.map(|u| u.as_str().trim_end_matches("/").to_string().into()); + let jwks_uri = jwks_uri.as_ref().and_then(|u| S::from_str(u.as_str()).ok()); Ok(OAuthClientMetadata { - client_id: client_id.into(), + client_id: S::from_str(client_id).unwrap(), client_uri, redirect_uris, - application_type, - token_endpoint_auth_method: Some(auth_method.into()), - grant_types: Some(metadata.grant_types.into_iter().map(|v| v.into()).collect()), - response_types: vec!["code".to_cowstr()], - scope: Some(Scope::serialize_multiple(metadata.scopes.as_slice())), + application_type: application_type, + token_endpoint_auth_method: Some(S::from_static(auth_method.into())), + grant_types: Some( + metadata + .grant_types + .iter() + .map(|v| S::from_static(v.clone().into())) + .collect(), + ), + response_types: vec![S::from_static("code")], + scope: Some( + S::from_str(Scope::serialize_multiple(metadata.scopes.as_slice()).as_str()).unwrap(), + ), dpop_bound_access_tokens: Some(true), jwks_uri, jwks, token_endpoint_auth_signing_alg: if keyset.is_some() { - Some(CowStr::new_static("ES256")) + Some(S::from_static("ES256")) } else { None }, - client_name: metadata.client_name, + client_name: metadata.client_name.as_ref().map(|c| c.clone()), logo_uri: metadata .logo_uri .as_ref() - .map(|u| u.as_str().to_string().into()), + .and_then(|u| S::from_str(u.as_str()).ok()), tos_uri: metadata .tos_uri .as_ref() - .map(|u| u.as_str().to_string().into()), + .and_then(|u| S::from_str(u.as_str()).ok()), privacy_policy_uri: metadata .privacy_policy_uri .as_ref() - .map(|u| u.as_str().to_string().into()), + .and_then(|u| S::from_str(u.as_str()).ok()), }) } @@ -342,22 +386,22 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 #[test] fn test_localhost_client_metadata_default() { assert_eq!( - atproto_client_metadata(AtprotoClientMetadata::new_localhost(None, None), &None) + atproto_client_metadata(&AtprotoClientMetadata::new_localhost(None, None), &None) .unwrap(), OAuthClientMetadata { - client_id: CowStr::new_static("http://localhost"), + client_id: SmolStr::new_static("http://localhost/"), client_uri: None, redirect_uris: vec![ - CowStr::new_static("http://127.0.0.1"), - CowStr::new_static("http://[::1]"), + SmolStr::new_static("http://127.0.0.1"), + SmolStr::new_static("http://[::1]"), ], - application_type: Some(CowStr::new_static("native")), - scope: Some(CowStr::new_static("atproto")), + application_type: Some(SmolStr::new_static("native")), + scope: Some(SmolStr::new_static("atproto")), grant_types: Some(vec![ - "authorization_code".to_cowstr(), - "refresh_token".to_cowstr() + SmolStr::new_static("authorization_code"), + SmolStr::new_static("refresh_token") ]), - response_types: vec!["code".to_cowstr()], + response_types: vec![SmolStr::new_static("code")], token_endpoint_auth_method: Some(AuthMethod::None.into()), dpop_bound_access_tokens: Some(true), jwks_uri: None, @@ -375,7 +419,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 fn test_localhost_client_metadata_custom() { assert_eq!( atproto_client_metadata( - AtprotoClientMetadata::new_localhost( + &AtprotoClientMetadata::new_localhost( Some(vec![ Uri::parse("http://127.0.0.1/callback".to_string()).unwrap(), Uri::parse("http://[::1]/callback".to_string()).unwrap(), @@ -390,23 +434,23 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 ) .expect("failed to convert metadata"), OAuthClientMetadata { - client_id: CowStr::new_static( + client_id: SmolStr::new_static( "http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&redirect_uri=http%3A%2F%2F%5B%3A%3A1%5D%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric" ), client_uri: None, redirect_uris: vec![ - CowStr::new_static("http://127.0.0.1/callback"), - CowStr::new_static("http://[::1]/callback"), + SmolStr::new_static("http://127.0.0.1/callback"), + SmolStr::new_static("http://[::1]/callback"), ], - scope: Some(CowStr::new_static( + scope: Some(SmolStr::new_static( "account:email atproto transition:generic" )), - application_type: Some(CowStr::new_static("native")), + application_type: Some(SmolStr::new_static("native")), grant_types: Some(vec![ - "authorization_code".to_cowstr(), - "refresh_token".to_cowstr() + SmolStr::new_static("authorization_code"), + SmolStr::new_static("refresh_token") ]), - response_types: vec!["code".to_cowstr()], + response_types: vec![SmolStr::new_static("code")], token_endpoint_auth_method: Some(AuthMethod::None.into()), dpop_bound_access_tokens: Some(true), jwks_uri: None, @@ -425,7 +469,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 // Invalid inputs are coerced to http://localhost rather than failing { let out = atproto_client_metadata( - AtprotoClientMetadata::new_localhost( + &AtprotoClientMetadata::new_localhost( Some(vec![Uri::parse("https://127.0.0.1".to_string()).unwrap()]), None, ), @@ -435,18 +479,18 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 assert_eq!( out, OAuthClientMetadata { - client_id: CowStr::new_static( + client_id: SmolStr::new_static( "http://localhost/?redirect_uri=https%3A%2F%2F127.0.0.1" ), - application_type: Some(CowStr::new_static("native")), + application_type: Some(SmolStr::new_static("native")), client_uri: None, - redirect_uris: vec![CowStr::new_static("https://127.0.0.1")], - scope: Some(CowStr::new_static("atproto")), + redirect_uris: vec![SmolStr::new_static("https://127.0.0.1")], + scope: Some(SmolStr::new_static("atproto")), grant_types: Some(vec![ - "authorization_code".to_cowstr(), - "refresh_token".to_cowstr() + SmolStr::new_static("authorization_code"), + SmolStr::new_static("refresh_token") ]), - response_types: vec!["code".to_cowstr()], + response_types: vec![SmolStr::new_static("code")], token_endpoint_auth_method: Some(AuthMethod::None.into()), dpop_bound_access_tokens: Some(true), jwks_uri: None, @@ -461,7 +505,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 } { let out = atproto_client_metadata( - AtprotoClientMetadata::new_localhost( + &AtprotoClientMetadata::new_localhost( Some(vec![ Uri::parse("http://localhost:8000".to_string()).unwrap(), ]), @@ -473,18 +517,18 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 assert_eq!( out, OAuthClientMetadata { - client_id: CowStr::new_static( + client_id: SmolStr::new_static( "http://localhost/?redirect_uri=http%3A%2F%2Flocalhost%3A8000" ), client_uri: None, - redirect_uris: vec![CowStr::new_static("http://localhost:8000")], - scope: Some(CowStr::new_static("atproto")), + redirect_uris: vec![SmolStr::new_static("http://localhost:8000")], + scope: Some(SmolStr::new_static("atproto")), grant_types: Some(vec![ - "authorization_code".to_cowstr(), - "refresh_token".to_cowstr() + SmolStr::new_static("authorization_code"), + SmolStr::new_static("refresh_token") ]), - application_type: Some(CowStr::new_static("native")), - response_types: vec!["code".to_cowstr()], + application_type: Some(SmolStr::new_static("native")), + response_types: vec![SmolStr::new_static("code")], token_endpoint_auth_method: Some(AuthMethod::None.into()), dpop_bound_access_tokens: Some(true), jwks_uri: None, @@ -499,7 +543,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 } { let out = atproto_client_metadata( - AtprotoClientMetadata::new_localhost( + &AtprotoClientMetadata::new_localhost( Some(vec![Uri::parse("http://192.168.0.0/".to_string()).unwrap()]), None, ), @@ -509,18 +553,18 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 assert_eq!( out, OAuthClientMetadata { - client_id: CowStr::new_static( + client_id: SmolStr::new_static( "http://localhost/?redirect_uri=http%3A%2F%2F192.168.0.0" ), client_uri: None, - redirect_uris: vec![CowStr::new_static("http://192.168.0.0")], - scope: Some(CowStr::new_static("atproto")), + redirect_uris: vec![SmolStr::new_static("http://192.168.0.0/")], + scope: Some(SmolStr::new_static("atproto")), grant_types: Some(vec![ - "authorization_code".to_cowstr(), - "refresh_token".to_cowstr() + SmolStr::new_static("authorization_code"), + SmolStr::new_static("refresh_token") ]), - application_type: Some(CowStr::new_static("native")), - response_types: vec!["code".to_cowstr()], + application_type: Some(SmolStr::new_static("native")), + response_types: vec![SmolStr::new_static("code")], token_endpoint_auth_method: Some(AuthMethod::None.into()), dpop_bound_access_tokens: Some(true), jwks_uri: None, @@ -552,7 +596,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 { // Non-loopback clients without a keyset should fail (must provide JWKS) let metadata = metadata.clone(); - let err = atproto_client_metadata(metadata, &None); + let err = atproto_client_metadata(&metadata, &None); assert!(err.is_ok()); } { @@ -568,21 +612,21 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 }]; let keyset = Keyset::try_from(keys.clone()).expect("failed to create keyset"); assert_eq!( - atproto_client_metadata(metadata, &Some(keyset.clone())) + atproto_client_metadata(&metadata, &Some(keyset.clone())) .expect("failed to convert metadata"), OAuthClientMetadata { - client_id: CowStr::new_static("https://example.com/client_metadata.json"), - client_uri: Some(CowStr::new_static("https://example.com")), - redirect_uris: vec![CowStr::new_static("https://example.com/callback")], - application_type: Some(CowStr::new_static("web")), - scope: Some(CowStr::new_static("atproto")), - grant_types: Some(vec![CowStr::new_static("authorization_code")]), + client_id: SmolStr::new_static("https://example.com/client_metadata.json"), + client_uri: Some(SmolStr::new_static("https://example.com")), + redirect_uris: vec![SmolStr::new_static("https://example.com/callback")], + application_type: Some(SmolStr::new_static("web")), + scope: Some(SmolStr::new_static("atproto")), + grant_types: Some(vec![SmolStr::new_static("authorization_code")]), token_endpoint_auth_method: Some(AuthMethod::PrivateKeyJwt.into()), dpop_bound_access_tokens: Some(true), - response_types: vec!["code".to_cowstr()], + response_types: vec![SmolStr::new_static("code")], jwks_uri: None, jwks: Some(keyset.public_jwks()), - token_endpoint_auth_signing_alg: Some(CowStr::new_static("ES256")), + token_endpoint_auth_signing_alg: Some(SmolStr::new_static("ES256")), client_name: None, logo_uri: None, tos_uri: None, diff --git a/crates/jacquard-oauth/src/authstore.rs b/crates/jacquard-oauth/src/authstore.rs index fe7125409..5f188b8a3 100644 --- a/crates/jacquard-oauth/src/authstore.rs +++ b/crates/jacquard-oauth/src/authstore.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use dashmap::DashMap; use jacquard_common::{ - IntoStatic, + bos::BosStr, session::{SessionStore, SessionStoreError}, types::did::Did, }; -use smol_str::{SmolStr, ToSmolStr, format_smolstr}; +use smol_str::{SmolStr, format_smolstr}; use crate::session::{AuthRequestData, ClientSessionData}; @@ -20,22 +20,22 @@ use crate::session::{AuthRequestData, ClientSessionData}; #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] pub trait ClientAuthStore { /// Retrieve an active session for the given DID and session identifier, if one exists. - fn get_session( + fn get_session( &self, - did: &Did<'_>, + did: &Did, session_id: &str, - ) -> impl Future>, SessionStoreError>>; + ) -> impl Future, SessionStoreError>>; /// Insert or update a session, replacing any existing entry for the same DID and session ID. fn upsert_session( &self, - session: ClientSessionData<'_>, + session: ClientSessionData, ) -> impl Future>; /// Delete the session for the given DID and session identifier. - fn delete_session( + fn delete_session( &self, - did: &Did<'_>, + did: &Did, session_id: &str, ) -> impl Future>; @@ -43,12 +43,12 @@ pub trait ClientAuthStore { fn get_auth_req_info( &self, state: &str, - ) -> impl Future>, SessionStoreError>>; + ) -> impl Future, SessionStoreError>>; /// Persist authorization request data so it can be retrieved after the OAuth redirect. fn save_auth_req_info( &self, - auth_req_info: &AuthRequestData<'_>, + auth_req_info: &AuthRequestData, ) -> impl Future>; /// Remove authorization request data after the callback has been handled. @@ -61,8 +61,8 @@ pub trait ClientAuthStore { /// An in-memory implementation of [`ClientAuthStore`], suitable for testing and single-process /// deployments where session persistence across restarts is not required. pub struct MemoryAuthStore { - sessions: DashMap>, - auth_reqs: DashMap>, + sessions: DashMap, + auth_reqs: DashMap, } impl MemoryAuthStore { @@ -76,27 +76,24 @@ impl MemoryAuthStore { } impl ClientAuthStore for MemoryAuthStore { - async fn get_session( + async fn get_session( &self, - did: &Did<'_>, + did: &Did, session_id: &str, - ) -> Result>, SessionStoreError> { + ) -> Result, SessionStoreError> { let key = format_smolstr!("{}_{}", did, session_id); Ok(self.sessions.get(&key).map(|v| v.clone())) } - async fn upsert_session( - &self, - session: ClientSessionData<'_>, - ) -> Result<(), SessionStoreError> { + async fn upsert_session(&self, session: ClientSessionData) -> Result<(), SessionStoreError> { let key = format_smolstr!("{}_{}", session.account_did, session.session_id); - self.sessions.insert(key, session.into_static()); + self.sessions.insert(key, session); Ok(()) } - async fn delete_session( + async fn delete_session( &self, - did: &Did<'_>, + did: &Did, session_id: &str, ) -> Result<(), SessionStoreError> { let key = format_smolstr!("{}_{}", did, session_id); @@ -107,18 +104,16 @@ impl ClientAuthStore for MemoryAuthStore { async fn get_auth_req_info( &self, state: &str, - ) -> Result>, SessionStoreError> { + ) -> Result, SessionStoreError> { Ok(self.auth_reqs.get(state).map(|v| v.clone())) } async fn save_auth_req_info( &self, - auth_req_info: &AuthRequestData<'_>, + auth_req_info: &AuthRequestData, ) -> Result<(), SessionStoreError> { - self.auth_reqs.insert( - auth_req_info.state.clone().to_smolstr(), - auth_req_info.clone().into_static(), - ); + self.auth_reqs + .insert(auth_req_info.state.clone(), auth_req_info.clone()); Ok(()) } @@ -128,29 +123,26 @@ impl ClientAuthStore for MemoryAuthStore { } } -impl - SessionStore<(Did<'static>, SmolStr), ClientSessionData<'static>> for Arc -{ +impl SessionStore<(Did, SmolStr), ClientSessionData> for Arc { /// Get the current session if present. - async fn get(&self, key: &(Did<'static>, SmolStr)) -> Option> { + async fn get(&self, key: &(Did, SmolStr)) -> Option { let (did, session_id) = key; self.as_ref() .get_session(did, session_id) .await .ok() .flatten() - .into_static() } /// Persist the given session. async fn set( &self, - _key: (Did<'static>, SmolStr), - session: ClientSessionData<'static>, + _key: (Did, SmolStr), + session: ClientSessionData, ) -> Result<(), SessionStoreError> { self.as_ref().upsert_session(session).await } /// Delete the given session. - async fn del(&self, key: &(Did<'static>, SmolStr)) -> Result<(), SessionStoreError> { + async fn del(&self, key: &(Did, SmolStr)) -> Result<(), SessionStoreError> { let (did, session_id) = key; self.as_ref().delete_session(did, session_id).await } diff --git a/crates/jacquard-oauth/src/client.rs b/crates/jacquard-oauth/src/client.rs index 580509acc..05f76ef6e 100644 --- a/crates/jacquard-oauth/src/client.rs +++ b/crates/jacquard-oauth/src/client.rs @@ -11,13 +11,13 @@ use crate::{ }; use jacquard_common::{ AuthorizationToken, CowStr, IntoStatic, - cowstr::ToCowStr, + bos::BosStr, deps::fluent_uri::Uri, error::{AuthError, ClientError, XrpcResult}, http_client::HttpClient, types::{did::Did, string::Handle}, xrpc::{ - CallOptions, Response, XrpcClient, XrpcError, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse, + CallOptions, Response, XrpcClient, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse, build_http_request, process_response, }, }; @@ -31,7 +31,8 @@ use jacquard_identity::{ resolver::{DidDocResponse, IdentityError, IdentityResolver, ResolverOptions}, }; use jose_jwk::JwkSet; -use std::{future::Future, sync::Arc}; +use smol_str::{SmolStr, ToSmolStr}; +use std::{str::FromStr, sync::Arc}; use tokio::sync::RwLock; /// The top-level OAuth client responsible for driving the authorization flow. @@ -41,7 +42,7 @@ where S: ClientAuthStore, { /// Shared session registry that mediates access to the backing auth store. - pub registry: Arc>, + pub registry: Arc>, /// Default call options applied to every outgoing XRPC request. pub options: RwLock>, /// Override for the XRPC base URI; falls back to the public Bluesky AppView when `None`. @@ -52,7 +53,7 @@ where impl OAuthClient { /// Create an `OAuthClient` using the default [`JacquardResolver`] for identity and metadata resolution. - pub fn new(store: S, client_data: ClientData<'static>) -> Self { + pub fn new(store: S, client_data: ClientData) -> Self { let client = JacquardResolver::default(); Self::new_from_resolver(store, client, client_data) } @@ -110,7 +111,7 @@ where S: ClientAuthStore, { /// Create an OAuth client from an explicit resolver instance, taking ownership of both. - pub fn new_from_resolver(store: S, client: T, client_data: ClientData<'static>) -> Self { + pub fn new_from_resolver(store: S, client: T, client_data: ClientData) -> Self { // #[cfg(feature = "tracing")] // tracing::info!( // redirect_uris = ?client_data.config.redirect_uris, @@ -133,7 +134,7 @@ where pub fn new_with_shared( store: Arc, client: Arc, - client_data: ClientData<'static>, + client_data: ClientData, ) -> Self { let registry = Arc::new(SessionRegistry::new_shared( store, @@ -172,13 +173,17 @@ where /// /// The caller is responsible for redirecting the user's browser to the returned URL. #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self, input), fields(input = input.as_ref())))] - pub async fn start_auth( + pub async fn start_auth( &self, input: impl AsRef, - options: AuthorizeOptions<'_>, - ) -> Result { + options: AuthorizeOptions, + ) -> Result + where + Str: FromStr + Ord + Clone + core::fmt::Debug, + ::Err: core::fmt::Debug, + { let client_metadata = atproto_client_metadata( - self.registry.client_data.config.clone(), + &self.registry.client_data.config, &self.registry.client_data.keyset, )?; let (server_metadata, identity) = self.client.resolve_oauth(input.as_ref()).await?; @@ -187,7 +192,7 @@ where } else { None }; - let metadata = OAuthMetadata { + let mut metadata = OAuthMetadata { server_metadata, client_metadata, keyset: self.registry.client_data.keyset.clone(), @@ -197,8 +202,8 @@ where self.client.as_ref(), login_hint, options.prompt, - &metadata, - options.state, + &mut metadata, + options.state.map(|s| s.as_ref().to_smolstr()), ) .await?; @@ -209,9 +214,9 @@ where .await?; #[derive(serde::Serialize)] - struct Parameters<'s> { - client_id: CowStr<'s>, - request_uri: CowStr<'s>, + struct Parameters { + client_id: smol_str::SmolStr, + request_uri: smol_str::SmolStr, } Ok(metadata.server_metadata.authorization_endpoint.to_string() + "?" @@ -227,21 +232,29 @@ where /// Validates the `state` and optional `iss` parameters, exchanges the authorization code for /// tokens via the token endpoint, verifies the `sub` claim against the expected issuer, and /// persists the resulting session. On success returns an [`OAuthSession`] ready for API calls. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "info", skip_all, fields(state = params.state.as_ref().map(|s| s.as_ref()))))] - pub async fn callback(&self, params: CallbackParams<'_>) -> Result> { + #[cfg_attr(feature = "tracing", tracing::instrument(level = "info", skip_all, fields(state = params.state.as_ref().map(|s| s.as_str()))))] + pub async fn callback(&self, params: CallbackParams) -> Result> { let Some(state_key) = params.state else { return Err(CallbackError::MissingState.into()); }; - let Some(auth_req_info) = self.registry.store.get_auth_req_info(&state_key).await? else { + let Some(auth_req_info) = self + .registry + .store + .get_auth_req_info(state_key.as_str()) + .await? + else { return Err(CallbackError::MissingState.into()); }; - self.registry.store.delete_auth_req_info(&state_key).await?; + self.registry + .store + .delete_auth_req_info(state_key.as_str()) + .await?; let metadata = self .client - .get_authorization_server_metadata(&auth_req_info.authserver_url.to_cowstr()) + .get_authorization_server_metadata(auth_req_info.authserver_url.as_str()) .await?; if let Some(iss) = params.iss { @@ -258,7 +271,7 @@ where let metadata = OAuthMetadata { server_metadata: metadata, client_metadata: atproto_client_metadata( - self.registry.client_data.config.clone(), + &self.registry.client_data.config, &self.registry.client_data.keyset, )?, keyset: self.registry.client_data.keyset.clone(), @@ -268,15 +281,15 @@ where match exchange_code( self.client.as_ref(), &mut auth_req_info.dpop_data.clone(), - ¶ms.code, - &auth_req_info.pkce_verifier, + params.code.as_str(), + auth_req_info.pkce_verifier.as_str(), &metadata, ) .await { Ok(token_set) => { let scopes = if let Some(scope) = &token_set.scope { - Scope::parse_multiple_reduced(&scope) + Scope::::parse_multiple_reduced(scope.as_str()) .expect("Failed to parse scopes") .into_static() } else { @@ -285,18 +298,18 @@ where let client_data = ClientSessionData { account_did: token_set.sub.clone(), session_id: auth_req_info.state, - host_url: Uri::parse(token_set.aud.as_ref())?.to_owned(), - authserver_url: auth_req_info.authserver_url.to_cowstr(), + host_url: Uri::parse(token_set.aud.as_str())?.to_owned(), + authserver_url: auth_req_info.authserver_url, authserver_token_endpoint: auth_req_info.authserver_token_endpoint, authserver_revocation_endpoint: auth_req_info.authserver_revocation_endpoint, scopes, dpop_data: DpopClientData { dpop_key: auth_req_info.dpop_data.dpop_key.clone(), - dpop_authserver_nonce: authserver_nonce.unwrap_or(CowStr::default()), + dpop_authserver_nonce: authserver_nonce.unwrap_or_default(), dpop_host_nonce: auth_req_info .dpop_data .dpop_authserver_nonce - .unwrap_or(CowStr::default()), + .unwrap_or_default(), }, token_set, }; @@ -307,7 +320,7 @@ where } } - async fn create_session(&self, data: ClientSessionData<'_>) -> Result> { + async fn create_session(&self, data: ClientSessionData) -> Result> { self.registry.set(data.clone()).await?; Ok(OAuthSession::new( self.registry.clone(), @@ -317,7 +330,11 @@ where } /// Restore a previously created session from the backing store, refreshing tokens if needed. - pub async fn restore(&self, did: &Did<'_>, session_id: &str) -> Result> { + pub async fn restore( + &self, + did: &Did, + session_id: &str, + ) -> Result> { self.create_session(self.registry.get(did, session_id, true).await?) .await } @@ -327,7 +344,11 @@ where /// Note: this removes the session from local storage but does **not** call the authorization /// server's revocation endpoint. To also invalidate the token server-side, prefer /// [`OAuthSession::logout`], which calls `revoke` on the token before deleting the session. - pub async fn revoke(&self, did: &Did<'_>, session_id: &str) -> Result<()> { + pub async fn revoke( + &self, + did: &Did, + session_id: &str, + ) -> Result<()> { Ok(self.registry.del(did, session_id).await?) } } @@ -356,16 +377,16 @@ where self.client.options() } - async fn resolve_handle( + async fn resolve_handle( &self, - handle: &Handle<'_>, - ) -> jacquard_identity::resolver::Result> { + handle: &Handle, + ) -> jacquard_identity::resolver::Result { self.client.resolve_handle(handle).await } - async fn resolve_did_doc( + async fn resolve_did_doc( &self, - did: &Did<'_>, + did: &Did, ) -> jacquard_identity::resolver::Result { self.client.resolve_did_doc(did).await } @@ -401,7 +422,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(); @@ -414,15 +435,17 @@ where opts: CallOptions<'_>, ) -> XrpcResult> where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync, { let base_uri = self.base_uri().await; - self.client - .xrpc(base_uri) - .with_options(opts.clone()) - .send(&request) + let http_request = build_http_request(&base_uri.borrow(), &request, &opts)?; + let http_response = self + .client + .send_http(http_request) .await + .map_err(|e| ClientError::transport(e).for_nsid(R::NSID))?; + process_response(http_response) } } @@ -439,13 +462,13 @@ where S: ClientAuthStore, { /// Shared registry used to persist and retrieve session data across refresh operations. - pub registry: Arc>, + pub registry: Arc>, /// Underlying HTTP/identity/OAuth resolver shared with the parent `OAuthClient`. pub client: Arc, /// Optional WebSocket client; `()` when WebSocket support is not required. pub ws_client: W, /// Mutable session data including DPoP key, nonces, and token set. - pub data: RwLock>, + pub data: RwLock, /// Default call options applied to every outgoing XRPC request from this session. pub options: RwLock>, } @@ -460,9 +483,9 @@ where /// This is the standard constructor used by [`OAuthClient::callback`] and /// [`OAuthClient::restore`]. For WebSocket support use [`OAuthSession::new_with_ws`]. pub fn new( - registry: Arc>, + registry: Arc>, client: Arc, - data: ClientSessionData<'static>, + data: ClientSessionData, ) -> Self { Self { registry, @@ -485,10 +508,10 @@ where /// to standard XRPC calls. The `ws_client` is exposed via [`OAuthSession::ws_client`] and /// is used by the `WebSocketClient` impl when the `websocket` feature is enabled. pub fn new_with_ws( - registry: Arc>, + registry: Arc>, client: Arc, ws_client: W, - data: ClientSessionData<'static>, + data: ClientSessionData, ) -> Self { Self { registry, @@ -527,7 +550,7 @@ where /// /// The session ID is the random `state` token generated during the PAR flow and can /// be used together with the DID to restore the session via [`OAuthClient::restore`]. - pub async fn session_info(&self) -> (Did<'_>, CowStr<'_>) { + pub async fn session_info(&self) -> (Did, smol_str::SmolStr) { let data = self.data.read().await; (data.account_did.clone(), data.session_id.clone()) } @@ -541,22 +564,24 @@ 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<'_> { - AuthorizationToken::Dpop(self.data.read().await.token_set.access_token.clone()) + pub async fn access_token(&self) -> AuthorizationToken<'static> { + AuthorizationToken::Dpop(CowStr::Owned( + 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 - .as_ref() - .map(|t| AuthorizationToken::Dpop(t.clone())) + .clone() + .map(|t| AuthorizationToken::Dpop(CowStr::Owned(t))) } /// Derive an unauthenticated [`OAuthClient`] that shares the same registry and resolver. @@ -628,14 +653,15 @@ 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(refreshed.token_set.access_token.clone()); + let token = + AuthorizationToken::Dpop(CowStr::Owned(refreshed.token_set.access_token.clone())); // Write back updated session *self.data.write().await = refreshed.clone().into_static(); // Store in the registry @@ -687,7 +713,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(); @@ -700,7 +726,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; @@ -711,7 +737,7 @@ where let http_response = self .client .dpop_call(&mut dpop) - .send(build_http_request(&base_uri, &request, &opts)?) + .send(build_http_request(&base_uri.borrow(), &request, &opts)?) .await .map_err(|e| ClientError::from(e).for_nsid(R::NSID))?; let resp = process_response(http_response); @@ -741,7 +767,7 @@ where let http_response = self .client .dpop_call(&mut dpop) - .send(build_http_request(&base_uri, &request, &opts)?) + .send(build_http_request(&base_uri.borrow(), &request, &opts)?) .await .map_err(|e| { ClientError::from(e) @@ -832,7 +858,7 @@ where request: R, ) -> core::result::Result where - R: XrpcRequest + Send + Sync, + R: XrpcRequest + Send + Sync + serde::Serialize, ::Response: Send + Sync, { use jacquard_common::StreamError; @@ -840,7 +866,7 @@ where let base_uri = ::base_uri(self).await; let mut opts = self.options.read().await.clone(); opts.auth = Some(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 guard = self.data.read().await; let mut dpop = guard.dpop_data.clone(); @@ -860,7 +886,7 @@ where .await .map_err(|e| StreamError::transport(e))?, ); - 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 guard = self.data.read().await; let mut dpop = guard.dpop_data.clone(); @@ -976,10 +1002,7 @@ fn is_invalid_token_response(response: &XrpcResult>) -> .is_ok_and(|s| s.starts_with("DPoP ") && s.contains("error=\"invalid_token\"")), _ => false, }, - Ok(resp) => match resp.parse() { - Err(XrpcError::Auth(AuthError::InvalidToken)) => true, - _ => false, - }, + Ok(_) => false, } } @@ -993,18 +1016,18 @@ where self.client.options() } - fn resolve_handle( + async fn resolve_handle( &self, - handle: &Handle<'_>, - ) -> impl Future, IdentityError>> { - async { self.client.resolve_handle(handle).await } + handle: &Handle, + ) -> std::result::Result { + self.client.resolve_handle(handle).await } - fn resolve_did_doc( + async fn resolve_did_doc( &self, - did: &Did<'_>, - ) -> impl Future> { - async { self.client.resolve_did_doc(did).await } + did: &Did, + ) -> std::result::Result { + self.client.resolve_did_doc(did).await } } @@ -1061,7 +1084,7 @@ where params: &Sub, ) -> std::result::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 @@ -1073,7 +1096,7 @@ where opts: jacquard_common::xrpc::SubscriptionOptions<'_>, ) -> std::result::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-oauth/src/dpop.rs b/crates/jacquard-oauth/src/dpop.rs index 97c20899b..ae5016105 100644 --- a/crates/jacquard-oauth/src/dpop.rs +++ b/crates/jacquard-oauth/src/dpop.rs @@ -5,14 +5,14 @@ use std::future::Future; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use chrono::Utc; use http::{Request, Response, header::InvalidHeaderValue}; -use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr, http_client::HttpClient}; +use jacquard_common::http_client::HttpClient; use jacquard_identity::JacquardResolver; use jose_jwa::{Algorithm, Signing}; use jose_jwk::{Jwk, Key, crypto}; use p256::ecdsa::SigningKey; use rand::{RngCore, SeedableRng}; use sha2::Digest; -use smol_str::SmolStr; +use smol_str::{SmolStr, ToSmolStr}; use crate::{ jose::{ @@ -461,33 +461,31 @@ impl<'r, C: HttpClient, N: DpopDataSource> DpopCall<'r, C, N> { } } -/// Extract authorization hash from request headers -fn extract_ath(headers: &http::HeaderMap) -> Option> { +/// Extract authorization hash from request headers. +fn extract_ath(headers: &http::HeaderMap) -> Option { headers .get("authorization") .filter(|v| v.to_str().is_ok_and(|s| s.starts_with("DPoP "))) .map(|auth| { - URL_SAFE_NO_PAD - .encode(sha2::Sha256::digest(&auth.as_bytes()[5..])) - .into() + SmolStr::new(URL_SAFE_NO_PAD.encode(sha2::Sha256::digest(&auth.as_bytes()[5..]))) }) } -/// Get nonce from data source based on target -fn get_nonce(data_source: &N, is_to_auth_server: bool) -> Option> { +/// Get nonce from data source based on target, returning an owned copy of the current nonce. +/// +/// Returning an owned `SmolStr` rather than a borrow ensures callers can later take a +/// mutable reference to the data source (e.g., to call `store_nonce`) without violating +/// Rust's aliasing rules. +fn get_nonce(data_source: &N, is_to_auth_server: bool) -> Option { if is_to_auth_server { - data_source.authserver_nonce() + data_source.authserver_nonce().map(SmolStr::new) } else { - data_source.host_nonce() + data_source.host_nonce().map(SmolStr::new) } } -/// Store nonce in data source based on target -fn store_nonce( - data_source: &mut N, - is_to_auth_server: bool, - nonce: CowStr<'static>, -) { +/// Store nonce in data source based on target. +fn store_nonce(data_source: &mut N, is_to_auth_server: bool, nonce: SmolStr) { if is_to_auth_server { data_source.set_authserver_nonce(nonce); } else { @@ -515,34 +513,34 @@ where } else { DpopTarget::ResourceServer }; - let uri = request.uri().clone(); - let method = request.method().to_cowstr().into_static(); - let url_str: SmolStr = uri.to_cowstr().as_ref().into(); - let uri = uri.to_cowstr(); + let method = request.method().to_smolstr(); + let uri = request.uri().to_smolstr(); let ath = extract_ath(request.headers()); let init_nonce = get_nonce(data_source, is_to_auth_server); let init_proof = build_dpop_proof( data_source.key(), - method.clone(), - uri.clone(), - init_nonce.clone(), - ath.clone(), + &method, + &uri, + init_nonce.as_deref(), + ath.as_deref(), )?; - request.headers_mut().insert("DPoP", init_proof.parse()?); + request + .headers_mut() + .insert("DPoP", init_proof.as_str().parse()?); let response = client .send_http(request.clone()) .await - .map_err(|e| DpopError::transport(target, url_str.clone(), e))?; + .map_err(|e| DpopError::transport(target, uri.clone(), e))?; - let next_nonce = response + let next_nonce: Option = response .headers() .get("dpop-nonce") .and_then(|v| v.to_str().ok()) - .map(|c| CowStr::copy_from_str(c)); + .map(SmolStr::new); match &next_nonce { - Some(s) if next_nonce != init_nonce => { - store_nonce(data_source, is_to_auth_server, s.clone()); + Some(_) if next_nonce.as_deref() != init_nonce.as_deref() => { + store_nonce(data_source, is_to_auth_server, next_nonce.clone().unwrap()); } _ => { return Ok(response); @@ -552,12 +550,20 @@ where if !is_use_dpop_nonce_error(is_to_auth_server, &response) { return Ok(response); } - let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?; - request.headers_mut().insert("DPoP", next_proof.parse()?); + let next_proof = build_dpop_proof( + data_source.key(), + &method, + &uri, + next_nonce.as_deref(), + ath.as_deref(), + )?; + request + .headers_mut() + .insert("DPoP", next_proof.as_str().parse()?); let response = client .send_http(request) .await - .map_err(|e| DpopError::nonce_retry(target, url_str, e))?; + .map_err(|e| DpopError::nonce_retry(target, uri.clone(), e))?; Ok(response) } @@ -584,53 +590,61 @@ where } else { DpopTarget::ResourceServer }; - let uri = request.uri().clone(); - let method = request.method().to_cowstr().into_static(); - let url_str: SmolStr = uri.to_cowstr().as_ref().into(); - let uri = uri.to_cowstr(); + let method = request.method().to_smolstr(); + let uri = request.uri().to_smolstr(); let ath = extract_ath(request.headers()); let init_nonce = get_nonce(data_source, is_to_auth_server); let init_proof = build_dpop_proof( data_source.key(), - method.clone(), - uri.clone(), - init_nonce.clone(), - ath.clone(), + &method, + &uri, + init_nonce.as_deref(), + ath.as_deref(), )?; - request.headers_mut().insert("DPoP", init_proof.parse()?); + request + .headers_mut() + .insert("DPoP", init_proof.as_str().parse()?); let http_response = client .send_http_streaming(request.clone()) .await - .map_err(|e| DpopError::transport(target, url_str.clone(), e))?; + .map_err(|e| DpopError::transport(target, uri.clone(), e))?; let (parts, body) = http_response.into_parts(); - let next_nonce = parts + let next_nonce: Option = parts .headers .get("DPoP-Nonce") .and_then(|v| v.to_str().ok()) - .map(|c| CowStr::from(c.to_string())); + .map(SmolStr::new); match &next_nonce { - Some(s) if next_nonce != init_nonce => { - store_nonce(data_source, is_to_auth_server, s.clone()); + Some(_) if next_nonce.as_deref() != init_nonce.as_deref() => { + store_nonce(data_source, is_to_auth_server, next_nonce.clone().unwrap()); } _ => { return Ok(StreamingResponse::new(parts, body)); } } - // For streaming responses, we can't easily check the body for use_dpop_nonce error - // We check status code + headers only + // For streaming responses, we can't easily check the body for use_dpop_nonce error. + // We check status code + headers only. if !is_use_dpop_nonce_error_streaming(is_to_auth_server, parts.status, &parts.headers) { return Ok(StreamingResponse::new(parts, body)); } - let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?; - request.headers_mut().insert("DPoP", next_proof.parse()?); + let next_proof = build_dpop_proof( + data_source.key(), + &method, + &uri, + next_nonce.as_deref(), + ath.as_deref(), + )?; + request + .headers_mut() + .insert("DPoP", next_proof.as_str().parse()?); let http_response = client .send_http_streaming(request) .await - .map_err(|e| DpopError::nonce_retry(target, url_str, e))?; + .map_err(|e| DpopError::nonce_retry(target, uri, e))?; let (parts, body) = http_response.into_parts(); Ok(StreamingResponse::new(parts, body)) } @@ -658,58 +672,62 @@ where } else { DpopTarget::ResourceServer }; - let uri = parts.uri.clone(); - let method = parts.method.to_cowstr().into_static(); - let url_str: SmolStr = uri.to_cowstr().as_ref().into(); - let uri = uri.to_cowstr(); + let method = parts.method.to_smolstr(); + let uri = parts.uri.to_smolstr(); let ath = extract_ath(&parts.headers); let init_nonce = get_nonce(data_source, is_to_auth_server); let init_proof = build_dpop_proof( data_source.key(), - method.clone(), - uri.clone(), - init_nonce.clone(), - ath.clone(), + &method, + &uri, + init_nonce.as_deref(), + ath.as_deref(), )?; - parts.headers.insert("DPoP", init_proof.parse()?); + parts.headers.insert("DPoP", init_proof.as_str().parse()?); - // Clone the stream for potential retry + // Clone the stream for potential retry. let (body1, body2) = body.tee(); let http_response = client .send_http_bidirectional(parts.clone(), body1.into_inner()) .await - .map_err(|e| DpopError::transport(target, url_str.clone(), e))?; + .map_err(|e| DpopError::transport(target, uri.clone(), e))?; let (resp_parts, resp_body) = http_response.into_parts(); - let next_nonce = resp_parts + let next_nonce: Option = resp_parts .headers .get("DPoP-Nonce") .and_then(|v| v.to_str().ok()) - .map(|c| CowStr::from(c.to_string())); + .map(SmolStr::new); match &next_nonce { - Some(s) if next_nonce != init_nonce => { - store_nonce(data_source, is_to_auth_server, s.clone()); + Some(_) if next_nonce.as_deref() != init_nonce.as_deref() => { + store_nonce(data_source, is_to_auth_server, next_nonce.clone().unwrap()); } _ => { return Ok(StreamingResponse::new(resp_parts, resp_body)); } } - // For streaming responses, we can't easily check the body for use_dpop_nonce error - // We check status code + headers only + // For streaming responses, we can't easily check the body for use_dpop_nonce error. + // We check status code + headers only. if !is_use_dpop_nonce_error_streaming(is_to_auth_server, resp_parts.status, &resp_parts.headers) { return Ok(StreamingResponse::new(resp_parts, resp_body)); } - let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?; - parts.headers.insert("DPoP", next_proof.parse()?); + let next_proof = build_dpop_proof( + data_source.key(), + &method, + &uri, + next_nonce.as_deref(), + ath.as_deref(), + )?; + parts.headers.insert("DPoP", next_proof.as_str().parse()?); let http_response = client .send_http_bidirectional(parts, body2.into_inner()) .await - .map_err(|e| DpopError::nonce_retry(target, url_str, e))?; + .map_err(|e| DpopError::nonce_retry(target, uri, e))?; let (parts, body) = http_response.into_parts(); Ok(StreamingResponse::new(parts, body)) } @@ -760,7 +778,7 @@ fn is_use_dpop_nonce_error(is_to_auth_server: bool, response: &Response> } #[inline] -pub(crate) fn generate_jti() -> CowStr<'static> { +pub(crate) fn generate_jti() -> SmolStr { let mut rng = rand::rngs::SmallRng::from_entropy(); let mut bytes = [0u8; 12]; rng.fill_bytes(&mut bytes); @@ -769,25 +787,26 @@ pub(crate) fn generate_jti() -> CowStr<'static> { /// Build a compact JWS (ES256) for DPoP with embedded public JWK. #[inline] -pub fn build_dpop_proof<'s>( +pub fn build_dpop_proof( key: &Key, - method: CowStr<'s>, - url: CowStr<'s>, - nonce: Option>, - ath: Option>, -) -> Result> { + method: &str, + url: &str, + nonce: Option<&str>, + ath: Option<&str>, +) -> Result { let secret = match crypto::Key::try_from(key).map_err(DpopError::crypto)? { crypto::Key::P256(crypto::Kind::Secret(sk)) => sk, _ => return Err(DpopError::unsupported_key()), }; - let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es256)); - header.typ = Some(JWT_HEADER_TYP_DPOP.into()); + let mut header: RegisteredHeader<&str> = + RegisteredHeader::from(Algorithm::Signing(Signing::Es256)); + header.typ = Some(JWT_HEADER_TYP_DPOP); header.jwk = Some(Jwk { key: Key::from(&crypto::Key::from(secret.public_key())), prm: Default::default(), }); - let claims = Claims { + let claims: Claims<&str> = Claims { registered: RegisteredClaims { jti: Some(generate_jti()), iat: Some(Utc::now().timestamp()), @@ -796,8 +815,8 @@ pub fn build_dpop_proof<'s>( public: PublicClaims { htm: Some(method), htu: Some(url), - ath: ath, - nonce: nonce, + ath, + nonce, }, }; Ok(signing::create_signed_jwt_es256( diff --git a/crates/jacquard-oauth/src/jose.rs b/crates/jacquard-oauth/src/jose.rs index 00944c39f..537c29009 100644 --- a/crates/jacquard-oauth/src/jose.rs +++ b/crates/jacquard-oauth/src/jose.rs @@ -5,6 +5,7 @@ pub mod jwt; /// Signed JWT creation for supported algorithms (ES256, ES384, ES256K, EdDSA). pub mod signing; +use jacquard_common::bos::{BosStr, DefaultStr}; use serde::{Deserialize, Serialize}; /// A JOSE header, covering the supported JWS formats. @@ -12,9 +13,9 @@ use serde::{Deserialize, Serialize}; /// Serialized as an untagged enum so the wire format matches the relevant JOSE spec directly. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] -pub enum Header<'a> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub enum Header { /// A JWS compact-serialization header. - #[serde(borrow)] - Jws(jws::Header<'a>), + Jws(jws::Header), } diff --git a/crates/jacquard-oauth/src/jose/jws.rs b/crates/jacquard-oauth/src/jose/jws.rs index beed6990a..2918c89d4 100644 --- a/crates/jacquard-oauth/src/jose/jws.rs +++ b/crates/jacquard-oauth/src/jose/jws.rs @@ -1,61 +1,60 @@ -use jacquard_common::{CowStr, IntoStatic}; +use jacquard_common::{IntoStatic, bos::{BosStr, DefaultStr}}; use jose_jwa::Algorithm; use jose_jwk::Jwk; use serde::{Deserialize, Serialize}; /// A JWS compact-serialization header, wrapping the registered header fields. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Header<'a> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct Header { /// The registered header parameters defined by the JWS specification. #[serde(flatten)] - #[serde(borrow)] - pub registered: RegisteredHeader<'a>, + pub registered: RegisteredHeader, } -impl<'a> From> for super::super::jose::Header<'a> { - fn from(header: Header<'a>) -> Self { +impl From> for super::super::jose::Header { + fn from(header: Header) -> Self { super::super::jose::Header::Jws(header) } } /// Registered JWS header parameters as defined in RFC 7515 §4.1. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] - -pub struct RegisteredHeader<'a> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct RegisteredHeader { /// The cryptographic algorithm used to sign the JWS (e.g., `ES256`). pub alg: Algorithm, /// JWK Set URL: a URI pointing to a resource containing the public key(s) used to sign the JWS. - #[serde(borrow)] #[serde(skip_serializing_if = "Option::is_none")] - pub jku: Option>, + pub jku: Option, /// JSON Web Key: the public key used to verify the JWS, embedded directly in the header. #[serde(skip_serializing_if = "Option::is_none")] pub jwk: Option, /// Key ID: a hint indicating which key was used to sign the JWS. #[serde(skip_serializing_if = "Option::is_none")] - pub kid: Option>, + pub kid: Option, /// X.509 URL: a URI pointing to a resource for the X.509 certificate used to sign the JWS. #[serde(skip_serializing_if = "Option::is_none")] - pub x5u: Option>, + pub x5u: Option, /// X.509 certificate chain: the certificate (and chain) corresponding to the key used to sign the JWS. #[serde(skip_serializing_if = "Option::is_none")] - pub x5c: Option>, + pub x5c: Option, /// X.509 certificate SHA-1 thumbprint: base64url-encoded SHA-1 digest of the DER-encoded certificate. #[serde(skip_serializing_if = "Option::is_none")] - pub x5t: Option>, + pub x5t: Option, /// X.509 certificate SHA-256 thumbprint: base64url-encoded SHA-256 digest of the DER-encoded certificate. #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "x5t#S256")] - pub x5ts256: Option>, + pub x5ts256: Option, /// Type: declares the media type of the complete JWS, used by applications to disambiguate among JOSe objects. #[serde(skip_serializing_if = "Option::is_none")] - pub typ: Option>, + pub typ: Option, /// Content type: declares the media type of the secured content (the payload). #[serde(skip_serializing_if = "Option::is_none")] - pub cty: Option>, + pub cty: Option, } -impl From for RegisteredHeader<'_> { +impl From for RegisteredHeader { fn from(alg: Algorithm) -> Self { Self { alg, @@ -72,14 +71,18 @@ impl From for RegisteredHeader<'_> { } } -impl<'a> From> for super::super::jose::Header<'a> { - fn from(registered: RegisteredHeader<'a>) -> Self { +impl From> for super::super::jose::Header { + fn from(registered: RegisteredHeader) -> Self { super::super::jose::Header::Jws(Header { registered }) } } -impl IntoStatic for RegisteredHeader<'_> { - type Output = RegisteredHeader<'static>; +impl IntoStatic for RegisteredHeader +where + S: BosStr + IntoStatic, + S::Output: BosStr, +{ + type Output = RegisteredHeader; fn into_static(self) -> Self::Output { RegisteredHeader { alg: self.alg, @@ -95,3 +98,16 @@ impl IntoStatic for RegisteredHeader<'_> { } } } + +impl IntoStatic for Header +where + S: BosStr + IntoStatic, + S::Output: BosStr, +{ + type Output = Header; + fn into_static(self) -> Self::Output { + Header { + registered: self.registered.into_static(), + } + } +} diff --git a/crates/jacquard-oauth/src/jose/jwt.rs b/crates/jacquard-oauth/src/jose/jwt.rs index f421b5089..b141ff5b6 100644 --- a/crates/jacquard-oauth/src/jose/jwt.rs +++ b/crates/jacquard-oauth/src/jose/jwt.rs @@ -1,32 +1,47 @@ -use jacquard_common::{CowStr, IntoStatic}; +use jacquard_common::{ + IntoStatic, + bos::{BosStr, DefaultStr}, +}; use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; /// Full JWT claims payload, combining registered and public (DPoP-specific) claims. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct Claims<'a> { +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct Claims { /// Standard registered JWT claims (iss, sub, aud, exp, etc.). #[serde(flatten)] - pub registered: RegisteredClaims<'a>, + pub registered: RegisteredClaims, /// Public claims used in DPoP proofs (htm, htu, ath, nonce). #[serde(flatten)] - #[serde(borrow)] - pub public: PublicClaims<'a>, + pub public: PublicClaims, } -/// Standard registered JWT claims as defined in RFC 7519 §4.1. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +/// Manual `Default` impl to avoid a spurious `S: Default` bound from the derive macro. +/// +/// All `S`-typed fields are wrapped in `Option`, which is `Default` regardless of `S`. +impl Default for Claims { + fn default() -> Self { + Self { + registered: RegisteredClaims::default(), + public: PublicClaims::default(), + } + } +} -pub struct RegisteredClaims<'a> { +/// Standard registered JWT claims as defined in RFC 7519 §4.1. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct RegisteredClaims { /// Issuer: identifies the principal that issued the JWT. - #[serde(borrow)] #[serde(skip_serializing_if = "Option::is_none")] - pub iss: Option>, + pub iss: Option, /// Subject: identifies the principal that is the subject of the JWT. #[serde(skip_serializing_if = "Option::is_none")] - pub sub: Option>, + pub sub: Option, /// Audience: recipients that the JWT is intended for. #[serde(skip_serializing_if = "Option::is_none")] - pub aud: Option>, + pub aud: Option>, /// Expiration time (Unix timestamp): the JWT must not be accepted on or after this time. #[serde(skip_serializing_if = "Option::is_none")] pub exp: Option, @@ -38,33 +53,63 @@ pub struct RegisteredClaims<'a> { pub iat: Option, /// JWT ID: unique identifier for the token, used to prevent replay attacks. #[serde(skip_serializing_if = "Option::is_none")] - pub jti: Option>, + pub jti: Option, +} + +/// Manual `Default` impl to avoid a spurious `S: Default` bound from the derive macro. +/// +/// All `S`-typed fields are wrapped in `Option`, which is `Default` regardless of `S`. +impl Default for RegisteredClaims { + fn default() -> Self { + Self { + iss: None, + sub: None, + aud: None, + exp: None, + nbf: None, + iat: None, + jti: None, + } + } } /// Public claims used in DPoP proof JWTs (RFC 9449). /// /// These claims bind the DPoP proof to a specific HTTP request, preventing /// the proof from being replayed against a different endpoint or method. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] - -pub struct PublicClaims<'a> { +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct PublicClaims { /// HTTP method of the request the DPoP proof is bound to (e.g., `"POST"`). - #[serde(borrow)] #[serde(skip_serializing_if = "Option::is_none")] - pub htm: Option>, + pub htm: Option, /// HTTP target URI of the request the DPoP proof is bound to. #[serde(skip_serializing_if = "Option::is_none")] - pub htu: Option>, + pub htu: Option, /// Access token hash: base64url-encoded SHA-256 of the access token, binding the proof to a specific token. #[serde(skip_serializing_if = "Option::is_none")] - pub ath: Option>, + pub ath: Option, /// Server-provided nonce, included to prevent replay attacks when required by the authorization server. #[serde(skip_serializing_if = "Option::is_none")] - pub nonce: Option>, + pub nonce: Option, +} + +/// Manual `Default` impl to avoid a spurious `S: Default` bound from the derive macro. +/// +/// All `S`-typed fields are wrapped in `Option`, which is `Default` regardless of `S`. +impl Default for PublicClaims { + fn default() -> Self { + Self { + htm: None, + htu: None, + ath: None, + nonce: None, + } + } } -impl<'a> From> for Claims<'a> { - fn from(registered: RegisteredClaims<'a>) -> Self { +impl From> for Claims { + fn from(registered: RegisteredClaims) -> Self { Self { registered, public: PublicClaims::default(), @@ -75,16 +120,20 @@ impl<'a> From> for Claims<'a> { /// The `aud` (audience) claim, which may be a single string or a list of strings per RFC 7519. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] -pub enum RegisteredClaimsAud<'a> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub enum RegisteredClaimsAud { /// A single audience identifier. - #[serde(borrow)] - Single(CowStr<'a>), + Single(S), /// Multiple audience identifiers. - Multiple(Vec>), + Multiple(Vec), } -impl IntoStatic for RegisteredClaims<'_> { - type Output = RegisteredClaims<'static>; +impl IntoStatic for RegisteredClaims +where + S: BosStr + IntoStatic, + S::Output: BosStr, +{ + type Output = RegisteredClaims; fn into_static(self) -> Self::Output { RegisteredClaims { iss: self.iss.map(IntoStatic::into_static), @@ -98,8 +147,12 @@ impl IntoStatic for RegisteredClaims<'_> { } } -impl IntoStatic for PublicClaims<'_> { - type Output = PublicClaims<'static>; +impl IntoStatic for PublicClaims +where + S: BosStr + IntoStatic, + S::Output: BosStr, +{ + type Output = PublicClaims; fn into_static(self) -> Self::Output { PublicClaims { htm: self.htm.map(IntoStatic::into_static), @@ -110,8 +163,26 @@ impl IntoStatic for PublicClaims<'_> { } } -impl IntoStatic for RegisteredClaimsAud<'_> { - type Output = RegisteredClaimsAud<'static>; +impl IntoStatic for Claims +where + S: BosStr + IntoStatic, + S::Output: BosStr, +{ + type Output = Claims; + fn into_static(self) -> Self::Output { + Claims { + registered: self.registered.into_static(), + public: self.public.into_static(), + } + } +} + +impl IntoStatic for RegisteredClaimsAud +where + S: BosStr + IntoStatic, + S::Output: BosStr, +{ + type Output = RegisteredClaimsAud; fn into_static(self) -> Self::Output { match self { RegisteredClaimsAud::Single(s) => RegisteredClaimsAud::Single(s.into_static()), diff --git a/crates/jacquard-oauth/src/jose/signing.rs b/crates/jacquard-oauth/src/jose/signing.rs index 99fa943b3..a45b9a041 100644 --- a/crates/jacquard-oauth/src/jose/signing.rs +++ b/crates/jacquard-oauth/src/jose/signing.rs @@ -1,27 +1,31 @@ use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use jacquard_common::CowStr; +use jacquard_common::bos::BosStr; +use smol_str::SmolStr; use super::{Header, jwt::Claims}; /// Builds the base64url-encoded `header.payload` signing input. -fn signing_input(header: &Header, claims: &Claims) -> serde_json::Result<(String, String)> { +fn signing_input( + header: &Header, + claims: &Claims<&str>, +) -> serde_json::Result<(String, String)> { let h = URL_SAFE_NO_PAD.encode(serde_json::to_string(header)?); let p = URL_SAFE_NO_PAD.encode(serde_json::to_string(claims)?); Ok((h, p)) } /// Assembles a compact JWS from pre-encoded parts and raw signature bytes. -fn assemble(header: &str, payload: &str, sig: &[u8]) -> CowStr<'static> { - format!("{header}.{payload}.{}", URL_SAFE_NO_PAD.encode(sig)).into() +fn assemble(header: &str, payload: &str, sig: &[u8]) -> SmolStr { + smol_str::format_smolstr!("{header}.{payload}.{}", URL_SAFE_NO_PAD.encode(sig)) } /// Creates a compact-serialized signed JWT using ES256 (P-256 ECDSA with SHA-256). -pub fn create_signed_jwt_es256( +pub fn create_signed_jwt_es256( key: p256::ecdsa::SigningKey, - header: Header, - claims: Claims, -) -> serde_json::Result> { + header: Header, + claims: Claims<&str>, +) -> serde_json::Result { use p256::ecdsa::signature::Signer; let (h, p) = signing_input(&header, &claims)?; let sig: p256::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes()); @@ -29,11 +33,11 @@ pub fn create_signed_jwt_es256( } /// Creates a compact-serialized signed JWT using ES384 (P-384 ECDSA with SHA-384). -pub fn create_signed_jwt_es384( +pub fn create_signed_jwt_es384( key: p384::ecdsa::SigningKey, - header: Header, - claims: Claims, -) -> serde_json::Result> { + header: Header, + claims: Claims<&str>, +) -> serde_json::Result { use p384::ecdsa::signature::Signer; let (h, p) = signing_input(&header, &claims)?; let sig: p384::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes()); @@ -41,11 +45,11 @@ pub fn create_signed_jwt_es384( } /// Creates a compact-serialized signed JWT using ES256K (secp256k1 ECDSA with SHA-256). -pub fn create_signed_jwt_es256k( +pub fn create_signed_jwt_es256k( key: k256::ecdsa::SigningKey, - header: Header, - claims: Claims, -) -> serde_json::Result> { + header: Header, + claims: Claims<&str>, +) -> serde_json::Result { use k256::ecdsa::signature::Signer; let (h, p) = signing_input(&header, &claims)?; let sig: k256::ecdsa::Signature = key.sign(format!("{h}.{p}").as_bytes()); @@ -53,11 +57,11 @@ pub fn create_signed_jwt_es256k( } /// Creates a compact-serialized signed JWT using EdDSA (Ed25519). -pub fn create_signed_jwt_eddsa( +pub fn create_signed_jwt_eddsa( key: ed25519_dalek::SigningKey, - header: Header, - claims: Claims, -) -> serde_json::Result> { + header: Header, + claims: Claims<&str>, +) -> serde_json::Result { use ed25519_dalek::Signer; let (h, p) = signing_input(&header, &claims)?; let sig = key.sign(format!("{h}.{p}").as_bytes()); diff --git a/crates/jacquard-oauth/src/keyset.rs b/crates/jacquard-oauth/src/keyset.rs index 4bfdb8d62..aff7470da 100644 --- a/crates/jacquard-oauth/src/keyset.rs +++ b/crates/jacquard-oauth/src/keyset.rs @@ -1,10 +1,10 @@ use crate::jose::jws::RegisteredHeader; use crate::jose::jwt::Claims; use crate::jose::signing; -use jacquard_common::CowStr; use jose_jwa::{Algorithm, Signing}; use jose_jwk::{Class, EcCurves, OkpCurves, crypto}; use jose_jwk::{Jwk, JwkSet, Key}; +use smol_str::{SmolStr, ToSmolStr}; use std::collections::HashSet; use thiserror::Error; @@ -87,7 +87,7 @@ impl Keyset { /// Signs a JWT with the best available key that matches one of the requested algorithms. /// /// Returns [`Error::NotFound`] if no key in the keyset supports any of the given algorithms. - pub fn create_jwt(&self, algs: &[Signing], claims: Claims) -> Result> { + pub fn create_jwt(&self, algs: &[Signing], claims: Claims<&str>) -> Result { let Some(jwk) = self.find_key(algs, Class::Signing) else { return Err(Error::NotFound(algs.to_vec())); }; @@ -116,8 +116,8 @@ impl Keyset { None } - fn create_jwt_with_key(&self, key: &Jwk, claims: Claims) -> Result> { - let kid = key.prm.kid.clone().unwrap(); + fn create_jwt_with_key(&self, key: &Jwk, claims: Claims<&str>) -> Result { + let kid = key.prm.kid.as_ref().unwrap().to_smolstr(); match &key.key { Key::Ec(ec) => { let d = ec.d.as_ref().ok_or(Error::MissingPrivateKey)?; @@ -127,7 +127,7 @@ impl Keyset { let signing_key = p256::ecdsa::SigningKey::from_bytes(d_bytes.into()) .map_err(|e| Error::InvalidKey(e.to_string()))?; let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es256)); - header.kid = Some(kid.into()); + header.kid = Some(kid); Ok(signing::create_signed_jwt_es256( signing_key, header.into(), @@ -137,8 +137,9 @@ impl Keyset { EcCurves::P384 => { let signing_key = p384::ecdsa::SigningKey::from_bytes(d_bytes.into()) .map_err(|e| Error::InvalidKey(e.to_string()))?; - let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::Es384)); - header.kid = Some(kid.into()); + let mut header: RegisteredHeader<&str> = + RegisteredHeader::from(Algorithm::Signing(Signing::Es384)); + header.kid = Some(kid.as_str()); Ok(signing::create_signed_jwt_es384( signing_key, header.into(), @@ -148,9 +149,9 @@ impl Keyset { EcCurves::P256K => { let signing_key = k256::ecdsa::SigningKey::from_bytes(d_bytes.into()) .map_err(|e| Error::InvalidKey(e.to_string()))?; - let mut header = + let mut header: RegisteredHeader<&str> = RegisteredHeader::from(Algorithm::Signing(Signing::Es256K)); - header.kid = Some(kid.into()); + header.kid = Some(kid.as_str()); Ok(signing::create_signed_jwt_es256k( signing_key, header.into(), @@ -166,8 +167,9 @@ impl Keyset { let d_bytes: &[u8] = d.as_ref(); let signing_key = ed25519_dalek::SigningKey::try_from(d_bytes) .map_err(|e| Error::InvalidKey(e.to_string()))?; - let mut header = RegisteredHeader::from(Algorithm::Signing(Signing::EdDsa)); - header.kid = Some(kid.into()); + let mut header: RegisteredHeader<&str> = + RegisteredHeader::from(Algorithm::Signing(Signing::EdDsa)); + header.kid = Some(kid.as_str()); Ok(signing::create_signed_jwt_eddsa( signing_key, header.into(), diff --git a/crates/jacquard-oauth/src/loopback.rs b/crates/jacquard-oauth/src/loopback.rs index bb2316762..01e85ef9a 100644 --- a/crates/jacquard-oauth/src/loopback.rs +++ b/crates/jacquard-oauth/src/loopback.rs @@ -53,9 +53,10 @@ use crate::{ resolver::OAuthResolver, types::{AuthorizeOptions, CallbackParams}, }; +use jacquard_common::IntoStatic; use jacquard_common::deps::fluent_uri::Uri; -use jacquard_common::{IntoStatic, cowstr::ToCowStr}; use rouille::Server; +use smol_str::{SmolStr, ToSmolStr}; use std::net::SocketAddr; use tokio::sync::mpsc; @@ -115,9 +116,9 @@ fn create_callback_router( let code = request.get_param("code").unwrap(); let iss = request.get_param("iss").unwrap(); let callback_params = CallbackParams { - state: Some(state.to_cowstr().into_static()), - code: code.to_cowstr().into_static(), - iss: Some(iss.to_cowstr().into_static()), + state: Some(state.to_smolstr()), + code: code.to_smolstr(), + iss: Some(iss.to_smolstr()), }; tx.try_send(callback_params).unwrap(); rouille::Response::text("Logged in!") @@ -131,7 +132,7 @@ pub struct CallbackHandle { #[allow(dead_code)] server_handle: std::thread::JoinHandle<()>, server_stop: std::sync::mpsc::Sender<()>, - callback_rx: mpsc::Receiver>, + callback_rx: mpsc::Receiver, } /// One-shot OAuth callback server. @@ -214,7 +215,7 @@ where pub async fn login_with_local_server( &self, input: impl AsRef, - opts: AuthorizeOptions<'_>, + opts: AuthorizeOptions, cfg: LoopbackConfig, ) -> crate::error::Result> { let port = match cfg.port { @@ -252,16 +253,24 @@ where pub fn build_localhost_client_data( &self, cfg: &LoopbackConfig, - opts: &AuthorizeOptions<'_>, + opts: &AuthorizeOptions, local_addr: SocketAddr, - ) -> crate::session::ClientData<'static> { + ) -> crate::session::ClientData { let redirect_uri = format!("http://{}:{}/oauth/callback", cfg.host, local_addr.port(),); let redirect = Uri::parse(redirect_uri).unwrap(); let scopes = if opts.scopes.is_empty() { - Some(self.registry.client_data.config.scopes.clone()) + Some( + self.registry + .client_data + .config + .scopes + .iter() + .cloned() + .collect(), + ) } else { - Some(opts.scopes.clone().into_static()) + Some(opts.scopes.clone()) }; crate::session::ClientData { diff --git a/crates/jacquard-oauth/src/request.rs b/crates/jacquard-oauth/src/request.rs index e211d8c34..da8582429 100644 --- a/crates/jacquard-oauth/src/request.rs +++ b/crates/jacquard-oauth/src/request.rs @@ -1,8 +1,10 @@ +use std::str::FromStr; + use chrono::{TimeDelta, Utc}; use http::{Method, Request, StatusCode}; use jacquard_common::{ CowStr, IntoStatic, - cowstr::ToCowStr, + bos::{BosStr, DefaultStr}, http_client::HttpClient, session::SessionStoreError, types::{ @@ -34,7 +36,7 @@ use crate::{ OAuthTokenResponse, ParParameters, RefreshRequestParameters, RevocationRequestParameters, TokenGrantType, TokenRequestParameters, TokenSet, }, - utils::{compare_algos, generate_dpop_key, generate_nonce, generate_pkce}, + utils::{generate_dpop_key, generate_nonce, generate_pkce}, }; // https://datatracker.ietf.org/doc/html/rfc7523#section-2.2 @@ -428,27 +430,27 @@ pub type Result = core::result::Result; #[allow(dead_code)] pub enum OAuthRequest<'a> { /// Standard authorization-code token exchange. - Token(TokenRequestParameters<'a>), + Token(TokenRequestParameters<&'a str>), /// Refresh-token grant to obtain a fresh access token. - Refresh(RefreshRequestParameters<'a>), + Refresh(RefreshRequestParameters<&'a str>), /// Token revocation request (RFC 7009). - Revocation(RevocationRequestParameters<'a>), + Revocation(RevocationRequestParameters<&'a str>), /// Token introspection request (RFC 7662). Introspection, /// Pushed authorization request (RFC 9126) for pre-registering auth parameters. - PushedAuthorizationRequest(ParParameters<'a>), + PushedAuthorizationRequest(ParParameters<&'a str>), } impl OAuthRequest<'_> { /// Return a human-readable name for this request variant, used in error messages. - pub fn name(&self) -> CowStr<'static> { - CowStr::new_static(match self { + pub fn name(&self) -> &'static str { + match self { Self::Token(_) => "token", Self::Refresh(_) => "refresh", Self::Revocation(_) => "revocation", Self::Introspection => "introspection", Self::PushedAuthorizationRequest(_) => "pushed_authorization_request", - }) + } } /// Returns the HTTP status code that a successful response to this request should carry. pub fn expected_status(&self) -> StatusCode { @@ -476,7 +478,7 @@ where client_assertion_type: Option>, /// A JWT signed with the client's private key, proving client identity to the server. #[serde(skip_serializing_if = "Option::is_none")] - client_assertion: Option>, + client_assertion: Option, /// The grant-specific parameters (token request, refresh, PAR, etc.) flattened into the body. #[serde(flatten)] parameters: T, @@ -488,33 +490,35 @@ where /// and the optional signing keyset into a single value that is passed to helper functions such /// as [`par`], [`exchange_code`], [`refresh`], and [`revoke`]. #[derive(Debug, Clone)] -pub struct OAuthMetadata { +pub struct OAuthMetadata { /// Metadata fetched from the authorization server's `/.well-known/oauth-authorization-server` document. - pub server_metadata: OAuthAuthorizationServerMetadata<'static>, + pub server_metadata: OAuthAuthorizationServerMetadata, /// This client's registered metadata, derived from [`crate::atproto::AtprotoClientMetadata`]. - pub client_metadata: OAuthClientMetadata<'static>, + pub client_metadata: OAuthClientMetadata, /// Optional signing keyset; required for `private_key_jwt` client authentication. pub keyset: Option, } -impl OAuthMetadata { +impl OAuthMetadata { /// Fetch server metadata and assemble an `OAuthMetadata` from an active session context. /// /// Contacts the authorization server recorded in `session_data` to retrieve its current /// metadata, then combines it with the client configuration. This is the preferred way to /// build an `OAuthMetadata` during token refresh or revocation. - pub async fn new<'r, T: HttpClient + OAuthResolver + Send + Sync>( + pub async fn new( client: &T, - ClientData { keyset, config }: &ClientData<'r>, - session_data: &ClientSessionData<'r>, - ) -> Result { + ClientData { keyset, config }: &ClientData, + session_data: &ClientSessionData, + ) -> Result + where + S: Clone + FromStr + Ord, + ::Err: core::fmt::Debug, + { Ok(OAuthMetadata { server_metadata: client - .get_authorization_server_metadata(&session_data.authserver_url) + .get_authorization_server_metadata(session_data.authserver_url.as_ref()) .await?, - client_metadata: atproto_client_metadata(config.clone(), &keyset) - .unwrap() - .into_static(), + client_metadata: atproto_client_metadata(&config, &keyset)?, keyset: keyset.clone(), }) } @@ -527,37 +531,36 @@ impl OAuthMetadata { /// persisted (e.g., in the auth store) so it can be retrieved and verified during /// [`crate::client::OAuthClient::callback`]. #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all, fields(login_hint = login_hint.as_ref().map(|h| h.as_ref()))))] -pub async fn par<'r, T: OAuthResolver + DpopExt + Send + Sync + 'static>( +pub async fn par< + S: BosStr + Clone + Send + Sync, + T: OAuthResolver + DpopExt + Send + Sync + 'static, +>( client: &T, - login_hint: Option>, + login_hint: Option, prompt: Option, - metadata: &OAuthMetadata, - state: Option>, -) -> crate::request::Result> { - let state = if let Some(state) = state { - state - } else { - generate_nonce() - }; + metadata: &mut OAuthMetadata, + state: Option, +) -> crate::request::Result { + let state = state.unwrap_or_else(generate_nonce); let (code_challenge, verifier) = generate_pkce(); - let Some(dpop_key) = generate_dpop_key(&metadata.server_metadata) else { + let Some(dpop_key) = generate_dpop_key(&mut metadata.server_metadata) else { return Err(RequestError::token_verification()); }; let mut dpop_data = DpopReqData { dpop_key, dpop_authserver_nonce: None, }; - let parameters = ParParameters { + let parameters: ParParameters<&str> = ParParameters { response_type: AuthorizationResponseType::Code, - redirect_uri: metadata.client_metadata.redirect_uris[0].to_cowstr(), - state: state.clone(), - scope: metadata.client_metadata.scope.clone(), + redirect_uri: metadata.client_metadata.redirect_uris[0].as_ref(), + state: state.as_ref(), + scope: metadata.client_metadata.scope.as_ref().map(|s| s.as_ref()), response_mode: None, - code_challenge, + code_challenge: code_challenge.as_str(), code_challenge_method: AuthorizationCodeChallengeMethod::S256, - login_hint: login_hint, - prompt: prompt.map(CowStr::from), + login_hint: login_hint.as_ref().map(|h| h.as_ref()), + prompt: prompt.map(|p| p.into()), }; if metadata @@ -565,7 +568,7 @@ pub async fn par<'r, T: OAuthResolver + DpopExt + Send + Sync + 'static>( .pushed_authorization_request_endpoint .is_some() { - let par_response = oauth_request::( + let par_response = oauth_request::( &client, &mut dpop_data, OAuthRequest::PushedAuthorizationRequest(parameters), @@ -574,21 +577,21 @@ pub async fn par<'r, T: OAuthResolver + DpopExt + Send + Sync + 'static>( .await?; let scopes = if let Some(scope) = &metadata.client_metadata.scope { - Scope::parse_multiple_reduced(&scope) + Scope::::parse_multiple_reduced(scope.as_ref()) .expect("Failed to parse scopes") .into_static() } else { vec![] }; - let auth_req_data = AuthRequestData { - state, - authserver_url: metadata.server_metadata.issuer.clone(), + let auth_req_data: AuthRequestData = AuthRequestData { + state: state.into(), + authserver_url: metadata.server_metadata.issuer.to_smolstr(), account_did: None, scopes, - request_uri: par_response.request_uri.to_cowstr().into_static(), - authserver_token_endpoint: metadata.server_metadata.token_endpoint.clone(), + request_uri: par_response.request_uri.clone(), + authserver_token_endpoint: metadata.server_metadata.token_endpoint.to_smolstr(), authserver_revocation_endpoint: metadata.server_metadata.revocation_endpoint.clone(), - pkce_verifier: verifier, + pkce_verifier: verifier.into(), dpop_data, }; @@ -606,12 +609,13 @@ pub async fn par<'r, T: OAuthResolver + DpopExt + Send + Sync + 'static>( /// Exchange a refresh token for a fresh token set and update the session data in place. #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all, fields(did = %session_data.account_did)))] -pub async fn refresh<'r, T>( +pub async fn refresh( client: &T, - mut session_data: ClientSessionData<'r>, - metadata: &OAuthMetadata, -) -> Result> + mut session_data: ClientSessionData, + metadata: &OAuthMetadata, +) -> Result where + S: BosStr + FromStr, T: OAuthResolver + DpopExt + Send + Sync + 'static, { let Some(refresh_token) = session_data.token_set.refresh_token.as_ref() else { @@ -631,12 +635,12 @@ where .await?; let iss = metadata.server_metadata.issuer.clone(); - let response = oauth_request::( + let response = oauth_request::( client, &mut session_data.dpop_data, OAuthRequest::Refresh(RefreshRequestParameters { grant_type: TokenGrantType::RefreshToken, - refresh_token: refresh_token.clone(), + refresh_token: refresh_token.as_ref(), scope: None, }), metadata, @@ -650,13 +654,13 @@ where .map(Datetime::new) }); - session_data.update_with_tokens(TokenSet { + session_data.update_with_tokens(&TokenSet { iss, sub: session_data.token_set.sub.clone(), - aud: CowStr::Owned(aud.to_smolstr()), - scope: response.scope.map(CowStr::Owned), - access_token: CowStr::Owned(response.access_token), - refresh_token: response.refresh_token.map(CowStr::Owned), + aud: SmolStr::from(aud.as_str()), + scope: response.scope, + access_token: response.access_token, + refresh_token: response.refresh_token, token_type: response.token_type, expires_at, }); @@ -671,28 +675,25 @@ where /// function performs that verification as part of the exchange, so callers receive a token /// set that is safe to persist. #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))] -pub async fn exchange_code<'r, T, D>( +pub async fn exchange_code( client: &T, - data_source: &'r mut D, + data_source: &mut D, code: &str, verifier: &str, - metadata: &OAuthMetadata, -) -> Result> + metadata: &OAuthMetadata, +) -> Result where + S: BosStr + Send + Sync, T: OAuthResolver + DpopExt + Send + Sync + 'static, D: DpopDataSource, { - let token_response = oauth_request::( + let token_response = oauth_request::( client, data_source, OAuthRequest::Token(TokenRequestParameters { grant_type: TokenGrantType::AuthorizationCode, code: code.into(), - redirect_uri: CowStr::Owned( - metadata.client_metadata.redirect_uris[0] - .clone() - .to_smolstr(), - ), + redirect_uri: metadata.client_metadata.redirect_uris[0].as_ref(), code_verifier: verifier.into(), }), metadata, @@ -720,10 +721,10 @@ where Ok(TokenSet { iss, sub, - aud: CowStr::Owned(aud.to_smolstr()), - scope: token_response.scope.map(CowStr::Owned), - access_token: CowStr::Owned(token_response.access_token), - refresh_token: token_response.refresh_token.map(CowStr::Owned), + aud: SmolStr::from(aud.as_str()), + scope: token_response.scope, + access_token: token_response.access_token, + refresh_token: token_response.refresh_token, token_type: token_response.token_type, expires_at, }) @@ -735,17 +736,17 @@ where /// by the server. The caller is responsible for deleting the session from local storage regardless /// of whether revocation succeeds. #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))] -pub async fn revoke<'r, T, D>( +pub async fn revoke( client: &T, - data_source: &'r mut D, + data_source: &mut D, token: &str, - metadata: &OAuthMetadata, + metadata: &OAuthMetadata, ) -> Result<()> where T: OAuthResolver + DpopExt + Send + Sync + 'static, D: DpopDataSource, { - oauth_request::<(), T, D>( + oauth_request::<(), T, D, _>( client, data_source, OAuthRequest::Revocation(RevocationRequestParameters { @@ -763,11 +764,11 @@ where /// client authentication, performs the DPoP-wrapped HTTP POST, and deserializes the response /// body into `O`. The type parameter `O` is inferred from the call site; use `()` for requests /// where the response body is empty (e.g., revocation). -pub async fn oauth_request<'de: 'r, 'r, O, T, D>( +pub async fn oauth_request<'r, O, T, D, S: BosStr>( client: &T, - data_source: &'r mut D, + data_source: &mut D, request: OAuthRequest<'r>, - metadata: &OAuthMetadata, + metadata: &OAuthMetadata, ) -> Result where T: OAuthResolver + DpopExt + Send + Sync + 'static, @@ -792,7 +793,7 @@ where _ => unimplemented!(), }; let req = Request::builder() - .uri(url.to_string()) + .uri(url) .method(Method::POST) .header("Content-Type", "application/x-www-form-urlencoded") .body(body.into_bytes())?; @@ -817,17 +818,26 @@ where } #[inline] -fn endpoint_for_req<'a, 'r>( - server_metadata: &'r OAuthAuthorizationServerMetadata<'a>, +fn endpoint_for_req<'r, S: BosStr>( + server_metadata: &'r OAuthAuthorizationServerMetadata, request: &'r OAuthRequest, -) -> Option<&'r CowStr<'a>> { +) -> Option<&'r str> { match request { - OAuthRequest::Token(_) | OAuthRequest::Refresh(_) => Some(&server_metadata.token_endpoint), - OAuthRequest::Revocation(_) => server_metadata.revocation_endpoint.as_ref(), - OAuthRequest::Introspection => server_metadata.introspection_endpoint.as_ref(), + OAuthRequest::Token(_) | OAuthRequest::Refresh(_) => { + Some(server_metadata.token_endpoint.as_ref()) + } + OAuthRequest::Revocation(_) => server_metadata + .revocation_endpoint + .as_ref() + .map(AsRef::as_ref), + OAuthRequest::Introspection => server_metadata + .introspection_endpoint + .as_ref() + .map(AsRef::as_ref), OAuthRequest::PushedAuthorizationRequest(_) => server_metadata .pushed_authorization_request_endpoint - .as_ref(), + .as_ref() + .map(AsRef::as_ref), } } @@ -856,7 +866,7 @@ pub struct ClientAuth<'a> { /// Either absent (for `none` auth) or `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. assertion_type: Option>, /// A signed JWT proving client identity; present only for `private_key_jwt` auth. - assertion: Option>, + assertion: Option, } impl<'s> ClientAuth<'s> { @@ -870,34 +880,39 @@ impl<'s> ClientAuth<'s> { } } -fn build_auth<'a>( +fn build_auth<'a, S: BosStr>( keyset: Option<&Keyset>, - server_metadata: &OAuthAuthorizationServerMetadata<'a>, - client_metadata: &OAuthClientMetadata<'a>, + server_metadata: &'a OAuthAuthorizationServerMetadata, + client_metadata: &'a OAuthClientMetadata, ) -> Result> { let method_supported = server_metadata .token_endpoint_auth_methods_supported .as_ref(); - let client_id = client_metadata.client_id.to_cowstr().into_static(); + let client_id = CowStr::Borrowed(client_metadata.client_id.as_ref()); if let Some(method) = client_metadata.token_endpoint_auth_method.as_ref() { - match (*method).as_ref() { + match method.as_ref() { "private_key_jwt" if method_supported .as_ref() - .is_some_and(|v| v.contains(&CowStr::new_static("private_key_jwt"))) => + .is_some_and(|v| v.iter().any(|s| s.as_str() == "private_key_jwt")) => { if let Some(keyset) = &keyset { - let mut alg_strs = server_metadata + let mut alg_strs: Vec<&str> = server_metadata .token_endpoint_auth_signing_alg_values_supported - .clone() - .unwrap_or(vec![FALLBACK_ALG.into()]); - alg_strs.sort_by(compare_algos); + .as_ref() + .map(|v| v.iter().map(|s| s.as_ref()).collect()) + .unwrap_or_default(); + if alg_strs.is_empty() { + alg_strs.push(FALLBACK_ALG); + } let algs: Vec = alg_strs .iter() .filter_map(|s| crate::keyset::parse_signing_alg(s)) .collect(); let iat = Utc::now().timestamp(); + let client_id_str: &str = client_metadata.client_id.as_ref(); + let issuer_str: &str = server_metadata.issuer.as_ref(); return Ok(ClientAuth { client_id: client_id.clone(), assertion_type: Some(CowStr::new_static(CLIENT_ASSERTION_TYPE_JWT_BEARER)), @@ -906,11 +921,9 @@ fn build_auth<'a>( &algs, // https://datatracker.ietf.org/doc/html/rfc7523#section-3 RegisteredClaims { - iss: Some(client_id.clone()), - sub: Some(client_id), - aud: Some(RegisteredClaimsAud::Single( - server_metadata.issuer.clone(), - )), + iss: Some(client_id_str), + sub: Some(client_id_str), + aud: Some(RegisteredClaimsAud::Single(issuer_str)), exp: Some(iat + 60), // "iat" is required and **MUST** be less than one minute // https://datatracker.ietf.org/doc/html/rfc9101 @@ -928,7 +941,7 @@ fn build_auth<'a>( "none" if method_supported .as_ref() - .is_some_and(|v| v.contains(&CowStr::new_static("none"))) => + .is_some_and(|v| v.iter().any(|s| s.as_str() == "none")) => { return Ok(ClientAuth::new_id(client_id)); } @@ -945,8 +958,11 @@ mod tests { use crate::types::{OAuthAuthorizationServerMetadata, OAuthClientMetadata}; use bytes::Bytes; use http::{Response as HttpResponse, StatusCode}; - use jacquard_common::{deps::fluent_uri::Uri, http_client::HttpClient, types::string::Did}; + use jacquard_common::{ + bos::BosStr, deps::fluent_uri::Uri, http_client::HttpClient, types::string::Did, + }; use jacquard_identity::resolver::IdentityResolver; + use smol_str::SmolStr; use std::sync::Arc; use tokio::sync::Mutex; @@ -976,15 +992,15 @@ mod tests { LazyLock::new(|| jacquard_identity::resolver::ResolverOptions::default()); &OPTS } - async fn resolve_handle( + async fn resolve_handle( &self, - _handle: &jacquard_common::types::string::Handle<'_>, - ) -> std::result::Result, jacquard_identity::resolver::IdentityError> { + _handle: &jacquard_common::types::string::Handle, + ) -> std::result::Result { Ok(Did::new_static("did:plc:alice").unwrap()) } - async fn resolve_did_doc( + async fn resolve_did_doc( &self, - _did: &Did<'_>, + _did: &Did, ) -> std::result::Result< jacquard_identity::resolver::DidDocResponse, jacquard_identity::resolver::IdentityError, @@ -1012,21 +1028,21 @@ mod tests { fn base_metadata() -> OAuthMetadata { let mut server = OAuthAuthorizationServerMetadata::default(); - server.issuer = CowStr::from("https://issuer"); - server.authorization_endpoint = CowStr::from("https://issuer/authorize"); - server.token_endpoint = CowStr::from("https://issuer/token"); - server.token_endpoint_auth_methods_supported = Some(vec![CowStr::from("none")]); + server.issuer = SmolStr::new_static("https://issuer"); + server.authorization_endpoint = SmolStr::new_static("https://issuer/authorize"); + server.token_endpoint = SmolStr::new_static("https://issuer/token"); + server.token_endpoint_auth_methods_supported = Some(vec![SmolStr::new_static("none")]); OAuthMetadata { server_metadata: server, client_metadata: OAuthClientMetadata { - client_id: CowStr::new_static("https://client"), + client_id: SmolStr::new_static("https://client"), client_uri: None, - redirect_uris: vec![CowStr::new_static("https://client/cb")], - scope: Some(CowStr::from("atproto")), + redirect_uris: vec![SmolStr::new_static("https://client/cb")], + scope: Some(SmolStr::new_static("atproto")), grant_types: None, - response_types: vec![CowStr::new_static("code")], - application_type: Some(CowStr::new_static("web")), - token_endpoint_auth_method: Some(CowStr::from("none")), + response_types: vec![SmolStr::new_static("code")], + application_type: Some(SmolStr::new_static("web")), + token_endpoint_auth_method: Some(SmolStr::new_static("none")), dpop_bound_access_tokens: None, jwks_uri: None, jwks: None, @@ -1046,7 +1062,7 @@ mod tests { meta.server_metadata.require_pushed_authorization_requests = Some(true); meta.server_metadata.pushed_authorization_request_endpoint = None; // require_pushed_authorization_requests is true and no endpoint - let err = super::par(&MockClient::default(), None, None, &meta, None) + let err = super::par(&MockClient::default(), None, None, &mut meta, None) .await .unwrap_err(); assert!( @@ -1060,24 +1076,24 @@ mod tests { let meta = base_metadata(); let session = ClientSessionData { account_did: Did::new_static("did:plc:alice").unwrap(), - session_id: CowStr::from("state"), + session_id: SmolStr::new_static("state"), host_url: Uri::parse("https://pds").expect("valid").to_owned(), - authserver_url: CowStr::new_static("https://issuer"), - authserver_token_endpoint: CowStr::from("https://issuer/token"), + authserver_url: SmolStr::new_static("https://issuer"), + authserver_token_endpoint: SmolStr::new_static("https://issuer/token"), authserver_revocation_endpoint: None, scopes: vec![], dpop_data: DpopClientData { - dpop_key: crate::utils::generate_key(&[CowStr::from("ES256")]).unwrap(), - dpop_authserver_nonce: CowStr::from(""), - dpop_host_nonce: CowStr::from(""), + dpop_key: crate::utils::generate_key(&[SmolStr::new_static("ES256")]).unwrap(), + dpop_authserver_nonce: SmolStr::default(), + dpop_host_nonce: SmolStr::default(), }, token_set: crate::types::TokenSet { - iss: CowStr::from("https://issuer"), + iss: SmolStr::new_static("https://issuer"), sub: Did::new_static("did:plc:alice").unwrap(), - aud: CowStr::from("https://pds"), + aud: SmolStr::new_static("https://pds"), scope: None, refresh_token: None, - access_token: CowStr::from("abc"), + access_token: SmolStr::new_static("abc"), token_type: crate::types::OAuthTokenType::DPoP, expires_at: None, }, @@ -1105,7 +1121,7 @@ mod tests { ); let meta = base_metadata(); let mut dpop = DpopReqData { - dpop_key: crate::utils::generate_key(&[CowStr::from("ES256")]).unwrap(), + dpop_key: crate::utils::generate_key(&[SmolStr::new_static("ES256")]).unwrap(), dpop_authserver_nonce: None, }; let err = super::exchange_code(&client, &mut dpop, "abc", "verifier", &meta) diff --git a/crates/jacquard-oauth/src/resolver.rs b/crates/jacquard-oauth/src/resolver.rs index 2aa998c4c..414430419 100644 --- a/crates/jacquard-oauth/src/resolver.rs +++ b/crates/jacquard-oauth/src/resolver.rs @@ -3,10 +3,9 @@ use std::future::Future; use crate::types::{OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata}; use http::{Request, StatusCode}; -use jacquard_common::CowStr; +#[cfg(not(target_arch = "wasm32"))] +use jacquard_common::BosStr; use jacquard_common::IntoStatic; -#[allow(unused_imports)] -use jacquard_common::cowstr::ToCowStr; use jacquard_common::deps::fluent_uri::Uri; use jacquard_common::types::did_doc::DidDocument; use jacquard_common::types::ident::AtIdentifier; @@ -96,7 +95,7 @@ pub enum ResolverErrorKind { code(jacquard_oauth::resolver::unsupported_did_method), help("supported DID methods: did:web, did:plc") )] - UnsupportedDidMethod(Did<'static>), + UnsupportedDidMethod(Did), /// HTTP transport error #[error("transport error")] @@ -245,7 +244,7 @@ impl ResolverError { } /// Create an unsupported DID method error - pub fn unsupported_did_method(did: Did<'static>) -> Self { + pub fn unsupported_did_method(did: Did) -> Self { Self::new(ResolverErrorKind::UnsupportedDidMethod(did), None) } @@ -319,47 +318,46 @@ impl From for ResolverError { // } #[cfg(not(target_arch = "wasm32"))] -async fn verify_issuer_impl( +async fn verify_issuer_impl( resolver: &T, - server_metadata: &OAuthAuthorizationServerMetadata<'_>, - sub: &Did<'_>, + server_metadata: &OAuthAuthorizationServerMetadata, + sub: &Did, ) -> Result> { let (metadata, identity) = resolver.resolve_from_identity(sub.as_str()).await?; - if metadata.issuer != server_metadata.issuer { + if metadata.issuer.as_str() != server_metadata.issuer.as_str() { return Err(ResolverError::authorization_server_metadata( "issuer mismatch", )); } Ok(identity .pds_endpoint() - .ok_or_else(|| ResolverError::did_document(smol_str::format_smolstr!("{:?}", identity)))?) + .ok_or_else(|| ResolverError::did_document(smol_str::format_smolstr!("{:?}", identity)))? + .to_owned()) } #[cfg(target_arch = "wasm32")] -async fn verify_issuer_impl( +async fn verify_issuer_impl( resolver: &T, - server_metadata: &OAuthAuthorizationServerMetadata<'_>, - sub: &Did<'_>, + server_metadata: &OAuthAuthorizationServerMetadata, + sub: &Did, ) -> Result> { let (metadata, identity) = resolver.resolve_from_identity(sub.as_str()).await?; - if metadata.issuer != server_metadata.issuer { + if metadata.issuer.as_str() != server_metadata.issuer.as_str() { return Err(ResolverError::authorization_server_metadata( "issuer mismatch", )); } Ok(identity .pds_endpoint() - .ok_or_else(|| ResolverError::did_document(smol_str::format_smolstr!("{:?}", identity)))?) + .ok_or_else(|| ResolverError::did_document(smol_str::format_smolstr!("{:?}", identity)))? + .to_owned()) } #[cfg(not(target_arch = "wasm32"))] async fn resolve_oauth_impl( resolver: &T, input: &str, -) -> Result<( - OAuthAuthorizationServerMetadata<'static>, - Option>, -)> { +) -> Result<(OAuthAuthorizationServerMetadata, Option)> { // Allow using an entryway, or PDS url, directly as login input (e.g. // when the user forgot their handle, or when the handle does not // resolve to a DID) @@ -370,10 +368,7 @@ async fn resolve_oauth_impl( err.with_context("failed to parse service URL") })? .to_owned(); - ( - resolver.resolve_from_service(&uri.as_str().into()).await?, - None, - ) + (resolver.resolve_from_service(uri.as_str()).await?, None) } else { let (metadata, identity) = resolver.resolve_from_identity(input).await?; (metadata, Some(identity)) @@ -384,10 +379,7 @@ async fn resolve_oauth_impl( async fn resolve_oauth_impl( resolver: &T, input: &str, -) -> Result<( - OAuthAuthorizationServerMetadata<'static>, - Option>, -)> { +) -> Result<(OAuthAuthorizationServerMetadata, Option)> { // Allow using an entryway, or PDS url, directly as login input (e.g. // when the user forgot their handle, or when the handle does not // resolve to a DID) @@ -398,10 +390,7 @@ async fn resolve_oauth_impl( err.with_context("failed to parse service URL") })? .to_owned(); - ( - resolver.resolve_from_service(&uri.as_str().into()).await?, - None, - ) + (resolver.resolve_from_service(uri.as_str()).await?, None) } else { let (metadata, identity) = resolver.resolve_from_identity(input).await?; (metadata, Some(identity)) @@ -411,8 +400,8 @@ async fn resolve_oauth_impl( #[cfg(not(target_arch = "wasm32"))] async fn resolve_from_service_impl( resolver: &T, - input: &CowStr<'_>, -) -> Result> { + input: &str, +) -> Result { // Assume first that input is a PDS URL (as required by ATPROTO) if let Ok(metadata) = resolver.get_resource_server_metadata(input).await { return Ok(metadata); @@ -424,8 +413,8 @@ async fn resolve_from_service_impl( #[cfg(target_arch = "wasm32")] async fn resolve_from_service_impl( resolver: &T, - input: &CowStr<'_>, -) -> Result> { + input: &str, +) -> Result { // Assume first that input is a PDS URL (as required by ATPROTO) if let Ok(metadata) = resolver.get_resource_server_metadata(input).await { return Ok(metadata); @@ -438,19 +427,12 @@ async fn resolve_from_service_impl( async fn resolve_from_identity_impl( resolver: &T, input: &str, -) -> Result<( - OAuthAuthorizationServerMetadata<'static>, - DidDocument<'static>, -)> { +) -> Result<(OAuthAuthorizationServerMetadata, DidDocument)> { let actor = AtIdentifier::new(input) .map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?; let identity = resolver.resolve_ident_owned(&actor).await?; if let Some(pds) = &identity.pds_endpoint() { - use jacquard_common::cowstr::ToCowStr; - - let metadata = resolver - .get_resource_server_metadata(&pds.to_cowstr()) - .await?; + let metadata = resolver.get_resource_server_metadata(pds.as_str()).await?; Ok((metadata, identity)) } else { Err(ResolverError::did_document("Did doc lacking pds")) @@ -461,17 +443,12 @@ async fn resolve_from_identity_impl( async fn resolve_from_identity_impl( resolver: &T, input: &str, -) -> Result<( - OAuthAuthorizationServerMetadata<'static>, - DidDocument<'static>, -)> { +) -> Result<(OAuthAuthorizationServerMetadata, DidDocument)> { let actor = AtIdentifier::new(input) .map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?; let identity = resolver.resolve_ident_owned(&actor).await?; if let Some(pds) = &identity.pds_endpoint() { - let metadata = resolver - .get_resource_server_metadata(&pds.to_cowstr()) - .await?; + let metadata = resolver.get_resource_server_metadata(pds.as_str()).await?; Ok((metadata, identity)) } else { Err(ResolverError::did_document("Did doc lacking pds")) @@ -481,28 +458,28 @@ async fn resolve_from_identity_impl( #[cfg(not(target_arch = "wasm32"))] async fn get_authorization_server_metadata_impl( client: &T, - issuer: &CowStr<'_>, -) -> Result> { + issuer: &str, +) -> Result { let mut md = resolve_authorization_server(client, issuer).await?; - md.issuer = issuer.clone().into_static(); + md.issuer = SmolStr::from(issuer); Ok(md) } #[cfg(target_arch = "wasm32")] async fn get_authorization_server_metadata_impl( client: &T, - issuer: &CowStr<'_>, -) -> Result> { + issuer: &str, +) -> Result { let mut md = resolve_authorization_server(client, issuer).await?; - md.issuer = issuer.clone().into_static(); + md.issuer = SmolStr::from(issuer); Ok(md) } #[cfg(not(target_arch = "wasm32"))] async fn get_resource_server_metadata_impl( resolver: &T, - pds: &CowStr<'_>, -) -> Result> { + pds: &str, +) -> Result { let rs_metadata = resolve_protected_resource_info(resolver, pds).await?; // ATPROTO requires one, and only one, authorization server entry // > That document MUST contain a single item in the authorization_servers array. @@ -524,14 +501,19 @@ async fn get_resource_server_metadata_impl( )); } }; - let as_metadata = resolver.get_authorization_server_metadata(issuer).await?; + let as_metadata = resolver + .get_authorization_server_metadata(issuer.as_ref()) + .await?; // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada if let Some(protected_resources) = &as_metadata.protected_resources { let resource_url = rs_metadata .resource .strip_suffix('/') .unwrap_or(rs_metadata.resource.as_str()); - if !protected_resources.contains(&CowStr::Borrowed(resource_url)) { + if !protected_resources + .iter() + .any(|s| s.as_str() == resource_url) + { return Err(ResolverError::authorization_server_metadata( smol_str::format_smolstr!( "pds {pds}, resource {0} not protected by issuer: {issuer}, protected resources: {1:?}", @@ -559,8 +541,8 @@ async fn get_resource_server_metadata_impl( #[cfg(target_arch = "wasm32")] async fn get_resource_server_metadata_impl( resolver: &T, - pds: &CowStr<'_>, -) -> Result> { + pds: &str, +) -> Result { let rs_metadata = resolve_protected_resource_info(resolver, pds).await?; // ATPROTO requires one, and only one, authorization server entry // > That document MUST contain a single item in the authorization_servers array. @@ -582,14 +564,19 @@ async fn get_resource_server_metadata_impl( )); } }; - let as_metadata = resolver.get_authorization_server_metadata(issuer).await?; + let as_metadata = resolver + .get_authorization_server_metadata(issuer.as_ref()) + .await?; // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada if let Some(protected_resources) = &as_metadata.protected_resources { let resource_url = rs_metadata .resource .strip_suffix('/') .unwrap_or(rs_metadata.resource.as_str()); - if !protected_resources.contains(&CowStr::Borrowed(resource_url)) { + if !protected_resources + .iter() + .any(|s| s.as_str() == resource_url) + { return Err(ResolverError::authorization_server_metadata( smol_str::format_smolstr!( "pds {pds}, resource {0} not protected by issuer: {issuer}, protected resources: {1:?}", @@ -628,10 +615,10 @@ async fn get_resource_server_metadata_impl( pub trait OAuthResolver: IdentityResolver + HttpClient { /// Verify that the authorization server in `server_metadata` is the correct issuer for `sub`. #[cfg(not(target_arch = "wasm32"))] - fn verify_issuer( + fn verify_issuer( &self, - server_metadata: &OAuthAuthorizationServerMetadata<'_>, - sub: &Did<'_>, + server_metadata: &OAuthAuthorizationServerMetadata, + sub: &Did, ) -> impl Future>> + Send where Self: Sync, @@ -641,10 +628,10 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { /// Verify that the authorization server in `server_metadata` is the correct issuer for `sub`. #[cfg(target_arch = "wasm32")] - fn verify_issuer( + fn verify_issuer( &self, - server_metadata: &OAuthAuthorizationServerMetadata<'_>, - sub: &Did<'_>, + server_metadata: &OAuthAuthorizationServerMetadata, + sub: &Did, ) -> impl Future>> { verify_issuer_impl(self, server_metadata, sub) } @@ -659,12 +646,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { fn resolve_oauth( &self, input: &str, - ) -> impl Future< - Output = Result<( - OAuthAuthorizationServerMetadata<'static>, - Option>, - )>, - > + Send + ) -> impl Future)>> + Send where Self: Sync, { @@ -681,12 +663,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { fn resolve_oauth( &self, input: &str, - ) -> impl Future< - Output = Result<( - OAuthAuthorizationServerMetadata<'static>, - Option>, - )>, - > { + ) -> impl Future)>> { resolve_oauth_impl(self, input) } @@ -697,8 +674,8 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { #[cfg(not(target_arch = "wasm32"))] fn resolve_from_service( &self, - input: &CowStr<'_>, - ) -> impl Future>> + Send + input: &str, + ) -> impl Future> + Send where Self: Sync, { @@ -712,8 +689,8 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { #[cfg(target_arch = "wasm32")] fn resolve_from_service( &self, - input: &CowStr<'_>, - ) -> impl Future>> { + input: &str, + ) -> impl Future> { resolve_from_service_impl(self, input) } @@ -722,12 +699,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { fn resolve_from_identity( &self, input: &str, - ) -> impl Future< - Output = Result<( - OAuthAuthorizationServerMetadata<'static>, - DidDocument<'static>, - )>, - > + Send + ) -> impl Future> + Send where Self: Sync, { @@ -739,12 +711,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { fn resolve_from_identity( &self, input: &str, - ) -> impl Future< - Output = Result<( - OAuthAuthorizationServerMetadata<'static>, - DidDocument<'static>, - )>, - > { + ) -> impl Future> { resolve_from_identity_impl(self, input) } @@ -755,8 +722,8 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { #[cfg(not(target_arch = "wasm32"))] fn get_authorization_server_metadata( &self, - issuer: &CowStr<'_>, - ) -> impl Future>> + Send + issuer: &str, + ) -> impl Future> + Send where Self: Sync, { @@ -770,8 +737,8 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { #[cfg(target_arch = "wasm32")] fn get_authorization_server_metadata( &self, - issuer: &CowStr<'_>, - ) -> impl Future>> { + issuer: &str, + ) -> impl Future> { get_authorization_server_metadata_impl(self, issuer) } @@ -779,8 +746,8 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { #[cfg(not(target_arch = "wasm32"))] fn get_resource_server_metadata( &self, - pds: &CowStr<'_>, - ) -> impl Future>> + Send + pds: &str, + ) -> impl Future> + Send where Self: Sync, { @@ -791,8 +758,8 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { #[cfg(target_arch = "wasm32")] fn get_resource_server_metadata( &self, - pds: &CowStr<'_>, - ) -> impl Future>> { + pds: &str, + ) -> impl Future> { get_resource_server_metadata_impl(self, pds) } } @@ -803,8 +770,8 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { /// this prevents a compromised server from claiming to be a different issuer. pub async fn resolve_authorization_server( client: &T, - server: &CowStr<'_>, -) -> Result> { + server: &str, +) -> Result { let url = format!( "{}/.well-known/oauth-authorization-server", server.trim_end_matches("/") @@ -821,7 +788,7 @@ pub async fn resolve_authorization_server( if res.status() == StatusCode::OK { let metadata = serde_json::from_slice::(res.body())?; // https://datatracker.ietf.org/doc/html/rfc8414#section-3.3 - if metadata.issuer == server.as_str() { + if metadata.issuer.as_str() == server { Ok(metadata.into_static()) } else { Err(ResolverError::authorization_server_metadata( @@ -839,8 +806,8 @@ pub async fn resolve_authorization_server( /// that the metadata belongs to the PDS we queried and not a different resource. pub async fn resolve_protected_resource_info( client: &T, - server: &CowStr<'_>, -) -> Result> { + server: &str, +) -> Result { let url = format!( "{}/.well-known/oauth-protected-resource", server.trim_end_matches("/") @@ -857,7 +824,7 @@ pub async fn resolve_protected_resource_info( if res.status() == StatusCode::OK { let metadata = serde_json::from_slice::(res.body())?; // https://datatracker.ietf.org/doc/html/rfc8414#section-3.3 - if metadata.resource == server.as_str() { + if metadata.resource.as_str() == server { Ok(metadata.into_static()) } else { Err(ResolverError::authorization_server_metadata( @@ -878,7 +845,7 @@ mod tests { use super::*; use http::{Request as HttpRequest, Response as HttpResponse, StatusCode}; - use jacquard_common::http_client::HttpClient; + use jacquard_common::{CowStr, http_client::HttpClient}; use tokio::sync::Mutex; #[derive(Default, Clone)] diff --git a/crates/jacquard-oauth/src/scopes.rs b/crates/jacquard-oauth/src/scopes.rs index fd1b96da6..120514632 100644 --- a/crates/jacquard-oauth/src/scopes.rs +++ b/crates/jacquard-oauth/src/scopes.rs @@ -21,29 +21,31 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt; +use std::marker::PhantomData; use std::str::FromStr; +use jacquard_common::bos::{BosStr, DefaultStr}; use jacquard_common::types::did::Did; use jacquard_common::types::nsid::Nsid; use jacquard_common::types::string::AtStrError; -use jacquard_common::{CowStr, IntoStatic}; +use jacquard_common::{Bos, FromStaticStr, IntoStatic}; use serde::de::Visitor; use serde::{Deserialize, Serialize}; -use smol_str::{SmolStr, ToSmolStr}; +use smol_str::{SmolStr, SmolStrBuilder, ToSmolStr, format_smolstr}; /// Represents an AT Protocol OAuth scope #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Scope<'s> { +pub enum Scope { /// Account scope for accessing account information Account(AccountScope), /// Identity scope for accessing identity information Identity(IdentityScope), /// Blob scope for blob operations with mime type constraints - Blob(BlobScope<'s>), + Blob(BlobScope), /// Repository scope for collection operations - Repo(RepoScope<'s>), + Repo(RepoScope), /// RPC scope for method access - Rpc(RpcScope<'s>), + Rpc(RpcScope), /// AT Protocol scope - required to indicate that other AT Protocol scopes will be used Atproto, /// Transition scope for migration operations @@ -56,24 +58,31 @@ pub enum Scope<'s> { Email, } -impl Serialize for Scope<'_> { - fn serialize(&self, serializer: S) -> Result +impl Serialize for Scope { + fn serialize(&self, serializer: Ser) -> Result where - S: serde::Serializer, + Ser: serde::Serializer, { serializer.serialize_str(&self.to_string_normalized()) } } -impl<'de> Deserialize<'de> for Scope<'_> { +impl<'de, S> Deserialize<'de> for Scope +where + S: BosStr + Ord + Deserialize<'de> + FromStr, + ::Err: core::fmt::Debug, +{ fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - struct ScopeVisitor; + struct ScopeVisitor(PhantomData); - impl Visitor<'_> for ScopeVisitor { - type Value = Scope<'static>; + impl Visitor<'_> for ScopeVisitor + where + ::Err: core::fmt::Debug, + { + type Value = Scope; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "a scope string") @@ -82,17 +91,20 @@ impl<'de> Deserialize<'de> for Scope<'_> { where E: serde::de::Error, { - Scope::parse(v) - .map(|s| s.into_static()) - .map_err(|e| serde::de::Error::custom(format!("{:?}", e))) + Scope::parse(v).map_err(|e| serde::de::Error::custom(format!("{:?}", e))) } } - deserializer.deserialize_str(ScopeVisitor) + deserializer + .deserialize_str(ScopeVisitor(PhantomData)) + .map(|scope| scope) } } -impl IntoStatic for Scope<'_> { - type Output = Scope<'static>; +impl IntoStatic for Scope +where + S::Output: BosStr + Ord, +{ + type Output = Scope; fn into_static(self) -> Self::Output { match self { @@ -159,13 +171,26 @@ pub enum TransitionScope { /// Blob scope with mime type constraints #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct BlobScope<'s> { +pub struct BlobScope { /// Accepted mime types - pub accept: BTreeSet>, + pub accept: BTreeSet>, } -impl IntoStatic for BlobScope<'_> { - type Output = BlobScope<'static>; +impl + Ord> BlobScope { + /// Convert to a `BlobScope` with a different backing type. + pub fn convert + From + AsRef + FromStaticStr + Ord>(self) -> BlobScope { + BlobScope { + accept: self.accept.into_iter().map(|p| p.convert()).collect(), + } + } +} + +impl IntoStatic for BlobScope +where + S::Output: BosStr, + MimePattern: Ord, +{ + type Output = BlobScope; fn into_static(self) -> Self::Output { BlobScope { @@ -176,17 +201,31 @@ impl IntoStatic for BlobScope<'_> { /// MIME type pattern for blob scope #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum MimePattern<'s> { +pub enum MimePattern { /// Match all types All, /// Match all subtypes of a type (e.g., "image/*") - TypeWildcard(CowStr<'s>), + TypeWildcard(S), /// Exact mime type match - Exact(CowStr<'s>), + Exact(S), +} + +impl MimePattern { + /// Convert to a `MimePattern` with a different backing type. + pub fn convert + From + AsRef + FromStaticStr>(self) -> MimePattern { + match self { + MimePattern::All => MimePattern::All, + MimePattern::TypeWildcard(s) => MimePattern::TypeWildcard(s.into()), + MimePattern::Exact(s) => MimePattern::Exact(s.into()), + } + } } -impl IntoStatic for MimePattern<'_> { - type Output = MimePattern<'static>; +impl IntoStatic for MimePattern +where + S::Output: BosStr, +{ + type Output = MimePattern; fn into_static(self) -> Self::Output { match self { @@ -199,15 +238,28 @@ impl IntoStatic for MimePattern<'_> { /// Repository scope with collection and action constraints #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct RepoScope<'s> { +pub struct RepoScope { /// Collection NSID or wildcard - pub collection: RepoCollection<'s>, + pub collection: RepoCollection, /// Allowed actions pub actions: BTreeSet, } -impl IntoStatic for RepoScope<'_> { - type Output = RepoScope<'static>; +impl RepoScope { + /// Convert to a `RepoScope` with a different backing type. + pub fn convert + From + AsRef + FromStaticStr + Ord>(self) -> RepoScope { + RepoScope { + collection: self.collection.convert(), + actions: self.actions, + } + } +} + +impl IntoStatic for RepoScope +where + S::Output: BosStr, +{ + type Output = RepoScope; fn into_static(self) -> Self::Output { RepoScope { @@ -219,15 +271,28 @@ impl IntoStatic for RepoScope<'_> { /// Repository collection identifier #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum RepoCollection<'s> { +pub enum RepoCollection { /// All collections (wildcard) All, /// Specific collection NSID - Nsid(Nsid<'s>), + Nsid(Nsid), +} + +impl RepoCollection { + /// Convert to an `Nsid` with a different backing type. + pub fn convert + From + AsRef + FromStaticStr>(self) -> RepoCollection { + match self { + RepoCollection::All => RepoCollection::All, + RepoCollection::Nsid(nsid) => RepoCollection::Nsid(nsid.convert()), + } + } } -impl IntoStatic for RepoCollection<'_> { - type Output = RepoCollection<'static>; +impl IntoStatic for RepoCollection +where + S::Output: BosStr, +{ + type Output = RepoCollection; fn into_static(self) -> Self::Output { match self { @@ -250,15 +315,30 @@ pub enum RepoAction { /// RPC scope with lexicon method and audience constraints #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct RpcScope<'s> { +pub struct RpcScope { /// Lexicon methods (NSIDs or wildcard) - pub lxm: BTreeSet>, + pub lxm: BTreeSet>, /// Audiences (DIDs or wildcard) - pub aud: BTreeSet>, + pub aud: BTreeSet>, +} + +impl RpcScope { + /// Convert to a `RpcScope` with a different backing type. + pub fn convert + From + AsRef + FromStaticStr + Ord>(self) -> RpcScope { + RpcScope { + lxm: self.lxm.into_iter().map(|s| s.convert()).collect(), + aud: self.aud.into_iter().map(|s| s.convert()).collect(), + } + } } -impl IntoStatic for RpcScope<'_> { - type Output = RpcScope<'static>; +impl IntoStatic for RpcScope +where + S::Output: BosStr, + RpcLexicon: Ord, + RpcAudience: Ord, +{ + type Output = RpcScope; fn into_static(self) -> Self::Output { RpcScope { @@ -270,15 +350,28 @@ impl IntoStatic for RpcScope<'_> { /// RPC lexicon identifier #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum RpcLexicon<'s> { +pub enum RpcLexicon { /// All lexicons (wildcard) All, /// Specific lexicon NSID - Nsid(Nsid<'s>), + Nsid(Nsid), } -impl IntoStatic for RpcLexicon<'_> { - type Output = RpcLexicon<'static>; +impl RpcLexicon { + /// Convert to an `Nsid` with a different backing type. + pub fn convert + From + AsRef + FromStaticStr>(self) -> RpcLexicon { + match self { + RpcLexicon::All => RpcLexicon::All, + RpcLexicon::Nsid(nsid) => RpcLexicon::Nsid(nsid.convert()), + } + } +} + +impl IntoStatic for RpcLexicon +where + S::Output: BosStr, +{ + type Output = RpcLexicon; fn into_static(self) -> Self::Output { match self { @@ -290,15 +383,28 @@ impl IntoStatic for RpcLexicon<'_> { /// RPC audience identifier #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum RpcAudience<'s> { +pub enum RpcAudience { /// All audiences (wildcard) All, /// Specific DID - Did(Did<'s>), + Did(Did), +} + +impl RpcAudience { + /// Convert to an `Nsid` with a different backing type. + pub fn convert + From + AsRef + FromStaticStr>(self) -> RpcAudience { + match self { + RpcAudience::All => RpcAudience::All, + RpcAudience::Did(did) => RpcAudience::Did(did.convert()), + } + } } -impl IntoStatic for RpcAudience<'_> { - type Output = RpcAudience<'static>; +impl IntoStatic for RpcAudience +where + S::Output: BosStr, +{ + type Output = RpcAudience; fn into_static(self) -> Self::Output { match self { @@ -308,7 +414,23 @@ impl IntoStatic for RpcAudience<'_> { } } -impl<'s> Scope<'s> { +impl Scope { + /// Convert to a `Scope` with a different backing type. + pub fn convert + From + AsRef + FromStaticStr + Ord>(self) -> Scope { + match self { + Scope::Account(scope) => Scope::Account(scope), + Scope::Identity(scope) => Scope::Identity(scope), + Scope::Blob(scope) => Scope::Blob(scope.convert()), + Scope::Repo(scope) => Scope::Repo(scope.convert()), + Scope::Rpc(scope) => Scope::Rpc(scope.convert()), + Scope::Atproto => Scope::Atproto, + Scope::Transition(scope) => Scope::Transition(scope), + Scope::OpenId => Scope::OpenId, + Scope::Profile => Scope::Profile, + Scope::Email => Scope::Email, + } + } + /// Parse multiple space-separated scopes from a string /// /// # Examples @@ -317,7 +439,11 @@ impl<'s> Scope<'s> { /// let scopes = Scope::parse_multiple("atproto repo:*").unwrap(); /// assert_eq!(scopes.len(), 2); /// ``` - pub fn parse_multiple(s: &'s str) -> Result, ParseError> { + pub fn parse_multiple<'a>(s: &'a str) -> Result, ParseError> + where + S: FromStr, + ::Err: core::fmt::Debug, + { if s.trim().is_empty() { return Ok(Vec::new()); } @@ -342,7 +468,11 @@ impl<'s> Scope<'s> { /// let scopes = Scope::parse_multiple_reduced("atproto repo:app.bsky.feed.post repo:*").unwrap(); /// assert_eq!(scopes.len(), 2); // atproto and repo:* /// ``` - pub fn parse_multiple_reduced(s: &'s str) -> Result, ParseError> { + pub fn parse_multiple_reduced<'a>(s: &'a str) -> Result, ParseError> + where + S: FromStr, + ::Err: core::fmt::Debug, + { let all_scopes = Self::parse_multiple(s)?; if all_scopes.is_empty() { @@ -403,18 +533,25 @@ impl<'s> Scope<'s> { /// let result = Scope::serialize_multiple(&scopes); /// assert_eq!(result, "account:email atproto repo:*"); /// ``` - pub fn serialize_multiple(scopes: &[Self]) -> CowStr<'static> { + pub fn serialize_multiple(scopes: &[Self]) -> SmolStr { if scopes.is_empty() { - return CowStr::default(); + return SmolStr::new_static(""); } - let mut serialized: Vec = scopes + let mut serialized: Vec = scopes .iter() .map(|scope| scope.to_string_normalized()) .collect(); serialized.sort(); - serialized.join(" ").into() + let mut builder = SmolStrBuilder::new(); + for (i, scope) in serialized.iter().enumerate() { + if i > 0 { + builder.push_str(" "); + } + builder.push_str(scope); + } + builder.finish() } /// Remove a scope from a list of scopes @@ -435,7 +572,10 @@ impl<'s> Scope<'s> { /// assert_eq!(result.len(), 2); /// assert!(!result.contains(&to_remove)); /// ``` - pub fn remove_scope(scopes: &[Self], scope_to_remove: &Self) -> Vec { + pub fn remove_scope(scopes: &[Self], scope_to_remove: &Self) -> Vec + where + S: Clone, + { scopes .iter() .filter(|s| *s != scope_to_remove) @@ -444,7 +584,11 @@ impl<'s> Scope<'s> { } /// Parse a scope from a string - pub fn parse(s: &'s str) -> Result { + pub fn parse<'a>(s: &'a str) -> Result + where + S: FromStr, + ::Err: core::fmt::Debug, + { // Determine the prefix first by checking for known prefixes let prefixes = [ "account", @@ -500,7 +644,7 @@ impl<'s> Scope<'s> { } } - fn parse_account(suffix: Option<&'s str>) -> Result { + fn parse_account(suffix: Option<&str>) -> Result { let (resource_str, params) = match suffix { Some(s) => { if let Some(pos) = s.find('?') { @@ -538,7 +682,7 @@ impl<'s> Scope<'s> { Ok(Scope::Account(AccountScope { resource, action })) } - fn parse_identity(suffix: Option<&'s str>) -> Result { + fn parse_identity(suffix: Option<&str>) -> Result { let scope = match suffix { Some("handle") => IdentityScope::Handle, Some("*") => IdentityScope::All, @@ -549,15 +693,19 @@ impl<'s> Scope<'s> { Ok(Scope::Identity(scope)) } - fn parse_blob(suffix: Option<&'s str>) -> Result { - let mut accept = BTreeSet::new(); + fn parse_blob<'a>(suffix: Option<&'a str>) -> Result + where + S: FromStr, + ::Err: core::fmt::Debug, + { + let mut accept: BTreeSet> = BTreeSet::new(); match suffix { Some(s) if s.starts_with('?') => { let params = parse_query_string(&s[1..]); if let Some(values) = params.get("accept") { for value in values { - accept.insert(MimePattern::from_str(value)?); + accept.insert(MimePattern::from_str(*value)?); } } } @@ -576,7 +724,10 @@ impl<'s> Scope<'s> { Ok(Scope::Blob(BlobScope { accept })) } - fn parse_repo(suffix: Option<&'s str>) -> Result { + fn parse_repo<'a>(suffix: Option<&'a str>) -> Result + where + S: FromStr, + { let (collection_str, params) = match suffix { Some(s) => { if let Some(pos) = s.find('?') { @@ -590,7 +741,7 @@ impl<'s> Scope<'s> { let collection = match collection_str { Some("*") | None => RepoCollection::All, - Some(nsid) => RepoCollection::Nsid(Nsid::new(nsid)?), + Some(nsid) => RepoCollection::Nsid(Nsid::from_str(nsid)?), }; let mut actions = BTreeSet::new(); @@ -631,7 +782,10 @@ impl<'s> Scope<'s> { })) } - fn parse_rpc(suffix: Option<&'s str>) -> Result { + fn parse_rpc<'a>(suffix: Option<&'a str>) -> Result + where + S: FromStr, + { let mut lxm = BTreeSet::new(); let mut aud = BTreeSet::new(); @@ -645,20 +799,20 @@ impl<'s> Scope<'s> { if let Some(values) = params.get("lxm") { for value in values { - if value.as_ref() == "*" { + if *value == "*" { lxm.insert(RpcLexicon::All); } else { - lxm.insert(RpcLexicon::Nsid(Nsid::new(value)?.into_static())); + lxm.insert(RpcLexicon::Nsid(Nsid::from_str(*value)?)); } } } if let Some(values) = params.get("aud") { for value in values { - if value.as_ref() == "*" { + if *value == "*" { aud.insert(RpcAudience::All); } else { - aud.insert(RpcAudience::Did(Did::new(value)?.into_static())); + aud.insert(RpcAudience::Did(Did::from_str(*value)?)); } } } @@ -669,19 +823,19 @@ impl<'s> Scope<'s> { let nsid = &s[..pos]; let params = parse_query_string(&s[pos + 1..]); - lxm.insert(RpcLexicon::Nsid(Nsid::new(nsid)?.into_static())); + lxm.insert(RpcLexicon::Nsid(Nsid::from_str(nsid)?)); if let Some(values) = params.get("aud") { for value in values { - if value.as_ref() == "*" { + if *value == "*" { aud.insert(RpcAudience::All); } else { - aud.insert(RpcAudience::Did(Did::new(value)?.into_static())); + aud.insert(RpcAudience::Did(Did::from_str(*value)?)); } } } } else { - lxm.insert(RpcLexicon::Nsid(Nsid::new(s)?.into_static())); + lxm.insert(RpcLexicon::Nsid(Nsid::from_str(s)?)); } } None => {} @@ -743,9 +897,11 @@ impl<'s> Scope<'s> { } Ok(Scope::Email) } +} +impl Scope { /// Convert the scope to its normalized string representation - pub fn to_string_normalized(&self) -> String { + pub fn to_string_normalized(&self) -> SmolStr { match self { Scope::Account(scope) => { let resource = match scope.resource { @@ -755,36 +911,42 @@ impl<'s> Scope<'s> { }; match scope.action { - AccountAction::Read => format!("account:{}", resource), - AccountAction::Manage => format!("account:{}?action=manage", resource), + AccountAction::Read => format_smolstr!("account:{}", resource), + AccountAction::Manage => format_smolstr!("account:{}?action=manage", resource), } } Scope::Identity(scope) => match scope { - IdentityScope::Handle => "identity:handle".to_string(), - IdentityScope::All => "identity:*".to_string(), + IdentityScope::Handle => "identity:handle".to_smolstr(), + IdentityScope::All => "identity:*".to_smolstr(), }, Scope::Blob(scope) => { if scope.accept.len() == 1 { if let Some(pattern) = scope.accept.iter().next() { match pattern { - MimePattern::All => "blob:*/*".to_string(), - MimePattern::TypeWildcard(t) => format!("blob:{}/*", t), - MimePattern::Exact(mime) => format!("blob:{}", mime), + MimePattern::All => "blob:*/*".to_smolstr(), + MimePattern::TypeWildcard(t) => { + format_smolstr!("blob:{}/*", t.as_ref()) + } + MimePattern::Exact(mime) => format_smolstr!("blob:{}", mime.as_ref()), } } else { - "blob:*/*".to_string() + "blob:*/*".to_smolstr() } } else { let mut params = Vec::new(); for pattern in &scope.accept { match pattern { - MimePattern::All => params.push("accept=*/*".to_string()), - MimePattern::TypeWildcard(t) => params.push(format!("accept={}/*", t)), - MimePattern::Exact(mime) => params.push(format!("accept={}", mime)), + MimePattern::All => params.push("accept=*/*".to_smolstr()), + MimePattern::TypeWildcard(t) => { + params.push(format_smolstr!("accept={}/*", t.as_ref())) + } + MimePattern::Exact(mime) => { + params.push(format_smolstr!("accept={}", mime.as_ref())) + } } } params.sort(); - format!("blob?{}", params.join("&")) + format_smolstr!("blob?{}", params.join("&")) } } Scope::Repo(scope) => { @@ -794,7 +956,7 @@ impl<'s> Scope<'s> { }; if scope.actions.len() == 3 { - format!("repo:{}", collection) + format_smolstr!("repo:{}", collection) } else { let mut params = Vec::new(); for action in &scope.actions { @@ -804,7 +966,7 @@ impl<'s> Scope<'s> { RepoAction::Delete => params.push("action=delete"), } } - format!("repo:{}?{}", collection, params.join("&")) + format_smolstr!("repo:{}?{}", collection, params.join("&")) } } Scope::Rpc(scope) => { @@ -813,58 +975,58 @@ impl<'s> Scope<'s> { && scope.aud.len() == 1 && scope.aud.contains(&RpcAudience::All) { - "rpc:*".to_string() + "rpc:*".to_smolstr() } else if scope.lxm.len() == 1 && scope.aud.len() == 1 && scope.aud.contains(&RpcAudience::All) { if let Some(lxm) = scope.lxm.iter().next() { match lxm { - RpcLexicon::All => "rpc:*".to_string(), - RpcLexicon::Nsid(nsid) => format!("rpc:{}", nsid), + RpcLexicon::All => "rpc:*".to_smolstr(), + RpcLexicon::Nsid(nsid) => format_smolstr!("rpc:{}", nsid), } } else { - "rpc:*".to_string() + "rpc:*".to_smolstr() } } else { let mut params = Vec::new(); for lxm in &scope.lxm { match lxm { - RpcLexicon::All => params.push("lxm=*".to_string()), - RpcLexicon::Nsid(nsid) => params.push(format!("lxm={}", nsid)), + RpcLexicon::All => params.push("lxm=*".to_smolstr()), + RpcLexicon::Nsid(nsid) => params.push(format_smolstr!("lxm={}", nsid)), } } for aud in &scope.aud { match aud { - RpcAudience::All => params.push("aud=*".to_string()), - RpcAudience::Did(did) => params.push(format!("aud={}", did)), + RpcAudience::All => params.push("aud=*".to_smolstr()), + RpcAudience::Did(did) => params.push(format_smolstr!("aud={}", did)), } } params.sort(); if params.is_empty() { - "rpc:*".to_string() + "rpc:*".to_smolstr() } else { - format!("rpc?{}", params.join("&")) + format_smolstr!("rpc?{}", params.join("&")) } } } - Scope::Atproto => "atproto".to_string(), + Scope::Atproto => "atproto".to_smolstr(), Scope::Transition(scope) => match scope { - TransitionScope::Generic => "transition:generic".to_string(), - TransitionScope::Email => "transition:email".to_string(), + TransitionScope::Generic => "transition:generic".to_smolstr(), + TransitionScope::Email => "transition:email".to_smolstr(), }, - Scope::OpenId => "openid".to_string(), - Scope::Profile => "profile".to_string(), - Scope::Email => "email".to_string(), + Scope::OpenId => "openid".to_smolstr(), + Scope::Profile => "profile".to_smolstr(), + Scope::Email => "email".to_smolstr(), } } /// Check if this scope grants the permissions of another scope - pub fn grants(&self, other: &Scope) -> bool { + pub fn grants(&self, other: &Scope) -> bool { match (self, other) { // Atproto only grants itself (it's a required scope, not a permission grant) (Scope::Atproto, Scope::Atproto) => true, @@ -916,7 +1078,8 @@ impl<'s> Scope<'s> { let collection_match = match (&a.collection, &b.collection) { (RepoCollection::All, _) => true, (RepoCollection::Nsid(a_nsid), RepoCollection::Nsid(b_nsid)) => { - a_nsid == b_nsid + // Compare as strings to support cross-type-parameter equality. + a_nsid.as_ref() == b_nsid.as_ref() } _ => false, }; @@ -928,21 +1091,29 @@ impl<'s> Scope<'s> { b.actions.is_subset(&a.actions) || a.actions.len() == 3 } (Scope::Rpc(a), Scope::Rpc(b)) => { - let lxm_match = if a.lxm.contains(&RpcLexicon::All) { + let lxm_match = if a.lxm.iter().any(|l| matches!(l, RpcLexicon::All)) { true } else { b.lxm.iter().all(|b_lxm| match b_lxm { RpcLexicon::All => false, - RpcLexicon::Nsid(_) => a.lxm.contains(b_lxm), + // Compare as strings to support cross-type-parameter equality. + RpcLexicon::Nsid(b_nsid) => a.lxm.iter().any(|a_lxm| match a_lxm { + RpcLexicon::All => false, + RpcLexicon::Nsid(a_nsid) => a_nsid.as_ref() == b_nsid.as_ref(), + }), }) }; - let aud_match = if a.aud.contains(&RpcAudience::All) { + let aud_match = if a.aud.iter().any(|a| matches!(a, RpcAudience::All)) { true } else { b.aud.iter().all(|b_aud| match b_aud { RpcAudience::All => false, - RpcAudience::Did(_) => a.aud.contains(b_aud), + // Compare as strings to support cross-type-parameter equality. + RpcAudience::Did(b_did) => a.aud.iter().any(|a_aud| match a_aud { + RpcAudience::All => false, + RpcAudience::Did(a_did) => a_did.as_ref() == b_did.as_ref(), + }), }) }; @@ -953,59 +1124,66 @@ impl<'s> Scope<'s> { } } -impl MimePattern<'_> { - fn grants(&self, other: &MimePattern) -> bool { +impl MimePattern { + fn grants(&self, other: &MimePattern) -> bool { match (self, other) { (MimePattern::All, _) => true, (MimePattern::TypeWildcard(a_type), MimePattern::TypeWildcard(b_type)) => { - a_type == b_type + // Compare as strings to support cross-type-parameter equality. + a_type.as_ref() == b_type.as_ref() } - (MimePattern::TypeWildcard(a_type), MimePattern::Exact(b_mime)) => { - b_mime.starts_with(&format!("{}/", a_type)) - } - (MimePattern::Exact(a), MimePattern::Exact(b)) => a == b, + (MimePattern::TypeWildcard(a_type), MimePattern::Exact(b_mime)) => b_mime + .as_ref() + .starts_with(&format!("{}/", a_type.as_ref())), + (MimePattern::Exact(a), MimePattern::Exact(b)) => a.as_ref() == b.as_ref(), _ => false, } } } -impl FromStr for MimePattern<'_> { +impl FromStr for MimePattern +where + ::Err: core::fmt::Debug, +{ type Err = ParseError; fn from_str(s: &str) -> Result { if s == "*/*" { Ok(MimePattern::All) } else if let Some(stripped) = s.strip_suffix("/*") { - Ok(MimePattern::TypeWildcard(CowStr::Owned( - stripped.to_smolstr(), - ))) + Ok(MimePattern::TypeWildcard(S::from_str(stripped).unwrap())) } else if s.contains('/') { - Ok(MimePattern::Exact(CowStr::Owned(s.to_smolstr()))) + Ok(MimePattern::Exact(S::from_str(s).unwrap())) } else { Err(ParseError::InvalidMimeType(s.to_string())) } } } -impl FromStr for Scope<'_> { - type Err = ParseError; +impl<'a, S: BosStr + From<&'a str>> TryFrom<&'a str> for MimePattern { + type Error = ParseError; - fn from_str(s: &str) -> Result, Self::Err> { - match Scope::parse(s) { - Ok(parsed) => Ok(parsed.into_static()), - Err(e) => Err(e), + fn try_from(s: &'a str) -> Result { + if s == "*/*" { + Ok(MimePattern::All) + } else if let Some(stripped) = s.strip_suffix("/*") { + Ok(MimePattern::TypeWildcard(S::from(stripped))) + } else if s.contains('/') { + Ok(MimePattern::Exact(S::from(s))) + } else { + Err(ParseError::InvalidMimeType(s.to_string())) } } } -impl fmt::Display for Scope<'_> { +impl fmt::Display for Scope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_string_normalized()) } } /// Parse a query string into a map of keys to lists of values -fn parse_query_string(query: &str) -> BTreeMap>> { +fn parse_query_string(query: &str) -> BTreeMap> { let mut params = BTreeMap::new(); for pair in query.split('&') { @@ -1015,7 +1193,7 @@ fn parse_query_string(query: &str) -> BTreeMap>> { params .entry(key.to_smolstr()) .or_insert_with(Vec::new) - .push(CowStr::Owned(value.to_smolstr())); + .push(value); } } @@ -1059,7 +1237,7 @@ mod tests { #[test] fn test_account_scope_parsing() { - let scope = Scope::parse("account:email").unwrap(); + let scope: Scope = Scope::parse("account:email").unwrap(); assert_eq!( scope, Scope::Account(AccountScope { @@ -1068,7 +1246,7 @@ mod tests { }) ); - let scope = Scope::parse("account:repo?action=manage").unwrap(); + let scope: Scope = Scope::parse("account:repo?action=manage").unwrap(); assert_eq!( scope, Scope::Account(AccountScope { @@ -1077,7 +1255,7 @@ mod tests { }) ); - let scope = Scope::parse("account:status?action=read").unwrap(); + let scope: Scope = Scope::parse("account:status?action=read").unwrap(); assert_eq!( scope, Scope::Account(AccountScope { @@ -1089,40 +1267,40 @@ mod tests { #[test] fn test_identity_scope_parsing() { - let scope = Scope::parse("identity:handle").unwrap(); + let scope: Scope = Scope::parse("identity:handle").unwrap(); assert_eq!(scope, Scope::Identity(IdentityScope::Handle)); - let scope = Scope::parse("identity:*").unwrap(); + let scope: Scope = Scope::parse("identity:*").unwrap(); assert_eq!(scope, Scope::Identity(IdentityScope::All)); } #[test] fn test_blob_scope_parsing() { - let scope = Scope::parse("blob:*/*").unwrap(); + let scope: Scope = Scope::parse("blob:*/*").unwrap(); let mut accept = BTreeSet::new(); accept.insert(MimePattern::All); assert_eq!(scope, Scope::Blob(BlobScope { accept })); - let scope = Scope::parse("blob:image/png").unwrap(); + let scope: Scope = Scope::parse("blob:image/png").unwrap(); let mut accept = BTreeSet::new(); - accept.insert(MimePattern::Exact(CowStr::new_static("image/png"))); + accept.insert(MimePattern::Exact(SmolStr::new_static("image/png"))); assert_eq!(scope, Scope::Blob(BlobScope { accept })); let scope = Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap(); let mut accept = BTreeSet::new(); - accept.insert(MimePattern::Exact(CowStr::new_static("image/png"))); - accept.insert(MimePattern::Exact(CowStr::new_static("image/jpeg"))); + accept.insert(MimePattern::Exact(SmolStr::new_static("image/png"))); + accept.insert(MimePattern::Exact(SmolStr::new_static("image/jpeg"))); assert_eq!(scope, Scope::Blob(BlobScope { accept })); let scope = Scope::parse("blob:image/*").unwrap(); let mut accept = BTreeSet::new(); - accept.insert(MimePattern::TypeWildcard(CowStr::new_static("image"))); + accept.insert(MimePattern::TypeWildcard(SmolStr::new_static("image"))); assert_eq!(scope, Scope::Blob(BlobScope { accept })); } #[test] fn test_repo_scope_parsing() { - let scope = Scope::parse("repo:*?action=create").unwrap(); + let scope: Scope = Scope::parse("repo:*?action=create").unwrap(); let mut actions = BTreeSet::new(); actions.insert(RepoAction::Create); assert_eq!( @@ -1133,19 +1311,20 @@ mod tests { }) ); - let scope = Scope::parse("repo:app.bsky.feed.post?action=create&action=update").unwrap(); + let scope: Scope = + Scope::parse("repo:app.bsky.feed.post?action=create&action=update").unwrap(); let mut actions = BTreeSet::new(); actions.insert(RepoAction::Create); actions.insert(RepoAction::Update); assert_eq!( scope, Scope::Repo(RepoScope { - collection: RepoCollection::Nsid(Nsid::new_static("app.bsky.feed.post").unwrap()), + collection: RepoCollection::Nsid(Nsid::new_owned("app.bsky.feed.post").unwrap()), actions, }) ); - let scope = Scope::parse("repo:app.bsky.feed.post").unwrap(); + let scope: Scope = Scope::parse("repo:app.bsky.feed.post").unwrap(); let mut actions = BTreeSet::new(); actions.insert(RepoAction::Create); actions.insert(RepoAction::Update); @@ -1153,7 +1332,7 @@ mod tests { assert_eq!( scope, Scope::Repo(RepoScope { - collection: RepoCollection::Nsid(Nsid::new_static("app.bsky.feed.post").unwrap()), + collection: RepoCollection::Nsid(Nsid::new_owned("app.bsky.feed.post").unwrap()), actions, }) ); @@ -1161,14 +1340,14 @@ mod tests { #[test] fn test_rpc_scope_parsing() { - let scope = Scope::parse("rpc:*").unwrap(); + let scope: Scope = Scope::parse("rpc:*").unwrap(); let mut lxm = BTreeSet::new(); let mut aud = BTreeSet::new(); lxm.insert(RpcLexicon::All); aud.insert(RpcAudience::All); assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud })); - let scope = Scope::parse("rpc:com.example.service").unwrap(); + let scope: Scope = Scope::parse("rpc:com.example.service").unwrap(); let mut lxm = BTreeSet::new(); let mut aud = BTreeSet::new(); lxm.insert(RpcLexicon::Nsid( @@ -1177,31 +1356,31 @@ mod tests { aud.insert(RpcAudience::All); assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud })); - let scope = + let scope: Scope = Scope::parse("rpc:com.example.service?aud=did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(); let mut lxm = BTreeSet::new(); let mut aud = BTreeSet::new(); lxm.insert(RpcLexicon::Nsid( - Nsid::new_static("com.example.service").unwrap(), + Nsid::new_owned("com.example.service").unwrap(), )); aud.insert(RpcAudience::Did( - Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(), + Did::new_owned("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(), )); assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud })); - let scope = + let scope: Scope = Scope::parse("rpc?lxm=com.example.method1&lxm=com.example.method2&aud=did:plc:yfvwmnlztr4dwkb7hwz55r2g") .unwrap(); let mut lxm = BTreeSet::new(); let mut aud = BTreeSet::new(); lxm.insert(RpcLexicon::Nsid( - Nsid::new_static("com.example.method1").unwrap(), + Nsid::new_owned("com.example.method1").unwrap(), )); lxm.insert(RpcLexicon::Nsid( - Nsid::new_static("com.example.method2").unwrap(), + Nsid::new_owned("com.example.method2").unwrap(), )); aud.insert(RpcAudience::Did( - Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(), + Did::new_owned("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap(), )); assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud })); } @@ -1226,16 +1405,16 @@ mod tests { ]; for (input, expected) in tests { - let scope = Scope::parse(input).unwrap(); + let scope: Scope = Scope::parse(input).unwrap(); assert_eq!(scope.to_string_normalized(), expected); } } #[test] fn test_account_scope_grants() { - let manage = Scope::parse("account:email?action=manage").unwrap(); - let read = Scope::parse("account:email?action=read").unwrap(); - let other_read = Scope::parse("account:repo?action=read").unwrap(); + let manage: Scope = Scope::parse("account:email?action=manage").unwrap(); + let read: Scope = Scope::parse("account:email?action=read").unwrap(); + let other_read: Scope = Scope::parse("account:repo?action=read").unwrap(); assert!(manage.grants(&read)); assert!(manage.grants(&manage)); @@ -1246,8 +1425,8 @@ mod tests { #[test] fn test_identity_scope_grants() { - let all = Scope::parse("identity:*").unwrap(); - let handle = Scope::parse("identity:handle").unwrap(); + let all: Scope = Scope::parse("identity:*").unwrap(); + let handle: Scope = Scope::parse("identity:handle").unwrap(); assert!(all.grants(&handle)); assert!(all.grants(&all)); @@ -1257,10 +1436,10 @@ mod tests { #[test] fn test_blob_scope_grants() { - let all = Scope::parse("blob:*/*").unwrap(); - let image_all = Scope::parse("blob:image/*").unwrap(); - let image_png = Scope::parse("blob:image/png").unwrap(); - let text_plain = Scope::parse("blob:text/plain").unwrap(); + let all: Scope = Scope::parse("blob:*/*").unwrap(); + let image_all: Scope = Scope::parse("blob:image/*").unwrap(); + let image_png: Scope = Scope::parse("blob:image/png").unwrap(); + let text_plain: Scope = Scope::parse("blob:text/plain").unwrap(); assert!(all.grants(&image_all)); assert!(all.grants(&image_png)); @@ -1272,11 +1451,12 @@ mod tests { #[test] fn test_repo_scope_grants() { - let all_all = Scope::parse("repo:*").unwrap(); - let all_create = Scope::parse("repo:*?action=create").unwrap(); - let specific_all = Scope::parse("repo:app.bsky.feed.post").unwrap(); - let specific_create = Scope::parse("repo:app.bsky.feed.post?action=create").unwrap(); - let other_create = Scope::parse("repo:pub.leaflet.publication?action=create").unwrap(); + let all_all: Scope = Scope::parse("repo:*").unwrap(); + let all_create: Scope = Scope::parse("repo:*?action=create").unwrap(); + let specific_all: Scope = Scope::parse("repo:app.bsky.feed.post").unwrap(); + let specific_create: Scope = Scope::parse("repo:app.bsky.feed.post?action=create").unwrap(); + let other_create: Scope = + Scope::parse("repo:pub.leaflet.publication?action=create").unwrap(); assert!(all_all.grants(&all_create)); assert!(all_all.grants(&specific_all)); @@ -1290,9 +1470,10 @@ mod tests { #[test] fn test_rpc_scope_grants() { - let all = Scope::parse("rpc:*").unwrap(); - let specific_lxm = Scope::parse("rpc:com.example.service").unwrap(); - let specific_both = Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap(); + let all: Scope = Scope::parse("rpc:*").unwrap(); + let specific_lxm: Scope = Scope::parse("rpc:com.example.service").unwrap(); + let specific_both: Scope = + Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap(); assert!(all.grants(&specific_lxm)); assert!(all.grants(&specific_both)); @@ -1303,8 +1484,8 @@ mod tests { #[test] fn test_cross_scope_grants() { - let account = Scope::parse("account:email").unwrap(); - let identity = Scope::parse("identity:handle").unwrap(); + let account: Scope = Scope::parse("account:email").unwrap(); + let identity: Scope = Scope::parse("identity:handle").unwrap(); assert!(!account.grants(&identity)); assert!(!identity.grants(&account)); @@ -1313,30 +1494,32 @@ mod tests { #[test] fn test_parse_errors() { assert!(matches!( - Scope::parse("unknown:test"), + Scope::::parse("unknown:test"), Err(ParseError::UnknownPrefix(_)) )); assert!(matches!( - Scope::parse("account"), + Scope::::parse("account"), Err(ParseError::MissingResource) )); assert!(matches!( - Scope::parse("account:invalid"), + Scope::::parse("account:invalid"), Err(ParseError::InvalidResource(_)) )); assert!(matches!( - Scope::parse("account:email?action=invalid"), + Scope::::parse("account:email?action=invalid"), Err(ParseError::InvalidAction(_)) )); } #[test] fn test_query_parameter_sorting() { - let scope = - Scope::parse("blob?accept=image/png&accept=application/pdf&accept=image/jpeg").unwrap(); + let scope = Scope::::parse( + "blob?accept=image/png&accept=application/pdf&accept=image/jpeg", + ) + .unwrap(); let normalized = scope.to_string_normalized(); assert!(normalized.contains("accept=application/pdf")); assert!(normalized.contains("accept=image/jpeg")); @@ -1350,7 +1533,7 @@ mod tests { #[test] fn test_repo_action_wildcard() { - let scope = Scope::parse("repo:app.bsky.feed.post?action=*").unwrap(); + let scope = Scope::::parse("repo:app.bsky.feed.post?action=*").unwrap(); let mut actions = BTreeSet::new(); actions.insert(RepoAction::Create); actions.insert(RepoAction::Update); @@ -1358,7 +1541,7 @@ mod tests { assert_eq!( scope, Scope::Repo(RepoScope { - collection: RepoCollection::Nsid(Nsid::new_static("app.bsky.feed.post").unwrap()), + collection: RepoCollection::Nsid(Nsid::new_owned("app.bsky.feed.post").unwrap()), actions, }) ); @@ -1366,15 +1549,15 @@ mod tests { #[test] fn test_multiple_blob_accepts() { - let scope = Scope::parse("blob?accept=image/*&accept=text/plain").unwrap(); - assert!(scope.grants(&Scope::parse("blob:image/png").unwrap())); - assert!(scope.grants(&Scope::parse("blob:text/plain").unwrap())); - assert!(!scope.grants(&Scope::parse("blob:application/json").unwrap())); + let scope = Scope::::parse("blob?accept=image/*&accept=text/plain").unwrap(); + assert!(scope.grants(&Scope::::parse("blob:image/png").unwrap())); + assert!(scope.grants(&Scope::::parse("blob:text/plain").unwrap())); + assert!(!scope.grants(&Scope::::parse("blob:application/json").unwrap())); } #[test] fn test_rpc_default_wildcards() { - let scope = Scope::parse("rpc").unwrap(); + let scope = Scope::::parse("rpc").unwrap(); let mut lxm = BTreeSet::new(); let mut aud = BTreeSet::new(); lxm.insert(RpcLexicon::All); @@ -1384,44 +1567,44 @@ mod tests { #[test] fn test_atproto_scope_parsing() { - let scope = Scope::parse("atproto").unwrap(); + let scope = Scope::::parse("atproto").unwrap(); assert_eq!(scope, Scope::Atproto); // Atproto should not accept suffixes - assert!(Scope::parse("atproto:something").is_err()); - assert!(Scope::parse("atproto?param=value").is_err()); + assert!(Scope::::parse("atproto:something").is_err()); + assert!(Scope::::parse("atproto?param=value").is_err()); } #[test] fn test_transition_scope_parsing() { - let scope = Scope::parse("transition:generic").unwrap(); + let scope = Scope::::parse("transition:generic").unwrap(); assert_eq!(scope, Scope::Transition(TransitionScope::Generic)); - let scope = Scope::parse("transition:email").unwrap(); + let scope = Scope::::parse("transition:email").unwrap(); assert_eq!(scope, Scope::Transition(TransitionScope::Email)); // Test invalid transition types assert!(matches!( - Scope::parse("transition:invalid"), + Scope::::parse("transition:invalid"), Err(ParseError::InvalidResource(_)) )); // Test missing suffix assert!(matches!( - Scope::parse("transition"), + Scope::::parse("transition"), Err(ParseError::MissingResource) )); // Test transition doesn't accept query parameters assert!(matches!( - Scope::parse("transition:generic?param=value"), + Scope::::parse("transition:generic?param=value"), Err(ParseError::InvalidResource(_)) )); } #[test] fn test_atproto_scope_normalization() { - let scope = Scope::parse("atproto").unwrap(); + let scope = Scope::::parse("atproto").unwrap(); assert_eq!(scope.to_string_normalized(), "atproto"); } @@ -1433,21 +1616,21 @@ mod tests { ]; for (input, expected) in tests { - let scope = Scope::parse(input).unwrap(); + let scope = Scope::::parse(input).unwrap(); assert_eq!(scope.to_string_normalized(), expected); } } #[test] fn test_atproto_scope_grants() { - let atproto = Scope::parse("atproto").unwrap(); - let account = Scope::parse("account:email").unwrap(); - let identity = Scope::parse("identity:handle").unwrap(); - let blob = Scope::parse("blob:image/png").unwrap(); - let repo = Scope::parse("repo:app.bsky.feed.post").unwrap(); - let rpc = Scope::parse("rpc:com.example.service").unwrap(); - let transition_generic = Scope::parse("transition:generic").unwrap(); - let transition_email = Scope::parse("transition:email").unwrap(); + let atproto = Scope::::parse("atproto").unwrap(); + let account = Scope::::parse("account:email").unwrap(); + let identity = Scope::::parse("identity:handle").unwrap(); + let blob = Scope::::parse("blob:image/png").unwrap(); + let repo = Scope::::parse("repo:app.bsky.feed.post").unwrap(); + let rpc = Scope::::parse("rpc:com.example.service").unwrap(); + let transition_generic = Scope::::parse("transition:generic").unwrap(); + let transition_email = Scope::::parse("transition:email").unwrap(); // Atproto only grants itself (it's a required scope, not a permission grant) assert!(atproto.grants(&atproto)); @@ -1471,9 +1654,9 @@ mod tests { #[test] fn test_transition_scope_grants() { - let transition_generic = Scope::parse("transition:generic").unwrap(); - let transition_email = Scope::parse("transition:email").unwrap(); - let account = Scope::parse("account:email").unwrap(); + let transition_generic = Scope::::parse("transition:generic").unwrap(); + let transition_email = Scope::::parse("transition:email").unwrap(); + let account = Scope::::parse("account:email").unwrap(); // Transition scopes only grant themselves assert!(transition_generic.grants(&transition_generic)); @@ -1493,7 +1676,7 @@ mod tests { #[test] fn test_parse_multiple() { // Test parsing multiple scopes - let scopes = Scope::parse_multiple("atproto repo:*").unwrap(); + let scopes = Scope::::parse_multiple("atproto repo:*").unwrap(); assert_eq!(scopes.len(), 2); assert_eq!(scopes[0], Scope::Atproto); assert_eq!( @@ -1511,46 +1694,49 @@ mod tests { ); // Test with more scopes - let scopes = Scope::parse_multiple("account:email identity:handle blob:image/png").unwrap(); + let scopes = + Scope::::parse_multiple("account:email identity:handle blob:image/png") + .unwrap(); assert_eq!(scopes.len(), 3); assert!(matches!(scopes[0], Scope::Account(_))); assert!(matches!(scopes[1], Scope::Identity(_))); assert!(matches!(scopes[2], Scope::Blob(_))); // Test with complex scopes - let scopes = Scope::parse_multiple( + let scopes = Scope::::parse_multiple( "account:email?action=manage repo:app.bsky.feed.post?action=create transition:email", ) .unwrap(); assert_eq!(scopes.len(), 3); // Test empty string - let scopes = Scope::parse_multiple("").unwrap(); + let scopes = Scope::::parse_multiple("").unwrap(); assert_eq!(scopes.len(), 0); // Test whitespace only - let scopes = Scope::parse_multiple(" ").unwrap(); + let scopes = Scope::::parse_multiple(" ").unwrap(); assert_eq!(scopes.len(), 0); // Test with extra whitespace - let scopes = Scope::parse_multiple(" atproto repo:* ").unwrap(); + let scopes = Scope::::parse_multiple(" atproto repo:* ").unwrap(); assert_eq!(scopes.len(), 2); // Test single scope - let scopes = Scope::parse_multiple("atproto").unwrap(); + let scopes = Scope::::parse_multiple("atproto").unwrap(); assert_eq!(scopes.len(), 1); assert_eq!(scopes[0], Scope::Atproto); // Test error propagation - assert!(Scope::parse_multiple("atproto invalid:scope").is_err()); - assert!(Scope::parse_multiple("account:invalid repo:*").is_err()); + assert!(Scope::::parse_multiple("atproto invalid:scope").is_err()); + assert!(Scope::::parse_multiple("account:invalid repo:*").is_err()); } #[test] fn test_parse_multiple_reduced() { // Test repo scope reduction - wildcard grants specific let scopes = - Scope::parse_multiple_reduced("atproto repo:app.bsky.feed.post repo:*").unwrap(); + Scope::::parse_multiple_reduced("atproto repo:app.bsky.feed.post repo:*") + .unwrap(); assert_eq!(scopes.len(), 2); assert!(scopes.contains(&Scope::Atproto)); assert!(scopes.contains(&Scope::Repo(RepoScope { @@ -1566,7 +1752,8 @@ mod tests { // Test reverse order - should get same result let scopes = - Scope::parse_multiple_reduced("atproto repo:* repo:app.bsky.feed.post").unwrap(); + Scope::::parse_multiple_reduced("atproto repo:* repo:app.bsky.feed.post") + .unwrap(); assert_eq!(scopes.len(), 2); assert!(scopes.contains(&Scope::Atproto)); assert!(scopes.contains(&Scope::Repo(RepoScope { @@ -1582,7 +1769,8 @@ mod tests { // Test account scope reduction - manage grants read let scopes = - Scope::parse_multiple_reduced("account:email account:email?action=manage").unwrap(); + Scope::::parse_multiple_reduced("account:email account:email?action=manage") + .unwrap(); assert_eq!(scopes.len(), 1); assert_eq!( scopes[0], @@ -1593,24 +1781,29 @@ mod tests { ); // Test identity scope reduction - wildcard grants specific - let scopes = Scope::parse_multiple_reduced("identity:handle identity:*").unwrap(); + let scopes = + Scope::::parse_multiple_reduced("identity:handle identity:*").unwrap(); assert_eq!(scopes.len(), 1); assert_eq!(scopes[0], Scope::Identity(IdentityScope::All)); // Test blob scope reduction - wildcard grants specific - let scopes = Scope::parse_multiple_reduced("blob:image/png blob:image/* blob:*/*").unwrap(); + let scopes = + Scope::::parse_multiple_reduced("blob:image/png blob:image/* blob:*/*") + .unwrap(); assert_eq!(scopes.len(), 1); let mut accept = BTreeSet::new(); accept.insert(MimePattern::All); assert_eq!(scopes[0], Scope::Blob(BlobScope { accept })); // Test no reduction needed - different scope types - let scopes = - Scope::parse_multiple_reduced("account:email identity:handle blob:image/png").unwrap(); + let scopes = Scope::::parse_multiple_reduced( + "account:email identity:handle blob:image/png", + ) + .unwrap(); assert_eq!(scopes.len(), 3); // Test repo action reduction - let scopes = Scope::parse_multiple_reduced( + let scopes = Scope::::parse_multiple_reduced( "repo:app.bsky.feed.post?action=create repo:app.bsky.feed.post", ) .unwrap(); @@ -1618,7 +1811,7 @@ mod tests { assert_eq!( scopes[0], Scope::Repo(RepoScope { - collection: RepoCollection::Nsid(Nsid::new_static("app.bsky.feed.post").unwrap()), + collection: RepoCollection::Nsid(Nsid::new_owned("app.bsky.feed.post").unwrap()), actions: { let mut actions = BTreeSet::new(); actions.insert(RepoAction::Create); @@ -1630,7 +1823,7 @@ mod tests { ); // Test RPC scope reduction - let scopes = Scope::parse_multiple_reduced( + let scopes = Scope::::parse_multiple_reduced( "rpc:com.example.service?aud=did:example:123 rpc:com.example.service rpc:*", ) .unwrap(); @@ -1652,22 +1845,24 @@ mod tests { ); // Test duplicate removal - let scopes = Scope::parse_multiple_reduced("atproto atproto atproto").unwrap(); + let scopes = Scope::::parse_multiple_reduced("atproto atproto atproto").unwrap(); assert_eq!(scopes.len(), 1); assert_eq!(scopes[0], Scope::Atproto); // Test transition scopes - only grant themselves - let scopes = Scope::parse_multiple_reduced("transition:generic transition:email").unwrap(); + let scopes = + Scope::::parse_multiple_reduced("transition:generic transition:email") + .unwrap(); assert_eq!(scopes.len(), 2); assert!(scopes.contains(&Scope::Transition(TransitionScope::Generic))); assert!(scopes.contains(&Scope::Transition(TransitionScope::Email))); // Test empty input - let scopes = Scope::parse_multiple_reduced("").unwrap(); + let scopes = Scope::::parse_multiple_reduced("").unwrap(); assert_eq!(scopes.len(), 0); // Test complex scenario with multiple reductions - let scopes = Scope::parse_multiple_reduced( + let scopes = Scope::::parse_multiple_reduced( "account:email?action=manage account:email account:repo account:repo?action=read identity:* identity:handle" ).unwrap(); assert_eq!(scopes.len(), 3); @@ -1683,7 +1878,8 @@ mod tests { assert!(scopes.contains(&Scope::Identity(IdentityScope::All))); // Test that atproto doesn't grant other scopes (per recent change) - let scopes = Scope::parse_multiple_reduced("atproto account:email repo:*").unwrap(); + let scopes = + Scope::::parse_multiple_reduced("atproto account:email repo:*").unwrap(); assert_eq!(scopes.len(), 3); assert!(scopes.contains(&Scope::Atproto)); assert!(scopes.contains(&Scope::Account(AccountScope { @@ -1705,46 +1901,46 @@ mod tests { #[test] fn test_openid_connect_scope_parsing() { // Test OpenID scope - let scope = Scope::parse("openid").unwrap(); + let scope = Scope::::parse("openid").unwrap(); assert_eq!(scope, Scope::OpenId); // Test Profile scope - let scope = Scope::parse("profile").unwrap(); + let scope = Scope::::parse("profile").unwrap(); assert_eq!(scope, Scope::Profile); // Test Email scope - let scope = Scope::parse("email").unwrap(); + let scope = Scope::::parse("email").unwrap(); assert_eq!(scope, Scope::Email); // Test that they don't accept suffixes - assert!(Scope::parse("openid:something").is_err()); - assert!(Scope::parse("profile:something").is_err()); - assert!(Scope::parse("email:something").is_err()); + assert!(Scope::::parse("openid:something").is_err()); + assert!(Scope::::parse("profile:something").is_err()); + assert!(Scope::::parse("email:something").is_err()); // Test that they don't accept query parameters - assert!(Scope::parse("openid?param=value").is_err()); - assert!(Scope::parse("profile?param=value").is_err()); - assert!(Scope::parse("email?param=value").is_err()); + assert!(Scope::::parse("openid?param=value").is_err()); + assert!(Scope::::parse("profile?param=value").is_err()); + assert!(Scope::::parse("email?param=value").is_err()); } #[test] fn test_openid_connect_scope_normalization() { - let scope = Scope::parse("openid").unwrap(); + let scope = Scope::::parse("openid").unwrap(); assert_eq!(scope.to_string_normalized(), "openid"); - let scope = Scope::parse("profile").unwrap(); + let scope = Scope::::parse("profile").unwrap(); assert_eq!(scope.to_string_normalized(), "profile"); - let scope = Scope::parse("email").unwrap(); + let scope = Scope::::parse("email").unwrap(); assert_eq!(scope.to_string_normalized(), "email"); } #[test] fn test_openid_connect_scope_grants() { - let openid = Scope::parse("openid").unwrap(); - let profile = Scope::parse("profile").unwrap(); - let email = Scope::parse("email").unwrap(); - let account = Scope::parse("account:email").unwrap(); + let openid = Scope::::parse("openid").unwrap(); + let profile = Scope::::parse("profile").unwrap(); + let email = Scope::::parse("email").unwrap(); + let account = Scope::::parse("account:email").unwrap(); // OpenID Connect scopes only grant themselves assert!(openid.grants(&openid)); @@ -1770,7 +1966,7 @@ mod tests { #[test] fn test_parse_multiple_with_openid_connect() { - let scopes = Scope::parse_multiple("openid profile email atproto").unwrap(); + let scopes = Scope::::parse_multiple("openid profile email atproto").unwrap(); assert_eq!(scopes.len(), 4); assert_eq!(scopes[0], Scope::OpenId); assert_eq!(scopes[1], Scope::Profile); @@ -1778,7 +1974,8 @@ mod tests { assert_eq!(scopes[3], Scope::Atproto); // Test with mixed scopes - let scopes = Scope::parse_multiple("openid account:email profile repo:*").unwrap(); + let scopes = + Scope::::parse_multiple("openid account:email profile repo:*").unwrap(); assert_eq!(scopes.len(), 4); assert!(scopes.contains(&Scope::OpenId)); assert!(scopes.contains(&Scope::Profile)); @@ -1787,14 +1984,15 @@ mod tests { #[test] fn test_parse_multiple_reduced_with_openid_connect() { // OpenID Connect scopes don't grant each other, so no reduction - let scopes = Scope::parse_multiple_reduced("openid profile email openid").unwrap(); + let scopes = + Scope::::parse_multiple_reduced("openid profile email openid").unwrap(); assert_eq!(scopes.len(), 3); assert!(scopes.contains(&Scope::OpenId)); assert!(scopes.contains(&Scope::Profile)); assert!(scopes.contains(&Scope::Email)); // Mixed with other scopes - let scopes = Scope::parse_multiple_reduced( + let scopes = Scope::::parse_multiple_reduced( "openid account:email account:email?action=manage profile", ) .unwrap(); @@ -1815,11 +2013,11 @@ mod tests { // Test single scope let scopes = vec![Scope::Atproto]; - assert_eq!(Scope::serialize_multiple(&scopes), "atproto"); + assert_eq!(Scope::::serialize_multiple(&scopes), "atproto"); // Test multiple scopes - should be sorted alphabetically let scopes = vec![ - Scope::parse("repo:*").unwrap(), + Scope::::parse("repo:*").unwrap(), Scope::Atproto, Scope::parse("account:email").unwrap(), ]; @@ -1830,7 +2028,7 @@ mod tests { // Test that sorting is consistent regardless of input order let scopes = vec![ - Scope::parse("identity:handle").unwrap(), + Scope::::parse("identity:handle").unwrap(), Scope::parse("blob:image/png").unwrap(), Scope::parse("account:repo?action=manage").unwrap(), ]; @@ -1842,13 +2040,13 @@ mod tests { // Test with OpenID Connect scopes let scopes = vec![Scope::Email, Scope::OpenId, Scope::Profile, Scope::Atproto]; assert_eq!( - Scope::serialize_multiple(&scopes), + Scope::::serialize_multiple(&scopes), "atproto email openid profile" ); // Test with complex scopes including query parameters let scopes = vec![ - Scope::parse("rpc:com.example.service?aud=did:plc:yfvwmnlztr4dwkb7hwz55r2g&lxm=com.example.method") + Scope::::parse("rpc:com.example.service?aud=did:plc:yfvwmnlztr4dwkb7hwz55r2g&lxm=com.example.method") .unwrap(), Scope::parse("repo:app.bsky.feed.post?action=create&action=update").unwrap(), Scope::parse("blob:image/*?accept=image/png&accept=image/jpeg").unwrap(), @@ -1869,7 +2067,7 @@ mod tests { Scope::Atproto, ]; assert_eq!( - Scope::serialize_multiple(&scopes), + Scope::<&str>::serialize_multiple(&scopes), "atproto transition:email transition:generic" ); @@ -1877,7 +2075,7 @@ mod tests { let scopes = vec![ Scope::Atproto, Scope::Atproto, - Scope::parse("account:email").unwrap(), + Scope::::parse("account:email").unwrap(), ]; assert_eq!( Scope::serialize_multiple(&scopes), @@ -1885,7 +2083,8 @@ mod tests { ); // Test normalization is preserved in serialization - let scopes = vec![Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap()]; + let scopes = + vec![Scope::::parse("blob?accept=image/png&accept=image/jpeg").unwrap()]; // Should normalize query parameters alphabetically assert_eq!( Scope::serialize_multiple(&scopes), @@ -1897,13 +2096,13 @@ mod tests { fn test_serialize_multiple_roundtrip() { // Test that parse_multiple and serialize_multiple are inverses (when sorted) let original = "account:email atproto blob:image/png identity:handle repo:*"; - let scopes = Scope::parse_multiple(original).unwrap(); + let scopes = Scope::::parse_multiple(original).unwrap(); let serialized = Scope::serialize_multiple(&scopes); assert_eq!(serialized, original); // Test with complex scopes let original = "account:repo?action=manage blob?accept=image/jpeg&accept=image/png rpc:*"; - let scopes = Scope::parse_multiple(original).unwrap(); + let scopes = Scope::::parse_multiple(original).unwrap(); let serialized = Scope::serialize_multiple(&scopes); // Parse again to verify it's valid let reparsed = Scope::parse_multiple(&serialized).unwrap(); @@ -1911,7 +2110,7 @@ mod tests { // Test with OpenID Connect scopes let original = "email openid profile"; - let scopes = Scope::parse_multiple(original).unwrap(); + let scopes = Scope::::parse_multiple(original).unwrap(); let serialized = Scope::serialize_multiple(&scopes); assert_eq!(serialized, original); } @@ -1920,7 +2119,7 @@ mod tests { fn test_remove_scope() { // Test removing a scope that exists let scopes = vec![ - Scope::parse("repo:*").unwrap(), + Scope::::parse("repo:*").unwrap(), Scope::Atproto, Scope::parse("account:email").unwrap(), ]; @@ -1933,7 +2132,7 @@ mod tests { // Test removing a scope that doesn't exist let scopes = vec![ - Scope::parse("repo:*").unwrap(), + Scope::::parse("repo:*").unwrap(), Scope::parse("account:email").unwrap(), ]; let to_remove = Scope::parse("identity:handle").unwrap(); @@ -1950,7 +2149,7 @@ mod tests { // Test removing all instances of a duplicate scope let scopes = vec![ Scope::Atproto, - Scope::parse("account:email").unwrap(), + Scope::::parse("account:email").unwrap(), Scope::Atproto, Scope::parse("repo:*").unwrap(), Scope::Atproto, @@ -1964,7 +2163,7 @@ mod tests { // Test removing complex scopes with query parameters let scopes = vec![ - Scope::parse("account:email?action=manage").unwrap(), + Scope::::parse("account:email?action=manage").unwrap(), Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap(), Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap(), ]; @@ -1976,7 +2175,7 @@ mod tests { // Test with OpenID Connect scopes let scopes = vec![Scope::OpenId, Scope::Profile, Scope::Email, Scope::Atproto]; let to_remove = Scope::Profile; - let result = Scope::remove_scope(&scopes, &to_remove); + let result = Scope::<&str>::remove_scope(&scopes, &to_remove); assert_eq!(result.len(), 3); assert!(!result.contains(&to_remove)); assert!(result.contains(&Scope::OpenId)); @@ -1990,7 +2189,7 @@ mod tests { Scope::Atproto, ]; let to_remove = Scope::Transition(TransitionScope::Email); - let result = Scope::remove_scope(&scopes, &to_remove); + let result = Scope::<&str>::remove_scope(&scopes, &to_remove); assert_eq!(result.len(), 2); assert!(!result.contains(&to_remove)); assert!(result.contains(&Scope::Transition(TransitionScope::Generic))); @@ -1998,7 +2197,7 @@ mod tests { // Test that only exact matches are removed let scopes = vec![ - Scope::parse("account:email").unwrap(), + Scope::::parse("account:email").unwrap(), Scope::parse("account:email?action=manage").unwrap(), Scope::parse("account:repo").unwrap(), ]; diff --git a/crates/jacquard-oauth/src/session.rs b/crates/jacquard-oauth/src/session.rs index fd3b2e5c9..794797ede 100644 --- a/crates/jacquard-oauth/src/session.rs +++ b/crates/jacquard-oauth/src/session.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use chrono::TimeDelta; @@ -15,7 +15,8 @@ use crate::{ use dashmap::DashMap; use jacquard_common::{ - CowStr, IntoStatic, + IntoStatic, + bos::{BosStr, DefaultStr}, deps::fluent_uri::Uri, http_client::HttpClient, session::SessionStoreError, @@ -37,55 +38,61 @@ pub trait DpopDataSource { /// Return the private JWK used to sign DPoP proofs. fn key(&self) -> &Key; /// Return the most recently observed nonce from the authorization server, if any. - fn authserver_nonce(&self) -> Option>; + fn authserver_nonce(&self) -> Option<&str>; /// Persist a new nonce received from the authorization server. - fn set_authserver_nonce(&mut self, nonce: CowStr<'_>); + fn set_authserver_nonce(&mut self, nonce: SmolStr); /// Return the most recently observed nonce from the resource server (PDS), if any. - fn host_nonce(&self) -> Option>; + fn host_nonce(&self) -> Option<&str>; /// Persist a new nonce received from the resource server (PDS). - fn set_host_nonce(&mut self, nonce: CowStr<'_>); + fn set_host_nonce(&mut self, nonce: SmolStr); } /// Persisted information about an OAuth session. Used to resume an active session. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct ClientSessionData<'s> { +#[serde(bound( + serialize = "S: serde::Serialize + BosStr + Ord", + deserialize = "S: serde::Deserialize<'de> + BosStr, Scope: serde::Deserialize<'de>" +))] +pub struct ClientSessionData { /// DID of the authenticated account; serves as the primary key for session storage /// because only one active session per account is assumed. - #[serde(borrow)] - pub account_did: Did<'s>, + pub account_did: Did, /// Opaque identifier that distinguishes this session from other sessions for the same account. /// /// Reuses the random `state` token generated during the PAR flow. - pub session_id: CowStr<'s>, + pub session_id: S, /// Base URL of the resource server (PDS): scheme, host, and port only pub host_url: Uri, /// Base URL of the authorization server (PDS or entryway): scheme, host, and port only - pub authserver_url: CowStr<'s>, + pub authserver_url: S, /// Full URL of the authorization server's token endpoint. - pub authserver_token_endpoint: CowStr<'s>, + pub authserver_token_endpoint: S, /// Full URL of the authorization server's revocation endpoint, if advertised. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub authserver_revocation_endpoint: Option>, + pub authserver_revocation_endpoint: Option, /// The set of OAuth scopes approved for this session, as returned in the initial token response. - pub scopes: Vec>, + pub scopes: Vec>, /// DPoP key and nonce state for ongoing requests in this session. #[serde(flatten)] - pub dpop_data: DpopClientData<'s>, + pub dpop_data: DpopClientData, /// Current token set (access token, refresh token, expiry, etc.). #[serde(flatten)] - pub token_set: TokenSet<'s>, + pub token_set: TokenSet, } -impl IntoStatic for ClientSessionData<'_> { - type Output = ClientSessionData<'static>; +impl IntoStatic for ClientSessionData +where + S::Output: BosStr + Ord, +{ + type Output = ClientSessionData; fn into_static(self) -> Self::Output { ClientSessionData { @@ -95,7 +102,7 @@ impl IntoStatic for ClientSessionData<'_> { .authserver_revocation_endpoint .map(IntoStatic::into_static), scopes: self.scopes.into_static(), - dpop_data: self.dpop_data.into_static(), + dpop_data: self.dpop_data, token_set: self.token_set.into_static(), account_did: self.account_did.into_static(), session_id: self.session_id.into_static(), @@ -104,20 +111,28 @@ impl IntoStatic for ClientSessionData<'_> { } } -impl ClientSessionData<'_> { +impl ClientSessionData { /// Update this session's token set and, if the new token set includes scopes, replace the scope list. /// /// Called after a successful token refresh so that any scope changes returned by the server /// are reflected in the persisted session without requiring a full re-authentication. - pub fn update_with_tokens(&mut self, token_set: TokenSet<'_>) { + /// + /// This method is only available on `DefaultStr`-backed sessions (the common case for + /// in-memory sessions). Zero-copy borrowed sessions are read-only by nature and would + /// not be refreshed in place. + pub fn update_with_tokens(&mut self, token_set: &TokenSet) + where + S: FromStr + Clone, + S::Err: std::fmt::Debug, + { if let Some(Ok(scopes)) = token_set .scope .as_ref() - .map(|scope| Scope::parse_multiple_reduced(&scope).map(IntoStatic::into_static)) + .map(|scope| Scope::::parse_multiple_reduced(scope.as_ref())) { - self.scopes = scopes; + self.scopes = scopes.into_iter().map(|s| s.convert()).collect(); } - self.token_set = token_set.into_static(); + self.token_set = token_set.clone(); } } @@ -126,46 +141,34 @@ impl ClientSessionData<'_> { /// Both nonces must be written back to the store after each request so that the next /// request to the same server includes the correct replay-protection nonce. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct DpopClientData<'s> { +pub struct DpopClientData { /// The private JWK bound to this session; used to sign all DPoP proofs. pub dpop_key: Key, /// Most recently observed DPoP nonce from the authorization server. - #[serde(borrow)] - pub dpop_authserver_nonce: CowStr<'s>, + pub dpop_authserver_nonce: SmolStr, /// Most recently observed DPoP nonce from the resource server (PDS). - pub dpop_host_nonce: CowStr<'s>, + pub dpop_host_nonce: SmolStr, } -impl IntoStatic for DpopClientData<'_> { - type Output = DpopClientData<'static>; - - fn into_static(self) -> Self::Output { - DpopClientData { - dpop_key: self.dpop_key, - dpop_authserver_nonce: self.dpop_authserver_nonce.into_static(), - dpop_host_nonce: self.dpop_host_nonce.into_static(), - } - } -} - -impl DpopDataSource for DpopClientData<'_> { +impl DpopDataSource for DpopClientData { fn key(&self) -> &Key { &self.dpop_key } - fn authserver_nonce(&self) -> Option> { - Some(self.dpop_authserver_nonce.clone()) + + fn authserver_nonce(&self) -> Option<&str> { + Some(self.dpop_authserver_nonce.as_ref()) } - fn host_nonce(&self) -> Option> { - Some(self.dpop_host_nonce.clone()) + fn host_nonce(&self) -> Option<&str> { + Some(self.dpop_host_nonce.as_ref()) } - fn set_authserver_nonce(&mut self, nonce: CowStr<'_>) { - self.dpop_authserver_nonce = nonce.into_static(); + fn set_authserver_nonce(&mut self, nonce: SmolStr) { + self.dpop_authserver_nonce = nonce; } - fn set_host_nonce(&mut self, nonce: CowStr<'_>) { - self.dpop_host_nonce = nonce.into_static(); + fn set_host_nonce(&mut self, nonce: SmolStr) { + self.dpop_host_nonce = nonce; } } @@ -175,45 +178,52 @@ impl DpopDataSource for DpopClientData<'_> { /// [`crate::client::OAuthClient::callback`] so that the callback can verify the /// `state`, reconstruct the token exchange, and create a full [`ClientSessionData`]. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct AuthRequestData<'s> { +#[serde(bound( + serialize = "S: serde::Serialize + BosStr + Ord", + deserialize = "S: serde::Deserialize<'de> + BosStr, Scope: serde::Deserialize<'de>" +))] +pub struct AuthRequestData { /// Random identifier generated for this authorization request; used as the primary key /// for storing and looking up this record during the callback. - #[serde(borrow)] - pub state: CowStr<'s>, + pub state: S, /// Base URL of the authorization server that was selected for this flow. - pub authserver_url: CowStr<'s>, + pub authserver_url: S, /// If the flow was initiated with a DID or handle, the resolved DID is stored here /// so it can be compared against the `sub` in the token response. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub account_did: Option>, + pub account_did: Option>, /// OAuth scopes requested for this authorization. - pub scopes: Vec>, + pub scopes: Vec>, /// The PAR `request_uri` returned by the authorization server; included in the redirect URL. - pub request_uri: CowStr<'s>, + pub request_uri: S, /// Full URL of the authorization server's token endpoint. - pub authserver_token_endpoint: CowStr<'s>, + pub authserver_token_endpoint: S, /// Full URL of the authorization server's revocation endpoint, if advertised. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub authserver_revocation_endpoint: Option>, + pub authserver_revocation_endpoint: Option, /// The PKCE code verifier whose SHA-256 hash was sent as the code challenge; required /// at the token exchange step to prove the initiator of the auth request. - pub pkce_verifier: CowStr<'s>, + pub pkce_verifier: S, /// DPoP key and any authserver nonce observed during the PAR request. #[serde(flatten)] - pub dpop_data: DpopReqData<'s>, + pub dpop_data: DpopReqData, } -impl IntoStatic for AuthRequestData<'_> { - type Output = AuthRequestData<'static>; - fn into_static(self) -> AuthRequestData<'static> { +impl IntoStatic for AuthRequestData +where + S::Output: BosStr + Ord, +{ + type Output = AuthRequestData; + + fn into_static(self) -> AuthRequestData { AuthRequestData { request_uri: self.request_uri.into_static(), authserver_token_endpoint: self.authserver_token_endpoint.into_static(), @@ -221,7 +231,7 @@ impl IntoStatic for AuthRequestData<'_> { .authserver_revocation_endpoint .map(|s| s.into_static()), pkce_verifier: self.pkce_verifier.into_static(), - dpop_data: self.dpop_data.into_static(), + dpop_data: self.dpop_data, state: self.state.into_static(), authserver_url: self.authserver_url.into_static(), account_did: self.account_did.into_static(), @@ -235,41 +245,31 @@ impl IntoStatic for AuthRequestData<'_> { /// Unlike [`DpopClientData`], this struct only tracks the authserver nonce—no resource-server /// nonce is needed until a full session is established. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct DpopReqData<'s> { +pub struct DpopReqData { /// The private JWK generated fresh for this authorization request and session. pub dpop_key: Key, /// DPoP nonce received from the authorization server during the PAR exchange, if any. - #[serde(borrow)] - pub dpop_authserver_nonce: Option>, -} - -impl IntoStatic for DpopReqData<'_> { - type Output = DpopReqData<'static>; - fn into_static(self) -> DpopReqData<'static> { - DpopReqData { - dpop_key: self.dpop_key, - dpop_authserver_nonce: self.dpop_authserver_nonce.into_static(), - } - } + pub dpop_authserver_nonce: Option, } -impl DpopDataSource for DpopReqData<'_> { +impl DpopDataSource for DpopReqData { fn key(&self) -> &Key { &self.dpop_key } - fn authserver_nonce(&self) -> Option> { - self.dpop_authserver_nonce.clone() + + fn authserver_nonce(&self) -> Option<&str> { + self.dpop_authserver_nonce.as_ref().map(|n| n.as_ref()) } - fn host_nonce(&self) -> Option> { + fn host_nonce(&self) -> Option<&str> { None } - fn set_authserver_nonce(&mut self, nonce: CowStr<'_>) { - self.dpop_authserver_nonce = Some(nonce.into_static()); + fn set_authserver_nonce(&mut self, nonce: SmolStr) { + self.dpop_authserver_nonce = Some(nonce); } - fn set_host_nonce(&mut self, _nonce: CowStr<'_>) {} + fn set_host_nonce(&mut self, _nonce: SmolStr) {} } /// Static configuration for an OAuth client: the signing keyset and registered client metadata. @@ -277,17 +277,27 @@ impl DpopDataSource for DpopReqData<'_> { /// `ClientData` is constructed once at startup and shared (via `Arc`) across all sessions /// managed by the same [`crate::client::OAuthClient`]. #[derive(Clone, Debug)] -pub struct ClientData<'s> { +pub struct ClientData +where + S: BosStr + FromStr + Ord, + ::Err: core::fmt::Debug, +{ /// Optional private key set used for `private_key_jwt` client authentication. /// When `None`, the `none` authentication method is used instead. pub keyset: Option, /// AT Protocol-specific client registration metadata (redirect URIs, scopes, etc.). - pub config: AtprotoClientMetadata<'s>, + pub config: AtprotoClientMetadata, } -impl<'s> IntoStatic for ClientData<'s> { - type Output = ClientData<'static>; - fn into_static(self) -> ClientData<'static> { +impl IntoStatic for ClientData +where + S: BosStr + FromStr + Ord + IntoStatic, + S::Output: BosStr + Ord + FromStr, + ::Err: core::fmt::Debug, + ::Err: core::fmt::Debug, +{ + type Output = ClientData; + fn into_static(self) -> ClientData { ClientData { keyset: self.keyset, config: self.config.into_static(), @@ -295,9 +305,12 @@ impl<'s> IntoStatic for ClientData<'s> { } } -impl<'s> ClientData<'s> { +impl ClientData +where + ::Err: core::fmt::Debug, +{ /// Create `ClientData` with an optional signing keyset and the given client metadata. - pub fn new(keyset: Option, config: AtprotoClientMetadata<'s>) -> Self { + pub fn new(keyset: Option, config: AtprotoClientMetadata) -> Self { Self { keyset, config } } @@ -305,7 +318,7 @@ impl<'s> ClientData<'s> { /// /// Suitable for public clients (e.g., single-page applications or native apps) that /// cannot securely store a private key. - pub fn new_public(config: AtprotoClientMetadata<'s>) -> Self { + pub fn new_public(config: AtprotoClientMetadata) -> Self { Self { keyset: None, config, @@ -318,20 +331,28 @@ impl<'s> ClientData<'s> { /// `ClientSession` is a convenience type that pairs a [`ClientData`] with a /// [`ClientSessionData`] so that methods like `metadata` can access both without requiring /// callers to pass them separately. -pub struct ClientSession<'s> { +pub struct ClientSession +where + S: FromStr + Ord, + ::Err: core::fmt::Debug, +{ /// Optional signing keyset, forwarded from [`ClientData`]. pub keyset: Option, /// Client registration metadata, forwarded from [`ClientData`]. - pub config: AtprotoClientMetadata<'s>, + pub config: AtprotoClientMetadata, /// The session state for the authenticated account. - pub session_data: ClientSessionData<'s>, + pub session_data: ClientSessionData, } -impl<'s> ClientSession<'s> { +impl ClientSession +where + S: FromStr + Ord + Clone, + ::Err: core::fmt::Debug, +{ /// Construct a `ClientSession` from a [`ClientData`] and an active session. pub fn new( - ClientData { keyset, config }: ClientData<'s>, - session_data: ClientSessionData<'s>, + ClientData { keyset, config }: ClientData, + session_data: ClientSessionData, ) -> Self { Self { keyset, @@ -344,15 +365,16 @@ impl<'s> ClientSession<'s> { pub async fn metadata( &self, client: &T, - ) -> Result { + ) -> Result, Error> + where + S: IntoStatic, + { Ok(OAuthMetadata { server_metadata: client - .get_authorization_server_metadata(&self.session_data.authserver_url) + .get_authorization_server_metadata(self.session_data.authserver_url.as_ref()) .await .map_err(|e| Error::ServerAgent(crate::request::RequestError::resolver(e)))?, - client_metadata: atproto_client_metadata(self.config.clone(), &self.keyset) - .unwrap() - .into_static(), + client_metadata: atproto_client_metadata(&self.config, &self.keyset).unwrap(), keyset: self.keyset.clone(), }) } @@ -403,28 +425,32 @@ impl Error { /// concurrent refresh attempts for the same `(DID, session_id)` pair are coalesced behind /// a per-key `Mutex` stored in `pending`, so only one refresh request is issued to the /// authorization server even when many concurrent requests detect an expired token. -pub struct SessionRegistry +pub struct SessionRegistry where T: OAuthResolver, S: ClientAuthStore, + Str: BosStr + FromStr + Ord, + ::Err: core::fmt::Debug, { /// Backing store for persisting session data across process restarts. pub store: Arc, /// Shared resolver used to fetch authorization server metadata during refresh. pub client: Arc, /// Static client configuration (keyset and registration metadata). - pub client_data: ClientData<'static>, + pub client_data: ClientData, /// Per-`(DID, session_id)` mutex that serializes concurrent refresh attempts. pending: DashMap>>, } -impl SessionRegistry +impl SessionRegistry where S: ClientAuthStore, T: OAuthResolver, + Str: BosStr + FromStr + Ord, + ::Err: core::fmt::Debug, { /// Create a new registry, taking ownership of the store. - pub fn new(store: S, client: Arc, client_data: ClientData<'static>) -> Self { + pub fn new(store: S, client: Arc, client_data: ClientData) -> Self { let store = Arc::new(store); Self { store: Arc::clone(&store), @@ -438,7 +464,7 @@ where /// /// Use this variant when the store needs to be accessed from outside the registry, /// for example to expose session listing or administration functionality. - pub fn new_shared(store: Arc, client: Arc, client_data: ClientData<'static>) -> Self { + pub fn new_shared(store: Arc, client: Arc, client_data: ClientData) -> Self { Self { store, client, @@ -448,16 +474,18 @@ where } } -impl SessionRegistry +impl SessionRegistry where S: ClientAuthStore + Send + Sync + 'static, T: OAuthResolver + DpopExt + Send + Sync + 'static, + Str: BosStr + FromStr + Ord + Clone, + ::Err: core::fmt::Debug, { - async fn get_refreshed( + async fn get_refreshed( &self, - did: &Did<'_>, + did: &Did, session_id: &str, - ) -> Result, Error> { + ) -> Result { let key = format_smolstr!("{}_{}", did, session_id); let lock = self .pending @@ -506,12 +534,12 @@ where /// When `refresh` is `true`, proactively /// renews the token if it is within 60 seconds of expiry. When `false`, returns the session /// data as-is without contacting the authorization server. - pub async fn get( + pub async fn get( &self, - did: &Did<'_>, + did: &Did, session_id: &str, refresh: bool, - ) -> Result, Error> { + ) -> Result { if refresh { self.get_refreshed(did, session_id).await } else { @@ -523,12 +551,16 @@ where } } /// Persist an updated session to the backing store. - pub async fn set(&self, value: ClientSessionData<'_>) -> Result<(), Error> { + pub async fn set(&self, value: ClientSessionData) -> Result<(), Error> { self.store.upsert_session(value).await?; Ok(()) } /// Delete a session from the backing store. - pub async fn del(&self, did: &Did<'_>, session_id: &str) -> Result<(), Error> { + pub async fn del( + &self, + did: &Did, + session_id: &str, + ) -> Result<(), Error> { self.store.delete_session(did, session_id).await?; Ok(()) } diff --git a/crates/jacquard-oauth/src/types.rs b/crates/jacquard-oauth/src/types.rs index 6c85a6819..a1be7f607 100644 --- a/crates/jacquard-oauth/src/types.rs +++ b/crates/jacquard-oauth/src/types.rs @@ -13,8 +13,10 @@ pub use self::response::*; pub use self::token::*; use jacquard_common::CowStr; use jacquard_common::IntoStatic; +use jacquard_common::bos::{BosStr, DefaultStr}; use jacquard_common::deps::fluent_uri::Uri; use serde::Deserialize; +use smol_str::SmolStr; /// The `prompt` parameter for an OAuth authorization request. /// @@ -33,30 +35,42 @@ pub enum AuthorizeOptionPrompt { } impl From for CowStr<'static> { + fn from(value: AuthorizeOptionPrompt) -> Self { + CowStr::new_static(value.into()) + } +} + +impl From for SmolStr { + fn from(value: AuthorizeOptionPrompt) -> Self { + SmolStr::new_static(value.into()) + } +} + +impl From for &'static str { fn from(value: AuthorizeOptionPrompt) -> Self { match value { - AuthorizeOptionPrompt::Login => CowStr::new_static("login"), - AuthorizeOptionPrompt::None => CowStr::new_static("none"), - AuthorizeOptionPrompt::Consent => CowStr::new_static("consent"), - AuthorizeOptionPrompt::SelectAccount => CowStr::new_static("select_account"), + AuthorizeOptionPrompt::Login => "login", + AuthorizeOptionPrompt::None => "none", + AuthorizeOptionPrompt::Consent => "consent", + AuthorizeOptionPrompt::SelectAccount => "select_account", } } } /// Options for initiating an OAuth authorization request. #[derive(Debug)] -pub struct AuthorizeOptions<'s> { +pub struct AuthorizeOptions { /// Override the redirect URI registered in the client metadata. pub redirect_uri: Option>, /// Scopes to request. Defaults to an empty list (server-defined defaults apply). - pub scopes: Vec>, + pub scopes: Vec>, /// Optional prompt hint for the authorization server's UI. pub prompt: Option, /// Opaque client-provided state value, echoed back in the callback for CSRF protection. - pub state: Option>, + pub state: Option, } -impl Default for AuthorizeOptions<'_> { +impl Default for AuthorizeOptions { fn default() -> Self { Self { redirect_uri: None, @@ -67,7 +81,7 @@ impl Default for AuthorizeOptions<'_> { } } -impl<'s> AuthorizeOptions<'s> { +impl AuthorizeOptions { /// Set the `prompt` parameter sent to the authorization server. pub fn with_prompt(mut self, prompt: AuthorizeOptionPrompt) -> Self { self.prompt = Some(prompt); @@ -75,7 +89,7 @@ impl<'s> AuthorizeOptions<'s> { } /// Set a CSRF-protection `state` value to be echoed in the callback. - pub fn with_state(mut self, state: CowStr<'s>) -> Self { + pub fn with_state(mut self, state: S) -> Self { self.state = Some(state); self } @@ -87,7 +101,7 @@ impl<'s> AuthorizeOptions<'s> { } /// Set the OAuth scopes to request. - pub fn with_scopes(mut self, scopes: Vec>) -> Self { + pub fn with_scopes(mut self, scopes: Vec>) -> Self { self.scopes = scopes; self } @@ -95,25 +109,28 @@ impl<'s> AuthorizeOptions<'s> { /// Query parameters delivered to the OAuth redirect URI after user authorization. #[derive(Debug, Deserialize)] -pub struct CallbackParams<'s> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct CallbackParams { /// The authorization code issued by the authorization server. - #[serde(borrow)] - pub code: CowStr<'s>, + pub code: S, /// The `state` value originally sent in the authorization request, used to /// verify the response belongs to this session. - pub state: Option>, + pub state: Option, /// The `iss` (issuer) parameter, required by RFC 9207 to prevent mix-up attacks. - pub iss: Option>, + pub iss: Option, } -impl IntoStatic for CallbackParams<'_> { - type Output = CallbackParams<'static>; +impl IntoStatic for CallbackParams +where + S::Output: BosStr, +{ + type Output = CallbackParams; fn into_static(self) -> Self::Output { CallbackParams { code: self.code.into_static(), - state: self.state.map(|s| s.into_static()), - iss: self.iss.map(|s| s.into_static()), + state: self.state.into_static(), + iss: self.iss.into_static(), } } } diff --git a/crates/jacquard-oauth/src/types/client_metadata.rs b/crates/jacquard-oauth/src/types/client_metadata.rs index b39653907..7c98ffe75 100644 --- a/crates/jacquard-oauth/src/types/client_metadata.rs +++ b/crates/jacquard-oauth/src/types/client_metadata.rs @@ -1,7 +1,7 @@ -use jacquard_common::{CowStr, IntoStatic}; +use jacquard_common::IntoStatic; +use jacquard_common::bos::{BosStr, DefaultStr}; use jose_jwk::JwkSet; use serde::{Deserialize, Serialize}; -use smol_str::SmolStr; /// OAuth 2.1 client metadata, used in the ATProto client ID metadata document. /// @@ -11,29 +11,29 @@ use smol_str::SmolStr; /// /// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct OAuthClientMetadata<'c> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct OAuthClientMetadata { /// The client identifier, typically a URL pointing to this metadata document. - pub client_id: CowStr<'c>, + pub client_id: S, /// URL of the client's home page, used for display purposes. #[serde(skip_serializing_if = "Option::is_none")] - pub client_uri: Option>, + pub client_uri: Option, /// List of redirect URIs the authorization server may send callbacks to. - pub redirect_uris: Vec>, + pub redirect_uris: Vec, /// Space-separated list of scopes the client is allowed to request. #[serde(skip_serializing_if = "Option::is_none")] - #[serde(borrow)] - pub scope: Option>, + pub scope: Option, /// Application type (`web` or `native`), used to enforce redirect URI constraints. #[serde(skip_serializing_if = "Option::is_none")] - pub application_type: Option>, + pub application_type: Option, /// OAuth 2.0 grant types the client will use. #[serde(skip_serializing_if = "Option::is_none")] - pub grant_types: Option>>, + pub grant_types: Option>, /// Authentication method the client uses at the token endpoint. #[serde(skip_serializing_if = "Option::is_none")] - pub token_endpoint_auth_method: Option>, + pub token_endpoint_auth_method: Option, /// Response types the client will use in authorization requests. - pub response_types: Vec>, + pub response_types: Vec, /// If `true`, the client requires DPoP-bound access tokens (RFC 9449 §5.2). /// /// @@ -43,7 +43,7 @@ pub struct OAuthClientMetadata<'c> { /// /// #[serde(skip_serializing_if = "Option::is_none")] - pub jwks_uri: Option>, + pub jwks_uri: Option, /// Inline JWK Set for verifying signed requests, alternative to `jwks_uri`. #[serde(skip_serializing_if = "Option::is_none")] pub jwks: Option, @@ -51,45 +51,44 @@ pub struct OAuthClientMetadata<'c> { /// /// #[serde(skip_serializing_if = "Option::is_none")] - pub token_endpoint_auth_signing_alg: Option>, + pub token_endpoint_auth_signing_alg: Option, /// Human-readable name of the client, shown to users during authorization. #[serde(skip_serializing_if = "Option::is_none")] - pub client_name: Option, + pub client_name: Option, /// URL of the client's logo image. #[serde(skip_serializing_if = "Option::is_none")] - pub logo_uri: Option>, + pub logo_uri: Option, /// URL of the client's terms of service. #[serde(skip_serializing_if = "Option::is_none")] - pub tos_uri: Option>, + pub tos_uri: Option, /// URL of the client's privacy policy. #[serde(skip_serializing_if = "Option::is_none")] - pub privacy_policy_uri: Option>, + pub privacy_policy_uri: Option, } -impl OAuthClientMetadata<'_> {} +impl OAuthClientMetadata {} -impl IntoStatic for OAuthClientMetadata<'_> { - type Output = OAuthClientMetadata<'static>; +impl IntoStatic for OAuthClientMetadata +where + S::Output: BosStr, +{ + type Output = OAuthClientMetadata; fn into_static(self) -> Self::Output { OAuthClientMetadata { client_id: self.client_id.into_static(), client_uri: self.client_uri.into_static(), redirect_uris: self.redirect_uris.into_static(), - scope: self.scope.map(|scope| scope.into_static()), - application_type: self.application_type.map(|app_type| app_type.into_static()), - grant_types: self.grant_types.map(|types| types.into_static()), + scope: self.scope.into_static(), + application_type: self.application_type.into_static(), + grant_types: self.grant_types.into_static(), response_types: self.response_types.into_static(), - token_endpoint_auth_method: self - .token_endpoint_auth_method - .map(|method| method.into_static()), + token_endpoint_auth_method: self.token_endpoint_auth_method.into_static(), dpop_bound_access_tokens: self.dpop_bound_access_tokens, jwks_uri: self.jwks_uri.into_static(), jwks: self.jwks, - token_endpoint_auth_signing_alg: self - .token_endpoint_auth_signing_alg - .map(|alg| alg.into_static()), - client_name: self.client_name, + token_endpoint_auth_signing_alg: self.token_endpoint_auth_signing_alg.into_static(), + client_name: self.client_name.into_static(), logo_uri: self.logo_uri.into_static(), tos_uri: self.tos_uri.into_static(), privacy_policy_uri: self.privacy_policy_uri.into_static(), diff --git a/crates/jacquard-oauth/src/types/metadata.rs b/crates/jacquard-oauth/src/types/metadata.rs index 0530814c9..3c5ace780 100644 --- a/crates/jacquard-oauth/src/types/metadata.rs +++ b/crates/jacquard-oauth/src/types/metadata.rs @@ -1,4 +1,5 @@ -use jacquard_common::{CowStr, IntoStatic, types::string::Language}; +use jacquard_common::bos::{BosStr, DefaultStr}; +use jacquard_common::{IntoStatic, types::string::Language}; use serde::{Deserialize, Serialize}; /// Authorization server metadata, as returned from the @@ -7,67 +8,67 @@ use serde::{Deserialize, Serialize}; /// Defined by [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414#section-2) /// with extensions from OpenID Connect Discovery, RFC 9126 (PAR), RFC 9207, /// RFC 9449 (DPoP), and the ATProto client ID metadata document draft. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] -pub struct OAuthAuthorizationServerMetadata<'s> { +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct OAuthAuthorizationServerMetadata { /// The issuer identifier URL of the authorization server. /// /// - #[serde(borrow)] - pub issuer: CowStr<'s>, + pub issuer: S, /// The URL of the authorization endpoint. - pub authorization_endpoint: CowStr<'s>, // optional? + pub authorization_endpoint: S, // optional? /// The URL of the token endpoint. - pub token_endpoint: CowStr<'s>, // optional? + pub token_endpoint: S, // optional? /// URL of the authorization server's JWK Set document. - pub jwks_uri: Option>, + pub jwks_uri: Option, /// URL of the dynamic client registration endpoint, if supported. - pub registration_endpoint: Option>, + pub registration_endpoint: Option, /// List of OAuth 2.0 scope values the server supports. - pub scopes_supported: Vec>, + pub scopes_supported: Vec, /// List of OAuth 2.0 response type values the server supports. - pub response_types_supported: Vec>, + pub response_types_supported: Vec, /// List of OAuth 2.0 response mode values the server supports. - pub response_modes_supported: Option>>, + pub response_modes_supported: Option>, /// List of OAuth 2.0 grant type values the server supports. - pub grant_types_supported: Option>>, + pub grant_types_supported: Option>, /// List of client authentication methods supported at the token endpoint. - pub token_endpoint_auth_methods_supported: Option>>, + pub token_endpoint_auth_methods_supported: Option>, /// List of JWS signing algorithms supported for token endpoint auth. - pub token_endpoint_auth_signing_alg_values_supported: Option>>, + pub token_endpoint_auth_signing_alg_values_supported: Option>, /// URL of a page with human-readable information about the server. - pub service_documentation: Option>, + pub service_documentation: Option, /// BCP 47 language tags for UI locales the server supports. pub ui_locales_supported: Option>, /// URL of the authorization server's privacy policy. - pub op_policy_uri: Option>, + pub op_policy_uri: Option, /// URL of the authorization server's terms of service. - pub op_tos_uri: Option>, + pub op_tos_uri: Option, /// URL of the token revocation endpoint (RFC 7009). - pub revocation_endpoint: Option>, + pub revocation_endpoint: Option, /// List of client authentication methods supported at the revocation endpoint. - pub revocation_endpoint_auth_methods_supported: Option>>, + pub revocation_endpoint_auth_methods_supported: Option>, /// List of JWS signing algorithms supported for revocation endpoint auth. - pub revocation_endpoint_auth_signing_alg_values_supported: Option>>, + pub revocation_endpoint_auth_signing_alg_values_supported: Option>, /// URL of the token introspection endpoint (RFC 7662). - pub introspection_endpoint: Option>, + pub introspection_endpoint: Option, /// List of client authentication methods supported at the introspection endpoint. - pub introspection_endpoint_auth_methods_supported: Option>>, + pub introspection_endpoint_auth_methods_supported: Option>, /// List of JWS signing algorithms supported for introspection endpoint auth. - pub introspection_endpoint_auth_signing_alg_values_supported: Option>>, + pub introspection_endpoint_auth_signing_alg_values_supported: Option>, /// PKCE code challenge methods supported by the server. - pub code_challenge_methods_supported: Option>>, + pub code_challenge_methods_supported: Option>, /// Subject identifier types supported (`public` or `pairwise`). /// /// - pub subject_types_supported: Option>>, + pub subject_types_supported: Option>, /// If `true`, clients must pre-register `request_uri` values. pub require_request_uri_registration: Option, /// URL of the Pushed Authorization Request (PAR) endpoint (RFC 9126). /// /// - pub pushed_authorization_request_endpoint: Option>, + pub pushed_authorization_request_endpoint: Option, /// If `true`, all authorization requests must use PAR. pub require_pushed_authorization_requests: Option, @@ -79,7 +80,7 @@ pub struct OAuthAuthorizationServerMetadata<'s> { /// DPoP JWS signing algorithms supported by this server (RFC 9449). /// /// - pub dpop_signing_alg_values_supported: Option>>, + pub dpop_signing_alg_values_supported: Option>, /// If `true`, the server supports the ATProto client ID metadata document extension. /// @@ -89,7 +90,44 @@ pub struct OAuthAuthorizationServerMetadata<'s> { /// Protected resources associated with this authorization server. /// /// - pub protected_resources: Option>>, + pub protected_resources: Option>, +} + +impl Default for OAuthAuthorizationServerMetadata { + fn default() -> Self { + OAuthAuthorizationServerMetadata { + issuer: S::from_static(""), + authorization_endpoint: S::from_static(""), + token_endpoint: S::from_static(""), + jwks_uri: None, + registration_endpoint: None, + scopes_supported: Vec::new(), + response_types_supported: Vec::new(), + response_modes_supported: None, + grant_types_supported: None, + token_endpoint_auth_methods_supported: None, + token_endpoint_auth_signing_alg_values_supported: None, + service_documentation: None, + ui_locales_supported: None, + op_policy_uri: None, + op_tos_uri: None, + revocation_endpoint: None, + revocation_endpoint_auth_methods_supported: None, + revocation_endpoint_auth_signing_alg_values_supported: None, + introspection_endpoint: None, + introspection_endpoint_auth_methods_supported: None, + introspection_endpoint_auth_signing_alg_values_supported: None, + code_challenge_methods_supported: None, + subject_types_supported: None, + require_request_uri_registration: None, + pushed_authorization_request_endpoint: None, + require_pushed_authorization_requests: None, + authorization_response_iss_parameter_supported: None, + dpop_signing_alg_values_supported: None, + client_id_metadata_document_supported: None, + protected_resources: None, + } + } } /// Protected resource metadata, returned from `.well-known/oauth-protected-resource`. @@ -97,50 +135,72 @@ pub struct OAuthAuthorizationServerMetadata<'s> { /// Allows clients to discover which authorization servers protect a given resource /// and what scopes and bearer methods are accepted. Defined by /// [draft-ietf-oauth-resource-metadata](https://datatracker.ietf.org/doc/draft-ietf-oauth-resource-metadata/). -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] -pub struct OAuthProtectedResourceMetadata<'s> { +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct OAuthProtectedResourceMetadata { /// The URL of the protected resource itself. - #[serde(borrow)] - pub resource: CowStr<'s>, + pub resource: S, /// URLs of authorization servers that can issue tokens for this resource. - pub authorization_servers: Option>>, + pub authorization_servers: Option>, /// URL of the resource server's JWK Set document. - pub jwks_uri: Option>, + pub jwks_uri: Option, /// List of OAuth 2.0 scope values the resource server supports. - pub scopes_supported: Vec>, + pub scopes_supported: Vec, /// Bearer token presentation methods supported (`header`, `body`, `query`). - pub bearer_methods_supported: Option>>, + pub bearer_methods_supported: Option>, /// JWS signing algorithms supported for resource-bound tokens. - pub resource_signing_alg_values_supported: Option>>, + pub resource_signing_alg_values_supported: Option>, /// URL of a page with human-readable information about the resource. - pub resource_documentation: Option>, + pub resource_documentation: Option, /// URL of the resource server's privacy policy. - pub resource_policy_uri: Option>, + pub resource_policy_uri: Option, /// URL of the resource server's terms of service. - pub resource_tos_uri: Option>, + pub resource_tos_uri: Option, } -impl IntoStatic for OAuthProtectedResourceMetadata<'_> { - type Output = OAuthProtectedResourceMetadata<'static>; +impl Default for OAuthProtectedResourceMetadata { + fn default() -> Self { + OAuthProtectedResourceMetadata { + resource: S::from_static(""), + authorization_servers: None, + jwks_uri: None, + scopes_supported: Vec::new(), + bearer_methods_supported: None, + resource_signing_alg_values_supported: None, + resource_documentation: None, + resource_policy_uri: None, + resource_tos_uri: None, + } + } +} + +impl IntoStatic for OAuthProtectedResourceMetadata +where + S::Output: BosStr, +{ + type Output = OAuthProtectedResourceMetadata; fn into_static(self) -> Self::Output { OAuthProtectedResourceMetadata { resource: self.resource.into_static(), authorization_servers: self.authorization_servers.into_static(), - jwks_uri: self.jwks_uri.map(|v| v.into_static()), + jwks_uri: self.jwks_uri.into_static(), scopes_supported: self.scopes_supported.into_static(), - bearer_methods_supported: self.bearer_methods_supported.map(|v| v.into_static()), + bearer_methods_supported: self.bearer_methods_supported.into_static(), resource_signing_alg_values_supported: self .resource_signing_alg_values_supported - .map(|v| v.into_static()), - resource_documentation: self.resource_documentation.map(|v| v.into_static()), - resource_policy_uri: self.resource_policy_uri.map(|v| v.into_static()), - resource_tos_uri: self.resource_tos_uri.map(|v| v.into_static()), + .into_static(), + resource_documentation: self.resource_documentation.into_static(), + resource_policy_uri: self.resource_policy_uri.into_static(), + resource_tos_uri: self.resource_tos_uri.into_static(), } } } -impl IntoStatic for OAuthAuthorizationServerMetadata<'_> { - type Output = OAuthAuthorizationServerMetadata<'static>; +impl IntoStatic for OAuthAuthorizationServerMetadata +where + S::Output: BosStr, +{ + type Output = OAuthAuthorizationServerMetadata; fn into_static(self) -> Self::Output { OAuthAuthorizationServerMetadata { issuer: self.issuer.into_static(), @@ -178,20 +238,15 @@ impl IntoStatic for OAuthAuthorizationServerMetadata<'_> { .into_static(), code_challenge_methods_supported: self.code_challenge_methods_supported.into_static(), subject_types_supported: self.subject_types_supported.into_static(), - require_request_uri_registration: self.require_request_uri_registration.into_static(), + require_request_uri_registration: self.require_request_uri_registration, pushed_authorization_request_endpoint: self .pushed_authorization_request_endpoint .into_static(), - require_pushed_authorization_requests: self - .require_pushed_authorization_requests - .into_static(), + require_pushed_authorization_requests: self.require_pushed_authorization_requests, authorization_response_iss_parameter_supported: self - .authorization_response_iss_parameter_supported - .into_static(), + .authorization_response_iss_parameter_supported, dpop_signing_alg_values_supported: self.dpop_signing_alg_values_supported.into_static(), - client_id_metadata_document_supported: self - .client_id_metadata_document_supported - .into_static(), + client_id_metadata_document_supported: self.client_id_metadata_document_supported, protected_resources: self.protected_resources.into_static(), } } diff --git a/crates/jacquard-oauth/src/types/request.rs b/crates/jacquard-oauth/src/types/request.rs index 7ce02008e..9e81e26e9 100644 --- a/crates/jacquard-oauth/src/types/request.rs +++ b/crates/jacquard-oauth/src/types/request.rs @@ -1,4 +1,5 @@ -use jacquard_common::{CowStr, IntoStatic}; +use jacquard_common::IntoStatic; +use jacquard_common::bos::{BosStr, DefaultStr}; use serde::{Deserialize, Serialize}; /// The `response_type` parameter for an OAuth 2.0 authorization request. @@ -51,18 +52,18 @@ pub enum AuthorizationCodeChallengeMethod { /// authorization server before redirecting the user, improving security by keeping /// parameters out of the browser URL. #[derive(Serialize, Deserialize, Debug)] -pub struct ParParameters<'a> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct ParParameters { /// The response type to request (e.g. `code`). /// /// pub response_type: AuthorizationResponseType, /// The redirect URI where the authorization response will be sent. - #[serde(borrow)] - pub redirect_uri: CowStr<'a>, + pub redirect_uri: S, /// An opaque CSRF state value to be echoed back in the callback. - pub state: CowStr<'a>, + pub state: S, /// Space-separated list of requested scopes. - pub scope: Option>, + pub scope: Option, /// How the authorization response parameters are delivered to the client. /// /// @@ -70,15 +71,15 @@ pub struct ParParameters<'a> { /// The PKCE code challenge derived from the code verifier. /// /// - pub code_challenge: CowStr<'a>, + pub code_challenge: S, /// The method used to derive the code challenge. pub code_challenge_method: AuthorizationCodeChallengeMethod, /// Hint to pre-fill the login form with a handle or email. /// /// - pub login_hint: Option>, + pub login_hint: Option, /// Prompt hint controlling authorization server UI behavior. - pub prompt: Option>, + pub prompt: Option, } /// The `grant_type` parameter for a token endpoint request. @@ -93,34 +94,34 @@ pub enum TokenGrantType { /// Parameters for exchanging an authorization code for tokens (RFC 6749 §4.1.3). #[derive(Serialize, Deserialize)] -pub struct TokenRequestParameters<'a> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct TokenRequestParameters { /// Must be `authorization_code` for the authorization code grant. /// /// pub grant_type: TokenGrantType, /// The authorization code received from the authorization server. - #[serde(borrow)] - pub code: CowStr<'a>, + pub code: S, /// The redirect URI used in the original authorization request. - pub redirect_uri: CowStr<'a>, + pub redirect_uri: S, /// The PKCE code verifier that was used to generate the code challenge (RFC 7636 §4.5). /// /// - pub code_verifier: CowStr<'a>, + pub code_verifier: S, } /// Parameters for refreshing an access token using a refresh token (RFC 6749 §6). #[derive(Serialize, Deserialize)] -pub struct RefreshRequestParameters<'a> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct RefreshRequestParameters { /// Must be `refresh_token` for the refresh grant. /// /// pub grant_type: TokenGrantType, /// The refresh token previously issued to the client. - #[serde(borrow)] - pub refresh_token: CowStr<'a>, + pub refresh_token: S, /// Optional scope to request; must not exceed the originally granted scope. - pub scope: Option>, + pub scope: Option, } /// Parameters for a token revocation request (RFC 7009 §2.1). @@ -130,16 +131,19 @@ pub struct RefreshRequestParameters<'a> { /// /// #[derive(Serialize, Deserialize)] -pub struct RevocationRequestParameters<'a> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct RevocationRequestParameters { /// The token to be revoked. - #[serde(borrow)] - pub token: CowStr<'a>, + pub token: S, // ? // pub token_type_hint: Option, } -impl IntoStatic for RevocationRequestParameters<'_> { - type Output = RevocationRequestParameters<'static>; +impl IntoStatic for RevocationRequestParameters +where + S::Output: BosStr, +{ + type Output = RevocationRequestParameters; fn into_static(self) -> Self::Output { Self::Output { @@ -148,8 +152,11 @@ impl IntoStatic for RevocationRequestParameters<'_> { } } -impl IntoStatic for TokenRequestParameters<'_> { - type Output = TokenRequestParameters<'static>; +impl IntoStatic for TokenRequestParameters +where + S::Output: BosStr, +{ + type Output = TokenRequestParameters; fn into_static(self) -> Self::Output { Self::Output { @@ -161,20 +168,26 @@ impl IntoStatic for TokenRequestParameters<'_> { } } -impl IntoStatic for RefreshRequestParameters<'_> { - type Output = RefreshRequestParameters<'static>; +impl IntoStatic for RefreshRequestParameters +where + S::Output: BosStr, +{ + type Output = RefreshRequestParameters; fn into_static(self) -> Self::Output { Self::Output { grant_type: self.grant_type, refresh_token: self.refresh_token.into_static(), - scope: self.scope.map(CowStr::into_static), + scope: self.scope.into_static(), } } } -impl IntoStatic for ParParameters<'_> { - type Output = ParParameters<'static>; +impl IntoStatic for ParParameters +where + S::Output: BosStr, +{ + type Output = ParParameters; fn into_static(self) -> Self::Output { Self::Output { diff --git a/crates/jacquard-oauth/src/types/token.rs b/crates/jacquard-oauth/src/types/token.rs index 1c4f165cb..5eea4bfc2 100644 --- a/crates/jacquard-oauth/src/types/token.rs +++ b/crates/jacquard-oauth/src/types/token.rs @@ -1,6 +1,7 @@ use super::response::OAuthTokenType; +use jacquard_common::IntoStatic; +use jacquard_common::bos::{BosStr, DefaultStr}; use jacquard_common::types::string::{Datetime, Did}; -use jacquard_common::{CowStr, IntoStatic}; use serde::{Deserialize, Serialize}; /// A complete set of OAuth tokens and associated claims for an authenticated session. @@ -9,21 +10,21 @@ use serde::{Deserialize, Serialize}; /// everything it needs to make authorized requests. This is stored in the session /// and refreshed transparently by `OAuthSession`. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct TokenSet<'s> { +#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))] +pub struct TokenSet { /// The issuer URL of the authorization server that issued these tokens. - #[serde(borrow)] - pub iss: CowStr<'s>, + pub iss: S, /// The subject DID identifying the authenticated user. - pub sub: Did<'s>, + pub sub: Did, /// The audience (resource server URL or DID) the tokens are intended for. - pub aud: CowStr<'s>, + pub aud: S, /// The scopes granted by the authorization server. - pub scope: Option>, + pub scope: Option, /// A refresh token that can be exchanged for new access tokens. - pub refresh_token: Option>, + pub refresh_token: Option, /// The current access token to include in API requests. - pub access_token: CowStr<'s>, + pub access_token: S, /// Whether the access token must be presented as a DPoP or Bearer token. pub token_type: OAuthTokenType, @@ -31,19 +32,38 @@ pub struct TokenSet<'s> { pub expires_at: Option, } -impl IntoStatic for TokenSet<'_> { - type Output = TokenSet<'static>; +impl TokenSet { + /// Convert to an `Nsid` with a different backing type. + pub fn convert + BosStr>(self) -> TokenSet { + TokenSet { + iss: self.iss.into(), + sub: self.sub.convert(), + aud: self.aud.into(), + scope: self.scope.map(|s| s.into()), + refresh_token: self.refresh_token.map(|t| t.into()), + access_token: self.access_token.into(), + token_type: self.token_type, + expires_at: self.expires_at, + } + } +} + +impl IntoStatic for TokenSet +where + S::Output: BosStr, +{ + type Output = TokenSet; fn into_static(self) -> Self::Output { TokenSet { iss: self.iss.into_static(), sub: self.sub.into_static(), aud: self.aud.into_static(), - scope: self.scope.map(|s| s.into_static()), - refresh_token: self.refresh_token.map(|s| s.into_static()), + scope: self.scope.into_static(), + refresh_token: self.refresh_token.into_static(), access_token: self.access_token.into_static(), token_type: self.token_type, - expires_at: self.expires_at.map(|s| s.into_static()), + expires_at: self.expires_at.into_static(), } } } diff --git a/crates/jacquard-oauth/src/utils.rs b/crates/jacquard-oauth/src/utils.rs index 4456b6fda..9303745f6 100644 --- a/crates/jacquard-oauth/src/utils.rs +++ b/crates/jacquard-oauth/src/utils.rs @@ -1,10 +1,11 @@ use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use elliptic_curve::SecretKey; -use jacquard_common::CowStr; +use jacquard_common::BosStr; use jose_jwk::{Key, crypto}; use rand::{CryptoRng, RngCore, rngs::ThreadRng}; use sha2::{Digest, Sha256}; +use smol_str::SmolStr; use std::cmp::Ordering; use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata}; @@ -13,7 +14,7 @@ use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata}; /// supported, returning `None` if none are supported. /// /// Currently only `ES256` (P-256 ECDSA) is implemented; other algorithm identifiers are skipped. -pub fn generate_key(allowed_algos: &[CowStr]) -> Option { +pub fn generate_key(allowed_algos: &[impl AsRef]) -> Option { for alg in allowed_algos { #[allow(clippy::single_match)] match alg.as_ref() { @@ -31,14 +32,14 @@ pub fn generate_key(allowed_algos: &[CowStr]) -> Option { } /// Generate a cryptographically random 16-byte nonce encoded as base64url (no padding). -pub fn generate_nonce() -> CowStr<'static> { +pub fn generate_nonce() -> SmolStr { URL_SAFE_NO_PAD .encode(get_random_values::<_, 16>(&mut ThreadRng::default())) .into() } /// Generate a cryptographically random 43-byte PKCE code verifier encoded as base64url (no padding). -pub fn generate_verifier() -> CowStr<'static> { +pub fn generate_verifier() -> SmolStr { URL_SAFE_NO_PAD .encode(get_random_values::<_, 43>(&mut ThreadRng::default())) .into() @@ -58,11 +59,13 @@ where /// /// The ordering is: ES256K > ES (256 > 384 > 512) > PS (256 > 384 > 512) > RS (256 > 384 > 512) > other. /// Algorithms within the same family are ordered by key length, preferring shorter (faster) keys first. -pub fn compare_algos(a: &CowStr, b: &CowStr) -> Ordering { - if a.as_ref() == "ES256K" { +pub fn compare_algos(a: &impl AsRef, b: &impl AsRef) -> Ordering { + let a = a.as_ref(); + let b = b.as_ref(); + if a == "ES256K" { return Ordering::Less; } - if b.as_ref() == "ES256K" { + if b == "ES256K" { return Ordering::Greater; } for prefix in ["ES", "PS", "RS"] { @@ -89,12 +92,12 @@ pub fn compare_algos(a: &CowStr, b: &CowStr) -> Ordering { /// of the verifier, per [RFC 7636 §4.1](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1). /// The verifier must be kept secret and sent at the token endpoint; the challenge is sent at /// the authorization endpoint. -pub fn generate_pkce() -> (CowStr<'static>, CowStr<'static>) { +pub fn generate_pkce() -> (SmolStr, SmolStr) { // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 let verifier = generate_verifier(); ( URL_SAFE_NO_PAD - .encode(Sha256::digest(&verifier.as_str())) + .encode(Sha256::digest(verifier.as_str())) .into(), verifier, ) @@ -105,11 +108,14 @@ pub fn generate_pkce() -> (CowStr<'static>, CowStr<'static>) { /// Reads `dpop_signing_alg_values_supported` from the server metadata, sorts by preference /// using [`compare_algos`], and attempts to generate a key for the most preferred supported /// algorithm. Falls back to [`crate::FALLBACK_ALG`] if the server does not advertise any algorithms. -pub fn generate_dpop_key(metadata: &OAuthAuthorizationServerMetadata) -> Option { - let mut algs = metadata +pub fn generate_dpop_key( + metadata: &mut OAuthAuthorizationServerMetadata, +) -> Option { + let mut fallback = vec![S::from_static(FALLBACK_ALG)]; + let algs = metadata .dpop_signing_alg_values_supported - .clone() - .unwrap_or(vec![FALLBACK_ALG.into()]); + .as_deref_mut() + .unwrap_or(&mut fallback); algs.sort_by(compare_algos); generate_key(&algs) } diff --git a/crates/jacquard-repo/src/commit/firehose.rs b/crates/jacquard-repo/src/commit/firehose.rs index a48484697..cc3010b37 100644 --- a/crates/jacquard-repo/src/commit/firehose.rs +++ b/crates/jacquard-repo/src/commit/firehose.rs @@ -7,13 +7,15 @@ pub use jacquard_api::com_atproto::sync::subscribe_repos::Commit as FirehoseCommit; pub use jacquard_api::com_atproto::sync::subscribe_repos::RepoOp; use jacquard_api::com_atproto::sync::subscribe_repos::{Commit, RepoOpAction}; +use jacquard_common::BosStr; use jacquard_common::types::crypto::PublicKey; +use smol_str::SmolStr; use smol_str::ToSmolStr; /// Convert to VerifiedWriteOp for v1.1 validation /// /// Validates that all required fields are present for inversion. -pub fn to_invertible_op(op: &RepoOp<'_>) -> Result { +pub fn to_invertible_op(op: &RepoOp) -> Result { let key = op.path.to_smolstr(); match op.action { RepoOpAction::Create => { @@ -91,8 +93,8 @@ use std::sync::Arc; /// 4. Verify result matches commit.data (new MST root) /// /// Returns the new MST root CID on success. -pub async fn validate_v1_0( - fh_commit: &Commit<'_>, +pub async fn validate_v1_0( + fh_commit: &Commit, prev_mst_root: Option, prev_storage: Arc, pubkey: &PublicKey<'_>, @@ -115,7 +117,7 @@ pub async fn validate_v1_0( .await? .ok_or_else(|| RepoError::not_found("commit block", &commit_cid))?; - let commit = super::Commit::from_cbor(&commit_bytes)?; + let commit = super::Commit::::from_cbor(&commit_bytes)?; // Verify DID matches if commit.did().as_ref() != fh_commit.repo.as_ref() { @@ -186,7 +188,10 @@ pub async fn validate_v1_0( /// this is far from the most efficient possible validation function possible. The repo /// tree struct carries extra information. However, /// it has the virtue of making everything self-validating. -pub async fn validate_v1_1(fh_commit: &Commit<'_>, pubkey: &PublicKey<'_>) -> Result { +pub async fn validate_v1_1( + fh_commit: &Commit, + pubkey: &PublicKey<'_>, +) -> Result { // 1. Require prev_data for v1.1 let prev_data_cid: IpldCid = fh_commit .prev_data @@ -210,7 +215,7 @@ pub async fn validate_v1_1(fh_commit: &Commit<'_>, pubkey: &PublicKey<'_>) -> Re .await? .ok_or_else(|| RepoError::not_found("commit block", &commit_cid))?; - let commit = super::Commit::from_cbor(&commit_bytes)?; + let commit = super::Commit::::from_cbor(&commit_bytes)?; // Verify DID matches if commit.did().as_ref() != fh_commit.repo.as_ref() { @@ -300,7 +305,9 @@ mod tests { record } - async fn create_test_repo(storage: Arc) -> Repository { + async fn create_test_repo<'a>( + storage: Arc, + ) -> Repository { let did = Did::new("did:plc:test").unwrap(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); @@ -332,10 +339,10 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("test1").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("test1").unwrap().into_static()); - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); @@ -381,10 +388,10 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("test1").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("test1").unwrap()).into_static(); - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); @@ -433,8 +440,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); @@ -442,12 +449,12 @@ mod tests { let ops1 = vec![ RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("post1").unwrap()), + rkey: RecordKey(Rkey::new("post1").unwrap()).into_static(), record: make_test_record(1), }, RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("post2").unwrap()), + rkey: RecordKey(Rkey::new("post2").unwrap()).into_static(), record: make_test_record(2), }, ]; @@ -476,14 +483,14 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); // First: create records - let rkey1 = RecordKey(Rkey::new("post1").unwrap()); - let rkey2 = RecordKey(Rkey::new("post2").unwrap()); + let rkey1 = RecordKey(Rkey::new("post1").unwrap()).into_static(); + let rkey2 = RecordKey(Rkey::new("post2").unwrap()).into_static(); let create_ops = vec![ RecordWriteOp::Create { @@ -552,14 +559,14 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); let ops = vec![RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("test1").unwrap()), + rkey: RecordKey(Rkey::new("test1").unwrap()).into_static(), record: make_test_record(1), }]; @@ -607,8 +614,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); @@ -616,12 +623,12 @@ mod tests { let ops = vec![ RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("aaa").unwrap()), + rkey: RecordKey(Rkey::new("aaa").unwrap()).into_static(), record: make_test_record(1), }, RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("zzz").unwrap()), + rkey: RecordKey(Rkey::new("zzz").unwrap()).into_static(), record: make_test_record(2), }, ]; @@ -669,14 +676,14 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); let ops = vec![RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("test1").unwrap()), + rkey: RecordKey(Rkey::new("test1").unwrap()).into_static(), record: make_test_record(1), }]; @@ -730,15 +737,15 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); - let wrong_did = Did::new("did:plc:wrong").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); + let wrong_did = Did::new("did:plc:wrong").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); let ops = vec![RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("test1").unwrap()), + rkey: RecordKey(Rkey::new("test1").unwrap()).into_static(), record: make_test_record(1), }]; @@ -778,8 +785,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); // Use a different key for verification @@ -788,7 +795,7 @@ mod tests { let ops = vec![RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("test1").unwrap()), + rkey: RecordKey(Rkey::new("test1").unwrap().into_static()), record: make_test_record(1), }]; @@ -819,14 +826,14 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); let ops = vec![RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("test1").unwrap()), + rkey: RecordKey(Rkey::new("test1").unwrap().into_static()), record: make_test_record(1), }]; @@ -866,14 +873,14 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); let ops = vec![RecordWriteOp::Create { collection: collection.clone(), - rkey: RecordKey(Rkey::new("test1").unwrap()), + rkey: RecordKey(Rkey::new("test1").unwrap()).into_static(), record: make_test_record(1), }]; diff --git a/crates/jacquard-repo/src/commit/mod.rs b/crates/jacquard-repo/src/commit/mod.rs index c9ed307ab..65c36fa4a 100644 --- a/crates/jacquard-repo/src/commit/mod.rs +++ b/crates/jacquard-repo/src/commit/mod.rs @@ -8,10 +8,10 @@ pub(crate) mod serde_bytes_helper; use crate::error::{CommitError, Result}; use bytes::Bytes; use cid::Cid as IpldCid; -use jacquard_common::IntoStatic; use jacquard_common::types::crypto::PublicKey; use jacquard_common::types::string::Did; use jacquard_common::types::tid::Tid; +use jacquard_common::{BosStr, IntoStatic}; /// Repository commit object /// @@ -22,10 +22,9 @@ use jacquard_common::types::tid::Tid; /// serialized (v2 uses it, v3 must include it even if null). This struct /// handles both by always including `prev` in serialization. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct Commit<'a> { +pub struct Commit { /// Repository DID - #[serde(borrow)] - pub did: Did<'a>, + pub did: Did, /// Commit version (2 or 3) pub version: i64, @@ -48,10 +47,9 @@ pub struct Commit<'a> { /// Explicitly a separate struct minus the sig field to match deserialization/signatures /// from other implementations. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct UnsignedCommit<'a> { +pub struct UnsignedCommit { /// Repository DID - #[serde(borrow)] - pub did: Did<'a>, + pub did: Did, /// Commit version (2 or 3) pub version: i64, @@ -66,14 +64,14 @@ pub struct UnsignedCommit<'a> { pub prev: Option, } -impl<'a> UnsignedCommit<'a> { +impl UnsignedCommit { /// Create new unsigned commit (version = 3, sig empty) pub fn new_unsigned( - did: Did<'a>, + did: Did, data: IpldCid, rev: Tid, prev: Option, - ) -> UnsignedCommit<'a> { + ) -> UnsignedCommit { Self { did, version: 3, @@ -85,13 +83,12 @@ impl<'a> UnsignedCommit<'a> { /// Get unsigned commit bytes (for signing/verification) pub(super) fn unsigned_bytes(&self) -> Result> { // Serialize without signature field - let unsigned = self.clone(); - serde_ipld_dagcbor::to_vec(&unsigned) + serde_ipld_dagcbor::to_vec(self) .map_err(|e| crate::error::CommitError::Serialization(Box::new(e)).into()) } /// Sign this commit with a key - pub fn sign(self, key: &impl SigningKey) -> Result> { + pub fn sign(self, key: &impl SigningKey) -> Result> { let unsigned = self.unsigned_bytes()?; let sig = key.sign_bytes(&unsigned)?; @@ -105,7 +102,7 @@ impl<'a> UnsignedCommit<'a> { }) } /// Get the repository DID - pub fn did(&self) -> &Did<'a> { + pub fn did(&self) -> &Did { &self.did } @@ -125,14 +122,14 @@ impl<'a> UnsignedCommit<'a> { } } -impl<'a> Commit<'a> { +impl Commit { /// Create new unsigned commit (version = 3, sig empty) pub fn new_unsigned( - did: Did<'a>, + did: Did, data: IpldCid, rev: Tid, prev: Option, - ) -> UnsignedCommit<'a> { + ) -> UnsignedCommit { UnsignedCommit { did, version: 3, @@ -157,7 +154,7 @@ impl<'a> Commit<'a> { } /// Get the repository DID - pub fn did(&self) -> &Did<'a> { + pub fn did(&self) -> &Did { &self.did } @@ -187,7 +184,10 @@ impl<'a> Commit<'a> { } /// Deserialize from DAG-CBOR - pub fn from_cbor(data: &'a [u8]) -> Result { + pub fn from_cbor<'a>(data: &'a [u8]) -> Result + where + S: serde::Deserialize<'a>, + { serde_ipld_dagcbor::from_slice(data) .map_err(|e| CommitError::Serialization(Box::new(e)).into()) } @@ -251,8 +251,12 @@ impl<'a> Commit<'a> { } } -impl IntoStatic for Commit<'_> { - type Output = Commit<'static>; +impl IntoStatic for Commit +where + S: BosStr + IntoStatic, + S::Output: BosStr, +{ + type Output = Commit; fn into_static(self) -> Self::Output { Commit { diff --git a/crates/jacquard-repo/src/commit/proof.rs b/crates/jacquard-repo/src/commit/proof.rs index 45763c8bc..fd378f4e0 100644 --- a/crates/jacquard-repo/src/commit/proof.rs +++ b/crates/jacquard-repo/src/commit/proof.rs @@ -22,9 +22,9 @@ use crate::error::ProofError; use crate::mst::Mst; use crate::storage::MemoryBlockStore; use cid::Cid as IpldCid; -use jacquard_common::CowStr; use jacquard_common::types::string::Did; -use smol_str::format_smolstr; +use jacquard_common::{BosStr, CowStr}; +use smol_str::{SmolStr, format_smolstr}; use std::sync::Arc; /// A claim about a record's CID at a specific path @@ -88,12 +88,15 @@ pub struct VerifyProofsOutput<'a> { /// let result = verify_proofs(car_bytes, claims, did, pubkey).await?; /// assert_eq!(result.verified.len(), 2); // Both claims verified /// ``` -pub async fn verify_proofs<'a>( +pub async fn verify_proofs<'a, 'de, S>( car_bytes: &[u8], claims: Vec>, - did: &Did<'_>, + did: &Did, pubkey: &jacquard_common::types::crypto::PublicKey<'_>, -) -> Result, ProofError> { +) -> Result, ProofError> +where + S: BosStr + Clone + serde::Serialize + serde::Deserialize<'de>, +{ // 1. Parse CAR file let parsed = crate::car::parse_car_bytes(car_bytes) @@ -113,7 +116,7 @@ pub async fn verify_proofs<'a>( .map_err(|_| ProofError::CommitNotFound)? .ok_or(ProofError::CommitNotFound)?; - let commit = super::Commit::from_cbor(&commit_bytes).map_err(|e| { + let commit = super::Commit::::from_cbor(&commit_bytes).map_err(|e| { ProofError::CommitDeserializeFailed { source: Box::new(e), } diff --git a/crates/jacquard-repo/src/mst/diff.rs b/crates/jacquard-repo/src/mst/diff.rs index a7bf59270..3b4fd1e4b 100644 --- a/crates/jacquard-repo/src/mst/diff.rs +++ b/crates/jacquard-repo/src/mst/diff.rs @@ -1,8 +1,10 @@ //! MST diff calculation use std::collections::BTreeMap; +use std::convert::{From, Infallible}; use std::future::Future; use std::pin::Pin; +use std::str::FromStr; use super::cursor::{CursorPosition, MstCursor}; use super::tree::Mst; @@ -13,6 +15,8 @@ use crate::mst::NodeEntry; use crate::storage::BlockStore; use bytes::Bytes; use cid::Cid as IpldCid; +use jacquard_api::com_atproto::sync::subscribe_repos::RepoOpAction; +use jacquard_common::BosStr; use jacquard_common::types::cid::CidLink; use smol_str::SmolStr; @@ -160,14 +164,14 @@ impl MstDiff { /// /// Returns operations in the format used by `com.atproto.sync.subscribeRepos`. /// All update/delete operations include prev CIDs for sync v1.1 validation. - pub fn to_repo_ops(&self) -> Vec> { + pub fn to_repo_ops<'s, S: BosStr + FromStr>(&'s self) -> Vec> { let mut ops = Vec::with_capacity(self.op_count()); // Add creates for (key, cid) in &self.creates { ops.push(RepoOp { - action: "create".into(), - path: key.as_str().into(), + action: RepoOpAction::from_value(S::from_static("create")), + path: S::from_str(key.as_str()).unwrap(), cid: Some(CidLink::from(*cid)), prev: None, extra_data: None, @@ -177,8 +181,8 @@ impl MstDiff { // Add updates for (key, new_cid, old_cid) in &self.updates { ops.push(RepoOp { - action: "update".into(), - path: key.as_str().into(), + action: RepoOpAction::from_value(S::from_static("update")), + path: S::from_str(key.as_str()).unwrap(), cid: Some(CidLink::from(*new_cid)), prev: Some(CidLink::from(*old_cid)), extra_data: None, @@ -188,8 +192,8 @@ impl MstDiff { // Add deletes for (key, old_cid) in &self.deletes { ops.push(RepoOp { - action: "delete".into(), - path: key.as_str().into(), + action: RepoOpAction::from_value(S::from_static("delete")), + path: S::from_str(key.as_str()).unwrap(), cid: None, // null for deletes prev: Some(CidLink::from(*old_cid)), extra_data: None, diff --git a/crates/jacquard-repo/src/mst/tree.rs b/crates/jacquard-repo/src/mst/tree.rs index 6e5022c25..e43243a71 100644 --- a/crates/jacquard-repo/src/mst/tree.rs +++ b/crates/jacquard-repo/src/mst/tree.rs @@ -8,6 +8,7 @@ use crate::storage::BlockStore; use bytes::Bytes; use cid::Cid as IpldCid; use core::fmt; +use jacquard_common::BosStr; use jacquard_common::types::recordkey::Rkey; use jacquard_common::types::string::{Nsid, RecordKey}; use jacquard_common::types::value::RawData; @@ -63,13 +64,13 @@ pub enum WriteOp { /// needs to be serialized and stored. The data is a generic IPLD map /// (similar to rsky's `RepoRecord = BTreeMap`). #[derive(Debug, Clone, PartialEq)] -pub enum RecordWriteOp<'a> { +pub enum RecordWriteOp<'a, S: BosStr> { /// Create new record with data Create { /// Collection NSID - collection: Nsid<'a>, + collection: Nsid, /// Record key - rkey: RecordKey>, + rkey: RecordKey>, /// Record data (will be serialized to DAG-CBOR and CID computed) record: std::collections::BTreeMap>, }, @@ -77,9 +78,9 @@ pub enum RecordWriteOp<'a> { /// Update existing record with new data Update { /// Collection NSID - collection: Nsid<'a>, + collection: Nsid, /// Record key - rkey: RecordKey>, + rkey: RecordKey>, /// New record data record: std::collections::BTreeMap>, /// Previous CID (optional for validation) @@ -89,17 +90,17 @@ pub enum RecordWriteOp<'a> { /// Delete record Delete { /// Collection NSID - collection: Nsid<'a>, + collection: Nsid, /// Record key - rkey: RecordKey>, + rkey: RecordKey>, /// Previous CID (optional for validation) prev: Option, }, } -impl<'a> RecordWriteOp<'a> { +impl<'a, S: BosStr> RecordWriteOp<'a, S> { /// Get the collection NSID for this operation - pub fn collection(&self) -> &Nsid<'a> { + pub fn collection(&self) -> &Nsid { match self { RecordWriteOp::Create { collection, .. } => collection, RecordWriteOp::Update { collection, .. } => collection, @@ -108,7 +109,7 @@ impl<'a> RecordWriteOp<'a> { } /// Get the record key for this operation - pub fn rkey(&self) -> &RecordKey> { + pub fn rkey(&self) -> &RecordKey> { match self { RecordWriteOp::Create { rkey, .. } => rkey, RecordWriteOp::Update { rkey, .. } => rkey, diff --git a/crates/jacquard-repo/src/repo.rs b/crates/jacquard-repo/src/repo.rs index 6fc20c8a2..79658a063 100644 --- a/crates/jacquard-repo/src/repo.rs +++ b/crates/jacquard-repo/src/repo.rs @@ -10,15 +10,17 @@ use crate::mst::{Mst, RecordWriteOp}; use crate::storage::BlockStore; use bytes::Bytes; use cid::Cid as IpldCid; -use jacquard_common::IntoStatic; +use jacquard_common::BosStr; use jacquard_common::types::cid::CidLink; use jacquard_common::types::recordkey::RecordKeyType; use jacquard_common::types::string::{Datetime, Did, Nsid, RecordKey, Tid}; use jacquard_common::types::tid::Ticker; use smol_str::format_smolstr; use std::collections::BTreeMap; +use std::convert::Infallible; use std::fmt::{self, Display, Formatter}; use std::path::Path; +use std::str::FromStr; use std::sync::Arc; /// Commit data for repository updates @@ -65,21 +67,21 @@ impl CommitData { /// /// Converts this commit into a `FirehoseCommit` with `prev_data` field /// and relevant blocks for inductive validation. - pub async fn to_firehose_commit( + pub async fn to_firehose_commit( &self, - repo: &Did<'_>, + repo: &Did, seq: i64, time: Datetime, - ops: Vec>, - blobs: Vec>, - ) -> Result> { + ops: Vec>, + blobs: Vec>, + ) -> Result> { let mut proof_blocks = self.blocks.clone(); proof_blocks.append(&mut self.relevant_blocks.clone()); // Convert relevant blocks to CAR format let blocks_car = crate::car::write_car_bytes(self.cid, proof_blocks).await?; Ok(FirehoseCommit { - repo: repo.clone().into_static(), + repo: repo.clone(), rev: self.rev.clone(), seq, since: Some(self.since.clone().unwrap_or_else(|| self.rev.clone())), @@ -124,18 +126,21 @@ impl CommitData { /// # Ok(()) /// # } /// ``` -pub struct Repository { - mst: Mst, - storage: Arc, - commit: Commit<'static>, +pub struct Repository { + mst: Mst, + storage: Arc, + commit: Commit, commit_cid: IpldCid, } -impl Repository { +impl Repository +where + S: BosStr + Clone + serde::Serialize + serde::de::DeserializeOwned + FromStr, +{ /// Create repository from existing components /// /// Static constructor for when you already have the MST, commit, and CID. - pub fn new(storage: Arc, mst: Mst, commit: Commit<'static>, commit_cid: IpldCid) -> Self { + pub fn new(storage: Arc, mst: Mst, commit: Commit, commit_cid: IpldCid) -> Self { Self { storage, mst, @@ -145,7 +150,7 @@ impl Repository { } /// Load repository from commit CID - pub async fn from_commit(storage: Arc, commit_cid: &IpldCid) -> Result { + pub async fn from_commit(storage: Arc, commit_cid: &IpldCid) -> Result { let commit_bytes = storage .get(commit_cid) .await? @@ -154,7 +159,7 @@ impl Repository { .with_help("Commit must be applied to storage before loading repository - use apply_commit() or ensure commit is persisted") })?; - let commit = Commit::from_cbor(&commit_bytes)?; + let commit = Commit::::from_cbor(&commit_bytes)?; let mst_root = commit.data(); let mst = Mst::load(storage.clone(), *mst_root, None); @@ -162,7 +167,7 @@ impl Repository { Ok(Self { mst, storage, - commit: commit.into_static(), + commit: commit, commit_cid: *commit_cid, }) } @@ -174,10 +179,10 @@ impl Repository { /// /// This does NOT persist to storage - use `create_from_commit` or `create` for that. pub async fn format_init_commit( - storage: Arc, - did: Did<'static>, + storage: Arc, + did: Did, signing_key: &K, - initial_writes: Option<&[RecordWriteOp<'_>]>, + initial_writes: Option<&[RecordWriteOp<'_, S>]>, ) -> Result where K: SigningKey, @@ -240,7 +245,7 @@ impl Repository { /// Create repository from CommitData /// /// Applies the commit to storage and loads the repository from it. - pub async fn create_from_commit(storage: Arc, commit_data: CommitData) -> Result { + pub async fn create_from_commit(storage: Arc, commit_data: CommitData) -> Result { let commit_cid = commit_data.cid; storage.apply_commit(commit_data).await?; Self::from_commit(storage, &commit_cid).await @@ -250,10 +255,10 @@ impl Repository { /// /// Convenience method that formats an initial commit and applies it to storage. pub async fn create( - storage: Arc, - did: Did<'static>, + storage: Arc, + did: Did, signing_key: &K, - initial_writes: Option<&[RecordWriteOp<'_>]>, + initial_writes: Option<&[RecordWriteOp<'_, S>]>, ) -> Result where K: SigningKey, @@ -266,7 +271,7 @@ impl Repository { /// Get a record by collection and rkey pub async fn get_record( &self, - collection: &Nsid<'_>, + collection: &Nsid, rkey: &RecordKey, ) -> Result> { let key = format!("{}/{}", collection.as_ref(), rkey.as_ref()); @@ -276,7 +281,7 @@ impl Repository { /// Create a record (error if exists) pub async fn create_record( &mut self, - collection: &Nsid<'_>, + collection: &Nsid, rkey: &RecordKey, record_cid: IpldCid, ) -> Result<()> { @@ -293,7 +298,7 @@ impl Repository { /// Update a record (error if not exists, returns previous CID) pub async fn update_record( &mut self, - collection: &Nsid<'_>, + collection: &Nsid, rkey: &RecordKey, record_cid: IpldCid, ) -> Result { @@ -312,7 +317,7 @@ impl Repository { /// Delete a record (error if not exists, returns deleted CID) pub async fn delete_record( &mut self, - collection: &Nsid<'_>, + collection: &Nsid, rkey: &RecordKey, ) -> Result { let key = format!("{}/{}", collection.as_ref(), rkey.as_ref()); @@ -359,13 +364,14 @@ impl Repository { /// Returns `(ops, CommitData)` - ops are needed for `to_firehose_commit()`. pub async fn create_commit( &mut self, - ops: &[RecordWriteOp<'_>], - did: &Did<'_>, + ops: &[RecordWriteOp<'_, S>], + did: &Did, prev: Option, signing_key: &K, - ) -> Result<(Vec>, CommitData)> + ) -> Result<(Vec>, CommitData)> where K: SigningKey, + S: FromStr, { // Step 1: Apply all write operations to build new MST and collect leaf blocks let mut updated_tree = self.mst.clone(); @@ -463,11 +469,7 @@ impl Repository { let diff = self.mst.diff(&updated_tree).await?; // Step 3: Extract everything we need from diff - let repo_ops = diff - .to_repo_ops() - .into_iter() - .map(|op| op.into_static()) - .collect(); + let repo_ops = diff.to_repo_ops().into_iter().collect(); // Step 4: Build blocks and relevant_blocks collections using diff tracking // @@ -506,8 +508,8 @@ impl Repository { // Step 5: Create and sign commit let rev = Ticker::new().next(Some(self.commit.rev.clone())); - let commit = Commit::new_unsigned(did.clone().into_static(), data, rev.clone(), prev) - .sign(signing_key)?; + let commit = + Commit::new_unsigned(did.clone(), data, rev.clone(), prev).sign(signing_key)?; let commit_cbor = commit.to_cbor()?; let commit_cid = crate::mst::util::compute_cid(&commit_cbor)?; @@ -555,9 +557,9 @@ impl Repository { RepoError::not_found("commit block", &commit_cid) .with_help("Commit block should have been persisted by apply_commit() - this indicates a storage inconsistency") })?; - let commit = Commit::from_cbor(&commit_bytes)?; + let commit = Commit::::from_cbor(&commit_bytes)?; - self.commit = commit.into_static(); + self.commit = commit; self.commit_cid = commit_cid; // Reload MST from new root @@ -573,10 +575,10 @@ impl Repository { /// record operations (e.g., `create_record()`, `update_record()`, `delete_record()`). pub async fn commit( &mut self, - did: &Did<'_>, + did: &Did, prev: Option, signing_key: &K, - ) -> Result<(Vec>, IpldCid)> + ) -> Result<(Vec>, IpldCid)> where K: SigningKey, { @@ -590,17 +592,17 @@ impl Repository { } /// Get the underlying MST - pub fn mst(&self) -> &Mst { + pub fn mst(&self) -> &Mst { &self.mst } /// Get reference to the storage - pub fn storage(&self) -> &Arc { + pub fn storage(&self) -> &Arc { &self.storage } /// Get the current commit - pub fn current_commit(&self) -> &Commit<'static> { + pub fn current_commit(&self) -> &Commit { &self.commit } @@ -610,20 +612,20 @@ impl Repository { } /// Get the DID from the current commit - pub fn did(&self) -> &Did<'_> { + pub fn did(&self) -> &Did { self.commit.did() } } -impl Display for Repository { +impl Display for Repository { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { use crate::mst::tree::short_cid; writeln!(f, "Repository {{")?; - writeln!(f, " DID: {}", self.commit.did())?; + writeln!(f, " DID: {}", self.commit.did)?; writeln!(f, " Commit: {}", short_cid(&self.commit_cid))?; writeln!(f, " Rev: {}", self.commit.rev)?; - writeln!(f, " Data: {}", short_cid(self.commit.data()))?; + writeln!(f, " Data: {}", short_cid(&self.commit.data))?; writeln!(f, " MST:")?; // Format MST with indentation @@ -642,10 +644,13 @@ mod tests { use super::*; use crate::storage::MemoryBlockStore; - use jacquard_common::types::{ - crypto::{KeyCodec, PublicKey}, - recordkey::Rkey, - value::RawData, + use jacquard_common::{ + IntoStatic, + types::{ + crypto::{KeyCodec, PublicKey}, + recordkey::Rkey, + value::RawData, + }, }; use smol_str::SmolStr; @@ -676,7 +681,9 @@ mod tests { record } - async fn create_test_repo(storage: Arc) -> Repository { + async fn create_test_repo( + storage: Arc, + ) -> Repository { let did = Did::new("did:plc:test").unwrap(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); @@ -701,8 +708,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let ops = vec![RecordWriteOp::Create { collection: collection.clone().into_static(), @@ -710,7 +717,7 @@ mod tests { record: make_test_record(1), }]; - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let (repo_ops, commit_data) = repo .create_commit( @@ -738,8 +745,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let cid = make_test_cid(1); repo.create_record(&collection, &rkey, cid).await.unwrap(); @@ -755,8 +762,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let cid1 = make_test_cid(1); let cid2 = make_test_cid(2); @@ -774,8 +781,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let cid = make_test_cid(1); let result = repo.update_record(&collection, &rkey, cid).await; @@ -787,8 +794,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let cid = make_test_cid(1); repo.create_record(&collection, &rkey, cid).await.unwrap(); @@ -805,8 +812,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let result = repo.delete_record(&collection, &rkey).await; assert!(result.is_err()); @@ -817,8 +824,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let cid = make_test_cid(1); repo.create_record(&collection, &rkey, cid).await.unwrap(); @@ -827,7 +834,7 @@ mod tests { repo.mst.persist().await.unwrap(); // Create commit (need a signing key for this test) - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let (_, commit_cid) = repo.commit(&did, None, &signing_key).await.unwrap(); @@ -843,14 +850,14 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let cid = make_test_cid(1); repo.create_record(&collection, &rkey, cid).await.unwrap(); repo.mst.persist().await.unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let (_, commit_cid) = repo.commit(&did, None, &signing_key).await.unwrap(); @@ -860,7 +867,7 @@ mod tests { // Verify commit can be deserialized let bytes = commit_bytes.unwrap(); - let commit = Commit::from_cbor(&bytes).unwrap(); + let commit = Commit::::from_cbor(&bytes).unwrap(); assert_eq!(commit.did().as_ref(), did.as_ref()); let root_cid = repo.mst.root().await.unwrap(); assert_eq!(commit.data(), &root_cid); @@ -871,8 +878,8 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("test1").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("test1").unwrap()).into_static(); let cid1 = make_test_cid(1); let cid2 = make_test_cid(2); @@ -899,14 +906,15 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); let mut ticker = Ticker::new(); // Add 100 records let mut records = Vec::new(); for i in 0..100 { let tid_str = ticker.next(None).into_static(); - let rkey = RecordKey(Rkey::from_str(tid_str.as_str()).unwrap()); + let rkey: RecordKey> = + RecordKey(Rkey::::from_str(tid_str.as_str()).unwrap()).into_static(); let cid = make_test_cid((i % 256) as u8); repo.create_record(&collection, &rkey, cid).await.unwrap(); records.push((rkey, cid)); @@ -946,14 +954,14 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey = RecordKey(Rkey::new("abc123").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey = RecordKey(Rkey::new("abc123").unwrap()).into_static(); let cid = make_test_cid(1); repo.create_record(&collection, &rkey, cid).await.unwrap(); repo.mst.persist().await.unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); // Get public key from signing key @@ -968,7 +976,7 @@ mod tests { // Load commit and verify signature let commit_bytes = storage.get(&commit_cid).await.unwrap().unwrap(); - let commit = Commit::from_cbor(&commit_bytes).unwrap(); + let commit = Commit::::from_cbor(&commit_bytes).unwrap(); // Signature verification should succeed commit.verify(&pubkey).unwrap(); @@ -979,14 +987,18 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:test").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); // Add some records and commit let mut records = Vec::new(); for i in 0..10 { - let rkey = RecordKey(Rkey::from_str(&format!("record{}", i)).unwrap()); + let rkey: RecordKey> = RecordKey( + Rkey::::from_str(&format!("record{}", i)) + .unwrap() + .into_static(), + ); let cid = make_test_cid(i as u8); repo.create_record(&collection, &rkey, cid).await.unwrap(); records.push((rkey, cid)); @@ -1021,11 +1033,11 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey1 = RecordKey(Rkey::new("test1").unwrap()); - let rkey2 = RecordKey(Rkey::new("test2").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey1 = RecordKey(Rkey::new("test1").unwrap()).into_static(); + let rkey2 = RecordKey(Rkey::new("test2").unwrap()).into_static(); - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); // Create records with actual data @@ -1085,9 +1097,9 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let rkey1 = RecordKey(Rkey::new("post1").unwrap()); - let rkey2 = RecordKey(Rkey::new("post2").unwrap()); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let rkey1 = RecordKey(Rkey::new("post1").unwrap()).into_static(); + let rkey2 = RecordKey(Rkey::new("post2").unwrap()).into_static(); // Create records with actual data let ops = vec![ @@ -1104,7 +1116,7 @@ mod tests { ]; // Format commit - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let (repo_ops, commit_data) = repo .create_commit( @@ -1159,14 +1171,14 @@ mod tests { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); // Pre-populate with some records - let rkey1 = RecordKey(Rkey::new("existing1").unwrap()); - let rkey2 = RecordKey(Rkey::new("existing2").unwrap()); - let rkey3 = RecordKey(Rkey::new("existing3").unwrap()); + let rkey1 = RecordKey(Rkey::new("existing1").unwrap()).into_static(); + let rkey2 = RecordKey(Rkey::new("existing2").unwrap()).into_static(); + let rkey3 = RecordKey(Rkey::new("existing3").unwrap()).into_static(); - let did = Did::new("did:plc:test").unwrap(); + let did = Did::new("did:plc:test").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let create_ops = vec![ @@ -1203,7 +1215,7 @@ mod tests { repo.apply_commit(commit_data).await.unwrap(); // Batch operation: create new, update existing, delete existing - let new_rkey = RecordKey(Rkey::new("new1").unwrap()); + let new_rkey = RecordKey(Rkey::new("new1").unwrap()).into_static(); let ops = vec![ RecordWriteOp::Create { collection: collection.clone(), diff --git a/crates/jacquard-repo/tests/large_proof_tests.rs b/crates/jacquard-repo/tests/large_proof_tests.rs index 0e7ec6abb..27e330e38 100644 --- a/crates/jacquard-repo/tests/large_proof_tests.rs +++ b/crates/jacquard-repo/tests/large_proof_tests.rs @@ -15,7 +15,7 @@ use jacquard_repo::mst::RecordWriteOp; use jacquard_repo::storage::{BlockStore, MemoryBlockStore}; use rand::Rng; use rand::seq::SliceRandom; -use smol_str::SmolStr; +use smol_str::{SmolStr, ToSmolStr}; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; @@ -50,7 +50,7 @@ fn get_public_key(signing_key: &k256::ecdsa::SigningKey) -> PublicKey<'static> { } } -async fn create_test_repo(storage: Arc) -> Repository { +async fn create_test_repo(storage: Arc) -> Repository { let did = Did::new("did:plc:stresstest").unwrap(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); @@ -61,7 +61,7 @@ async fn create_test_repo(storage: Arc) -> Repository, + records: HashMap, ticker: Ticker, } @@ -73,16 +73,16 @@ impl RecordTracker { } } - fn gen_new_rkey(&mut self) -> String { - self.ticker.next(None).into_static().to_string() + fn gen_new_rkey(&mut self) -> SmolStr { + self.ticker.next(None).into_static().to_smolstr() } - fn pick_random_existing(&self, rng: &mut R) -> Option { + fn pick_random_existing(&self, rng: &mut R) -> Option { let keys: Vec<_> = self.records.keys().cloned().collect(); keys.choose(rng).cloned() } - fn add(&mut self, rkey: String, counter: u32) { + fn add(&mut self, rkey: SmolStr, counter: u32) { self.records.insert(rkey, counter); } @@ -97,9 +97,9 @@ impl RecordTracker { #[derive(Debug, Clone)] enum TestOp { - Create { rkey: String, counter: u32 }, - Update { rkey: String, counter: u32 }, - Delete { rkey: String }, + Create { rkey: SmolStr, counter: u32 }, + Update { rkey: SmolStr, counter: u32 }, + Delete { rkey: SmolStr }, } fn generate_creates_only( @@ -167,24 +167,27 @@ fn generate_random_ops( ops } -fn test_ops_to_record_writes(ops: Vec, collection: &Nsid) -> Vec> { +fn test_ops_to_record_writes( + ops: Vec, + collection: &Nsid, +) -> Vec> { let collection_static = collection.clone().into_static(); ops.into_iter() .map(|op| match op { TestOp::Create { rkey, counter } => RecordWriteOp::Create { collection: collection_static.clone(), - rkey: RecordKey(Rkey::new(&rkey).unwrap()).into_static(), + rkey: RecordKey(Rkey::new(rkey).unwrap()).into_static(), record: make_test_record(counter, "Random post"), }, TestOp::Update { rkey, counter } => RecordWriteOp::Update { collection: collection_static.clone(), - rkey: RecordKey(Rkey::new(&rkey).unwrap()).into_static(), + rkey: RecordKey(Rkey::new(rkey).unwrap()).into_static(), record: make_test_record(counter, "Updated post"), prev: None, }, TestOp::Delete { rkey } => RecordWriteOp::Delete { collection: collection_static.clone(), - rkey: RecordKey(Rkey::new(&rkey).unwrap()).into_static(), + rkey: RecordKey(Rkey::new(rkey).unwrap()).into_static(), prev: None, }, }) @@ -196,8 +199,8 @@ async fn test_stress_random_operations() { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:stresstest").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:stresstest").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); @@ -308,8 +311,8 @@ async fn test_stress_large_batches() { let storage = Arc::new(MemoryBlockStore::new()); let mut repo = create_test_repo(storage.clone()).await; - let collection = Nsid::new("app.bsky.feed.post").unwrap(); - let did = Did::new("did:plc:stresstest").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); + let did = Did::new("did:plc:stresstest").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); @@ -408,7 +411,7 @@ async fn test_stress_with_fixture() { repo.current_commit_cid() ); - let collection = Nsid::new("app.bsky.feed.post").unwrap(); + let collection = Nsid::new("app.bsky.feed.post").unwrap().into_static(); let signing_key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); let pubkey = get_public_key(&signing_key); let did = repo.did().clone().into_static(); -- 2.51.2