From c7cafbcbd274f1789e383a0fe5de6451ca03cd75 Mon Sep 17 00:00:00 2001 From: Orual Date: Fri, 20 Mar 2026 15:14:21 -0400 Subject: [PATCH] oauth docs --- Cargo.lock | 1163 +++++++++-------- crates/jacquard-common/src/lib.rs | 2 +- .../jacquard-identity/src/lexicon_resolver.rs | 88 +- crates/jacquard-identity/src/lib.rs | 16 +- crates/jacquard-identity/src/resolver.rs | 2 + crates/jacquard-oauth/src/atproto.rs | 71 +- crates/jacquard-oauth/src/authstore.rs | 15 + crates/jacquard-oauth/src/client.rs | 88 ++ crates/jacquard-oauth/src/dpop.rs | 39 + crates/jacquard-oauth/src/error.rs | 28 +- crates/jacquard-oauth/src/jose.rs | 7 + crates/jacquard-oauth/src/jose/jws.rs | 14 +- crates/jacquard-oauth/src/jose/jwt.rs | 22 + crates/jacquard-oauth/src/jose/signing.rs | 1 + crates/jacquard-oauth/src/keyset.rs | 17 + crates/jacquard-oauth/src/lib.rs | 15 + crates/jacquard-oauth/src/loopback.rs | 13 + crates/jacquard-oauth/src/request.rs | 71 +- crates/jacquard-oauth/src/resolver.rs | 57 +- crates/jacquard-oauth/src/scopes.rs | 4 +- crates/jacquard-oauth/src/session.rs | 127 +- crates/jacquard-oauth/src/types.rs | 22 + .../src/types/client_metadata.rs | 32 +- crates/jacquard-oauth/src/types/metadata.rs | 75 +- crates/jacquard-oauth/src/types/request.rs | 78 +- crates/jacquard-oauth/src/types/response.rs | 19 +- crates/jacquard-oauth/src/types/token.rs | 13 + crates/jacquard-oauth/src/utils.rs | 23 +- crates/jacquard/src/lib.rs | 5 +- 29 files changed, 1522 insertions(+), 605 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f65567e..38bf1fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -118,15 +118,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -153,9 +153,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -171,7 +171,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -203,13 +203,12 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "async-compression" -version = "0.4.36" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", - "futures-core", "pin-project-lite", "tokio", ] @@ -222,7 +221,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -261,7 +260,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.17", + "thiserror 2.0.18", "v_frame", "y4m", ] @@ -282,9 +281,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" dependencies = [ "arrayvec 0.7.6", ] @@ -349,14 +348,14 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "axum-test" -version = "18.5.0" +version = "18.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf48df8b4be768081e11b7bb6d50e7dd96a3616b0b728f9e8d49bfbd8116f3c6" +checksum = "0ce2a8627e8d8851f894696b39f2b67807d6375c177361d376173ace306a21e2" dependencies = [ "anyhow", "axum", @@ -441,9 +440,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bit-set" @@ -468,9 +467,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitstream-io" @@ -492,9 +491,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.8.1" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" dependencies = [ "bon-macros", "rustversion", @@ -502,9 +501,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.8.1" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ "darling", "ident_case", @@ -512,7 +511,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.112", + "syn", ] [[package]] @@ -523,10 +522,11 @@ checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "borsh" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ + "bytes", "cfg_aliases", ] @@ -579,15 +579,15 @@ checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -603,9 +603,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -633,9 +633,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.51" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -643,12 +643,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -663,9 +657,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -724,9 +718,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -734,9 +728,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -746,36 +740,36 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.64" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0da80818b2d95eca9aa614a30783e42f62bf5fdfee24e68cfb960b071ba8d1" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clap_mangen" -version = "0.2.31" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" +checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" dependencies = [ "clap", "roff", @@ -787,7 +781,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -798,9 +792,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -814,9 +808,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" dependencies = [ "compression-core", "flate2", @@ -1040,14 +1034,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -1055,27 +1049,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.112", + "syn", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -1094,15 +1087,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "data-encoding-macro" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1110,12 +1103,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.112", + "syn", ] [[package]] @@ -1141,9 +1134,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -1165,7 +1158,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", "unicode-xid", ] @@ -1201,7 +1194,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -1334,7 +1327,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -1354,7 +1347,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -1365,9 +1358,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -1386,9 +1379,9 @@ dependencies = [ [[package]] name = "expect-json" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf3355a7ef83e52c9383ab0c7719acd1da54be5fed7c6572d87ddc4d8589753" +checksum = "869f97f4abe8e78fc812a94ad6b721d72c4fb5532877c79610f2c238d7ccf6c4" dependencies = [ "chrono", "email_address", @@ -1397,20 +1390,20 @@ dependencies = [ "regex", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "typetag", "uuid", ] [[package]] name = "expect-json-macros" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ff9262e5b5f9760f60c57ada4fffd25201ae9fefd426f29f097dcc573d86e6" +checksum = "6e6fdf550180a6c29a28cb9aac262dc0064c25735641d2317f670075e9a469d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -1451,7 +1444,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -1481,27 +1474,26 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide 0.8.9", @@ -1551,9 +1543,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1566,9 +1558,9 @@ dependencies = [ [[package]] name = "futures-buffered" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" dependencies = [ "cordyceps", "diatomic-waker", @@ -1579,9 +1571,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1589,15 +1581,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1606,9 +1598,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1625,32 +1617,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1660,7 +1652,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1692,9 +1683,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -1712,11 +1703,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -1789,9 +1793,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1971,7 +1975,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -2061,14 +2065,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -2077,7 +2080,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "system-configuration", "tokio", "tower-service", @@ -2087,9 +2090,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2190,6 +2193,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2219,9 +2228,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -2236,9 +2245,9 @@ dependencies = [ "ravif", "rayon", "rgb", - "tiff 0.10.3", - "zune-core 0.5.0", - "zune-jpeg 0.5.8", + "tiff 0.11.3", + "zune-core", + "zune-jpeg", ] [[package]] @@ -2259,12 +2268,14 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -2275,14 +2286,14 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" dependencies = [ "rustversion", ] @@ -2301,9 +2312,9 @@ dependencies = [ [[package]] name = "ipld-core" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" +checksum = "090f624976d72f0b0bb71b86d58dc16c15e069193067cb3a3a09d655246cbbda" dependencies = [ "cid", "serde", @@ -2312,9 +2323,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -2365,9 +2376,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jacquard" @@ -2375,7 +2386,7 @@ version = "0.10.0" dependencies = [ "bytes", "clap", - "getrandom 0.2.16", + "getrandom 0.2.17", "gloo-storage", "http", "image", @@ -2394,7 +2405,7 @@ dependencies = [ "serde_html_form", "serde_json", "smol_str", - "thiserror 2.0.17", + "thiserror 2.0.18", "tiff 0.6.1", "tokio", "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2412,7 +2423,7 @@ dependencies = [ "jacquard-lexicon", "miette", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2437,12 +2448,12 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror 2.0.17", + "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.22 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-subscriber 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2460,7 +2471,7 @@ dependencies = [ "fluent-uri", "futures", "futures-lite", - "getrandom 0.2.16", + "getrandom 0.2.17", "getrandom 0.3.4", "hashbrown 0.15.5", "http", @@ -2490,7 +2501,7 @@ dependencies = [ "signature", "smol_str", "spin 0.10.0", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-tungstenite-wasm", "tokio-util", @@ -2512,7 +2523,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.112", + "syn", "unicode-segmentation", ] @@ -2534,7 +2545,7 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "trait-variant", @@ -2559,9 +2570,9 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.112", + "syn", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "toml 0.8.23", "walkdir", @@ -2590,9 +2601,9 @@ dependencies = [ "serde_repr", "serde_with", "sha2", - "syn 2.0.112", + "syn", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-segmentation", ] @@ -2620,7 +2631,7 @@ dependencies = [ "serde_json", "sha2", "smol_str", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "trait-variant", @@ -2654,32 +2665,59 @@ dependencies = [ "sha2", "smol_str", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "trait-variant", ] [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", + "jni-macros", "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "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.3.0" +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 = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jobserver" @@ -2735,9 +2773,9 @@ checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2793,8 +2831,8 @@ dependencies = [ "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.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tracing-subscriber 0.3.22 (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]] @@ -2806,6 +2844,12 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -2814,15 +2858,15 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", @@ -2830,19 +2874,20 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags", "libc", - "redox_syscall 0.7.0", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -2859,9 +2904,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2894,7 +2939,7 @@ dependencies = [ "generator", "scoped-tls", "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", - "tracing-subscriber 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-subscriber 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2995,13 +3040,13 @@ dependencies = [ [[package]] name = "match-lookup" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -3031,9 +3076,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miette" @@ -3062,7 +3107,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -3089,7 +3134,7 @@ dependencies = [ "crossbeam-channel", "crossbeam-utils", "dashmap", - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "smallvec", "tagptr", @@ -3142,9 +3187,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -3311,9 +3356,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -3323,7 +3368,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -3388,9 +3433,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -3422,9 +3467,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -3440,9 +3485,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "ouroboros" @@ -3465,14 +3510,14 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "oxilangtag" @@ -3601,7 +3646,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -3615,29 +3660,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -3672,11 +3717,17 @@ 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.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ "bitflags", "crc32fast", @@ -3687,9 +3738,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postcard" @@ -3751,7 +3802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.112", + "syn", ] [[package]] @@ -3765,9 +3816,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3780,7 +3831,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", "version_check", "yansi", ] @@ -3801,14 +3852,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", @@ -3825,12 +3876,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "qoi" @@ -3866,8 +3914,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.3", + "thiserror 2.0.18", "tokio", "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "web-time", @@ -3875,9 +3923,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3888,7 +3936,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "web-time", @@ -3903,16 +3951,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3923,6 +3971,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -3941,7 +3995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3961,7 +4015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3970,14 +4024,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -3988,7 +4042,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4021,16 +4075,16 @@ dependencies = [ "rand 0.9.2", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.17", + "thiserror 2.0.18", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ "avif-serialize", "imgref", @@ -4072,9 +4126,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags", ] @@ -4096,14 +4150,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -4113,9 +4167,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4124,15 +4178,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -4180,11 +4234,11 @@ dependencies = [ [[package]] name = "reserve-port" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" +checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4205,9 +4259,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" dependencies = [ "bytemuck", ] @@ -4220,7 +4274,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -4228,9 +4282,9 @@ dependencies = [ [[package]] name = "roff" -version = "0.2.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" [[package]] name = "rouille" @@ -4258,9 +4312,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -4288,14 +4342,14 @@ dependencies = [ "http", "mime", "rand 0.9.2", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -4327,22 +4381,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -4366,9 +4420,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -4376,9 +4430,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -4405,9 +4459,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safemem" @@ -4426,9 +4480,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -4461,9 +4515,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -4474,9 +4528,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -4531,7 +4585,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -4571,9 +4625,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "indexmap", "itoa", @@ -4602,7 +4656,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -4637,9 +4691,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -4652,14 +4706,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -4731,6 +4785,16 @@ 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" @@ -4740,17 +4804,23 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -4760,9 +4830,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smol_str" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" dependencies = [ "borsh", "serde_core", @@ -4780,12 +4850,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4885,20 +4955,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.112" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4922,14 +4981,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", "core-foundation 0.9.4", @@ -4960,14 +5019,14 @@ checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4997,7 +5056,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.60.2", ] @@ -5022,11 +5081,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5037,18 +5096,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -5082,23 +5141,23 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", "half", "quick-error 2.0.1", "weezl", - "zune-jpeg 0.4.21", + "zune-jpeg", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -5106,22 +5165,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -5151,9 +5210,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -5166,9 +5225,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -5176,20 +5235,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -5239,9 +5298,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5265,17 +5324,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" dependencies = [ "indexmap", "serde_core", "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.14", + "winnow 1.0.0", ] [[package]] @@ -5289,9 +5348,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" dependencies = [ "serde_core", ] @@ -5307,16 +5366,16 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow 0.7.14", + "winnow 1.0.0", ] [[package]] @@ -5327,15 +5386,15 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -5398,7 +5457,7 @@ dependencies = [ [[package]] name = "tracing" version = "0.1.44" -source = "git+https://github.com/tokio-rs/tracing#efc690fa6bd1d9c3a57528b9bc8ac80504a7a6ed" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" dependencies = [ "pin-project-lite", "tracing-attributes 0.1.31 (git+https://github.com/tokio-rs/tracing)", @@ -5413,17 +5472,17 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "tracing-attributes" version = "0.1.31" -source = "git+https://github.com/tokio-rs/tracing#efc690fa6bd1d9c3a57528b9bc8ac80504a7a6ed" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -5439,7 +5498,7 @@ dependencies = [ [[package]] name = "tracing-core" version = "0.1.36" -source = "git+https://github.com/tokio-rs/tracing#efc690fa6bd1d9c3a57528b9bc8ac80504a7a6ed" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" dependencies = [ "valuable", ] @@ -5458,7 +5517,7 @@ dependencies = [ [[package]] name = "tracing-log" version = "0.2.0" -source = "git+https://github.com/tokio-rs/tracing#efc690fa6bd1d9c3a57528b9bc8ac80504a7a6ed" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" dependencies = [ "log", "once_cell", @@ -5467,9 +5526,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -5486,8 +5545,8 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" -source = "git+https://github.com/tokio-rs/tracing#efc690fa6bd1d9c3a57528b9bc8ac80504a7a6ed" +version = "0.3.23" +source = "git+https://github.com/tokio-rs/tracing#54ede4d5d85a536aed5485c5213011d9ec961935" dependencies = [ "matchers", "nu-ansi-term", @@ -5509,7 +5568,7 @@ checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -5526,9 +5585,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17e807bff86d2a06b52bca4276746584a78375055b6e45843925ce2802b335" +checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e" dependencies = [ "glob", "serde", @@ -5536,7 +5595,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 0.9.10+spec-1.1.0", + "toml 1.0.7+spec-1.1.0", ] [[package]] @@ -5601,7 +5660,7 @@ checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -5612,15 +5671,15 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-linebreak" @@ -5672,9 +5731,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -5702,9 +5761,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.19.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "js-sys", "wasm-bindgen", @@ -5785,18 +5844,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -5807,11 +5875,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5820,9 +5889,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5830,31 +5899,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.112", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.56" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e90e66d265d3a1efc0e72a54809ab90b9c0c515915c67cdf658689d2c22c6c" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" dependencies = [ "async-trait", "cast", @@ -5869,17 +5938,46 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.56" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7150335716dce6028bead2b848e72f47b45e7b9422f64cccdc23bedca89affc1" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", ] [[package]] @@ -5895,11 +5993,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -5917,9 +6027,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" dependencies = [ "core-foundation 0.10.1", "jni", @@ -5945,9 +6055,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -6016,7 +6126,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -6027,7 +6137,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -6065,15 +6175,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -6119,21 +6220,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -6182,12 +6268,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6206,12 +6286,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6230,12 +6304,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6266,12 +6334,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6290,12 +6352,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6314,12 +6370,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6338,12 +6388,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6373,13 +6417,19 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "winreg" version = "0.50.0" @@ -6392,9 +6442,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -6444,28 +6576,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] @@ -6485,7 +6617,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", "synstructure", ] @@ -6528,14 +6660,14 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.112", + "syn", ] [[package]] name = "zmij" -version = "1.0.6" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" @@ -6567,15 +6699,9 @@ dependencies = [ [[package]] name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-core" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-inflate" @@ -6588,18 +6714,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.8" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" dependencies = [ - "zune-core 0.5.0", + "zune-core", ] diff --git a/crates/jacquard-common/src/lib.rs b/crates/jacquard-common/src/lib.rs index 906a1ae8..e8fb5a39 100644 --- a/crates/jacquard-common/src/lib.rs +++ b/crates/jacquard-common/src/lib.rs @@ -190,7 +190,7 @@ //! It's also not too bad to write, once you're aware of the pattern and why it works. If you run //! into a lifetime/borrowing inference issue in jacquard, please contact the crate author. She'd //! be happy to debug, and if it's using a method from one of the jacquard crates and seems like -//! it *should* just work, that is a bug in jacquard, and you should [file an issue](https://tangled.org/@nonbinary.computer/jacquard/). +//! it *should* just work, that is a bug in jacquard, and you should [file an issue](https://tangled.org/nonbinary.computer/jacquard/). #![no_std] #![warn(missing_docs)] diff --git a/crates/jacquard-identity/src/lexicon_resolver.rs b/crates/jacquard-identity/src/lexicon_resolver.rs index 065fa302..ffc75d73 100644 --- a/crates/jacquard-identity/src/lexicon_resolver.rs +++ b/crates/jacquard-identity/src/lexicon_resolver.rs @@ -67,6 +67,7 @@ pub struct LexiconResolutionError { } impl LexiconResolutionError { + /// Create a new error with the given kind and optional source. pub fn new( kind: LexiconResolutionErrorKind, source: Option>, @@ -78,6 +79,7 @@ impl LexiconResolutionError { } } + /// Return the error kind. pub fn kind(&self) -> &LexiconResolutionErrorKind { &self.kind } @@ -93,6 +95,7 @@ impl LexiconResolutionError { self.context.as_deref() } + /// Create an error for a failed DNS TXT lookup while resolving a lexicon authority. pub fn dns_lookup_failed( authority: impl Into, source: impl std::error::Error + Send + Sync + 'static, @@ -105,6 +108,7 @@ impl LexiconResolutionError { ) } + /// Create an error for when DNS records exist but contain no `did=...` entry. pub fn no_did_found(authority: impl Into) -> Self { Self::new( LexiconResolutionErrorKind::NoDIDFound { @@ -114,6 +118,7 @@ impl LexiconResolutionError { ) } + /// Create an error for a syntactically invalid DID found in DNS for the given authority. pub fn invalid_did(authority: impl Into, value: impl Into) -> Self { Self::new( LexiconResolutionErrorKind::InvalidDID { @@ -124,10 +129,12 @@ impl LexiconResolutionError { ) } + /// Create an error for when DNS is not available (feature disabled or WASM target). pub fn dns_not_configured() -> Self { Self::new(LexiconResolutionErrorKind::DnsNotConfigured, None) } + /// Create an error for a failure to fetch the lexicon record for an NSID. pub fn fetch_failed( nsid: impl Into, source: impl std::error::Error + Send + Sync + 'static, @@ -138,6 +145,7 @@ impl LexiconResolutionError { ) } + /// Create an error for a failure to parse a fetched lexicon schema document. pub fn parse_failed( nsid: impl Into, source: impl std::error::Error + Send + Sync + 'static, @@ -148,6 +156,7 @@ impl LexiconResolutionError { ) } + /// Create a generic resolution failure error with a descriptive message. pub fn resolution_failed(nsid: impl Into, message: impl Into) -> Self { Self::new( LexiconResolutionErrorKind::ResolutionFailed { @@ -158,6 +167,7 @@ impl LexiconResolutionError { ) } + /// Create an error for a non-success HTTP status received while fetching a lexicon. pub fn http_error(nsid: impl Into, status: u16) -> Self { Self::new( LexiconResolutionErrorKind::HttpError { @@ -168,6 +178,7 @@ impl LexiconResolutionError { ) } + /// Create an error for a required field missing from the XRPC response. pub fn missing_response_field(nsid: impl Into, field: &'static str) -> Self { Self::new( LexiconResolutionErrorKind::MissingResponseField { @@ -178,10 +189,12 @@ impl LexiconResolutionError { ) } + /// Create an error for an invalid lexicon collection NSID. pub fn invalid_collection() -> Self { Self::new(LexiconResolutionErrorKind::InvalidCollection, None) } + /// Create an error for a lexicon record response that is missing its CID. pub fn missing_cid(nsid: impl Into) -> Self { Self::new( LexiconResolutionErrorKind::MissingCID { nsid: nsid.into() }, @@ -200,21 +213,36 @@ impl From for LexiconResolutionError { #[derive(Debug, thiserror::Error, miette::Diagnostic)] #[non_exhaustive] pub enum LexiconResolutionErrorKind { + /// DNS TXT lookup for the lexicon authority failed. #[error("DNS lookup failed for authority {authority}")] #[diagnostic(code(jacquard::lexicon::dns_lookup_failed))] - DnsLookupFailed { authority: SmolStr }, + DnsLookupFailed { + /// The NSID authority segment that was being looked up. + authority: SmolStr, + }, + /// DNS records were reachable but contained no `did=...` entry. #[error("no DID found in DNS for authority {authority}")] #[diagnostic( code(jacquard::lexicon::no_did_found), help("ensure _lexicon.{{reversed-authority}} TXT record exists with did=...") )] - NoDIDFound { authority: SmolStr }, + NoDIDFound { + /// The NSID authority segment that was being looked up. + authority: SmolStr, + }, + /// DNS returned a `did=...` entry but its value is not a valid DID. #[error("invalid DID in DNS for authority {authority}: {value}")] #[diagnostic(code(jacquard::lexicon::invalid_did))] - InvalidDID { authority: SmolStr, value: SmolStr }, - + InvalidDID { + /// The NSID authority segment. + authority: SmolStr, + /// The raw invalid DID string found in DNS. + value: SmolStr, + }, + + /// DNS is not available on this build (the `dns` feature is disabled or target is WASM). #[error("DNS not configured (dns feature disabled or WASM target)")] #[diagnostic( code(jacquard::lexicon::dns_not_configured), @@ -222,39 +250,69 @@ pub enum LexiconResolutionErrorKind { )] DnsNotConfigured, + /// XRPC or HTTP request to fetch the lexicon record failed. #[error("failed to fetch lexicon record for {nsid}")] #[diagnostic(code(jacquard::lexicon::fetch_failed))] - FetchFailed { nsid: SmolStr }, + FetchFailed { + /// The NSID of the lexicon that could not be fetched. + nsid: SmolStr, + }, + /// The fetched lexicon record could not be deserialized as a `LexiconDoc`. #[error("failed to parse lexicon schema for {nsid}")] #[diagnostic(code(jacquard::lexicon::parse_failed))] - ParseFailed { nsid: SmolStr }, + ParseFailed { + /// The NSID of the lexicon that could not be parsed. + nsid: SmolStr, + }, + /// Generic resolution failure with a descriptive message. #[error("failed to parse lexicon schema for {nsid}")] #[diagnostic(code(jacquard::lexicon::resolution_failed))] - ResolutionFailed { nsid: SmolStr, message: SmolStr }, - - /// HTTP non-success status from lexicon fetch + ResolutionFailed { + /// The NSID of the lexicon being resolved. + nsid: SmolStr, + /// Human-readable description of what went wrong. + message: SmolStr, + }, + + /// HTTP non-success status from lexicon fetch. #[error("HTTP {status} fetching lexicon {nsid}")] #[diagnostic(code(jacquard::lexicon::http_error))] - HttpError { nsid: SmolStr, status: u16 }, - - /// Required field missing in XRPC response + HttpError { + /// The NSID of the lexicon being fetched. + nsid: SmolStr, + /// The HTTP status code received. + status: u16, + }, + + /// Required field missing in XRPC response. #[error("missing '{field}' field in response for {nsid}")] #[diagnostic( code(jacquard::lexicon::missing_response_field), help("the XRPC response is missing a required field") )] - MissingResponseField { nsid: SmolStr, field: &'static str }, - + MissingResponseField { + /// The NSID of the lexicon being fetched. + nsid: SmolStr, + /// Name of the missing field. + field: &'static str, + }, + + /// The lexicon collection NSID was not valid. #[error("invalid collection NSID")] #[diagnostic(code(jacquard::lexicon::invalid_collection))] InvalidCollection, + /// The `getRecord` response did not include a CID for the lexicon record. #[error("record missing CID for {nsid}")] #[diagnostic(code(jacquard::lexicon::missing_cid))] - MissingCID { nsid: SmolStr }, + MissingCID { + /// The NSID of the lexicon whose record was missing a CID. + nsid: SmolStr, + }, + /// Identity resolution failed while locating the PDS that hosts the lexicon. #[error(transparent)] #[diagnostic(code(jacquard::lexicon::identity_resolution_failed))] IdentityResolution(#[from] crate::resolver::IdentityError), diff --git a/crates/jacquard-identity/src/lib.rs b/crates/jacquard-identity/src/lib.rs index 1236a025..4303b3fb 100644 --- a/crates/jacquard-identity/src/lib.rs +++ b/crates/jacquard-identity/src/lib.rs @@ -65,8 +65,7 @@ //! //! Both support `.parse()` for borrowing and validation. -// use crate::CowStr; // not currently needed directly here - +#![warn(missing_docs)] #![cfg_attr(target_arch = "wasm32", allow(unused))] pub mod lexicon_resolver; pub mod resolver; @@ -284,14 +283,19 @@ impl CacheConfig { #[cfg(feature = "cache")] #[derive(Clone)] pub struct ResolverCaches { + /// Cache mapping handles to their resolved DIDs. pub handle_to_did: cache_impl::Cache, Did<'static>>, + /// Cache mapping DIDs to their full DID documents. pub did_to_doc: cache_impl::Cache, Arc>, + /// Cache mapping authority strings (e.g., PDS hosts) to DIDs. pub authority_to_did: cache_impl::Cache>, + /// Cache mapping NSIDs to their resolved lexicon schemas. pub nsid_to_schema: cache_impl::Cache, Arc>>, } #[cfg(feature = "cache")] impl ResolverCaches { + /// Creates a new set of resolver caches from the given configuration. pub fn new(config: &CacheConfig) -> Self { Self { handle_to_did: cache_impl::new_cache( @@ -1182,7 +1186,7 @@ impl MiniDocResponse { /// Resolver specialized for unauthenticated/public flows using reqwest and stateless XRPC pub type PublicResolver = JacquardResolver; -impl Default for PublicResolver { +impl Default for JacquardResolver { /// Build a resolver with: /// - reqwest HTTP client /// - Public fallbacks enabled for handle resolution @@ -1190,8 +1194,8 @@ impl Default for PublicResolver { /// /// Example /// ```ignore - /// use jacquard::identity::resolver::PublicResolver; - /// let resolver = PublicResolver::default(); + /// use jacquard::identity::resolver::JacquardResolver; + /// let resolver = JacquardResolver::default(); /// ``` fn default() -> Self { let http = reqwest::Client::new(); @@ -1207,7 +1211,7 @@ impl Default for PublicResolver { /// Build a resolver configured to use Slingshot (`https://slingshot.microcosm.blue`) for PLC and /// mini-doc fallbacks, unauthenticated by default. -pub fn slingshot_resolver_default() -> PublicResolver { +pub fn slingshot_resolver_default() -> JacquardResolver { let http = reqwest::Client::new(); let mut opts = ResolverOptions::default(); opts.plc_source = PlcSource::slingshot_default(); diff --git a/crates/jacquard-identity/src/resolver.rs b/crates/jacquard-identity/src/resolver.rs index 9dcb012d..d2293af5 100644 --- a/crates/jacquard-identity/src/resolver.rs +++ b/crates/jacquard-identity/src/resolver.rs @@ -661,7 +661,9 @@ pub enum IdentityErrorKind { help("document id differs from requested DID; do not trust this document") )] DocIdMismatch { + /// The DID that was requested and expected to appear as the document `id`. expected: Did<'static>, + /// The DID document we *actually* got doc: DidDocument<'static>, }, } diff --git a/crates/jacquard-oauth/src/atproto.rs b/crates/jacquard-oauth/src/atproto.rs index 542cd6bd..948bc3f0 100644 --- a/crates/jacquard-oauth/src/atproto.rs +++ b/crates/jacquard-oauth/src/atproto.rs @@ -7,49 +7,74 @@ use serde::{Deserialize, Serialize}; use smol_str::{SmolStr, ToSmolStr}; use thiserror::Error; +/// Errors that can occur when building AT Protocol OAuth client metadata. #[derive(Error, Debug)] #[non_exhaustive] pub enum Error { + /// The `client_id` is not a valid URL. #[error("`client_id` must be a valid URL")] InvalidClientId, + /// The `grant_types` list does not include `authorization_code`, which is required by atproto. #[error("`grant_types` must include `authorization_code`")] InvalidGrantTypes, + /// The `scope` list does not include `atproto`, which is required for all atproto clients. #[error("`scope` must not include `atproto`")] InvalidScope, + /// No redirect URIs were provided; at least one is required. #[error("`redirect_uris` must not be empty")] EmptyRedirectUris, + /// The `private_key_jwt` auth method was requested but no JWK keys were provided. #[error("`private_key_jwt` auth method requires `jwks` keys")] EmptyJwks, + /// Signing algorithm mismatch: `private_key_jwt` requires `token_endpoint_auth_signing_alg`, + /// and non-`private_key_jwt` methods must not provide it. #[error( "`private_key_jwt` auth method requires `token_endpoint_auth_signing_alg`, otherwise must not be provided" )] AuthSigningAlg, + /// HTML form serialization of the loopback `client_id` query string failed. #[error(transparent)] SerdeHtmlForm(#[from] serde_html_form::ser::Error), + /// A localhost-specific validation error occurred. #[error(transparent)] LocalhostClient(#[from] LocalhostClientError), } +/// Errors specific to validating a loopback (localhost) OAuth client's redirect URIs. +/// +/// The AT Protocol spec has specific requirements for loopback clients: redirect URIs must +/// use the `http` scheme and must point to actual loopback addresses (not the hostname `localhost`). #[derive(Error, Debug)] #[non_exhaustive] pub enum LocalhostClientError { + /// The redirect URI could not be parsed. #[error("invalid redirect_uri: {0}")] Invalid(#[from] jacquard_common::deps::fluent_uri::ParseError), + /// Loopback redirect URIs must use `http:`, not `https:` or any other scheme. #[error("loopback client_id must use `http:` redirect_uri")] NotHttpScheme, + /// The hostname `localhost` is not allowed; use a numeric loopback address instead. #[error("loopback client_id must not use `localhost` as redirect_uri hostname")] Localhost, + /// The redirect URI host is not a loopback address (127.x.x.x or ::1). #[error("loopback client_id must not use loopback addresses as redirect_uri")] NotLoopbackHost, } +/// Convenience result type for AT Protocol client metadata operations. pub type Result = core::result::Result; +/// The token endpoint authentication method for an OAuth client. +/// +/// AT Protocol clients either authenticate with no client secret (public/loopback clients) +/// or with a private key JWT signed by a key from the client's JWK set. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AuthMethod { + /// No client authentication; used for public and loopback clients. None, - // https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + /// Authenticate using a JWT signed with a private key from the client's JWK set. + /// PrivateKeyJwt, } @@ -62,10 +87,13 @@ impl From for CowStr<'static> { } } +/// OAuth 2.0 grant types supported by AT Protocol clients. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum GrantType { + /// Standard authorization code grant, required by atproto. AuthorizationCode, + /// Refresh token grant, used to obtain new access tokens without re-authorization. RefreshToken, } @@ -78,18 +106,34 @@ impl From for CowStr<'static> { } } +/// AT Protocol-specific OAuth client metadata, used to describe a client before converting to +/// the generic [`OAuthClientMetadata`] format for server registration. +/// +/// This type provides a validated, atproto-aware view of client registration data, with +/// 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> { + /// 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. pub client_uri: Option>, + /// The list of allowed redirect URIs for the authorization code flow. pub redirect_uris: Vec>, + /// 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>, + /// 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, + /// URI of the client's logo image. pub logo_uri: Option>, + /// URI of the client's terms of service document. pub tos_uri: Option>, + /// URI of the client's privacy policy document. pub privacy_policy_uri: Option>, } @@ -112,6 +156,10 @@ impl<'m> IntoStatic for AtprotoClientMetadata<'m> { } impl<'m> AtprotoClientMetadata<'m> { + /// 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, @@ -126,6 +174,11 @@ impl<'m> AtprotoClientMetadata<'m> { self } + /// Create a default loopback client metadata with the `atproto` and `transition:generic` scopes. + /// + /// This is a convenience constructor for local development and CLI tools. The resulting + /// metadata uses `http://localhost` as the `client_id` with both IPv4 and IPv6 loopback + /// redirect URIs. pub fn default_localhost() -> Self { Self::new_localhost( None, @@ -133,6 +186,12 @@ impl<'m> AtprotoClientMetadata<'m> { ) } + /// Create loopback client metadata with optional custom redirect URIs and scopes. + /// + /// Encodes non-default redirect URIs and scopes into the `client_id` query string as + /// required by the AT Protocol loopback client specification. When `redirect_uris` or + /// `scopes` are `None`, sensible defaults (IPv4 + IPv6 loopback addresses, `atproto` scope) + /// are used. pub fn new_localhost( redirect_uris: Option>>, scopes: Option>>, @@ -181,11 +240,16 @@ impl<'m> AtprotoClientMetadata<'m> { } } +/// Convert [`AtprotoClientMetadata`] into the [`OAuthClientMetadata`] wire format. +/// +/// Validates all atproto-specific constraints (required scopes, grant types, redirect URIs), +/// 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>, keyset: &Option, ) -> Result> { - // For non-loopback clients, require a keyset/JWKs. 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 { @@ -193,9 +257,6 @@ pub fn atproto_client_metadata<'m>( } else { Some(CowStr::new_static("web")) }; - // if !is_loopback && keyset.is_none() { - // return Err(Error::EmptyJwks); - // } if metadata.redirect_uris.is_empty() { return Err(Error::EmptyRedirectUris); } diff --git a/crates/jacquard-oauth/src/authstore.rs b/crates/jacquard-oauth/src/authstore.rs index dfdc0a3e..fe712540 100644 --- a/crates/jacquard-oauth/src/authstore.rs +++ b/crates/jacquard-oauth/src/authstore.rs @@ -11,47 +11,62 @@ use smol_str::{SmolStr, ToSmolStr, format_smolstr}; use crate::session::{AuthRequestData, ClientSessionData}; +/// Persistent storage backend for OAuth client sessions and in-flight authorization requests. +/// +/// Implementors are responsible for durably storing two categories of data: +/// - Active client sessions (access tokens, refresh tokens, nonces) keyed by DID + session ID. +/// - Pending authorization request state, keyed by the OAuth `state` parameter, which must +/// survive the round-trip to the authorization server and be cleaned up after use. #[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( &self, did: &Did<'_>, session_id: &str, ) -> 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<'_>, ) -> impl Future>; + /// Delete the session for the given DID and session identifier. fn delete_session( &self, did: &Did<'_>, session_id: &str, ) -> impl Future>; + /// Retrieve the authorization request data associated with the given OAuth `state` value. fn get_auth_req_info( &self, state: &str, ) -> 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<'_>, ) -> impl Future>; + /// Remove authorization request data after the callback has been handled. fn delete_auth_req_info( &self, state: &str, ) -> impl Future>; } +/// 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>, } impl MemoryAuthStore { + /// Create a new, empty in-memory auth store. pub fn new() -> Self { Self { sessions: DashMap::new(), diff --git a/crates/jacquard-oauth/src/client.rs b/crates/jacquard-oauth/src/client.rs index 90fb20af..580509ac 100644 --- a/crates/jacquard-oauth/src/client.rs +++ b/crates/jacquard-oauth/src/client.rs @@ -34,18 +34,24 @@ use jose_jwk::JwkSet; use std::{future::Future, sync::Arc}; use tokio::sync::RwLock; +/// The top-level OAuth client responsible for driving the authorization flow. pub struct OAuthClient where T: OAuthResolver, S: ClientAuthStore, { + /// Shared session registry that mediates access to the backing auth store. 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`. pub endpoint: RwLock>>, + /// Underlying HTTP/identity/OAuth resolver used for all network operations. pub client: Arc, } impl OAuthClient { + /// Create an `OAuthClient` using the default [`JacquardResolver`] for identity and metadata resolution. pub fn new(store: S, client_data: ClientData<'static>) -> Self { let client = JacquardResolver::default(); Self::new_from_resolver(store, client, client_data) @@ -103,6 +109,7 @@ where T: OAuthResolver, 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 { // #[cfg(feature = "tracing")] // tracing::info!( @@ -122,6 +129,7 @@ where } } + /// Create an OAuth client from already-`Arc`-wrapped store and resolver. pub fn new_with_shared( store: Arc, client: Arc, @@ -146,6 +154,7 @@ where S: ClientAuthStore + Send + Sync + 'static, T: OAuthResolver + DpopExt + Send + Sync + 'static, { + /// Return the public JWK set for this client's keyset, or an empty set if no keyset is configured. pub fn jwks(&self) -> JwkSet { self.registry .client_data @@ -154,6 +163,14 @@ where .map(|keyset| keyset.public_jwks()) .unwrap_or_default() } + /// Begin an OAuth authorization flow and return the URL to which the user should be redirected. + /// + /// This resolves OAuth metadata for the given `input` (a handle, DID, or PDS/entryway URL), + /// performs a Pushed Authorization Request (PAR) to the authorization server, persists the + /// resulting state for later callback verification, and returns a fully-constructed + /// authorization endpoint URL. + /// + /// 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( &self, @@ -205,6 +222,11 @@ where .unwrap()) } + /// Complete the OAuth authorization flow after the authorization server redirects back to the client. + /// + /// 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> { let Some(state_key) = params.state else { @@ -294,11 +316,17 @@ 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> { self.create_session(self.registry.get(did, session_id, true).await?) .await } + /// Revoke a session by deleting it from the backing store. + /// + /// 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<()> { Ok(self.registry.del(did, session_id).await?) } @@ -398,15 +426,27 @@ where } } +/// An active OAuth session for a specific account, used to make authenticated API requests. +/// +/// `OAuthSession` holds the DPoP-bound token set for one account and handles transparent +/// token refresh on `401 invalid_token` responses. The optional `W` type parameter allows +/// attaching a WebSocket client (defaults to `()` when WebSocket support is not needed). +/// +/// Obtain an `OAuthSession` from [`OAuthClient::callback`] or [`OAuthClient::restore`]. pub struct OAuthSession where T: OAuthResolver, S: ClientAuthStore, { + /// Shared registry used to persist and retrieve session data across refresh operations. 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>, + /// Default call options applied to every outgoing XRPC request from this session. pub options: RwLock>, } @@ -415,6 +455,10 @@ where T: OAuthResolver, S: ClientAuthStore, { + /// Create a new session without a WebSocket client. + /// + /// 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>, client: Arc, @@ -435,6 +479,11 @@ where T: OAuthResolver, S: ClientAuthStore, { + /// Create a new session with an attached WebSocket client. + /// + /// Use this variant when the session needs to support WebSocket subscriptions in addition + /// 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>, client: Arc, @@ -450,6 +499,10 @@ where } } + /// Consume this session and return a new one with the given call options pre-applied. + /// + /// Useful for setting request-level defaults (e.g., `atproto-proxy` or custom headers) once + /// at construction time rather than passing them to every individual XRPC call. pub fn with_options(self, options: CallOptions<'_>) -> Self { Self { registry: self.registry, @@ -465,23 +518,37 @@ where &self.ws_client } + /// Replace the default call options for this session without consuming it. pub async fn set_options(&self, options: CallOptions<'_>) { *self.options.write().await = options.into_static(); } + /// Return the DID and session ID for this session. + /// + /// 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<'_>) { let data = self.data.read().await; (data.account_did.clone(), data.session_id.clone()) } + /// Return the resource server (PDS) base URI for this session. pub async fn endpoint(&self) -> Uri { self.data.read().await.host_url.clone() } + /// Return the current DPoP-bound access token for this session. + /// + /// 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()) } + /// 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> { self.data .read() @@ -492,6 +559,10 @@ where .map(|t| AuthorizationToken::Dpop(t.clone())) } + /// Derive an unauthenticated [`OAuthClient`] that shares the same registry and resolver. + /// + /// Useful when you need to initiate a new authorization flow from within an existing + /// session context (e.g., to add a second account) without constructing a fresh client. pub fn to_client(&self) -> OAuthClient { OAuthClient::from_session(self) } @@ -501,6 +572,11 @@ where S: ClientAuthStore + Send + Sync + 'static, T: OAuthResolver + DpopExt + Send + Sync + 'static, { + /// Revoke the access token at the authorization server and delete the session from the store. + /// + /// Revocation is best-effort: if the server does not advertise a revocation endpoint, or if + /// the revocation call fails, the session is still deleted locally. This prevents a dangling + /// session record from blocking future logins for the same account. pub async fn logout(&self) -> Result<()> { use crate::request::{OAuthMetadata, revoke}; let mut data = self.data.write().await; @@ -525,6 +601,10 @@ where T: OAuthResolver, S: ClientAuthStore, { + /// Construct an `OAuthClient` that shares the registry and resolver of an existing session. + /// + /// Equivalent to [`OAuthSession::to_client`]; provided on `OAuthClient` for symmetry so + /// callers can obtain an unauthenticated client without holding a session reference. pub fn from_session(session: &OAuthSession) -> Self { Self { registry: session.registry.clone(), @@ -539,6 +619,14 @@ where S: ClientAuthStore + Send + Sync + 'static, T: OAuthResolver + DpopExt + Send + Sync + 'static, { + /// Explicitly refresh the access token using the stored refresh token. + /// + /// On success the new token set is written back into both the in-memory session data and + /// the backing store. The returned `AuthorizationToken` is the new access token, which + /// callers can immediately use to retry a failed request. + /// + /// 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> { // Read identifiers without holding the lock across await diff --git a/crates/jacquard-oauth/src/dpop.rs b/crates/jacquard-oauth/src/dpop.rs index d763ee20..93d9ed56 100644 --- a/crates/jacquard-oauth/src/dpop.rs +++ b/crates/jacquard-oauth/src/dpop.rs @@ -23,6 +23,7 @@ use crate::{ session::DpopDataSource, }; +/// The `typ` header value required in all DPoP proof JWTs, per RFC 9449. pub const JWT_HEADER_TYP_DPOP: &str = "dpop+jwt"; #[derive(serde::Deserialize)] @@ -332,23 +333,33 @@ impl From for jacquard_common::error::ClientError { type Result = core::result::Result; +/// An HTTP client capable of making DPoP-protected requests to both auth servers and resource servers. +/// +/// Implementors must be able to attach a DPoP proof header, handle nonce challenges, and +/// retry transparently on `use_dpop_nonce` errors. #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] pub trait DpopClient: HttpClient { + /// Send a DPoP-protected request to an authorization server (token endpoint, PAR, etc.). fn dpop_server( &self, request: Request>, ) -> impl Future>>>; + /// Send a DPoP-protected request to a resource server (PDS, AppView, etc.). fn dpop_client( &self, request: Request>, ) -> impl Future>>>; + /// Send a DPoP-protected request, inferring the target type from the request context. fn wrap_request( &self, request: Request>, ) -> impl Future>>>; } +/// Extension trait for any [`HttpClient`] that adds builder methods for constructing +/// DPoP-protected request calls without requiring a full [`DpopClient`] implementation. pub trait DpopExt: HttpClient { + /// Begin building a DPoP-protected request targeting an authorization server. fn dpop_server_call<'r, D>(&'r self, data_source: &'r mut D) -> DpopCall<'r, Self, D> where Self: Sized, @@ -357,6 +368,7 @@ pub trait DpopExt: HttpClient { DpopCall::server(self, data_source) } + /// Begin building a DPoP-protected request targeting a resource server. fn dpop_call<'r, N>(&'r self, data_source: &'r mut N) -> DpopCall<'r, Self, N> where Self: Sized, @@ -366,13 +378,22 @@ pub trait DpopExt: HttpClient { } } +/// A builder for a single DPoP-protected HTTP request, holding references to the underlying +/// client and the session data source that supplies nonces and the DPoP signing key. pub struct DpopCall<'r, C: HttpClient, D: DpopDataSource> { + /// The HTTP client that will send the request. pub client: &'r C, + /// Whether the request targets an authorization server rather than a resource server. + /// + /// This controls which nonce slot is read from and written to, and how `use_dpop_nonce` + /// errors are detected in the response. pub is_to_auth_server: bool, + /// The session data source providing the DPoP key and current nonces. pub data_source: &'r mut D, } impl<'r, C: HttpClient, N: DpopDataSource> DpopCall<'r, C, N> { + /// Create a call builder targeting an authorization server. pub fn server(client: &'r C, data_source: &'r mut N) -> Self { Self { client, @@ -381,6 +402,7 @@ impl<'r, C: HttpClient, N: DpopDataSource> DpopCall<'r, C, N> { } } + /// Create a call builder targeting a resource server. pub fn client(client: &'r C, data_source: &'r mut N) -> Self { Self { client, @@ -389,6 +411,7 @@ impl<'r, C: HttpClient, N: DpopDataSource> DpopCall<'r, C, N> { } } + /// Send the request with a DPoP proof, retrying once if the server provides a new nonce. pub async fn send(self, request: Request>) -> Result>> { wrap_request_with_dpop( self.client, @@ -399,6 +422,7 @@ impl<'r, C: HttpClient, N: DpopDataSource> DpopCall<'r, C, N> { .await } + /// Sends the request with DPoP proof and returns a streaming response. #[cfg(feature = "streaming")] pub async fn send_streaming( self, @@ -416,6 +440,7 @@ impl<'r, C: HttpClient, N: DpopDataSource> DpopCall<'r, C, N> { .await } + /// Sends the request with DPoP proof using bidirectional streaming. #[cfg(feature = "streaming")] pub async fn send_bidirectional( self, @@ -470,6 +495,11 @@ fn store_nonce( } } +/// Attach a DPoP proof to `request`, send it, and transparently retry once if the server +/// responds with a `use_dpop_nonce` error and a fresh nonce. +/// +/// The nonce is read from and written back to `data_source` based on `is_to_auth_server`, +/// keeping the two nonce slots (auth server vs. resource server) independent. pub async fn wrap_request_with_dpop( client: &T, data_source: &mut N, @@ -531,6 +561,11 @@ where Ok(response) } +/// Wraps an HTTP request with a DPoP proof and returns a streaming response. +/// +/// Like [`wrap_request_with_dpop`], but returns a [`StreamingResponse`](jacquard_common::xrpc::StreamingResponse) +/// instead of buffering the body. Nonce retry is limited to status/header inspection +/// since the body stream cannot be rewound. #[cfg(feature = "streaming")] pub async fn wrap_request_with_dpop_streaming( client: &T, @@ -600,6 +635,10 @@ where Ok(StreamingResponse::new(parts, body)) } +/// Wraps an HTTP request with a DPoP proof using bidirectional streaming. +/// +/// Similar to [`wrap_request_with_dpop_streaming`] but accepts a [`ByteStream`](jacquard_common::stream::ByteStream) +/// request body for upload streaming scenarios. #[cfg(feature = "streaming")] pub async fn wrap_request_with_dpop_bidirectional( client: &T, diff --git a/crates/jacquard-oauth/src/error.rs b/crates/jacquard-oauth/src/error.rs index acd842db..a636668f 100644 --- a/crates/jacquard-oauth/src/error.rs +++ b/crates/jacquard-oauth/src/error.rs @@ -8,46 +8,57 @@ use crate::resolver::ResolverError; #[derive(Debug, thiserror::Error, Diagnostic)] #[non_exhaustive] pub enum OAuthError { + /// An error occurred during identity or metadata resolution. #[error(transparent)] #[diagnostic(code(jacquard_oauth::resolver))] Resolver(#[from] ResolverError), + /// An error occurred while making an OAuth HTTP request. #[error(transparent)] #[diagnostic(code(jacquard_oauth::request))] Request(#[from] RequestError), + /// An error occurred reading or writing session state. #[error(transparent)] #[diagnostic(code(jacquard_oauth::storage))] Storage(#[from] SessionStoreError), + /// An error occurred during DPoP proof generation or validation. #[error(transparent)] #[diagnostic(code(jacquard_oauth::dpop))] Dpop(#[from] crate::dpop::DpopError), + /// An error occurred with the client's key set. #[error(transparent)] #[diagnostic(code(jacquard_oauth::keyset))] Keyset(#[from] crate::keyset::Error), + /// An ATProto-specific OAuth error (e.g. scope validation, client ID). #[error(transparent)] #[diagnostic(code(jacquard_oauth::atproto))] Atproto(#[from] crate::atproto::Error), + /// An error occurred managing or refreshing an OAuth session. #[error(transparent)] #[diagnostic(code(jacquard_oauth::session))] Session(#[from] crate::session::Error), + /// A JSON serialization or deserialization error. #[error(transparent)] #[diagnostic(code(jacquard_oauth::serde_json))] SerdeJson(#[from] serde_json::Error), + /// A URI parse error. #[error(transparent)] #[diagnostic(code(jacquard_oauth::url))] Url(#[from] jacquard_common::deps::fluent_uri::ParseError), + /// A form (URL-encoded) serialization error. #[error(transparent)] #[diagnostic(code(jacquard_oauth::form))] Form(#[from] serde_html_form::ser::Error), + /// An error validating an authorization callback. #[error(transparent)] #[diagnostic(code(jacquard_oauth::callback))] Callback(#[from] CallbackError), @@ -57,18 +68,33 @@ pub enum OAuthError { #[derive(Debug, thiserror::Error, Diagnostic)] #[non_exhaustive] pub enum CallbackError { + /// The `state` parameter was absent from the authorization callback. + /// + /// State is required to prevent CSRF attacks per RFC 6749 §10.12. #[error("missing state parameter in callback")] #[diagnostic(code(jacquard_oauth::callback::missing_state))] MissingState, + /// The `iss` (issuer) parameter was absent from the authorization callback. + /// + /// RFC 9207 requires `iss` to be present so that clients can reject + /// mix-up attacks from malicious authorization servers. #[error("missing `iss` parameter")] #[diagnostic(code(jacquard_oauth::callback::missing_iss))] MissingIssuer, + /// The issuer in the callback did not match the expected authorization server. #[error("issuer mismatch: expected {expected}, got {got}")] #[diagnostic(code(jacquard_oauth::callback::issuer_mismatch))] - IssuerMismatch { expected: String, got: String }, + IssuerMismatch { + /// The issuer that was expected. + expected: String, + /// The issuer that was actually present in the callback. + got: String, + }, + /// The authorization request timed out before a callback was received. #[error("timeout")] #[diagnostic(code(jacquard_oauth::callback::timeout))] Timeout, } +/// Convenience alias for `Result`. pub type Result = core::result::Result; diff --git a/crates/jacquard-oauth/src/jose.rs b/crates/jacquard-oauth/src/jose.rs index 1291ba3a..1b2f2973 100644 --- a/crates/jacquard-oauth/src/jose.rs +++ b/crates/jacquard-oauth/src/jose.rs @@ -1,12 +1,19 @@ +/// JWS (JSON Web Signature) header types. pub mod jws; +/// JWT (JSON Web Token) claims types. pub mod jwt; +/// Signed JWT creation using ES256 keys. pub mod signing; use serde::{Deserialize, Serialize}; +/// A JOSE header, covering the supported JWS formats. +/// +/// 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> { + /// A JWS compact-serialization header. #[serde(borrow)] Jws(jws::Header<'a>), } diff --git a/crates/jacquard-oauth/src/jose/jws.rs b/crates/jacquard-oauth/src/jose/jws.rs index 7dd81162..beed6990 100644 --- a/crates/jacquard-oauth/src/jose/jws.rs +++ b/crates/jacquard-oauth/src/jose/jws.rs @@ -3,8 +3,10 @@ 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> { + /// The registered header parameters defined by the JWS specification. #[serde(flatten)] #[serde(borrow)] pub registered: RegisteredHeader<'a>, @@ -16,29 +18,39 @@ impl<'a> From> for super::super::jose::Header<'a> { } } +/// Registered JWS header parameters as defined in RFC 7515 §4.1. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RegisteredHeader<'a> { + /// 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>, + /// 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>, + /// 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>, + /// 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>, + /// 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>, + /// 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>, - + /// 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>, + /// Content type: declares the media type of the secured content (the payload). #[serde(skip_serializing_if = "Option::is_none")] pub cty: Option>, } diff --git a/crates/jacquard-oauth/src/jose/jwt.rs b/crates/jacquard-oauth/src/jose/jwt.rs index 4652e47b..f421b508 100644 --- a/crates/jacquard-oauth/src/jose/jwt.rs +++ b/crates/jacquard-oauth/src/jose/jwt.rs @@ -1,45 +1,64 @@ use jacquard_common::{CowStr, IntoStatic}; use serde::{Deserialize, Serialize}; +/// Full JWT claims payload, combining registered and public (DPoP-specific) claims. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct Claims<'a> { + /// Standard registered JWT claims (iss, sub, aud, exp, etc.). #[serde(flatten)] pub registered: RegisteredClaims<'a>, + /// Public claims used in DPoP proofs (htm, htu, ath, nonce). #[serde(flatten)] #[serde(borrow)] pub public: PublicClaims<'a>, } +/// Standard registered JWT claims as defined in RFC 7519 §4.1. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RegisteredClaims<'a> { + /// Issuer: identifies the principal that issued the JWT. #[serde(borrow)] #[serde(skip_serializing_if = "Option::is_none")] pub iss: Option>, + /// Subject: identifies the principal that is the subject of the JWT. #[serde(skip_serializing_if = "Option::is_none")] pub sub: Option>, + /// Audience: recipients that the JWT is intended for. #[serde(skip_serializing_if = "Option::is_none")] 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, + /// Not before (Unix timestamp): the JWT must not be accepted before this time. #[serde(skip_serializing_if = "Option::is_none")] pub nbf: Option, + /// Issued at (Unix timestamp): identifies when the JWT was created. #[serde(skip_serializing_if = "Option::is_none")] 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>, } +/// 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> { + /// 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>, + /// HTTP target URI of the request the DPoP proof is bound to. #[serde(skip_serializing_if = "Option::is_none")] 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>, + /// Server-provided nonce, included to prevent replay attacks when required by the authorization server. #[serde(skip_serializing_if = "Option::is_none")] pub nonce: Option>, } @@ -53,11 +72,14 @@ 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> { + /// A single audience identifier. #[serde(borrow)] Single(CowStr<'a>), + /// Multiple audience identifiers. Multiple(Vec>), } diff --git a/crates/jacquard-oauth/src/jose/signing.rs b/crates/jacquard-oauth/src/jose/signing.rs index 64be56eb..2074a623 100644 --- a/crates/jacquard-oauth/src/jose/signing.rs +++ b/crates/jacquard-oauth/src/jose/signing.rs @@ -5,6 +5,7 @@ use p256::ecdsa::{Signature, SigningKey, signature::Signer}; use super::{Header, jwt::Claims}; +/// Creates a compact-serialized signed JWT using an ES256 (P-256 ECDSA) key. pub fn create_signed_jwt( key: SigningKey, header: Header, diff --git a/crates/jacquard-oauth/src/keyset.rs b/crates/jacquard-oauth/src/keyset.rs index 4ccde773..2fd4c2c6 100644 --- a/crates/jacquard-oauth/src/keyset.rs +++ b/crates/jacquard-oauth/src/keyset.rs @@ -8,27 +8,40 @@ use jose_jwk::{Jwk, JwkSet, Key}; use std::collections::HashSet; use thiserror::Error; +/// Errors that can occur when constructing or using a [`Keyset`]. #[derive(Error, Debug)] #[non_exhaustive] pub enum Error { + /// Two keys in the set share the same `kid`, which would make key selection ambiguous. #[error("duplicate kid: {0}")] DuplicateKid(String), + /// A keyset with no keys cannot sign anything. #[error("keys must not be empty")] EmptyKeys, + /// Each key must carry a `kid` so it can be referenced in JWS headers. #[error("key at index {0} must have a `kid`")] EmptyKid(usize), + /// No key in the set matches any of the requested signing algorithms. #[error("no signing key found for algorithms: {0:?}")] NotFound(Vec>), + /// Only secret (private) keys may be used for signing; a public key was provided. #[error("key for signing must be a secret key")] PublicKey, + /// An error from the underlying JWK cryptographic operation. #[error("crypto error: {0:?}")] JwkCrypto(crypto::Error), + /// JSON serialization of a JWT header or claims payload failed. #[error(transparent)] SerdeJson(#[from] serde_json::Error), } +/// Convenience result type for keyset operations. pub type Result = core::result::Result; +/// A validated collection of JWK secret keys used for signing DPoP proofs and client assertions. +/// +/// Key selection follows a preference order defined in [`PREFERRED_SIGNING_ALGORITHMS`](Self::PREFERRED_SIGNING_ALGORITHMS), +/// though currently only P-256 (ES256) keys are supported. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Keyset(Vec); @@ -36,6 +49,7 @@ impl Keyset { const PREFERRED_SIGNING_ALGORITHMS: [&'static str; 9] = [ "EdDSA", "ES256K", "ES256", "PS256", "PS384", "PS512", "HS256", "HS384", "HS512", ]; + /// Returns a [`JwkSet`] containing the public halves of all keys in this keyset. pub fn public_jwks(&self) -> JwkSet { let mut keys = Vec::with_capacity(self.0.len()); for mut key in self.0.clone() { @@ -49,6 +63,9 @@ impl Keyset { } JwkSet { keys } } + /// 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: &[CowStr], claims: Claims) -> Result> { let Some(jwk) = self.find_key(algs, Class::Signing) else { return Err(Error::NotFound(algs.to_vec().into_static())); diff --git a/crates/jacquard-oauth/src/lib.rs b/crates/jacquard-oauth/src/lib.rs index 4d7d3a29..36fcc3b1 100644 --- a/crates/jacquard-oauth/src/lib.rs +++ b/crates/jacquard-oauth/src/lib.rs @@ -47,21 +47,36 @@ //! See [`atproto`] module for AT Protocol-specific metadata helpers. #![warn(missing_docs)] +/// AT Protocol-specific OAuth client metadata helpers and builder types. pub mod atproto; +/// Storage trait and in-memory implementation for OAuth client auth state. pub mod authstore; +/// High-level OAuth client for driving the full authorization code flow. pub mod client; +/// DPoP (Demonstrating Proof-of-Possession) key generation and request signing. pub mod dpop; +/// Top-level OAuth error types for the authorization flow. pub mod error; +/// JOSE primitives: JWS headers, JWT claims, and signing utilities. pub mod jose; +/// JWK keyset management for signing keys used in DPoP and client auth. pub mod keyset; +/// Low-level OAuth request helpers: PAR, token exchange, and refresh. pub mod request; +/// OAuth server metadata resolution: authorization server and protected resource discovery. pub mod resolver; +/// pub mod scopes; +/// OAuth session types, token storage, and DPoP session state. pub mod session; +/// OAuth protocol types: client metadata, token sets, and server metadata. pub mod types; +/// Miscellaneous cryptographic utilities: key generation, PKCE, and hashing helpers. pub mod utils; +/// Fallback signing algorithm used when no preferred algorithm is negotiated with the server. pub const FALLBACK_ALG: &str = "ES256"; +/// Loopback server helpers for the local redirect-based OAuth flow. #[cfg(feature = "loopback")] pub mod loopback; diff --git a/crates/jacquard-oauth/src/loopback.rs b/crates/jacquard-oauth/src/loopback.rs index 9025e7e3..bb231676 100644 --- a/crates/jacquard-oauth/src/loopback.rs +++ b/crates/jacquard-oauth/src/loopback.rs @@ -59,17 +59,25 @@ use rouille::Server; use std::net::SocketAddr; use tokio::sync::mpsc; +/// Port selection strategy for the loopback OAuth callback server. #[derive(Clone, Debug)] pub enum LoopbackPort { + /// Bind to a specific port number. Fixed(u16), + /// Let the OS assign an available port. Ephemeral, } +/// Configuration for the loopback OAuth callback server. #[derive(Clone, Debug)] pub struct LoopbackConfig { + /// The host address to bind to (e.g., `"127.0.0.1"`). pub host: String, + /// Port selection strategy. pub port: LoopbackPort, + /// Whether to attempt opening the authorization URL in the user's browser. pub open_browser: bool, + /// How long to wait for the callback before timing out, in milliseconds. pub timeout_ms: u64, } @@ -84,10 +92,14 @@ impl Default for LoopbackConfig { } } +/// Attempts to open the given URL in the user's default browser. +/// +/// Returns `true` if the browser was opened successfully, `false` otherwise. #[cfg(feature = "browser-open")] pub fn try_open_in_browser(url: &str) -> bool { webbrowser::open(url).is_ok() } +/// Stub for when the `browser-open` feature is disabled. Always returns `false`. #[cfg(not(feature = "browser-open"))] pub fn try_open_in_browser(_url: &str) -> bool { false @@ -114,6 +126,7 @@ fn create_callback_router( ) } +/// Handle to a running loopback callback server, used to await the OAuth redirect. pub struct CallbackHandle { #[allow(dead_code)] server_handle: std::thread::JoinHandle<()>, diff --git a/crates/jacquard-oauth/src/request.rs b/crates/jacquard-oauth/src/request.rs index a9b85385..53570fc0 100644 --- a/crates/jacquard-oauth/src/request.rs +++ b/crates/jacquard-oauth/src/request.rs @@ -41,6 +41,7 @@ const CLIENT_ASSERTION_TYPE_JWT_BEARER: &str = use smol_str::SmolStr; +/// Convenience alias for a heap-allocated, thread-safe, `'static` error value. pub type BoxError = Box; /// OAuth request error for token operations and auth flows @@ -130,7 +131,12 @@ pub enum RequestErrorKind { code(jacquard_oauth::request::http_status_body), help("server returned error JSON; inspect fields like `error`, `error_description`") )] - HttpStatusWithBody { status: StatusCode, body: Value }, + HttpStatusWithBody { + /// HTTP status code returned by the server. + status: StatusCode, + /// Parsed JSON body containing OAuth error fields such as `error` and `error_description`. + body: Value, + }, /// Identity resolution error #[error("identity error")] @@ -413,18 +419,26 @@ impl From for RequestError { } } +/// Convenience `Result` type for OAuth request operations, defaulting to [`RequestError`]. pub type Result = core::result::Result; +/// Represents the different OAuth token-endpoint request types sent by this crate. #[allow(dead_code)] pub enum OAuthRequest<'a> { + /// Standard authorization-code token exchange. Token(TokenRequestParameters<'a>), + /// Refresh-token grant to obtain a fresh access token. Refresh(RefreshRequestParameters<'a>), + /// Token revocation request (RFC 7009). Revocation(RevocationRequestParameters<'a>), + /// Token introspection request (RFC 7662). Introspection, + /// Pushed authorization request (RFC 9126) for pre-registering auth parameters. PushedAuthorizationRequest(ParParameters<'a>), } 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 { Self::Token(_) => "token", @@ -434,6 +448,7 @@ impl OAuthRequest<'_> { 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 { match self { Self::Token(_) | Self::Refresh(_) => StatusCode::OK, @@ -445,28 +460,47 @@ impl OAuthRequest<'_> { } } +/// The serialized body of an OAuth token-endpoint request. #[derive(Debug, Serialize)] pub struct RequestPayload<'a, T> where T: Serialize, { + /// The OAuth `client_id` advertised in the client metadata document. client_id: CowStr<'a>, + /// The assertion type URI; set to `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` + /// when using `private_key_jwt` client authentication. #[serde(skip_serializing_if = "Option::is_none")] 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>, + /// The grant-specific parameters (token request, refresh, PAR, etc.) flattened into the body. #[serde(flatten)] parameters: T, } +/// Bundled OAuth metadata needed to perform token-endpoint operations. +/// +/// Aggregates the server's authorization server metadata, the client's own registered metadata, +/// 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 { + /// Metadata fetched from the authorization server's `/.well-known/oauth-authorization-server` document. pub server_metadata: OAuthAuthorizationServerMetadata<'static>, + /// This client's registered metadata, derived from [`crate::atproto::AtprotoClientMetadata`]. pub client_metadata: OAuthClientMetadata<'static>, + /// Optional signing keyset; required for `private_key_jwt` client authentication. pub keyset: Option, } 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>( client: &T, ClientData { keyset, config }: &ClientData<'r>, @@ -484,6 +518,12 @@ impl OAuthMetadata { } } +/// Perform a Pushed Authorization Request (PAR) and return the resulting state for the auth flow. +/// +/// Generates a PKCE code challenge, a fresh DPoP key, and a random `state` token, then POSTs +/// them to the authorization server's PAR endpoint. The returned [`AuthRequestData`] must be +/// 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>( client: &T, @@ -562,6 +602,7 @@ 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>( client: &T, @@ -621,6 +662,12 @@ where Ok(session_data) } +/// Exchange an authorization code for a token set and return a fully-verified [`TokenSet`]. +/// +/// Per the AT Protocol OAuth spec, the `sub` claim in the token response **must** be verified +/// against the expected authorization server issuer before the token can be trusted. This +/// 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>( client: &T, @@ -680,6 +727,11 @@ where }) } +/// Send a token revocation request (RFC 7009) to the authorization server. +/// +/// This function is called by [`crate::client::OAuthSession::logout`] when a revocation endpoint is advertised +/// 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>( client: &T, @@ -703,6 +755,12 @@ where Ok(()) } +/// Low-level function for sending an OAuth token-endpoint request and deserializing the response. +/// +/// Selects the correct server endpoint for `request`, builds the form-encoded body with +/// 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>( client: &T, data_source: &'r mut D, @@ -784,14 +842,23 @@ where })?) } +/// Client identity fields appended to every token-endpoint request body. +/// +/// Encapsulates the result of choosing a client authentication method (`none` vs. +/// `private_key_jwt`). The `build_auth` helper selects the appropriate variant based +/// on server capabilities and client configuration. #[derive(Debug, Clone, Default)] pub struct ClientAuth<'a> { + /// The OAuth `client_id` for this client. client_id: CowStr<'a>, - assertion_type: Option>, // either none or `CLIENT_ASSERTION_TYPE_JWT_BEARER` + /// 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>, } impl<'s> ClientAuth<'s> { + /// Construct a `ClientAuth` with only a `client_id` and no assertion (the `none` method). pub fn new_id(client_id: CowStr<'s>) -> Self { Self { client_id, diff --git a/crates/jacquard-oauth/src/resolver.rs b/crates/jacquard-oauth/src/resolver.rs index c08d0aaa..2aa998c4 100644 --- a/crates/jacquard-oauth/src/resolver.rs +++ b/crates/jacquard-oauth/src/resolver.rs @@ -14,6 +14,7 @@ use jacquard_common::{http_client::HttpClient, types::did::Did}; use jacquard_identity::resolver::{IdentityError, IdentityResolver}; use smol_str::SmolStr; +/// Convenience alias for a heap-allocated, thread-safe, `'static` error value. pub type BoxError = Box; /// OAuth resolver error for identity and metadata resolution @@ -613,8 +614,19 @@ async fn get_resource_server_metadata_impl( Ok(as_metadata) } +/// Resolver trait for the AT Protocol OAuth flow. +/// +/// `OAuthResolver` extends [`IdentityResolver`] and [`HttpClient`] with the methods needed to +/// drive the full OAuth flow: resolving an AT identifier (handle or DID) to the authorization +/// server that protects its PDS, fetching server metadata, and verifying that a token's `sub` +/// claim is authorized by the expected issuer. +/// +/// A default implementation based on [`jacquard_identity::JacquardResolver`] is provided. +/// Custom implementations are possible for testing or for environments that require +/// non-standard identity resolution (e.g., federated or offline setups). #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 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( &self, @@ -627,6 +639,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { verify_issuer_impl(self, server_metadata, sub) } + /// Verify that the authorization server in `server_metadata` is the correct issuer for `sub`. #[cfg(target_arch = "wasm32")] fn verify_issuer( &self, @@ -636,6 +649,12 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { verify_issuer_impl(self, server_metadata, sub) } + /// Resolve `input` (a handle, DID, PDS URL, or entryway URL) to OAuth metadata. + /// + /// When `input` starts with `https://`, it is treated as a service URL and resolved + /// directly via [`OAuthResolver::resolve_from_service`]. Otherwise it is treated as an + /// AT identifier and resolved via [`OAuthResolver::resolve_from_identity`]. Returns the + /// authorization server metadata and, when `input` was an identity, the resolved DID document. #[cfg(not(target_arch = "wasm32"))] fn resolve_oauth( &self, @@ -652,6 +671,12 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { resolve_oauth_impl(self, input) } + /// Resolve `input` (a handle, DID, PDS URL, or entryway URL) to OAuth metadata. + /// + /// When `input` starts with `https://`, it is treated as a service URL and resolved + /// directly via [`OAuthResolver::resolve_from_service`]. Otherwise it is treated as an + /// AT identifier and resolved via [`OAuthResolver::resolve_from_identity`]. Returns the + /// authorization server metadata and, when `input` was an identity, the resolved DID document. #[cfg(target_arch = "wasm32")] fn resolve_oauth( &self, @@ -665,6 +690,10 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { resolve_oauth_impl(self, input) } + /// Resolve a service URL (PDS or entryway) to its authorization server metadata. + /// + /// First attempts to fetch the PDS's protected resource metadata; if that fails, falls back + /// to treating the URL as an entryway and fetching authorization server metadata directly. #[cfg(not(target_arch = "wasm32"))] fn resolve_from_service( &self, @@ -676,6 +705,10 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { resolve_from_service_impl(self, input) } + /// Resolve a service URL to its authorization server metadata. + /// + /// First attempts to fetch the PDS's protected resource metadata; if that fails, falls back + /// to treating the URL as an entryway and fetching authorization server metadata directly. #[cfg(target_arch = "wasm32")] fn resolve_from_service( &self, @@ -684,6 +717,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { resolve_from_service_impl(self, input) } + /// Resolve an AT identifier (handle or DID) to its authorization server metadata and DID document. #[cfg(not(target_arch = "wasm32"))] fn resolve_from_identity( &self, @@ -700,6 +734,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { resolve_from_identity_impl(self, input) } + /// Resolve an AT identifier to its authorization server metadata and DID document. #[cfg(target_arch = "wasm32")] fn resolve_from_identity( &self, @@ -713,6 +748,10 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { resolve_from_identity_impl(self, input) } + /// Fetch and validate the authorization server metadata for the given issuer URL. + /// + /// Retrieves the `/.well-known/oauth-authorization-server` document and confirms that + /// the `issuer` field in the response matches the requested URL, as required by RFC 8414 §3.3. #[cfg(not(target_arch = "wasm32"))] fn get_authorization_server_metadata( &self, @@ -724,6 +763,10 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { get_authorization_server_metadata_impl(self, issuer) } + /// Fetch and validate the authorization server metadata for the given issuer URL. + /// + /// Retrieves the `/.well-known/oauth-authorization-server` document and confirms that + /// the `issuer` field in the response matches the requested URL, as required by RFC 8414 §3.3. #[cfg(target_arch = "wasm32")] fn get_authorization_server_metadata( &self, @@ -732,6 +775,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { get_authorization_server_metadata_impl(self, issuer) } + /// Resolve a PDS base URL to its authorization server metadata. #[cfg(not(target_arch = "wasm32"))] fn get_resource_server_metadata( &self, @@ -743,6 +787,7 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { get_resource_server_metadata_impl(self, pds) } + /// Resolve a PDS base URL to its authorization server metadata. #[cfg(target_arch = "wasm32")] fn get_resource_server_metadata( &self, @@ -752,6 +797,10 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { } } +/// Fetch and validate the `/.well-known/oauth-authorization-server` document for `server`. +/// +/// Per RFC 8414 §3.3 the `issuer` field in the response must equal the `server` URL exactly; +/// this prevents a compromised server from claiming to be a different issuer. pub async fn resolve_authorization_server( client: &T, server: &CowStr<'_>, @@ -772,9 +821,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 - // Accept semantically equivalent issuer (normalize to the requested URL form) if metadata.issuer == server.as_str() { - // if equivalent, keep the canonical form Ok(metadata.into_static()) } else { Err(ResolverError::authorization_server_metadata( @@ -786,6 +833,10 @@ pub async fn resolve_authorization_server( } } +/// Fetch the `/.well-known/oauth-protected-resource` document for `server`. +/// +/// The `resource` field in the response must equal the requested `server` URL, ensuring +/// 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<'_>, @@ -806,9 +857,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 - // Accept semantically equivalent resource URL (normalize to the requested URL form) if metadata.resource == server.as_str() { - // if equivalent, keep the canonical form Ok(metadata.into_static()) } else { Err(ResolverError::authorization_server_metadata( diff --git a/crates/jacquard-oauth/src/scopes.rs b/crates/jacquard-oauth/src/scopes.rs index 8eedc6c8..fd1b96da 100644 --- a/crates/jacquard-oauth/src/scopes.rs +++ b/crates/jacquard-oauth/src/scopes.rs @@ -1,5 +1,6 @@ //! AT Protocol OAuth scopes -//! Derived from +//! +//! Derived from //! //! This module provides comprehensive support for AT Protocol OAuth scopes, //! including parsing, serialization, normalization, and permission checking. @@ -1035,6 +1036,7 @@ pub enum ParseError { InvalidAction(String), /// Invalid MIME type InvalidMimeType(String), + /// An AT Protocol string type (DID, NSID, etc.) failed validation during scope parsing. ParseError(#[from] AtStrError), } diff --git a/crates/jacquard-oauth/src/session.rs b/crates/jacquard-oauth/src/session.rs index fb277347..fd3b2e5c 100644 --- a/crates/jacquard-oauth/src/session.rs +++ b/crates/jacquard-oauth/src/session.rs @@ -26,43 +26,60 @@ use serde::{Deserialize, Serialize}; use smol_str::{SmolStr, format_smolstr}; use tokio::sync::Mutex; +/// Provides DPoP key material and per-server nonces to the DPoP proof-building machinery. +/// +/// This trait abstracts over two different holders of DPoP state: [`DpopReqData`] (used +/// during the initial authorization request, where only an authserver nonce is tracked) and +/// [`DpopClientData`] (used in active sessions, where both authserver and host nonces are +/// maintained). Implementors must store nonces durably so that the next request to the same +/// server includes the most recently observed nonce. 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>; + /// Persist a new nonce received from the authorization server. fn set_authserver_nonce(&mut self, nonce: CowStr<'_>); + /// Return the most recently observed nonce from the resource server (PDS), if any. fn host_nonce(&self) -> Option>; + /// Persist a new nonce received from the resource server (PDS). fn set_host_nonce(&mut self, nonce: CowStr<'_>); } /// Persisted information about an OAuth session. Used to resume an active session. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ClientSessionData<'s> { - // Account DID for this session. Assuming only one active session per account, this can be used as "primary key" for storing and retrieving this information. + /// 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>, - // Identifier to distinguish this particular session for the account. Server backends generally support multiple sessions for the same account. This package will re-use the random 'state' token from the auth flow as the session ID. + /// 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>, - // Base URL of the "resource server" (eg, PDS). Should include scheme, hostname, port; no path or auth info. + /// Base URL of the resource server (PDS): scheme, host, and port only pub host_url: Uri, - // Base URL of the "auth server" (eg, PDS or entryway). Should include scheme, hostname, port; no path or auth info. + /// Base URL of the authorization server (PDS or entryway): scheme, host, and port only pub authserver_url: CowStr<'s>, - // Full token endpoint + /// Full URL of the authorization server's token endpoint. pub authserver_token_endpoint: CowStr<'s>, - // Full revocation endpoint, if it exists + /// 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>, - // The set of scopes approved for this session (returned in the initial token request) + /// The set of OAuth scopes approved for this session, as returned in the initial token response. pub scopes: Vec>, + /// DPoP key and nonce state for ongoing requests in this session. #[serde(flatten)] pub dpop_data: DpopClientData<'s>, + /// Current token set (access token, refresh token, expiry, etc.). #[serde(flatten)] pub token_set: TokenSet<'s>, } @@ -88,6 +105,10 @@ impl IntoStatic for 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<'_>) { if let Some(Ok(scopes)) = token_set .scope @@ -100,13 +121,18 @@ impl ClientSessionData<'_> { } } +/// DPoP state for an active OAuth session, persisted alongside the token set. +/// +/// 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> { + /// The private JWK bound to this session; used to sign all DPoP proofs. pub dpop_key: Key, - // Current auth server DPoP nonce + /// Most recently observed DPoP nonce from the authorization server. #[serde(borrow)] pub dpop_authserver_nonce: CowStr<'s>, - // Current host ("resource server", eg PDS) DPoP nonce + /// Most recently observed DPoP nonce from the resource server (PDS). pub dpop_host_nonce: CowStr<'s>, } @@ -143,35 +169,44 @@ impl DpopDataSource for DpopClientData<'_> { } } +/// Transient state created during the PAR flow and consumed by the callback handler. +/// +/// This struct is persisted to the auth store between [`crate::request::par`] and +/// [`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> { - // The random identifier generated by the client for the auth request flow. Can be used as "primary key" for storing and retrieving this information. + /// 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>, - // URL of the auth server (eg, PDS or entryway) + /// Base URL of the authorization server that was selected for this flow. pub authserver_url: CowStr<'s>, - // If the flow started with an account identifier (DID or handle), it should be persisted, to verify against the initial token response. + /// 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>, - // OAuth scope strings + /// OAuth scopes requested for this authorization. pub scopes: Vec>, - // unique token in URI format, which will be used by the client in the auth flow redirect + /// The PAR `request_uri` returned by the authorization server; included in the redirect URL. pub request_uri: CowStr<'s>, - // Full token endpoint URL + /// Full URL of the authorization server's token endpoint. pub authserver_token_endpoint: CowStr<'s>, - // Full revocation endpoint, if it exists + /// 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>, - // The secret token/nonce which a code challenge was generated from + /// 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>, + /// DPoP key and any authserver nonce observed during the PAR request. #[serde(flatten)] pub dpop_data: DpopReqData<'s>, } @@ -195,11 +230,15 @@ impl IntoStatic for AuthRequestData<'_> { } } +/// DPoP state for an in-progress authorization request (PAR through code exchange). +/// +/// 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> { - // The secret cryptographic key generated by the client for this specific OAuth session + /// The private JWK generated fresh for this authorization request and session. pub dpop_key: Key, - // Server-provided DPoP nonce from auth request (PAR) + /// DPoP nonce received from the authorization server during the PAR exchange, if any. #[serde(borrow)] pub dpop_authserver_nonce: Option>, } @@ -233,9 +272,16 @@ impl DpopDataSource for DpopReqData<'_> { fn set_host_nonce(&mut self, _nonce: CowStr<'_>) {} } +/// Static configuration for an OAuth client: the signing keyset and registered client metadata. +/// +/// `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> { + /// 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>, } @@ -250,10 +296,15 @@ impl<'s> IntoStatic for ClientData<'s> { } impl<'s> ClientData<'s> { + /// Create `ClientData` with an optional signing keyset and the given client metadata. pub fn new(keyset: Option, config: AtprotoClientMetadata<'s>) -> Self { Self { keyset, config } } + /// Create `ClientData` without a signing keyset, relying on the `none` auth method. + /// + /// 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 { Self { keyset: None, @@ -262,13 +313,22 @@ impl<'s> ClientData<'s> { } } +/// A bundle of client configuration and an active session, used for operations that need both. +/// +/// `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> { + /// Optional signing keyset, forwarded from [`ClientData`]. pub keyset: Option, + /// Client registration metadata, forwarded from [`ClientData`]. pub config: AtprotoClientMetadata<'s>, + /// The session state for the authenticated account. pub session_data: ClientSessionData<'s>, } impl<'s> ClientSession<'s> { + /// Construct a `ClientSession` from a [`ClientData`] and an active session. pub fn new( ClientData { keyset, config }: ClientData<'s>, session_data: ClientSessionData<'s>, @@ -280,6 +340,7 @@ impl<'s> ClientSession<'s> { } } + /// Fetch and assemble an [`OAuthMetadata`] for the authorization server of this session. pub async fn metadata( &self, client: &T, @@ -297,18 +358,24 @@ impl<'s> ClientSession<'s> { } } +/// Errors that can occur during OAuth session management. #[derive(thiserror::Error, Debug, miette::Diagnostic)] #[non_exhaustive] pub enum Error { + /// A token-endpoint or metadata operation failed. #[error(transparent)] #[diagnostic(code(jacquard_oauth::session::request))] ServerAgent(#[from] crate::request::RequestError), + /// The backing session store returned an error. #[error(transparent)] #[diagnostic(code(jacquard_oauth::session::storage))] Store(#[from] SessionStoreError), + /// The requested session does not exist in the store. #[error("session does not exist")] #[diagnostic(code(jacquard_oauth::session::not_found))] SessionNotFound, + /// Token refresh failed with a permanent error (e.g., `invalid_grant`); the session + /// has already been removed from the store and the user must re-authenticate. #[error("session refresh failed permanently")] #[diagnostic( code(jacquard_oauth::session::refresh_failed), @@ -330,14 +397,24 @@ impl Error { } } +/// Central coordinator for OAuth session storage and token refresh. +/// +/// `SessionRegistry` wraps the [`ClientAuthStore`] and provides serialized token refresh: +/// 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 where T: OAuthResolver, S: ClientAuthStore, { + /// 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>, + /// Per-`(DID, session_id)` mutex that serializes concurrent refresh attempts. pending: DashMap>>, } @@ -346,6 +423,7 @@ where S: ClientAuthStore, T: OAuthResolver, { + /// Create a new registry, taking ownership of the store. pub fn new(store: S, client: Arc, client_data: ClientData<'static>) -> Self { let store = Arc::new(store); Self { @@ -356,6 +434,10 @@ where } } + /// Create a new registry from an already-`Arc`-wrapped store. + /// + /// 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 { Self { store, @@ -419,6 +501,11 @@ where Err(e) => Err(Error::ServerAgent(e)), } } + /// Retrieve a session from the store, optionally refreshing it first. + /// + /// 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( &self, did: &Did<'_>, @@ -435,10 +522,12 @@ where .ok_or(Error::SessionNotFound) } } + /// Persist an updated session to the backing store. 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> { 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 a665abec..6c85a681 100644 --- a/crates/jacquard-oauth/src/types.rs +++ b/crates/jacquard-oauth/src/types.rs @@ -16,11 +16,19 @@ use jacquard_common::IntoStatic; use jacquard_common::deps::fluent_uri::Uri; use serde::Deserialize; +/// The `prompt` parameter for an OAuth authorization request. +/// +/// Controls whether the authorization server prompts the user for +/// re-authentication or re-consent, as defined in OpenID Connect Core §3.1.2.1. #[derive(Debug, Deserialize, Clone, Copy)] pub enum AuthorizeOptionPrompt { + /// Prompt the user to re-authenticate. Login, + /// Do not display any authentication or consent UI; fail if interaction is required. None, + /// Prompt the user for explicit consent before issuing tokens. Consent, + /// Prompt the user to select an account when multiple sessions are active. SelectAccount, } @@ -35,11 +43,16 @@ impl From for CowStr<'static> { } } +/// Options for initiating an OAuth authorization request. #[derive(Debug)] pub struct AuthorizeOptions<'s> { + /// 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>, + /// 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>, } @@ -55,32 +68,41 @@ impl Default for AuthorizeOptions<'_> { } impl<'s> AuthorizeOptions<'s> { + /// Set the `prompt` parameter sent to the authorization server. pub fn with_prompt(mut self, prompt: AuthorizeOptionPrompt) -> Self { self.prompt = Some(prompt); self } + /// Set a CSRF-protection `state` value to be echoed in the callback. pub fn with_state(mut self, state: CowStr<'s>) -> Self { self.state = Some(state); self } + /// Override the redirect URI for this specific authorization request. pub fn with_redirect_uri(mut self, redirect_uri: Uri) -> Self { self.redirect_uri = Some(redirect_uri); self } + /// Set the OAuth scopes to request. pub fn with_scopes(mut self, scopes: Vec>) -> Self { self.scopes = scopes; self } } +/// Query parameters delivered to the OAuth redirect URI after user authorization. #[derive(Debug, Deserialize)] pub struct CallbackParams<'s> { + /// The authorization code issued by the authorization server. #[serde(borrow)] pub code: CowStr<'s>, + /// The `state` value originally sent in the authorization request, used to + /// verify the response belongs to this session. pub state: Option>, + /// The `iss` (issuer) parameter, required by RFC 9207 to prevent mix-up attacks. pub iss: Option>, } diff --git a/crates/jacquard-oauth/src/types/client_metadata.rs b/crates/jacquard-oauth/src/types/client_metadata.rs index fd3640b1..b3965390 100644 --- a/crates/jacquard-oauth/src/types/client_metadata.rs +++ b/crates/jacquard-oauth/src/types/client_metadata.rs @@ -3,39 +3,65 @@ 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. +/// +/// In ATProto's OAuth profile, clients are identified by a URL that serves this +/// metadata document. Fields follow RFC 7591 (Dynamic Client Registration), +/// RFC 9449 (DPoP), and OpenID Connect Registration. +/// +/// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct OAuthClientMetadata<'c> { + /// The client identifier, typically a URL pointing to this metadata document. pub client_id: CowStr<'c>, + /// URL of the client's home page, used for display purposes. #[serde(skip_serializing_if = "Option::is_none")] pub client_uri: Option>, + /// List of redirect URIs the authorization server may send callbacks to. 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>, + /// Application type (`web` or `native`), used to enforce redirect URI constraints. #[serde(skip_serializing_if = "Option::is_none")] pub application_type: Option>, + /// OAuth 2.0 grant types the client will use. #[serde(skip_serializing_if = "Option::is_none")] 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>, + /// Response types the client will use in authorization requests. pub response_types: Vec>, - // https://datatracker.ietf.org/doc/html/rfc9449#section-5.2 + /// If `true`, the client requires DPoP-bound access tokens (RFC 9449 §5.2). + /// + /// #[serde(skip_serializing_if = "Option::is_none")] pub dpop_bound_access_tokens: Option, - // https://datatracker.ietf.org/doc/html/rfc7591#section-2 + /// URL of the client's JWK Set document for verifying signed requests (RFC 7591 §2). + /// + /// #[serde(skip_serializing_if = "Option::is_none")] 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, - // https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata + /// JWS algorithm the client uses to sign token endpoint authentication assertions. + /// + /// #[serde(skip_serializing_if = "Option::is_none")] 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, + /// URL of the client's logo image. #[serde(skip_serializing_if = "Option::is_none")] pub logo_uri: Option>, + /// URL of the client's terms of service. #[serde(skip_serializing_if = "Option::is_none")] pub tos_uri: Option>, + /// URL of the client's privacy policy. #[serde(skip_serializing_if = "Option::is_none")] pub privacy_policy_uri: Option>, } diff --git a/crates/jacquard-oauth/src/types/metadata.rs b/crates/jacquard-oauth/src/types/metadata.rs index 84348446..0530814c 100644 --- a/crates/jacquard-oauth/src/types/metadata.rs +++ b/crates/jacquard-oauth/src/types/metadata.rs @@ -1,67 +1,122 @@ use jacquard_common::{CowStr, IntoStatic, types::string::Language}; use serde::{Deserialize, Serialize}; +/// Authorization server metadata, as returned from the +/// `.well-known/oauth-authorization-server` discovery document. +/// +/// 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> { - // https://datatracker.ietf.org/doc/html/rfc8414#section-2 + /// The issuer identifier URL of the authorization server. + /// + /// #[serde(borrow)] pub issuer: CowStr<'s>, + /// The URL of the authorization endpoint. pub authorization_endpoint: CowStr<'s>, // optional? - pub token_endpoint: CowStr<'s>, // optional? + /// The URL of the token endpoint. + pub token_endpoint: CowStr<'s>, // optional? + /// URL of the authorization server's JWK Set document. pub jwks_uri: Option>, + /// URL of the dynamic client registration endpoint, if supported. pub registration_endpoint: Option>, + /// List of OAuth 2.0 scope values the server supports. pub scopes_supported: Vec>, + /// List of OAuth 2.0 response type values the server supports. pub response_types_supported: Vec>, + /// List of OAuth 2.0 response mode values the server supports. pub response_modes_supported: Option>>, + /// List of OAuth 2.0 grant type values the server supports. pub grant_types_supported: Option>>, + /// List of client authentication methods supported at the token endpoint. 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>>, + /// URL of a page with human-readable information about the server. 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>, + /// URL of the authorization server's terms of service. pub op_tos_uri: Option>, + /// URL of the token revocation endpoint (RFC 7009). pub revocation_endpoint: Option>, + /// List of client authentication methods supported at the revocation endpoint. 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>>, + /// URL of the token introspection endpoint (RFC 7662). pub introspection_endpoint: Option>, + /// List of client authentication methods supported at the introspection endpoint. 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>>, + /// PKCE code challenge methods supported by the server. pub code_challenge_methods_supported: Option>>, - // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + /// Subject identifier types supported (`public` or `pairwise`). + /// + /// pub subject_types_supported: Option>>, + /// If `true`, clients must pre-register `request_uri` values. pub require_request_uri_registration: Option, - // https://datatracker.ietf.org/doc/html/rfc9126#section-5 + /// URL of the Pushed Authorization Request (PAR) endpoint (RFC 9126). + /// + /// pub pushed_authorization_request_endpoint: Option>, + /// If `true`, all authorization requests must use PAR. pub require_pushed_authorization_requests: Option, - // https://datatracker.ietf.org/doc/html/rfc9207#section-3 + /// If `true`, the server includes `iss` in authorization responses to prevent mix-up attacks. + /// + /// pub authorization_response_iss_parameter_supported: Option, - // https://datatracker.ietf.org/doc/html/rfc9449#section-5.1 + /// DPoP JWS signing algorithms supported by this server (RFC 9449). + /// + /// pub dpop_signing_alg_values_supported: Option>>, - // https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html#section-5 + /// If `true`, the server supports the ATProto client ID metadata document extension. + /// + /// pub client_id_metadata_document_supported: Option, - // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada + /// Protected resources associated with this authorization server. + /// + /// pub protected_resources: Option>>, } -// https://datatracker.ietf.org/doc/draft-ietf-oauth-resource-metadata/ -// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#section-2 +/// Protected resource metadata, returned from `.well-known/oauth-protected-resource`. +/// +/// 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> { + /// The URL of the protected resource itself. #[serde(borrow)] pub resource: CowStr<'s>, + /// URLs of authorization servers that can issue tokens for this resource. pub authorization_servers: Option>>, + /// URL of the resource server's JWK Set document. pub jwks_uri: Option>, + /// List of OAuth 2.0 scope values the resource server supports. pub scopes_supported: Vec>, + /// Bearer token presentation methods supported (`header`, `body`, `query`). pub bearer_methods_supported: Option>>, + /// JWS signing algorithms supported for resource-bound tokens. pub resource_signing_alg_values_supported: Option>>, + /// URL of a page with human-readable information about the resource. pub resource_documentation: Option>, + /// URL of the resource server's privacy policy. pub resource_policy_uri: Option>, + /// URL of the resource server's terms of service. pub resource_tos_uri: Option>, } diff --git a/crates/jacquard-oauth/src/types/request.rs b/crates/jacquard-oauth/src/types/request.rs index 69e98ab5..7ce02008 100644 --- a/crates/jacquard-oauth/src/types/request.rs +++ b/crates/jacquard-oauth/src/types/request.rs @@ -1,79 +1,137 @@ use jacquard_common::{CowStr, IntoStatic}; use serde::{Deserialize, Serialize}; +/// The `response_type` parameter for an OAuth 2.0 authorization request. +/// +/// Determines what the authorization server returns in the redirect response. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub enum AuthorizationResponseType { + /// Authorization code flow — server returns a short-lived code for token exchange. Code, + /// Implicit flow — server returns an access token directly (not recommended for new clients). Token, - // OIDC (https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) + /// OpenID Connect ID token response (see the + /// [multiple response types spec](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)). IdToken, } +/// The `response_mode` parameter controlling how the authorization response is returned. +/// +/// Defaults to `query` for `code` response type and `fragment` for `token`. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub enum AuthorizationResponseMode { + /// Parameters are appended as query string components to the redirect URI. Query, + /// Parameters are appended as URI fragment components to the redirect URI. Fragment, - // https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html#FormPostResponseMode + /// Parameters are encoded in an HTML form POSTed to the redirect URI. + /// + /// FormPost, } +/// PKCE code challenge method, as defined in RFC 7636. +/// +/// `S256` is strongly preferred; `Plain` should only be used when the client +/// cannot perform SHA-256. #[derive(Serialize, Deserialize, Debug)] pub enum AuthorizationCodeChallengeMethod { + /// SHA-256 hash of the code verifier, base64url-encoded (recommended). S256, + /// Raw code verifier used as the challenge (not recommended). #[serde(rename = "plain")] Plain, } +/// Parameters for a Pushed Authorization Request (PAR), as defined in RFC 9126. +/// +/// PAR allows clients to push their authorization parameters directly to the +/// authorization server before redirecting the user, improving security by keeping +/// parameters out of the browser URL. #[derive(Serialize, Deserialize, Debug)] pub struct ParParameters<'a> { - // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1 + /// 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>, + /// An opaque CSRF state value to be echoed back in the callback. pub state: CowStr<'a>, + /// Space-separated list of requested scopes. pub scope: Option>, - // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes + /// How the authorization response parameters are delivered to the client. + /// + /// pub response_mode: Option, - // https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 + /// The PKCE code challenge derived from the code verifier. + /// + /// pub code_challenge: CowStr<'a>, + /// The method used to derive the code challenge. pub code_challenge_method: AuthorizationCodeChallengeMethod, - // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + /// Hint to pre-fill the login form with a handle or email. + /// + /// pub login_hint: Option>, + /// Prompt hint controlling authorization server UI behavior. pub prompt: Option>, } +/// The `grant_type` parameter for a token endpoint request. #[derive(Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TokenGrantType { + /// Exchange an authorization code for tokens. AuthorizationCode, + /// Use a refresh token to obtain a new access token. RefreshToken, } +/// Parameters for exchanging an authorization code for tokens (RFC 6749 §4.1.3). #[derive(Serialize, Deserialize)] pub struct TokenRequestParameters<'a> { - // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 + /// 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>, + /// The redirect URI used in the original authorization request. pub redirect_uri: CowStr<'a>, - // https://datatracker.ietf.org/doc/html/rfc7636#section-4.5 + /// The PKCE code verifier that was used to generate the code challenge (RFC 7636 §4.5). + /// + /// pub code_verifier: CowStr<'a>, } +/// Parameters for refreshing an access token using a refresh token (RFC 6749 §6). #[derive(Serialize, Deserialize)] pub struct RefreshRequestParameters<'a> { - // https://datatracker.ietf.org/doc/html/rfc6749#section-6 + /// 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>, + /// Optional scope to request; must not exceed the originally granted scope. pub scope: Option>, } -// https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 +/// Parameters for a token revocation request (RFC 7009 §2.1). +/// +/// Sent to the revocation endpoint to invalidate an access or refresh token, +/// for example on logout. +/// +/// #[derive(Serialize, Deserialize)] pub struct RevocationRequestParameters<'a> { + /// The token to be revoked. #[serde(borrow)] pub token: CowStr<'a>, // ? diff --git a/crates/jacquard-oauth/src/types/response.rs b/crates/jacquard-oauth/src/types/response.rs index fe89e460..f58a8a4d 100644 --- a/crates/jacquard-oauth/src/types/response.rs +++ b/crates/jacquard-oauth/src/types/response.rs @@ -1,19 +1,29 @@ use serde::{Deserialize, Serialize}; use smol_str::SmolStr; +/// The response from a Pushed Authorization Request (PAR) endpoint. +/// +/// The returned `request_uri` is used in place of inline authorization parameters +/// when redirecting the user to the authorization server. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct OAuthParResponse { + /// A short-lived URI representing the pushed authorization request. pub request_uri: SmolStr, + /// Number of seconds until the `request_uri` expires. pub expires_in: Option, } +/// The token type returned by the authorization server, indicating how to present the token. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum OAuthTokenType { + /// Demonstration of Proof of Possession (DPoP) token (RFC 9449). Requires a DPoP proof header. DPoP, + /// Standard Bearer token (RFC 6750). Sent as `Authorization: Bearer `. Bearer, } impl OAuthTokenType { + /// Returns the string representation used in HTTP `Authorization` headers. pub fn as_str(&self) -> &'static str { match self { OAuthTokenType::DPoP => "DPoP", @@ -22,16 +32,23 @@ impl OAuthTokenType { } } -// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 +/// A successful token response from the authorization server (RFC 6749 §5.1). +/// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct OAuthTokenResponse { + /// The issued access token. pub access_token: SmolStr, + /// The type of token, indicating the presentation scheme to use. pub token_type: OAuthTokenType, + /// Lifetime of the access token in seconds from the time of issuance. pub expires_in: Option, + /// A refresh token that can be used to obtain new access tokens. pub refresh_token: Option, + /// The scopes actually granted, if different from those requested. pub scope: Option, // ATPROTO extension: add the sub claim to the token response to allow // clients to resolve the PDS url (audience) using the did resolution // mechanism. + /// The subject (DID) the token was issued for; ATProto extension for PDS discovery. pub sub: Option, } diff --git a/crates/jacquard-oauth/src/types/token.rs b/crates/jacquard-oauth/src/types/token.rs index 8a7146b2..1c4f165c 100644 --- a/crates/jacquard-oauth/src/types/token.rs +++ b/crates/jacquard-oauth/src/types/token.rs @@ -3,18 +3,31 @@ 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. +/// +/// Combines the token response with resolved identity claims to give the client +/// 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> { + /// The issuer URL of the authorization server that issued these tokens. #[serde(borrow)] pub iss: CowStr<'s>, + /// The subject DID identifying the authenticated user. pub sub: Did<'s>, + /// The audience (resource server URL or DID) the tokens are intended for. pub aud: CowStr<'s>, + /// The scopes granted by the authorization server. pub scope: Option>, + /// A refresh token that can be exchanged for new access tokens. pub refresh_token: Option>, + /// The current access token to include in API requests. pub access_token: CowStr<'s>, + /// Whether the access token must be presented as a DPoP or Bearer token. pub token_type: OAuthTokenType, + /// The point in time at which the access token expires. pub expires_at: Option, } diff --git a/crates/jacquard-oauth/src/utils.rs b/crates/jacquard-oauth/src/utils.rs index 7c3584aa..4456b6fd 100644 --- a/crates/jacquard-oauth/src/utils.rs +++ b/crates/jacquard-oauth/src/utils.rs @@ -9,6 +9,10 @@ use std::cmp::Ordering; use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata}; +/// Generate a fresh JWK secret key using the first algorithm from `allowed_algos` that is +/// 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 { for alg in allowed_algos { #[allow(clippy::single_match)] @@ -26,18 +30,21 @@ pub fn generate_key(allowed_algos: &[CowStr]) -> Option { None } +/// Generate a cryptographically random 16-byte nonce encoded as base64url (no padding). pub fn generate_nonce() -> CowStr<'static> { 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> { URL_SAFE_NO_PAD .encode(get_random_values::<_, 43>(&mut ThreadRng::default())) .into() } +/// Fill a `LEN`-byte array with cryptographically random bytes from `rng`. pub fn get_random_values(rng: &mut R) -> [u8; LEN] where R: RngCore + CryptoRng, @@ -47,7 +54,10 @@ where bytes } -// 256K > ES (256 > 384 > 512) > PS (256 > 384 > 512) > RS (256 > 384 > 512) > other (in original order) +/// Compare two algorithm identifier strings by preference order for DPoP key generation. +/// +/// 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" { return Ordering::Less; @@ -73,6 +83,12 @@ pub fn compare_algos(a: &CowStr, b: &CowStr) -> Ordering { Ordering::Equal } +/// Generate a PKCE challenge/verifier pair. +/// +/// Returns `(challenge, verifier)` where `challenge` is the base64url-encoded SHA-256 hash +/// 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>) { // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 let verifier = generate_verifier(); @@ -84,6 +100,11 @@ pub fn generate_pkce() -> (CowStr<'static>, CowStr<'static>) { ) } +/// Generate a DPoP signing key compatible with the algorithms advertised by the authorization server. +/// +/// 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 .dpop_signing_alg_values_supported diff --git a/crates/jacquard/src/lib.rs b/crates/jacquard/src/lib.rs index 3ec3e02a..b7c1fb06 100644 --- a/crates/jacquard/src/lib.rs +++ b/crates/jacquard/src/lib.rs @@ -224,7 +224,7 @@ pub mod client; #[cfg(feature = "streaming")] -/// Experimental streaming endpoints +/// Streaming endpoints pub mod streaming; #[cfg(feature = "api_bluesky")] @@ -247,6 +247,7 @@ pub use jacquard_oauth as oauth; /// Prelude with the extension traits you're likely to want and some other stuff pub mod prelude { + pub use crate::client::Agent; pub use crate::client::AgentSession; #[cfg(feature = "api")] pub use crate::client::AgentSessionExt; @@ -254,7 +255,7 @@ pub mod prelude { pub use crate::common::http_client::HttpClient; pub use crate::common::xrpc::XrpcClient; pub use crate::common::xrpc::XrpcExt; - pub use crate::identity::PublicResolver; + pub use crate::identity::JacquardResolver; pub use crate::identity::resolver::IdentityResolver; pub use crate::oauth::dpop::DpopExt; pub use crate::oauth::resolver::OAuthResolver; -- 2.51.2