From 2e39f1e70cb7d9c57d5c9eef6334b82830d4e14c Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 26 Nov 2025 12:44:49 -0600 Subject: [PATCH] Feature: Captcha and Migrations only blocker Adds two new features to allow PDS admins to gatekeep their PDS from bots creating accounts instead of invite codes --- Cargo.lock | 1518 ++++++++++++++++++++-- Cargo.toml | 9 +- README.md | 172 ++- examples/Caddyfile | 44 +- examples/coolify-compose.yml | 2 +- html_templates/captcha.hbs | 166 +++ justfile | 2 +- migrations/20251126000000_gate_codes.sql | 10 + src/gate.rs | 247 ++++ src/helpers.rs | 188 ++- src/main.rs | 186 ++- src/oauth_provider.rs | 2 +- src/xrpc/com_atproto_server.rs | 317 ++++- 13 files changed, 2614 insertions(+), 249 deletions(-) create mode 100644 html_templates/captcha.hbs create mode 100644 migrations/20251126000000_gate_codes.sql create mode 100644 src/gate.rs diff --git a/Cargo.lock b/Cargo.lock index 7c5adbd..78826d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,25 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "abnf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" +dependencies = [ + "abnf-core", + "nom 7.1.3", +] + +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -39,16 +58,16 @@ dependencies = [ ] [[package]] -name = "allocator-api2" -version = "0.2.21" +name = "aliasable" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" @@ -71,6 +90,7 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" dependencies = [ + "flate2", "futures-core", "memchr", "pin-project-lite", @@ -87,7 +107,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -198,7 +218,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -228,6 +248,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + [[package]] name = "base64" version = "0.22.1" @@ -257,9 +299,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", - "syn", + "syn 2.0.105", "which", ] @@ -281,6 +323,40 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +dependencies = [ + "darling 0.21.3", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.105", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "cfg_aliases", +] + [[package]] name = "bstr" version = "1.12.0" @@ -291,6 +367,30 @@ dependencies = [ "serde", ] +[[package]] +name = "btree-range-map" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" +dependencies = [ + "btree-slab", + "cc-traits", + "range-traits", + "serde", + "slab", +] + +[[package]] +name = "btree-slab" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" +dependencies = [ + "cc-traits", + "slab", + "smallvec", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -308,6 +408,18 @@ name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "cbor4ii" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" +dependencies = [ + "serde", +] [[package]] name = "cc" @@ -320,6 +432,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cc-traits" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" +dependencies = [ + "slab", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -335,18 +456,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -386,6 +513,20 @@ dependencies = [ "half", ] +[[package]] +name = "cid" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" +dependencies = [ + "core2", + "multibase", + "multihash", + "serde", + "serde_bytes", + "unsigned-varint", +] + [[package]] name = "cipher" version = "0.4.4" @@ -431,12 +572,37 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -461,6 +627,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -482,6 +657,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -498,8 +685,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -513,7 +710,21 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.105", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.105", ] [[package]] @@ -522,9 +733,20 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -541,6 +763,32 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn 2.0.105", +] + [[package]] name = "der" version = "0.7.10" @@ -552,6 +800,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -567,10 +825,10 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -580,7 +838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.105", ] [[package]] @@ -603,7 +861,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -618,6 +876,26 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" version = "1.15.0" @@ -627,6 +905,26 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email-encoding" version = "0.4.1" @@ -643,6 +941,15 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -687,6 +994,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.1" @@ -710,6 +1037,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -779,6 +1121,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -805,6 +1158,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -821,6 +1175,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -830,21 +1185,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -869,7 +1226,7 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", + "regex-automata 0.4.13", "regex-syntax 0.8.5", ] @@ -884,7 +1241,7 @@ dependencies = [ "futures-sink", "futures-timer", "futures-util", - "getrandom 0.3.3", + "getrandom 0.3.4", "hashbrown 0.15.5", "nonzero_ext", "parking_lot", @@ -896,6 +1253,17 @@ dependencies = [ "web-time", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.12" @@ -908,7 +1276,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -942,6 +1310,12 @@ dependencies = [ "thiserror 2.0.14", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -974,9 +1348,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.5.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" @@ -984,6 +1364,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + [[package]] name = "hkdf" version = "0.12.4" @@ -1011,6 +1397,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "1.3.1" @@ -1078,6 +1473,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.2", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1097,6 +1509,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64", "bytes", "futures-channel", "futures-core", @@ -1104,12 +1517,16 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1249,6 +1666,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.10.0" @@ -1257,6 +1685,16 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.5", + "serde", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", ] [[package]] @@ -1268,6 +1706,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "io-uring" version = "0.7.9" @@ -1279,6 +1726,33 @@ dependencies = [ "libc", ] +[[package]] +name = "ipld-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" +dependencies = [ + "cid", + "serde", + "serde_bytes", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1294,16 +1768,156 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jacquard-api" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbfd6e2b10fa1731f4d4e40c8f791956b0d4f804fb3efef891afec903f20597" +dependencies = [ + "bon", + "bytes", + "jacquard-common", + "jacquard-derive", + "jacquard-lexicon", + "miette", + "rustversion", + "serde", + "serde_ipld_dagcbor", + "thiserror 2.0.14", + "unicode-segmentation", +] + +[[package]] +name = "jacquard-common" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df86cb117d9f1c2b0251ba67c3f0e3f963fd22abc6cf8de0e02a7fc846c288ca" +dependencies = [ + "base64", + "bon", + "bytes", + "chrono", + "cid", + "getrandom 0.2.16", + "getrandom 0.3.4", + "http", + "ipld-core", + "k256", + "langtag", + "miette", + "multibase", + "multihash", + "ouroboros", + "p256", + "rand 0.9.2", + "regex", + "regex-lite", + "reqwest", + "serde", + "serde_html_form", + "serde_ipld_dagcbor", + "serde_json", + "signature", + "smol_str", + "thiserror 2.0.14", + "tokio", + "tokio-util", + "trait-variant", + "url", +] + +[[package]] +name = "jacquard-derive" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ca61a69dc7aa8fb2d7163416514ff7df5d79f2e8b22e269f4610afa85572fe" +dependencies = [ + "heck 0.5.0", + "jacquard-lexicon", + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "jacquard-identity" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef714cacebfca486558a9f8e205daf466bfba0466c4d0c450fd6d0252400a53" +dependencies = [ + "bon", + "bytes", + "http", + "jacquard-api", + "jacquard-common", + "jacquard-lexicon", + "miette", + "percent-encoding", + "reqwest", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.14", + "tokio", + "trait-variant", + "url", + "urlencoding", +] + +[[package]] +name = "jacquard-lexicon" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de87f2c938faea1b1f1b32d5b9e0c870e7b5bb5efbf96e3692ae2d8f6b2beb7a" +dependencies = [ + "cid", + "dashmap", + "heck 0.5.0", + "inventory", + "jacquard-common", + "miette", + "multihash", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "serde_ipld_dagcbor", + "serde_json", + "serde_repr", + "serde_with", + "sha2", + "syn 2.0.105", + "thiserror 2.0.14", + "unicode-segmentation", +] + [[package]] name = "jobserver" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] +[[package]] +name = "josekit" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c" +dependencies = [ + "anyhow", + "base64", + "flate2", + "openssl", + "regex", + "serde", + "serde_json", + "thiserror 2.0.14", + "time", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1336,6 +1950,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "langtag" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600" +dependencies = [ + "serde", + "static-regular-grammar", + "thiserror 1.0.69", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1451,6 +2088,23 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "match-lookup" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1482,6 +2136,28 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "mime" version = "0.3.17" @@ -1501,6 +2177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1510,10 +2187,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.59.0", ] +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +dependencies = [ + "core2", + "serde", + "unsigned-varint", +] + [[package]] name = "nom" version = "7.1.3" @@ -1572,6 +2272,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1632,12 +2338,86 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.105", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.1" @@ -1700,10 +2480,16 @@ dependencies = [ "dotenvy", "handlebars", "hex", + "html-escape", "hyper-util", + "jacquard-common", + "jacquard-identity", + "josekit", "jwt-compact", "lettre", + "multibase", "rand 0.9.2", + "reqwest", "rust-embed", "rustls", "scrypt", @@ -1716,6 +2502,7 @@ dependencies = [ "tower_governor", "tracing", "tracing-subscriber", + "urlencoding", ] [[package]] @@ -1764,7 +2551,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1794,7 +2581,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -1851,6 +2638,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1867,7 +2660,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.105", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -1879,6 +2705,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", + "version_check", + "yansi", +] + [[package]] name = "psm" version = "0.1.26" @@ -1898,11 +2737,66 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.14", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.14", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -1980,9 +2874,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] +[[package]] +name = "range-traits" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" + [[package]] name = "raw-cpuid" version = "11.5.0" @@ -2001,15 +2901,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.13", "regex-syntax 0.8.5", ] @@ -2023,28 +2943,89 @@ dependencies = [ ] [[package]] -name = "regex-automata" -version = "0.4.9" +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-lite" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "async-compression", + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.2", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.5", + "hmac", + "subtle", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "ring" version = "0.17.14" @@ -2099,7 +3080,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 2.0.105", "walkdir", ] @@ -2126,6 +3107,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -2161,6 +3148,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] @@ -2206,6 +3194,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2224,6 +3236,20 @@ dependencies = [ "sha2", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "secp256k1" version = "0.28.2" @@ -2244,34 +3270,81 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", +] + +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap 2.10.0", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "serde_ipld_dagcbor" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778" +dependencies = [ + "cbor4ii", + "ipld-core", + "scopeguard", + "serde", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ + "indexmap 2.10.0", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -2284,6 +3357,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2296,6 +3380,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2352,6 +3467,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.11" @@ -2367,6 +3488,16 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5" +dependencies = [ + "borsh", + "serde_core", +] + [[package]] name = "socket2" version = "0.6.0" @@ -2437,7 +3568,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap", + "indexmap 2.10.0", "log", "memchr", "once_cell", @@ -2465,7 +3596,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.105", ] [[package]] @@ -2476,7 +3607,7 @@ checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -2488,7 +3619,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.105", "tokio", "url", ] @@ -2618,6 +3749,32 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "static-regular-grammar" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" +dependencies = [ + "abnf", + "btree-range-map", + "ciborium", + "hex_fmt", + "indoc", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "sha2", + "syn 2.0.105", + "thiserror 1.0.69", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.5" @@ -2641,6 +3798,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.105" @@ -2657,6 +3825,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2666,7 +3837,28 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -2695,7 +3887,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -2706,7 +3898,7 @@ checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -2718,6 +3910,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -2770,7 +3993,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -2796,9 +4019,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -2844,7 +4067,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.10.0", "pin-project-lite", "slab", "sync_wrapper", @@ -2865,11 +4088,14 @@ dependencies = [ "bitflags", "bytes", "futures-core", + "futures-util", "http", "http-body", + "iri-string", "pin-project-lite", "tokio", "tokio-util", + "tower", "tower-layer", "tower-service", ] @@ -2923,7 +4149,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -2965,6 +4191,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -3010,6 +4247,24 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + [[package]] name = "untrusted" version = "0.7.1" @@ -3031,8 +4286,21 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3083,12 +4351,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -3119,10 +4387,23 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.105", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -3141,7 +4422,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3155,6 +4436,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -3254,7 +4548,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -3267,7 +4561,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3278,7 +4572,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3287,13 +4581,30 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3302,7 +4613,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3454,13 +4765,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -3468,6 +4776,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.0" @@ -3488,7 +4802,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", "synstructure", ] @@ -3509,7 +4823,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3529,7 +4843,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", "synstructure", ] @@ -3550,7 +4864,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] @@ -3583,7 +4897,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.105", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 535e0e0..9c4e19f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,5 +28,12 @@ rust-embed = "8.7.2" axum-template = { version = "3.0.0", features = ["handlebars"] } rand = "0.9.2" anyhow = "1.0.99" -chrono = "0.4.41" +chrono = { version = "0.4.42", features = ["default", "serde"] } sha2 = "0.10" +jacquard-common = "0.9.2" +jacquard-identity = "0.9.2" +multibase = "0.9.2" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +urlencoding = "2.1" +html-escape = "0.2.13" +josekit = "0.10.3" diff --git a/README.md b/README.md index a2c965a..ca66790 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,36 @@ logic of these endpoints still happens on the PDS via a proxied request, just so - Overrides The login endpoint to add 2FA for both Bluesky client logged in and OAuth logins - Overrides the settings endpoints as well. As long as you have a confirmed email you can turn on 2FA -## Captcha on Create Account - -Future feature? +## Captcha on account creation + +Require a `verificationCode` set on the `createAccount` request. This is gotten from completing a captcha challenge +hosted on the +PDS mimicking what the Bluesky Entryway does. Migration tools will need to support this, but social-apps will support +and redirect to `GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT`. This is how the clients know to get the code to prove a captcha +was successful. + +- Requires `GATEKEEPER_CREATE_ACCOUNT_CAPTCHA` to be set to true. +- Requires `PDS_HCAPTCHA_SITE_KEY` and `PDS_HCAPTCHA_SECRET_KEY` to be set. Can sign up at https://www.hcaptcha.com/ +- Requires proxying `/xrpc/com.atproto.server.describeServer`, `/xrpc/com.atproto.server.createAccount` and `/gate/*` to + PDS + Gatekeeper +- Optional `GATEKEEPER_JWE_KEY` key to encrypt the captcha verification code. Defaults to a random 32 byte key. Not + strictly needed unless you're scaling +- Optional`GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT` default redirect on captcha success. Defaults to `https://bsky.app`. +- Optional `GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS` allowed redirect urls for captcha success. You want these to match the + url showing the captcha. Defaults are: + - https://bsky.app + - https://pdsmoover.com + - https://blacksky.community + - https://tektite.cc + +## Block account creation unless it's a migration + +You can set `GATEKEEPER_ALLOW_ONLY_MIGRATIONS` to block createAccount unless it's via a migration. This does not require +a change for migration tools, but social-apps create a new account will no longer work and to create a brand new account +users will need to do this via the Oauth account create screen on the PDS. We recommend setting `PDS_HCAPTCHA_SITE_KEY` +and `PDS_HCAPTCHA_SECRET_KEY` so the OAuth screen is protected by a captcha if you use this with invite codes turned +off. # Setup @@ -49,48 +76,49 @@ This is usually found at `/pds/compose.yaml`on your PDS> - pds ``` -For Coolify, if you're using Traefik as your proxy you'll need to make sure the labels for the container are set up correctly. A full example can be found at [./examples/coolify-compose.yml](./examples/coolify-compose.yml). +For Coolify, if you're using Traefik as your proxy you'll need to make sure the labels for the container are set up +correctly. A full example can be found at [./examples/coolify-compose.yml](./examples/coolify-compose.yml). ```yml gatekeeper: - container_name: gatekeeper - image: 'fatfingers23/pds_gatekeeper:latest' - restart: unless-stopped - volumes: - - '/pds:/pds' - environment: - - 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}' - - 'PDS_BASE_URL=http://pds:3000' - - GATEKEEPER_HOST=0.0.0.0 - depends_on: - - pds - healthcheck: - test: - - CMD - - timeout - - '1' - - bash - - '-c' - - 'cat < /dev/null > /dev/tcp/0.0.0.0/8080' - interval: 10s - timeout: 5s - retries: 3 - start_period: 10s - labels: - - traefik.enable=true - - 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`))' - - traefik.http.routers.pds-gatekeeper.entrypoints=https - - traefik.http.routers.pds-gatekeeper.tls=true - - traefik.http.routers.pds-gatekeeper.priority=100 - - traefik.http.routers.pds-gatekeeper.middlewares=gatekeeper-cors - - traefik.http.services.pds-gatekeeper.loadbalancer.server.port=8080 - - traefik.http.services.pds-gatekeeper.loadbalancer.server.scheme=http - - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS,PATCH' - - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowheaders=*' - - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolalloworiginlist=*' - - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolmaxage=100 - - traefik.http.middlewares.gatekeeper-cors.headers.addvaryheader=true - - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowcredentials=true + container_name: gatekeeper + image: 'fatfingers23/pds_gatekeeper:latest' + restart: unless-stopped + volumes: + - '/pds:/pds' + environment: + - 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}' + - 'PDS_BASE_URL=http://pds:3000' + - GATEKEEPER_HOST=0.0.0.0 + depends_on: + - pds + healthcheck: + test: + - CMD + - timeout + - '1' + - bash + - '-c' + - 'cat < /dev/null > /dev/tcp/0.0.0.0/8080' + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + labels: + - traefik.enable=true + - 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`))' + - traefik.http.routers.pds-gatekeeper.entrypoints=https + - traefik.http.routers.pds-gatekeeper.tls=true + - traefik.http.routers.pds-gatekeeper.priority=100 + - traefik.http.routers.pds-gatekeeper.middlewares=gatekeeper-cors + - traefik.http.services.pds-gatekeeper.loadbalancer.server.port=8080 + - traefik.http.services.pds-gatekeeper.loadbalancer.server.scheme=http + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS,PATCH' + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowheaders=*' + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolalloworiginlist=*' + - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolmaxage=100 + - traefik.http.middlewares.gatekeeper-cors.headers.addvaryheader=true + - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowcredentials=true ``` ## Caddy setup @@ -99,43 +127,46 @@ For the reverse proxy I use caddy. This part is what overwrites the endpoints an in extra functionality. The main part is below, for a full example see [./examples/Caddyfile](./examples/Caddyfile). This is usually found at `/pds/caddy/etc/caddy/Caddyfile` on your PDS. -```caddyfile +``` @gatekeeper { - path /xrpc/com.atproto.server.getSession - path /xrpc/com.atproto.server.updateEmail - path /xrpc/com.atproto.server.createSession - path /xrpc/com.atproto.server.createAccount - path /@atproto/oauth-provider/~api/sign-in + path /xrpc/com.atproto.server.getSession + path /xrpc/com.atproto.server.describeServer + path /xrpc/com.atproto.server.updateEmail + path /xrpc/com.atproto.server.createSession + path /xrpc/com.atproto.server.createAccount + path /@atproto/oauth-provider/~api/sign-in + path /gate/* } handle @gatekeeper { - reverse_proxy http://localhost:8080 - } + reverse_proxy http://localhost:8080 + } - reverse_proxy http://localhost:3000 + reverse_proxy http://localhost:3000 ``` If you use a cloudflare tunnel then your caddyfile would look a bit more like below with your tunnel proxying to `localhost:8081` (or w/e port you want). -```caddyfile +``` http://*.localhost:8082, http://localhost:8082 { - @gatekeeper { - path /xrpc/com.atproto.server.getSession - path /xrpc/com.atproto.server.updateEmail - path /xrpc/com.atproto.server.createSession - path /xrpc/com.atproto.server.createAccount - path /@atproto/oauth-provider/~api/sign-in - } - - handle @gatekeeper { - reverse_proxy http://localhost:8080 { - #Makes sure the cloudflare ip is proxied and able to be picked up by pds gatekeeper - header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} - } - } + @gatekeeper { + path /xrpc/com.atproto.server.getSession + path /xrpc/com.atproto.server.describeServer + path /xrpc/com.atproto.server.updateEmail + path /xrpc/com.atproto.server.createSession + path /xrpc/com.atproto.server.createAccount + path /@atproto/oauth-provider/~api/sign-in + path /gate/* + } - reverse_proxy http://localhost:3000 + handle @gatekeeper { + #This is the address for PDS gatekeeper, default is 8080 + reverse_proxy http://localhost:8080 + #Makes sure the cloudflare ip is proxied and able to be picked up by pds gatekeeper + header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} + } + reverse_proxy http://localhost:3000 } ``` @@ -168,4 +199,9 @@ limit of 5 and set to 60, then in 60 seconds you will be able to make one more. `GATEKEEPER_CREATE_ACCOUNT_BURST` - Sets how many requests can be made in a burst. In the prior example this is where the 5 comes from. Example can set this to 10 to allow for 10 requests in a burst, and after 60 seconds it will drop one -off. \ No newline at end of file +off. + +`GATEKEEPER_ALLOW_ONLY_MIGRATIONS` - Defaults false. If set to true, will only allow the +`/xrpc/com.atproto.server.createAccount` endpoint to be used for migrations. Meaning it will check for the serviceAuth +token and verify it is valid. + diff --git a/examples/Caddyfile b/examples/Caddyfile index 26b8fa3..e341521 100644 --- a/examples/Caddyfile +++ b/examples/Caddyfile @@ -1,30 +1,30 @@ { - email youremail@myemail.com - on_demand_tls { - ask http://localhost:3000/tls-check - } + email youremail@myemail.com + on_demand_tls { + ask http://localhost:3000/tls-check + } } *.yourpds.com, yourpds.com { - tls { - on_demand - } - # You'll most likely just want from here to.... - @gatekeeper { - path /xrpc/com.atproto.server.getSession - path /xrpc/com.atproto.server.updateEmail - path /xrpc/com.atproto.server.createSession - path /xrpc/com.atproto.server.createAccount - path /@atproto/oauth-provider/~api/sign-in + tls { + on_demand } +# You'll most likely just want from here to.... + @gatekeeper { + path /xrpc/com.atproto.server.getSession + path /xrpc/com.atproto.server.describeServer + path /xrpc/com.atproto.server.updateEmail + path /xrpc/com.atproto.server.createSession + path /xrpc/com.atproto.server.createAccount + path /@atproto/oauth-provider/~api/sign-in + path /gate/* + } - handle @gatekeeper { - #This is the address for PDS gatekeeper, default is 8080 - reverse_proxy http://localhost:8080 - } + handle @gatekeeper { + #This is the address for PDS gatekeeper, default is 8080 + reverse_proxy http://localhost:8080 + } - reverse_proxy http://localhost:3000 - #..here. Copy and paste this replacing the reverse_proxy http://localhost:3000 line + reverse_proxy http://localhost:3000 +#..here. Copy and paste this replacing the reverse_proxy http://localhost:3000 line } - - diff --git a/examples/coolify-compose.yml b/examples/coolify-compose.yml index 1ee1acf..67346d7 100644 --- a/examples/coolify-compose.yml +++ b/examples/coolify-compose.yml @@ -58,7 +58,7 @@ services: start_period: 10s labels: - traefik.enable=true - - 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`))' + - 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.describeServer`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`) || Path(`/gate`))' - traefik.http.routers.pds-gatekeeper.entrypoints=https - traefik.http.routers.pds-gatekeeper.tls=true - traefik.http.routers.pds-gatekeeper.priority=100 diff --git a/html_templates/captcha.hbs b/html_templates/captcha.hbs new file mode 100644 index 0000000..1f3897b --- /dev/null +++ b/html_templates/captcha.hbs @@ -0,0 +1,166 @@ + + + + + + + + {{pds}} - Captcha + + + + + + + + + +
+
{{pds}}
+
+
+ +
+ {{#if error_message }} +
{{error_message}}
+ {{/if}} + +
+ + diff --git a/justfile b/justfile index 55f6d86..2e59bc2 100644 --- a/justfile +++ b/justfile @@ -2,5 +2,5 @@ release: docker buildx build \ --platform linux/arm64,linux/amd64 \ --tag fatfingers23/pds_gatekeeper:latest \ - --tag fatfingers23/pds_gatekeeper:0.1.0.3 \ + --tag fatfingers23/pds_gatekeeper:0.1.0.5 \ --push . \ No newline at end of file diff --git a/migrations/20251126000000_gate_codes.sql b/migrations/20251126000000_gate_codes.sql new file mode 100644 index 0000000..579605d --- /dev/null +++ b/migrations/20251126000000_gate_codes.sql @@ -0,0 +1,10 @@ +-- Add migration script here +CREATE TABLE IF NOT EXISTS gate_codes +( + code VARCHAR PRIMARY KEY, + handle VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Index on created_at for efficient cleanup of expired codes +CREATE INDEX IF NOT EXISTS idx_gate_codes_created_at ON gate_codes(created_at); diff --git a/src/gate.rs b/src/gate.rs new file mode 100644 index 0000000..71ec69b --- /dev/null +++ b/src/gate.rs @@ -0,0 +1,247 @@ +use crate::AppState; +use crate::helpers::{generate_gate_token, json_error_response}; +use axum::Form; +use axum::extract::{Query, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Redirect, Response}; +use axum_template::RenderHtml; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::env; +use tracing::log; + +#[derive(Deserialize)] +pub struct GateQuery { + handle: String, + state: String, + #[serde(default)] + error: Option, + #[serde(default)] + redirect_url: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct CaptchaPage { + handle: String, + state: String, + captcha_site_key: String, + error_message: Option, + pds: String, + redirect_url: Option, +} + +#[derive(Deserialize)] +pub struct CaptchaForm { + #[serde(rename = "h-captcha-response")] + h_captcha_response: String, + #[serde(default)] + redirect_url: Option, +} + +/// GET /gate - Display the captcha page +pub async fn get_gate( + Query(params): Query, + State(state): State, +) -> impl IntoResponse { + let hcaptcha_site_key = match env::var("PDS_HCAPTCHA_SITE_KEY") { + Ok(key) => key, + Err(_) => { + return json_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "ServerError", + "hCaptcha is not configured", + ) + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); + } + }; + + let error_message = match params.error { + None => None, + Some(error) => Some(html_escape::encode_safe(&error).to_string()), + }; + + RenderHtml( + "captcha.hbs", + state.template_engine, + CaptchaPage { + handle: params.handle, + state: params.state, + captcha_site_key: hcaptcha_site_key, + error_message, + pds: state.app_config.pds_service_did.replace("did:web:", ""), + redirect_url: params.redirect_url, + }, + ) + .into_response() +} + +/// POST /gate - Verify captcha and redirect +pub async fn post_gate( + State(state): State, + Query(params): Query, + Form(form): Form, +) -> Response { + // Verify hCaptcha response + let hcaptcha_secret = match env::var("PDS_HCAPTCHA_SECRET_KEY") { + Ok(secret) => secret, + Err(_) => { + return json_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "ServerError", + "hCaptcha is not configured", + ) + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); + } + }; + + let client = match reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + { + Ok(c) => c, + Err(e) => { + log::error!("Failed to create HTTP client: {}", e); + return json_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "ServerError", + "Failed to verify captcha", + ) + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); + } + }; + + #[derive(Deserialize, Serialize)] + struct HCaptchaResponse { + success: bool, + challenge_ts: DateTime, + hostname: String, + #[serde(rename = "error-codes", default)] + error_codes: Vec, + } + + let verification_result = client + .post("https://api.hcaptcha.com/siteverify") + .form(&[ + ("secret", hcaptcha_secret.as_str()), + ("response", form.h_captcha_response.as_str()), + ]) + .send() + .await; + + let verification_response = match verification_result { + Ok(resp) => resp, + Err(e) => { + log::error!("Failed to verify hCaptcha: {}", e); + + return Redirect::to(&format!( + "/gate?handle={}&state={}&error={}", + url_encode(¶ms.handle), + url_encode(¶ms.state), + url_encode("Verification failed. Please try again.") + )) + .into_response(); + } + }; + + let captcha_result: HCaptchaResponse = match verification_response.json().await { + Ok(result) => result, + Err(e) => { + log::error!("Failed to parse hCaptcha response: {}", e); + + return Redirect::to(&format!( + "/gate?handle={}&state={}&error={}", + url_encode(¶ms.handle), + url_encode(¶ms.state), + url_encode("Verification failed. Please try again.") + )) + .into_response(); + } + }; + + if !captcha_result.success { + log::warn!( + "hCaptcha verification failed for handle {}: {:?}", + params.handle, + captcha_result.error_codes + ); + return Redirect::to(&format!( + "/gate?handle={}&state={}&error={}", + url_encode(¶ms.handle), + url_encode(¶ms.state), + url_encode("Verification failed. Please try again.") + )) + .into_response(); + } + + // Generate secure JWE verification token + let code = match generate_gate_token(¶ms.handle, &state.app_config.gate_jwe_key) { + Ok(token) => token, + Err(e) => { + log::error!("Failed to generate gate token: {}", e); + return json_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "ServerError", + "Failed to create verification code", + ) + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); + } + }; + + let now = Utc::now(); + + // Store the encrypted token in the database + let result = sqlx::query( + "INSERT INTO gate_codes (code, handle, created_at) + VALUES (?, ?, ?)", + ) + .bind(&code) + .bind(¶ms.handle) + .bind(now) + .execute(&state.pds_gatekeeper_pool) + .await; + + if let Err(e) = result { + log::error!("Failed to store gate code: {}", e); + return json_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "ServerError", + "Failed to create verification code", + ) + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); + } + + // Redirects by origin if it's found. If not redirect to the configured URL. + let mut base_redirect = state.app_config.default_successful_redirect_url.clone(); + if let Some(ref redirect_url) = form.redirect_url { + let trimmed = redirect_url.trim(); + if !trimmed.is_empty() + && (trimmed.starts_with("https://") || trimmed.starts_with("http://")) + { + base_redirect = trimmed.trim_end_matches('/').to_string(); + } + } + + let base_redirect = match state + .app_config + .captcha_success_redirects + .contains(&base_redirect) + { + true => base_redirect, + false => state.app_config.default_successful_redirect_url.clone(), + }; + + // Redirect to client app with code and state + let redirect_url = format!( + "{}/?code={}&state={}", + base_redirect, + url_encode(&code), + url_encode(¶ms.state) + ); + + Redirect::to(&redirect_url).into_response() +} + +/// Simple URL encode function +fn url_encode(s: &str) -> String { + urlencoding::encode(s).to_string() +} diff --git a/src/helpers.rs b/src/helpers.rs index fab7f67..1b54c2e 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,21 +1,31 @@ use crate::AppState; use crate::helpers::TokenCheckError::InvalidToken; use anyhow::anyhow; -use axum::body::{Body, to_bytes}; -use axum::extract::Request; -use axum::http::header::CONTENT_TYPE; -use axum::http::{HeaderMap, StatusCode, Uri}; -use axum::response::{IntoResponse, Response}; +use axum::{ + body::{Body, to_bytes}, + extract::Request, + http::header::CONTENT_TYPE, + http::{HeaderMap, StatusCode, Uri}, + response::{IntoResponse, Response}, +}; use axum_template::TemplateEngine; use chrono::Utc; -use lettre::message::{MultiPart, SinglePart, header}; -use lettre::{AsyncTransport, Message}; +use jacquard_common::{ + service_auth, service_auth::PublicKey, types::did::Did, types::did_doc::VerificationMethod, + types::nsid::Nsid, +}; +use jacquard_identity::{PublicResolver, resolver::IdentityResolver}; +use josekit::jwe::alg::direct::DirectJweAlgorithm; +use lettre::{ + AsyncTransport, Message, + message::{MultiPart, SinglePart, header}, +}; use rand::Rng; use serde::de::DeserializeOwned; use serde_json::{Map, Value}; use sha2::{Digest, Sha256}; use sqlx::SqlitePool; -use std::env; +use std::sync::Arc; use tracing::{error, log}; ///Used to generate the email 2fa code @@ -40,7 +50,7 @@ pub async fn proxy_get_json( where T: DeserializeOwned, { - let uri = format!("{}{}", state.pds_base_url, path); + let uri = format!("{}{}", state.app_config.pds_base_url, path); *req.uri_mut() = Uri::try_from(uri).map_err(|_| StatusCode::BAD_REQUEST)?; let result = state @@ -333,14 +343,12 @@ pub async fn preauth_check( let email_body = state .template_engine .render("two_factor_code.hbs", email_data)?; - let email_subject = env::var("GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT") - .unwrap_or("Sign in to Bluesky".to_string()); let email_message = Message::builder() //TODO prob get the proper type in the state - .from(state.mailer_from.parse()?) + .from(state.app_config.mailer_from.parse()?) .to(email.parse()?) - .subject(email_subject) + .subject(&state.app_config.email_subject) .multipart( MultiPart::alternative() // This is composed of two parts. .singlepart( @@ -523,3 +531,157 @@ pub fn mask_email(email: String) -> String { format!("{masked_local}@{masked_domain}") } + +pub enum VerifyServiceAuthError { + AuthFailed, + Error(anyhow::Error), +} + +/// Verifies the service auth token that is appended to an XRPC proxy request +pub async fn verify_service_auth( + jwt: &str, + lxm: &Nsid<'static>, + public_resolver: Arc, + service_did: &Did<'static>, + //The did of the user wanting to create an account + requested_did: &Did<'static>, +) -> Result<(), VerifyServiceAuthError> { + let parsed = + service_auth::parse_jwt(jwt).map_err(|e| VerifyServiceAuthError::Error(e.into()))?; + + let claims = parsed.claims(); + + let did_doc = public_resolver + .resolve_did_doc(&requested_did) + .await + .map_err(|err| { + log::error!("Error resolving the service auth for: {}", claims.iss); + return VerifyServiceAuthError::Error(err.into()); + })?; + + // Parse the DID document response to get verification methods + let doc = did_doc.parse().map_err(|err| { + log::error!("Error parsing the service auth did doc: {}", claims.iss); + VerifyServiceAuthError::Error(anyhow::anyhow!(err)) + })?; + + let verification_methods = doc.verification_method.as_deref().ok_or_else(|| { + VerifyServiceAuthError::Error(anyhow::anyhow!( + "No verification methods in did doc: {}", + &claims.iss + )) + })?; + + let signing_key = extract_signing_key(verification_methods).ok_or_else(|| { + VerifyServiceAuthError::Error(anyhow::anyhow!( + "No signing key found in did doc: {}", + &claims.iss + )) + })?; + + service_auth::verify_signature(&parsed, &signing_key).map_err(|err| { + log::error!("Error verifying service auth signature: {}", err); + VerifyServiceAuthError::AuthFailed + })?; + + // Now validate claims (audience, expiration, etc.) + claims.validate(service_did).map_err(|e| { + log::error!("Error validating service auth claims: {}", e); + VerifyServiceAuthError::AuthFailed + })?; + + if claims.aud != *service_did { + log::error!("Invalid audience (did:web): {}", claims.aud); + return Err(VerifyServiceAuthError::AuthFailed); + } + + let lxm_from_claims = claims.lxm.as_ref().ok_or_else(|| { + VerifyServiceAuthError::Error(anyhow::anyhow!("No lxm claim in service auth JWT")) + })?; + + if lxm_from_claims != lxm { + return Err(VerifyServiceAuthError::Error(anyhow::anyhow!( + "Invalid XRPC endpoint requested" + ))); + } + Ok(()) +} + +/// Ripped from Jacquard +/// +/// Extract the signing key from a DID document's verification methods. +/// +/// This looks for a key with type "atproto" or the first available key +/// if no atproto-specific key is found. +fn extract_signing_key(methods: &[VerificationMethod]) -> Option { + // First try to find an atproto-specific key + let atproto_method = methods + .iter() + .find(|m| m.r#type.as_ref() == "Multikey" || m.r#type.as_ref() == "atproto"); + + let method = atproto_method.or_else(|| methods.first())?; + + // Parse the multikey + let public_key_multibase = method.public_key_multibase.as_ref()?; + + // Decode multibase + let (_, key_bytes) = multibase::decode(public_key_multibase.as_ref()).ok()?; + + // First two bytes are the multicodec prefix + if key_bytes.len() < 2 { + return None; + } + + let codec = &key_bytes[..2]; + let key_material = &key_bytes[2..]; + + match codec { + // p256-pub (0x1200) + [0x80, 0x24] => PublicKey::from_p256_bytes(key_material).ok(), + // secp256k1-pub (0xe7) + [0xe7, 0x01] => PublicKey::from_k256_bytes(key_material).ok(), + _ => None, + } +} + +/// Payload for gate JWE tokens +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct GateTokenPayload { + pub handle: String, + pub created_at: String, +} + +/// Generate a secure JWE token for gate verification +pub fn generate_gate_token(handle: &str, encryption_key: &[u8]) -> Result { + use josekit::jwe::{JweHeader, alg::direct::DirectJweAlgorithm}; + + let payload = GateTokenPayload { + handle: handle.to_string(), + created_at: Utc::now().to_rfc3339(), + }; + + let payload_json = serde_json::to_string(&payload)?; + + let mut header = JweHeader::new(); + header.set_token_type("JWT"); + header.set_content_encryption("A128CBC-HS256"); + + let encrypter = DirectJweAlgorithm::Dir.encrypter_from_bytes(encryption_key)?; + + // Encrypt + let jwe = josekit::jwe::serialize_compact(payload_json.as_bytes(), &header, &encrypter)?; + + Ok(jwe) +} + +/// Verify and decrypt a gate JWE token, returning the payload if valid +pub fn verify_gate_token( + token: &str, + encryption_key: &[u8], +) -> Result { + let decrypter = DirectJweAlgorithm::Dir.decrypter_from_bytes(encryption_key)?; + let (payload_bytes, _header) = josekit::jwe::deserialize_compact(token, &decrypter)?; + let payload: GateTokenPayload = serde_json::from_slice(&payload_bytes)?; + + Ok(payload) +} diff --git a/src/main.rs b/src/main.rs index fae5590..b6f731c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,43 @@ #![warn(clippy::unwrap_used)] +use crate::gate::{get_gate, post_gate}; use crate::oauth_provider::sign_in; -use crate::xrpc::com_atproto_server::{create_account, create_session, get_session, update_email}; -use axum::body::Body; -use axum::handler::Handler; -use axum::http::{Method, header}; -use axum::middleware as ax_middleware; -use axum::routing::post; -use axum::{Router, routing::get}; +use crate::xrpc::com_atproto_server::{ + create_account, create_session, describe_server, get_session, update_email, +}; +use axum::{ + Router, + body::Body, + handler::Handler, + http::{Method, header}, + middleware as ax_middleware, + routing::get, + routing::post, +}; use axum_template::engine::Engine; use handlebars::Handlebars; -use hyper_util::client::legacy::connect::HttpConnector; -use hyper_util::rt::TokioExecutor; +use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; +use jacquard_common::types::did::Did; +use jacquard_identity::{PublicResolver, resolver::PlcSource}; use lettre::{AsyncSmtpTransport, Tokio1Executor}; +use rand::Rng; use rust_embed::RustEmbed; use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; use std::path::Path; +use std::sync::Arc; use std::time::Duration; use std::{env, net::SocketAddr}; -use tower_governor::GovernorLayer; -use tower_governor::governor::GovernorConfigBuilder; -use tower_governor::key_extractor::SmartIpKeyExtractor; -use tower_http::compression::CompressionLayer; -use tower_http::cors::{Any, CorsLayer}; +use tower_governor::{ + GovernorLayer, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, +}; +use tower_http::{ + compression::CompressionLayer, + cors::{Any, CorsLayer}, +}; use tracing::log; use tracing_subscriber::{EnvFilter, fmt, prelude::*}; +mod gate; pub mod helpers; mod middleware; mod oauth_provider; @@ -38,15 +50,110 @@ type HyperUtilClient = hyper_util::client::legacy::Client; #[include = "*.hbs"] struct EmailTemplates; +#[derive(RustEmbed)] +#[folder = "html_templates"] +#[include = "*.hbs"] +struct HtmlTemplates; + +/// Mostly the env variables that are used in the app +#[derive(Clone, Debug)] +pub struct AppConfig { + pds_base_url: String, + mailer_from: String, + email_subject: String, + allow_only_migrations: bool, + use_captcha: bool, + //The url to redirect to after a successful captcha. Defaults to https://bsky.app, but you may have another social-app fork you rather your users use + //that need to capture this redirect url for creating an account + default_successful_redirect_url: String, + pds_service_did: Did<'static>, + gate_jwe_key: Vec, + captcha_success_redirects: Vec, +} + +impl AppConfig { + pub fn new() -> Self { + let pds_base_url = + env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()); + let mailer_from = env::var("PDS_EMAIL_FROM_ADDRESS") + .expect("PDS_EMAIL_FROM_ADDRESS is not set in your pds.env file"); + //Hack not my favorite, but it does work + let allow_only_migrations = env::var("GATEKEEPER_ALLOW_ONLY_MIGRATIONS") + .map(|val| val.parse::().unwrap_or(false)) + .unwrap_or(false); + + let use_captcha = env::var("GATEKEEPER_CREATE_ACCOUNT_CAPTCHA") + .map(|val| val.parse::().unwrap_or(false)) + .unwrap_or(false); + + // PDS_SERVICE_DID is the did:web if set, if not it's PDS_HOSTNAME + let pds_service_did = + env::var("PDS_SERVICE_DID").unwrap_or_else(|_| match env::var("PDS_HOSTNAME") { + Ok(pds_hostname) => format!("did:web:{}", pds_hostname), + Err(_) => { + panic!("PDS_HOSTNAME or PDS_SERVICE_DID must be set in your pds.env file") + } + }); + + let email_subject = env::var("GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT") + .unwrap_or("Sign in to Bluesky".to_string()); + + // Load or generate JWE encryption key (32 bytes for AES-256) + let gate_jwe_key = env::var("GATEKEEPER_JWE_KEY") + .ok() + .and_then(|key_hex| hex::decode(key_hex).ok()) + .unwrap_or_else(|| { + // Generate a random 32-byte key if not provided + let key: Vec = (0..32).map(|_| rand::rng().random()).collect(); + log::warn!("WARNING: No GATEKEEPER_JWE_KEY found in the environment. Generated random key (hex): {}", hex::encode(&key)); + log::warn!("This is not strictly needed unless you scale PDS Gatekeeper. Will not also be able to verify tokens between reboots, but they are short lived (5mins)."); + key + }); + + if gate_jwe_key.len() != 32 { + panic!( + "GATEKEEPER_JWE_KEY must be 32 bytes (64 hex characters) for AES-256 encryption" + ); + } + + let captcha_success_redirects = match env::var("GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS") { + Ok(from_env) => from_env.split(",").map(|s| s.trim().to_string()).collect(), + Err(_) => { + vec![ + String::from("https://bsky.app"), + String::from("https://pdsmoover.com"), + String::from("https://blacksky.community"), + String::from("https://tektite.cc"), + ] + } + }; + + AppConfig { + pds_base_url, + mailer_from, + email_subject, + allow_only_migrations, + use_captcha, + default_successful_redirect_url: env::var("GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT") + .unwrap_or("https://bsky.app".to_string()), + pds_service_did: pds_service_did + .parse() + .expect("PDS_SERVICE_DID is not a valid did or could not infer from PDS_HOSTNAME"), + gate_jwe_key, + captcha_success_redirects, + } + } +} + #[derive(Clone)] pub struct AppState { account_pool: SqlitePool, pds_gatekeeper_pool: SqlitePool, reverse_proxy_client: HyperUtilClient, - pds_base_url: String, mailer: AsyncSmtpTransport, - mailer_from: String, template_engine: Engine>, + resolver: Arc, + app_config: AppConfig, } async fn root_handler() -> impl axum::response::IntoResponse { @@ -137,8 +244,6 @@ async fn main() -> Result<(), Box> { //Emailer set up let smtp_url = env::var("PDS_EMAIL_SMTP_URL").expect("PDS_EMAIL_SMTP_URL is not set in your pds.env file"); - let sent_from = env::var("PDS_EMAIL_FROM_ADDRESS") - .expect("PDS_EMAIL_FROM_ADDRESS is not set in your pds.env file"); let mailer: AsyncSmtpTransport = AsyncSmtpTransport::::from_url(smtp_url.as_str())?.build(); @@ -155,22 +260,30 @@ async fn main() -> Result<(), Box> { let _ = hbs.register_embed_templates::(); } - let pds_base_url = - env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()); + let _ = hbs.register_embed_templates::(); + + //Reads the PLC source from the pds env's or defaults to ol faithful + let plc_source_url = + env::var("PDS_DID_PLC_URL").unwrap_or_else(|_| "https://plc.directory".to_string()); + let plc_source = PlcSource::PlcDirectory { + base: plc_source_url.parse().unwrap(), + }; + let mut resolver = PublicResolver::default(); + resolver = resolver.with_plc_source(plc_source.clone()); let state = AppState { account_pool, pds_gatekeeper_pool, reverse_proxy_client: client, - pds_base_url, mailer, - mailer_from: sent_from, template_engine: Engine::from(hbs), + resolver: Arc::new(resolver), + app_config: AppConfig::new(), }; // Rate limiting //Allows 5 within 60 seconds, and after 60 should drop one off? So hit 5, then goes to 4 after 60 seconds. - let create_session_governor_conf = GovernorConfigBuilder::default() + let captcha_governor_conf = GovernorConfigBuilder::default() .per_second(60) .burst_size(5) .key_extractor(SmartIpKeyExtractor) @@ -216,16 +329,18 @@ async fn main() -> Result<(), Box> { "failed to create governor config for create account. this should not happen and is a bug", ); - let create_session_governor_limiter = create_session_governor_conf.limiter().clone(); + let captcha_governor_limiter = captcha_governor_conf.limiter().clone(); let sign_in_governor_limiter = sign_in_governor_conf.limiter().clone(); let create_account_governor_limiter = create_account_governor_conf.limiter().clone(); + let sign_in_governor_layer = GovernorLayer::new(sign_in_governor_conf); + let interval = Duration::from_secs(60); // a separate background task to clean up std::thread::spawn(move || { loop { std::thread::sleep(interval); - create_session_governor_limiter.retain_recent(); + captcha_governor_limiter.retain_recent(); sign_in_governor_limiter.retain_recent(); create_account_governor_limiter.retain_recent(); } @@ -236,25 +351,38 @@ async fn main() -> Result<(), Box> { .allow_methods([Method::GET, Method::OPTIONS, Method::POST]) .allow_headers(Any); - let app = Router::new() + let mut app = Router::new() .route("/", get(root_handler)) .route("/xrpc/com.atproto.server.getSession", get(get_session)) + .route( + "/xrpc/com.atproto.server.describeServer", + get(describe_server), + ) .route( "/xrpc/com.atproto.server.updateEmail", post(update_email).layer(ax_middleware::from_fn(middleware::extract_did)), ) .route( "/@atproto/oauth-provider/~api/sign-in", - post(sign_in).layer(GovernorLayer::new(sign_in_governor_conf)), + post(sign_in).layer(sign_in_governor_layer.clone()), ) .route( "/xrpc/com.atproto.server.createSession", - post(create_session.layer(GovernorLayer::new(create_session_governor_conf))), + post(create_session.layer(sign_in_governor_layer)), ) .route( "/xrpc/com.atproto.server.createAccount", post(create_account).layer(GovernorLayer::new(create_account_governor_conf)), - ) + ); + + if state.app_config.use_captcha { + app = app.route( + "/gate/signup", + get(get_gate).post(post_gate.layer(GovernorLayer::new(captcha_governor_conf))), + ); + } + + let app = app .layer(CompressionLayer::new()) .layer(cors) .with_state(state); diff --git a/src/oauth_provider.rs b/src/oauth_provider.rs index 53160dc..4bdc9a5 100644 --- a/src/oauth_provider.rs +++ b/src/oauth_provider.rs @@ -57,7 +57,7 @@ pub async fn sign_in( //No 2FA or already passed let uri = format!( "{}{}", - state.pds_base_url, "/@atproto/oauth-provider/~api/sign-in" + state.app_config.pds_base_url, "/@atproto/oauth-provider/~api/sign-in" ); let mut req = axum::http::Request::post(uri); diff --git a/src/xrpc/com_atproto_server.rs b/src/xrpc/com_atproto_server.rs index 38a072b..0d40afd 100644 --- a/src/xrpc/com_atproto_server.rs +++ b/src/xrpc/com_atproto_server.rs @@ -1,13 +1,16 @@ use crate::AppState; use crate::helpers::{ - AuthResult, ProxiedResult, TokenCheckError, json_error_response, preauth_check, proxy_get_json, + AuthResult, ProxiedResult, TokenCheckError, VerifyServiceAuthError, json_error_response, + preauth_check, proxy_get_json, verify_gate_token, verify_service_auth, }; use crate::middleware::Did; -use axum::body::Body; +use axum::body::{Body, to_bytes}; use axum::extract::State; -use axum::http::{HeaderMap, StatusCode}; +use axum::http::{HeaderMap, StatusCode, header}; use axum::response::{IntoResponse, Response}; use axum::{Extension, Json, debug_handler, extract, extract::Request}; +use chrono::{Duration, Utc}; +use jacquard_common::types::did::Did as JacquardDid; use serde::{Deserialize, Serialize}; use serde_json; use tracing::log; @@ -61,6 +64,57 @@ pub struct CreateSessionRequest { allow_takendown: Option, } +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CreateAccountRequest { + handle: String, + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + did: Option, + #[serde(skip_serializing_if = "Option::is_none")] + invite_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + verification_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + plc_op: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DescribeServerContact { + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DescribeServerLinks { + #[serde(skip_serializing_if = "Option::is_none")] + privacy_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + terms_of_service: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DescribeServerResponse { + #[serde(skip_serializing_if = "Option::is_none")] + invite_code_required: Option, + #[serde(skip_serializing_if = "Option::is_none")] + phone_verification_required: Option, + #[serde(skip_serializing_if = "Option::is_none")] + available_user_domains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + links: Option, + #[serde(skip_serializing_if = "Option::is_none")] + contact: Option, + #[serde(skip_serializing_if = "Option::is_none")] + did: Option, +} + pub async fn create_session( State(state): State, headers: HeaderMap, @@ -90,7 +144,7 @@ pub async fn create_session( //No 2FA or already passed let uri = format!( "{}{}", - state.pds_base_url, "/xrpc/com.atproto.server.createSession" + state.app_config.pds_base_url, "/xrpc/com.atproto.server.createSession" ); let mut req = axum::http::Request::post(uri); @@ -230,7 +284,7 @@ pub async fn update_email( // Updating the actual email address by sending it on to the PDS let uri = format!( "{}{}", - state.pds_base_url, "/xrpc/com.atproto.server.updateEmail" + state.app_config.pds_base_url, "/xrpc/com.atproto.server.updateEmail" ); let mut req = axum::http::Request::post(uri); if let Some(req_headers) = req.headers_mut() { @@ -283,23 +337,264 @@ pub async fn get_session( } } +pub async fn describe_server( + State(state): State, + req: Request, +) -> Result, StatusCode> { + match proxy_get_json::( + &state, + req, + "/xrpc/com.atproto.server.describeServer", + ) + .await? + { + ProxiedResult::Parsed { + value: mut server_info, + .. + } => { + //This signifies the server is configured for captcha verification + server_info.phone_verification_required = Some(state.app_config.use_captcha); + Ok(Json(server_info).into_response()) + } + ProxiedResult::Passthrough(resp) => Ok(resp), + } +} + +/// Verify a gate code matches the handle and is not expired +async fn verify_gate_code( + state: &AppState, + code: &str, + handle: &str, +) -> Result { + // First, decrypt and verify the JWE token + let payload = match verify_gate_token(code, &state.app_config.gate_jwe_key) { + Ok(p) => p, + Err(e) => { + log::warn!("Failed to decrypt gate token: {}", e); + return Ok(false); + } + }; + + // Verify the handle matches + if payload.handle != handle { + log::warn!( + "Gate code handle mismatch: expected {}, got {}", + handle, + payload.handle + ); + return Ok(false); + } + + let created_at = chrono::DateTime::parse_from_rfc3339(&payload.created_at) + .map_err(|e| anyhow::anyhow!("Failed to parse created_at from token: {}", e))? + .with_timezone(&Utc); + + let now = Utc::now(); + let age = now - created_at; + + // Check if the token is expired (5 minutes) + if age > Duration::minutes(5) { + log::warn!("Gate code expired for handle {}", handle); + return Ok(false); + } + + // Verify the token exists in the database (to prevent reuse) + let row: Option<(String,)> = + sqlx::query_as("SELECT code FROM gate_codes WHERE code = ? and handle = ? LIMIT 1") + .bind(code) + .bind(handle) + .fetch_optional(&state.pds_gatekeeper_pool) + .await?; + + if row.is_none() { + log::warn!("Gate code not found in database or already used"); + return Ok(false); + } + + // Token is valid, delete it so it can't be reused + //TODO probably also delete expired codes? Will need to do that at some point probably altho the where is on code and handle + + sqlx::query("DELETE FROM gate_codes WHERE code = ?") + .bind(code) + .execute(&state.pds_gatekeeper_pool) + .await?; + + Ok(true) +} + pub async fn create_account( State(state): State, - mut req: Request, + req: Request, ) -> Result, StatusCode> { - //TODO if I add the block of only accounts authenticated just take the body as json here and grab the lxm token. No middle ware is needed + let headers = req.headers().clone(); + let body_bytes = to_bytes(req.into_body(), usize::MAX) + .await + .map_err(|_| StatusCode::BAD_REQUEST)?; + + // Parse the body to check for verification code + let account_request: CreateAccountRequest = + serde_json::from_slice(&body_bytes).map_err(|e| { + log::error!("Failed to parse create account request: {}", e); + StatusCode::BAD_REQUEST + })?; + + // Check for service auth (migrations) if configured + if state.app_config.allow_only_migrations { + // Expect Authorization: Bearer + let auth_header = headers + .get(header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .map(str::to_string); + let Some(value) = auth_header else { + log::error!("No Authorization header found in the request"); + return json_error_response( + StatusCode::UNAUTHORIZED, + "InvalidAuth", + "This PDS is configured to only allow accounts created by migrations via this endpoint.", + ); + }; + + // Ensure Bearer prefix + let token = value.strip_prefix("Bearer ").unwrap_or("").trim(); + if token.is_empty() { + log::error!("No Service Auth token found in the Authorization header"); + return json_error_response( + StatusCode::UNAUTHORIZED, + "InvalidAuth", + "This PDS is configured to only allow accounts created by migrations via this endpoint.", + ); + } + + // Ensure a non-empty DID was provided when migrations are enabled + let requested_did_str = match account_request.did.as_deref() { + Some(s) if !s.trim().is_empty() => s, + _ => { + return json_error_response( + StatusCode::BAD_REQUEST, + "InvalidRequest", + "The 'did' field is required when migrations are enforced.", + ); + } + }; + + // Parse the DID into the expected type for verification + let requested_did: JacquardDid<'static> = match requested_did_str.parse() { + Ok(d) => d, + Err(e) => { + log::error!( + "Invalid DID format provided in createAccount: {} | error: {}", + requested_did_str, + e + ); + return json_error_response( + StatusCode::BAD_REQUEST, + "InvalidRequest", + "The 'did' field is not a valid DID.", + ); + } + }; + + let nsid = "com.atproto.server.createAccount".parse().unwrap(); + match verify_service_auth( + token, + &nsid, + state.resolver.clone(), + &state.app_config.pds_service_did, + &requested_did, + ) + .await + { + //Just do nothing if it passes so it continues. + Ok(_) => {} + Err(err) => match err { + VerifyServiceAuthError::AuthFailed => { + return json_error_response( + StatusCode::UNAUTHORIZED, + "InvalidAuth", + "This PDS is configured to only allow accounts created by migrations via this endpoint.", + ); + } + VerifyServiceAuthError::Error(err) => { + log::error!("Error verifying service auth token: {err}"); + return json_error_response( + StatusCode::BAD_REQUEST, + "InvalidRequest", + "There has been an error, please contact your PDS administrator for help and for them to review the server logs.", + ); + } + }, + } + } + + // Check for captcha verification if configured + if state.app_config.use_captcha { + if let Some(ref verification_code) = account_request.verification_code { + match verify_gate_code(&state, verification_code, &account_request.handle).await { + //TODO has a few errors to support + + //expired token + // { + // "error": "ExpiredToken", + // "message": "Token has expired" + // } + + //TODO ALSO add rate limits on the /gate endpoints so they can't be abused + Ok(true) => { + log::info!("Gate code verified for handle: {}", account_request.handle); + } + Ok(false) => { + log::warn!( + "Invalid or expired gate code for handle: {}", + account_request.handle + ); + return json_error_response( + StatusCode::BAD_REQUEST, + "InvalidToken", + "Token could not be verified", + ); + } + Err(e) => { + log::error!("Error verifying gate code: {}", e); + return json_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InvalidToken", + "Token could not be verified", + ); + } + } + } else { + // No verification code provided but captcha is required + log::warn!( + "No verification code provided for account creation: {}", + account_request.handle + ); + return json_error_response( + StatusCode::BAD_REQUEST, + "InvalidRequest", + "Verification is now required on this server.", + ); + } + } + + // Rebuild the request with the same body and headers let uri = format!( "{}{}", - state.pds_base_url, "/xrpc/com.atproto.server.createAccount" + state.app_config.pds_base_url, "/xrpc/com.atproto.server.createAccount" ); - // Rewrite the URI to point at the upstream PDS; keep headers, method, and body intact - *req.uri_mut() = uri.parse().map_err(|_| StatusCode::BAD_REQUEST)?; + let mut new_req = axum::http::Request::post(&uri); + if let Some(req_headers) = new_req.headers_mut() { + *req_headers = headers; + } + + let new_req = new_req + .body(Body::from(body_bytes)) + .map_err(|_| StatusCode::BAD_REQUEST)?; let proxied = state .reverse_proxy_client - .request(req) + .request(new_req) .await .map_err(|_| StatusCode::BAD_REQUEST)? .into_response(); -- 2.51.2