From 9c326604bddbf99973c4ad5fd5f7e12c2570c73f Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 3 Dec 2025 18:07:07 +0200 Subject: [PATCH] Initial idea --- .env.example | 17 + .gitignore | 6 + Cargo.lock | 5620 ++++++++++++++++++++ Cargo.toml | 28 + TODO.md | 108 + docker-compose.yaml | 50 + migrations/202512211400_initial_tables.sql | 80 + src/api/mod.rs | 2 + src/api/repo.rs | 219 + src/api/server.rs | 483 ++ src/auth.rs | 59 + src/main.rs | 73 + src/repo/mod.rs | 94 + src/state.rs | 15 + tests/actor.rs | 36 + tests/common/mod.rs | 101 + tests/feed.rs | 53 + tests/graph.rs | 68 + tests/identity.rs | 18 + tests/lifecycle.rs | 936 ++++ tests/notification.rs | 31 + tests/repo.rs | 354 ++ tests/server.rs | 164 + tests/sync.rs | 34 + 24 files changed, 8649 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 TODO.md create mode 100644 docker-compose.yaml create mode 100644 migrations/202512211400_initial_tables.sql create mode 100644 src/api/mod.rs create mode 100644 src/api/repo.rs create mode 100644 src/api/server.rs create mode 100644 src/auth.rs create mode 100644 src/main.rs create mode 100644 src/repo/mod.rs create mode 100644 src/state.rs create mode 100644 tests/actor.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/feed.rs create mode 100644 tests/graph.rs create mode 100644 tests/identity.rs create mode 100644 tests/lifecycle.rs create mode 100644 tests/notification.rs create mode 100644 tests/repo.rs create mode 100644 tests/server.rs create mode 100644 tests/sync.rs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9620138 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +SERVER_HOST=127.0.0.1 +SERVER_PORT=3000 + +DATABASE_URL=postgres://postgres:postgres@localhost:5432/pds + +OBJECT_STORAGE_ENDPOINT= +OBJECT_STORAGE_REGION=us-east-1 +OBJECT_STORAGE_BUCKET=pds-blobs +OBJECT_STORAGE_ACCESS_KEY= +OBJECT_STORAGE_SECRET_KEY= + +# Set to 'true' for MinIO or other services that need path-style addressing +OBJECT_STORAGE_FORCE_PATH_STYLE=false + +JWT_SECRET=your-super-secret-jwt-key-please-change-me +PDS_HOSTNAME=localhost:3000 # The public-facing hostname of the PDS +PLC_URL=plc.directory diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23c0a89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +src_old +.sqlx + +.env + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b5a9d89 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5620 @@ +# This file is automatically @generated by Cargo. +# 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", +] + +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "async-compression" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bcrypt" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abaf6da45c74385272ddf00e1ac074c7d8a6c1a1dda376902bd6a427522a8b2c" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.3.4", + "subtle", + "zeroize", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[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", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.111", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "cfg_aliases", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bspds" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "bcrypt", + "bytes", + "chrono", + "cid", + "dotenvy", + "jacquard", + "jacquard-axum", + "jacquard-repo", + "jsonwebtoken", + "multihash", + "reqwest", + "serde", + "serde_ipld_dagcbor", + "serde_json", + "sha2", + "sqlx", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[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 = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + +[[package]] +name = "cbor4ii" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "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 = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[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.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "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 0.8.0", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +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 = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + +[[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" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +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-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[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.111", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "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.111", +] + +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", + "gzip-header", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "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_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[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 = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +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", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[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 = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +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 = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[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.111", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "gzip-header" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +dependencies = [ + "crc32fast", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +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 = "hickory-proto" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.8.5", + "thiserror 1.0.69", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand 0.8.5", + "resolv-conf", + "smallvec", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "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.4", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "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.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +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 = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipld-core" +version = "0.4.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 = "iroh-car" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f8cd4cb9aa083fba8b52e921764252d0b4dcb1cd6d120b809dbfe1106e81a" +dependencies = [ + "anyhow", + "cid", + "futures", + "serde", + "serde_ipld_dagcbor", + "thiserror 1.0.69", + "tokio", + "unsigned-varint 0.7.2", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jacquard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19864761c8d69d23201fd19dd021cddf1fb7acbebb6e6b50e2b1776ec982768" +dependencies = [ + "bytes", + "getrandom 0.2.16", + "gloo-storage", + "http", + "jacquard-api", + "jacquard-common", + "jacquard-derive", + "jacquard-identity", + "jacquard-oauth", + "jose-jwk", + "miette", + "regex", + "regex-lite", + "reqwest", + "serde", + "serde_html_form", + "serde_json", + "smol_str", + "thiserror 2.0.17", + "tokio", + "trait-variant", + "url", + "webpage", +] + +[[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.17", + "unicode-segmentation", +] + +[[package]] +name = "jacquard-axum" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1b58111f0a2a08ee18525ac661a0448fdc84d4ecb423d64aaaf88074460380" +dependencies = [ + "axum", + "bytes", + "jacquard", + "jacquard-common", + "jacquard-derive", + "jacquard-identity", + "miette", + "multibase", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower-http", + "tracing", +] + +[[package]] +name = "jacquard-common" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df86cb117d9f1c2b0251ba67c3f0e3f963fd22abc6cf8de0e02a7fc846c288ca" +dependencies = [ + "base64 0.22.1", + "bon", + "bytes", + "chrono", + "cid", + "ed25519-dalek", + "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.17", + "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.111", +] + +[[package]] +name = "jacquard-identity" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef714cacebfca486558a9f8e205daf466bfba0466c4d0c450fd6d0252400a53" +dependencies = [ + "bon", + "bytes", + "hickory-resolver", + "http", + "jacquard-api", + "jacquard-common", + "jacquard-lexicon", + "miette", + "mini-moka", + "percent-encoding", + "reqwest", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.17", + "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 6.1.0", + "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.111", + "thiserror 2.0.17", + "unicode-segmentation", +] + +[[package]] +name = "jacquard-oauth" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafe9b4b2160cb57cd48d02d84d2c09706853d098e053baacb06a59fcd59a898" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "dashmap 6.1.0", + "elliptic-curve", + "http", + "jacquard-common", + "jacquard-identity", + "jose-jwa", + "jose-jwk", + "miette", + "p256", + "rand 0.8.5", + "rouille", + "serde", + "serde_html_form", + "serde_json", + "sha2", + "smol_str", + "thiserror 2.0.17", + "tokio", + "trait-variant", + "url", + "webbrowser", +] + +[[package]] +name = "jacquard-repo" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ccdadfea11df142fbfb11cf7e5c1f7b8c0548758dedb0ce0eac182777f91f18" +dependencies = [ + "bytes", + "cid", + "ed25519-dalek", + "iroh-car", + "jacquard-common", + "jacquard-derive", + "k256", + "miette", + "multihash", + "n0-future", + "p256", + "serde", + "serde_bytes", + "serde_ipld_dagcbor", + "sha2", + "smol_str", + "thiserror 2.0.17", + "tokio", + "trait-variant", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jose-b64" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" +dependencies = [ + "base64ct", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "jose-jwa" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" +dependencies = [ + "serde", +] + +[[package]] +name = "jose-jwk" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" +dependencies = [ + "jose-b64", + "jose-jwa", + "p256", + "p384", + "rsa", + "serde", + "zeroize", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "10.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.16", + "hmac", + "js-sys", + "p256", + "p384", + "pem", + "rand 0.8.5", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", + "simple_asn1", +] + +[[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", + "once_cell", + "sha2", + "signature", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "match-lookup" +version = "0.1.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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[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.111", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap 5.5.3", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[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 0.8.0", +] + +[[package]] +name = "multipart" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand 0.8.5", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "n0-future" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", +] + +[[package]] +name = "once_cell" +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.111", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[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.111", +] + +[[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 = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + +[[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]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +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.111", + "version_check", + "yansi", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[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", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "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", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "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 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +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.111", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +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", +] + +[[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.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "async-compression", + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.4", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rouille" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921" +dependencies = [ + "base64 0.13.1", + "brotli", + "chrono", + "deflate", + "filetime", + "multipart", + "percent-encoding", + "rand 0.8.5", + "serde", + "serde_derive", + "serde_json", + "sha1_smol", + "threadpool", + "time", + "tiny_http", + "url", +] + +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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 = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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 = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +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 = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[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.12.1", + "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.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[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.111", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "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 = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +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.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.12.1", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.111", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.111", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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.111", + "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 = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +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.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +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" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "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]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "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 = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "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.111", +] + +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[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 = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +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 = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "rand 0.9.2", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +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.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", +] + +[[package]] +name = "webpage" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" +dependencies = [ + "html5ever", + "markup5ever_rcdom", + "serde_json", + "url", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.4", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-link" +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-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "serde", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d27d937 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "bspds" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.100" +axum = "0.8.7" +bcrypt = "0.17.1" +bytes = "1.11.0" +chrono = { version = "0.4.42", features = ["serde"] } +cid = "0.11.1" +dotenvy = "0.15.7" +jacquard = "0.9.3" +jacquard-axum = "0.9.2" +jacquard-repo = "0.9.2" +jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] } +multihash = "0.19.3" +reqwest = { version = "0.12.24", features = ["json"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_ipld_dagcbor = "0.6.4" +serde_json = "1.0.145" +sha2 = "0.10.9" +sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] } +tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "time"] } +tracing = "0.1.43" +tracing-subscriber = "0.3.22" +uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..3f49a9d --- /dev/null +++ b/TODO.md @@ -0,0 +1,108 @@ +# Implementation TODOs + +Lewis' special big boy todofile + +## 1. Server Infrastructure & Health +- [x] Health Check + - [x] Implement `GET /health` endpoint (returns "OK"). +- [x] Server Description + - [x] Implement `com.atproto.server.describeServer` (returns available user domains). + +## 2. Authentication & Account Management (`com.atproto.server`) +- [x] Account Creation + - [x] Implement `com.atproto.server.createAccount`. + - [x] Validate handle format (reject invalid characters). + - [x] Create DID for new user. + - [x] Initialize user repository. + - [x] Return access JWT and DID. + - [x] MST stuff I think... + +- [x] Session Management + - [x] Implement `com.atproto.server.createSession` (Login). + - [x] Validate identifier (handle/email) and password. + - [x] Return access JWT, refresh JWT, and DID. + - [x] Implement `com.atproto.server.getSession`. + - [x] Verify JWT validity. + - [x] Implement `com.atproto.server.refreshSession`. + - [x] Implement `com.atproto.server.deleteSession` (Logout). + - [x] Invalidate current session/token. + +## 3. Repository Operations (`com.atproto.repo`) +- [ ] Record CRUD + - [ ] Implement `com.atproto.repo.createRecord`. + - [ ] Generate `rkey` if not provided. + - [ ] Validate schema against Lexicon. + - [ ] Handle `swapCommit` for optimistic locking. + - [ ] Implement `com.atproto.repo.putRecord`. + - [ ] Handle create vs update logic. + - [ ] Validate `repo` matches authenticated user. + - [ ] Validate record schema (e.g., missing required fields). + - [ ] Implement `com.atproto.repo.getRecord`. + - [ ] Handle missing params (400 Bad Request). + - [ ] Handle non-existent record (404 Not Found). + - [ ] Implement `com.atproto.repo.deleteRecord`. + - [ ] Implement `com.atproto.repo.listRecords`. + - [ ] Support pagination (`limit`, `cursor`). +- [ ] Blob Management + - [ ] Implement `com.atproto.repo.uploadBlob`. + - [ ] Enforce authentication. + - [ ] Validate MIME types (reject unsupported). + - [ ] Return blob reference (`$link`). +- [ ] Repo Meta + - [ ] Implement `com.atproto.repo.describeRepo`. + +## 4. Actor & Profile (`app.bsky.actor`) +- [ ] Profile Management + - [ ] Implement `app.bsky.actor.getProfile`. + - [ ] Resolve handle to DID. + - [ ] Return profile record data. +- [ ] Discovery + - [ ] Implement `app.bsky.actor.searchActors`. + +## 5. Feed & Timeline (`app.bsky.feed`) +- [ ] Feed Retrieval + - [ ] Implement `app.bsky.feed.getTimeline`. + - [ ] Implement `app.bsky.feed.getAuthorFeed`. + - [ ] Filter by actor. + - [ ] Respect mutes (if viewer is authenticated). + - [ ] Implement `app.bsky.feed.getPostThread`. + - [ ] Construct thread tree (parents, replies). + - [ ] Handle deleted posts (return `notFoundPost` view). +- [ ] Record Types + - [ ] Support `app.bsky.feed.post` record type. + - [ ] Support `app.bsky.feed.like` record type. + - [ ] Support `app.bsky.embed.images` in posts. + +## 6. Social Graph (`app.bsky.graph`) +- [ ] Relationships + - [ ] Implement `app.bsky.graph.getFollows`. + - [ ] Implement `app.bsky.graph.getFollowers`. + - [ ] Implement `app.bsky.graph.getMutes`. + - [ ] Implement `app.bsky.graph.getBlocks`. +- [ ] Record Types + - [ ] Support `app.bsky.graph.follow` record type. + - [ ] Support `app.bsky.graph.mute` record type. + +## 7. Notifications (`app.bsky.notification`) +- [ ] Notification Management + - [ ] Implement `app.bsky.notification.listNotifications`. + - [ ] Aggregate notifications (likes, follows, replies). + - [ ] Implement `app.bsky.notification.getUnreadCount`. + - [ ] Track read state. + - [ ] Reset count on list/read. + +## 8. Identity (`com.atproto.identity`) +- [ ] Resolution + - [ ] Implement `com.atproto.identity.resolveHandle`. + +## 9. Sync & Federation (`com.atproto.sync`) +- [ ] Data Export + - [ ] Implement `com.atproto.sync.getRepo` (Export CAR file). + - [ ] Implement `com.atproto.sync.getBlocks`. + +## 10. General Requirements +- [ ] Validation + - [ ] Ensure all endpoints validate input parameters. + - [ ] Ensure proper error codes (400, 401, 404, 409). +- [ ] Concurrency + - [ ] Ensure thread safety for repo updates. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..9a99732 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,50 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + image: bspds + ports: + - "3000:3000" + environment: + SERVER_HOST: 0.0.0.0 + SERVER_PORT: 3000 + DATABASE_URL: postgres://postgres:postgres@db:5432/pds + OBJECT_STORAGE_ENDPOINT: http://objsto:9000 + OBJECT_STORAGE_REGION: us-east-1 + OBJECT_STORAGE_BUCKET: pds-blobs + OBJECT_STORAGE_ACCESS_KEY: minioadmin + OBJECT_STORAGE_SECRET_KEY: minioadmin + OBJECT_STORAGE_FORCE_PATH_STYLE: "true" + JWT_SECRET: your-super-secret-jwt-key-please-change-me + PDS_HOSTNAME: localhost:3000 + depends_on: + - db + - objsto + + db: + image: postgres:latest + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: pds + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql + + objsto: + image: minio/minio + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + volumes: + - minio_data:/data + command: server /data --console-address ":9001" + +volumes: + postgres_data: + minio_data: diff --git a/migrations/202512211400_initial_tables.sql b/migrations/202512211400_initial_tables.sql new file mode 100644 index 0000000..6d2bfe2 --- /dev/null +++ b/migrations/202512211400_initial_tables.sql @@ -0,0 +1,80 @@ +-- A very basic schema to get started. +-- TODO: PRODUCTIONIZE BABY + +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + handle TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + did TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS invite_codes ( + code TEXT PRIMARY KEY, + available_uses INT NOT NULL DEFAULT 1, + created_by_user UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS invite_code_uses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code TEXT NOT NULL REFERENCES invite_codes(code), + used_by_user UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(code, used_by_user) +); + +-- OIII THIS TABLE CONTAINS PLAINTEXT PRIVATE KEYS, TODO: encrypt at rest! +CREATE TABLE IF NOT EXISTS user_keys ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + -- Storing as raw bytes + -- secp256k1 is 32 bytes + key_bytes BYTEA NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS repos ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + repo_root_cid TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS blocks ( + cid BYTEA PRIMARY KEY, + data BYTEA NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- A denormalized table to quickly query for records +-- TODO: Do I actually need this? +CREATE TABLE IF NOT EXISTS records ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + repo_id UUID NOT NULL REFERENCES repos(user_id) ON DELETE CASCADE, + collection TEXT NOT NULL, + rkey TEXT NOT NULL, + record_cid TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(repo_id, collection, rkey) +); + +CREATE TABLE IF NOT EXISTS blobs ( + cid TEXT PRIMARY KEY, + mime_type TEXT NOT NULL, + size_bytes BIGINT NOT NULL, + created_by_user UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- The key/path in the S3 bucket + storage_key TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS sessions ( + access_jwt TEXT PRIMARY KEY, + refresh_jwt TEXT NOT NULL UNIQUE, + did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..1c8487a --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,2 @@ +pub mod server; +pub mod repo; diff --git a/src/api/repo.rs b/src/api/repo.rs new file mode 100644 index 0000000..0242f03 --- /dev/null +++ b/src/api/repo.rs @@ -0,0 +1,219 @@ +use axum::{ + extract::State, + Json, + response::{IntoResponse, Response}, + http::StatusCode, +}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use crate::state::AppState; +use chrono::Utc; +use sqlx::Row; +use cid::Cid; +use std::str::FromStr; +use jacquard_repo::{mst::Mst, commit::Commit, storage::BlockStore}; +use jacquard::types::{string::{Nsid, Tid}, did::Did, integer::LimitedU32}; +use tracing::error; +use std::sync::Arc; + +#[derive(Deserialize)] +#[allow(dead_code)] +pub struct CreateRecordInput { + pub repo: String, + pub collection: String, + pub rkey: Option, + pub validate: Option, + pub record: serde_json::Value, + #[serde(rename = "swapCommit")] + pub swap_commit: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateRecordOutput { + pub uri: String, + pub cid: String, +} + +pub async fn create_record( + State(state): State, + headers: axum::http::HeaderMap, + Json(input): Json, +) -> Response { + let auth_header = headers.get("Authorization"); + if auth_header.is_none() { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); + } + let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); + + if let Err(_) = crate::auth::verify_token(&token) { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token"}))).into_response(); + } + + let session = sqlx::query("SELECT did FROM sessions WHERE access_jwt = $1") + .bind(&token) + .fetch_optional(&state.db) + .await + .unwrap_or(None); + + let did = match session { + Some(row) => row.get::("did"), + None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(), + }; + + if input.repo != did { + return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response(); + } + + let user_query = sqlx::query("SELECT id FROM users WHERE did = $1") + .bind(&did) + .fetch_optional(&state.db) + .await; + + let user_id: uuid::Uuid = match user_query { + Ok(Some(row)) => row.get("id"), + _ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "User not found"}))).into_response(), + }; + + let repo_root_query = sqlx::query("SELECT repo_root_cid FROM repos WHERE user_id = $1") + .bind(user_id) + .fetch_optional(&state.db) + .await; + + let current_root_cid = match repo_root_query { + Ok(Some(row)) => { + let cid_str: String = row.get("repo_root_cid"); + Cid::from_str(&cid_str).ok() + }, + _ => None, + }; + + if current_root_cid.is_none() { + error!("Repo root not found for user {}", did); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Repo root not found"}))).into_response(); + } + let current_root_cid = current_root_cid.unwrap(); + + let commit_bytes = match state.block_store.get(¤t_root_cid).await { + Ok(Some(b)) => b, + Ok(None) => { + error!("Commit block not found: {}", current_root_cid); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + }, + Err(e) => { + error!("Failed to load commit block: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let commit = match Commit::from_cbor(&commit_bytes) { + Ok(c) => c, + Err(e) => { + error!("Failed to parse commit: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let mst_root = commit.data; + let store = Arc::new(state.block_store.clone()); + let mst = Mst::load(store.clone(), mst_root, None); + + let collection_nsid = match input.collection.parse::() { + Ok(n) => n, + Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(), + }; + + let rkey = input.rkey.unwrap_or_else(|| { + Utc::now().format("%Y%m%d%H%M%S%f").to_string() + }); + + let mut record_bytes = Vec::new(); + if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) { + error!("Error serializing record: {:?}", e); + return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response(); + } + + let record_cid = match state.block_store.put(&record_bytes).await { + Ok(c) => c, + Err(e) => { + error!("Failed to save record block: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let key = format!("{}/{}", collection_nsid, rkey); + if let Err(e) = mst.update(&key, record_cid).await { + error!("Failed to update MST: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + + let new_mst_root = match mst.root().await { + Ok(c) => c, + Err(e) => { + error!("Failed to get new MST root: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let did_obj = match Did::new(&did) { + Ok(d) => d, + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(), + }; + + let rev = Tid::now(LimitedU32::MIN); + + let new_commit = Commit::new_unsigned( + did_obj, + new_mst_root, + rev, + Some(current_root_cid) + ); + + let new_commit_bytes = match new_commit.to_cbor() { + Ok(b) => b, + Err(e) => { + error!("Failed to serialize new commit: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let new_root_cid = match state.block_store.put(&new_commit_bytes).await { + Ok(c) => c, + Err(e) => { + error!("Failed to save new commit: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let update_repo = sqlx::query("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2") + .bind(new_root_cid.to_string()) + .bind(user_id) + .execute(&state.db) + .await; + + if let Err(e) = update_repo { + error!("Failed to update repo root in DB: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + + let record_insert = sqlx::query( + "INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4) + ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()" + ) + .bind(user_id) + .bind(&input.collection) + .bind(&rkey) + .bind(record_cid.to_string()) + .execute(&state.db) + .await; + + if let Err(e) = record_insert { + error!("Error inserting record index: {:?}", e); + } + + let output = CreateRecordOutput { + uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey), + cid: record_cid.to_string(), + }; + (StatusCode::OK, Json(output)).into_response() +} diff --git a/src/api/server.rs b/src/api/server.rs new file mode 100644 index 0000000..72b2d6f --- /dev/null +++ b/src/api/server.rs @@ -0,0 +1,483 @@ +use axum::{ + extract::State, + Json, + response::{IntoResponse, Response}, + http::StatusCode, +}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use crate::state::AppState; +use sqlx::Row; +use bcrypt::{hash, verify, DEFAULT_COST}; +use tracing::{info, error, warn}; +use jacquard_repo::{mst::Mst, commit::Commit, storage::BlockStore}; +use jacquard::types::{string::Tid, did::Did, integer::LimitedU32}; +use std::sync::Arc; + +#[derive(Deserialize)] +pub struct CreateAccountInput { + pub handle: String, + pub email: String, + pub password: String, + #[serde(rename = "inviteCode")] + pub invite_code: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateAccountOutput { + pub access_jwt: String, + pub refresh_jwt: String, + pub handle: String, + pub did: String, +} + +pub async fn create_account( + State(state): State, + Json(input): Json, +) -> Response { + info!("create_account hit: {}", input.handle); + if input.handle.contains('!') || input.handle.contains('@') { + return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}))).into_response(); + } + + let mut tx = match state.db.begin().await { + Ok(tx) => tx, + Err(e) => { + error!("Error starting transaction: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let exists_query = sqlx::query("SELECT 1 FROM users WHERE handle = $1") + .bind(&input.handle) + .fetch_optional(&mut *tx) + .await; + + match exists_query { + Ok(Some(_)) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "HandleTaken", "message": "Handle already taken"}))).into_response(), + Err(e) => { + error!("Error checking handle: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + Ok(None) => {} + } + + if let Some(code) = &input.invite_code { + let invite_query = sqlx::query("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE") + .bind(code) + .fetch_optional(&mut *tx) + .await; + + match invite_query { + Ok(Some(row)) => { + let uses: i32 = row.get("available_uses"); + if uses <= 0 { + return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response(); + } + + let update_invite = sqlx::query("UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1") + .bind(code) + .execute(&mut *tx) + .await; + + if let Err(e) = update_invite { + error!("Error updating invite code: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }, + Ok(None) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"}))).into_response(), + Err(e) => { + error!("Error checking invite code: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + } + } + + let did = format!("did:plc:{}", uuid::Uuid::new_v4()); + + let password_hash = match hash(&input.password, DEFAULT_COST) { + Ok(h) => h, + Err(e) => { + error!("Error hashing password: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let user_insert = sqlx::query("INSERT INTO users (handle, email, did, password_hash) VALUES ($1, $2, $3, $4) RETURNING id") + .bind(&input.handle) + .bind(&input.email) + .bind(&did) + .bind(&password_hash) + .fetch_one(&mut *tx) + .await; + + let user_id: uuid::Uuid = match user_insert { + Ok(row) => row.get("id"), + Err(e) => { + error!("Error inserting user: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let store = Arc::new(state.block_store.clone()); + let mst = Mst::new(store.clone()); + let mst_root = match mst.root().await { + Ok(c) => c, + Err(e) => { + error!("Error creating MST root: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let did_obj = match Did::new(&did) { + Ok(d) => d, + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Invalid DID"}))).into_response(), + }; + + let rev = Tid::now(LimitedU32::MIN); + + let commit = Commit::new_unsigned( + did_obj, + mst_root, + rev, + None + ); + + let commit_bytes = match commit.to_cbor() { + Ok(b) => b, + Err(e) => { + error!("Error serializing genesis commit: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let commit_cid = match state.block_store.put(&commit_bytes).await { + Ok(c) => c, + Err(e) => { + error!("Error saving genesis commit: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let repo_insert = sqlx::query("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)") + .bind(user_id) + .bind(commit_cid.to_string()) + .execute(&mut *tx) + .await; + + if let Err(e) = repo_insert { + error!("Error initializing repo: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + + if let Some(code) = &input.invite_code { + let use_insert = sqlx::query("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)") + .bind(code) + .bind(user_id) + .execute(&mut *tx) + .await; + + if let Err(e) = use_insert { + error!("Error recording invite usage: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + } + + let access_jwt = crate::auth::create_access_token(&did).map_err(|e| { + error!("Error creating access token: {:?}", e); + (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response() + }); + let access_jwt = match access_jwt { + Ok(t) => t, + Err(r) => return r, + }; + + let refresh_jwt = crate::auth::create_refresh_token(&did).map_err(|e| { + error!("Error creating refresh token: {:?}", e); + (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response() + }); + let refresh_jwt = match refresh_jwt { + Ok(t) => t, + Err(r) => return r, + }; + + let session_insert = sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)") + .bind(&access_jwt) + .bind(&refresh_jwt) + .bind(&did) + .execute(&mut *tx) + .await; + + if let Err(e) = session_insert { + error!("Error inserting session: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + + if let Err(e) = tx.commit().await { + error!("Error committing transaction: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + + (StatusCode::OK, Json(CreateAccountOutput { + access_jwt, + refresh_jwt, + handle: input.handle, + did, + })).into_response() +} + +#[derive(Deserialize)] +pub struct CreateSessionInput { + pub identifier: String, + pub password: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateSessionOutput { + pub access_jwt: String, + pub refresh_jwt: String, + pub handle: String, + pub did: String, +} + +pub async fn create_session( + State(state): State, + Json(input): Json, +) -> Response { + info!("create_session: identifier='{}'", input.identifier); + + let user_row = sqlx::query("SELECT did, handle, password_hash FROM users WHERE handle = $1 OR email = $1") + .bind(&input.identifier) + .fetch_optional(&state.db) + .await; + + match user_row { + Ok(Some(row)) => { + let stored_hash: String = row.get("password_hash"); + + if verify(&input.password, &stored_hash).unwrap_or(false) { + let did: String = row.get("did"); + let handle: String = row.get("handle"); + + let access_jwt = match crate::auth::create_access_token(&did) { + Ok(t) => t, + Err(e) => { + error!("Failed to create access token: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let refresh_jwt = match crate::auth::create_refresh_token(&did) { + Ok(t) => t, + Err(e) => { + error!("Failed to create refresh token: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let session_insert = sqlx::query("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)") + .bind(&access_jwt) + .bind(&refresh_jwt) + .bind(&did) + .execute(&state.db) + .await; + + match session_insert { + Ok(_) => { + return (StatusCode::OK, Json(CreateSessionOutput { + access_jwt, + refresh_jwt, + handle, + did, + })).into_response(); + }, + Err(e) => { + error!("Failed to insert session: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + } + } else { + warn!("Password verification failed for identifier: {}", input.identifier); + } + }, + Ok(None) => { + warn!("User not found for identifier: {}", input.identifier); + }, + Err(e) => { + error!("Database error fetching user: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + } + + (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid identifier or password"}))).into_response() +} + +pub async fn get_session( + State(state): State, + headers: axum::http::HeaderMap, +) -> Response { + let auth_header = headers.get("Authorization"); + if auth_header.is_none() { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); + } + + let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); + + if let Err(_) = crate::auth::verify_token(&token) { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token"}))).into_response(); + } + + let result = sqlx::query( + r#" + SELECT u.handle, u.did, u.email + FROM sessions s + JOIN users u ON s.did = u.did + WHERE s.access_jwt = $1 + "# + ) + .bind(token) + .fetch_optional(&state.db) + .await; + + match result { + Ok(Some(row)) => { + let handle: String = row.get("handle"); + let did: String = row.get("did"); + let email: String = row.get("email"); + + return (StatusCode::OK, Json(json!({ + "handle": handle, + "did": did, + "email": email, + "didDoc": {} + }))).into_response(); + }, + Ok(None) => { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response(); + }, + Err(e) => { + error!("Database error in get_session: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + } +} + +pub async fn delete_session( + State(state): State, + headers: axum::http::HeaderMap, +) -> Response { + let auth_header = headers.get("Authorization"); + if auth_header.is_none() { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); + } + + let token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); + + let result = sqlx::query("DELETE FROM sessions WHERE access_jwt = $1") + .bind(token) + .execute(&state.db) + .await; + + match result { + Ok(res) => { + if res.rows_affected() > 0 { + return (StatusCode::OK, Json(json!({}))).into_response(); + } + }, + Err(e) => { + error!("Database error in delete_session: {:?}", e); + } + } + + (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed"}))).into_response() +} + +pub async fn refresh_session( + State(state): State, + headers: axum::http::HeaderMap, +) -> Response { + let auth_header = headers.get("Authorization"); + if auth_header.is_none() { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationRequired"}))).into_response(); + } + + let refresh_token = auth_header.unwrap().to_str().unwrap_or("").replace("Bearer ", ""); + + if let Err(_) = crate::auth::verify_token(&refresh_token) { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"}))).into_response(); + } + + let session = sqlx::query("SELECT did FROM sessions WHERE refresh_jwt = $1") + .bind(&refresh_token) + .fetch_optional(&state.db) + .await; + + match session { + Ok(Some(session_row)) => { + let did: String = session_row.get("did"); + let new_access_jwt = match crate::auth::create_access_token(&did) { + Ok(t) => t, + Err(e) => { + error!("Failed to create access token: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + let new_refresh_jwt = match crate::auth::create_refresh_token(&did) { + Ok(t) => t, + Err(e) => { + error!("Failed to create refresh token: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + }; + + let update = sqlx::query("UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3") + .bind(&new_access_jwt) + .bind(&new_refresh_jwt) + .bind(&refresh_token) + .execute(&state.db) + .await; + + match update { + Ok(_) => { + let user = sqlx::query("SELECT handle FROM users WHERE did = $1") + .bind(&did) + .fetch_optional(&state.db) + .await; + + match user { + Ok(Some(u)) => { + let handle: String = u.get("handle"); + return (StatusCode::OK, Json(json!({ + "accessJwt": new_access_jwt, + "refreshJwt": new_refresh_jwt, + "handle": handle, + "did": did + }))).into_response(); + }, + Ok(None) => { + error!("User not found for existing session: {}", did); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + }, + Err(e) => { + error!("Database error fetching user: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + } + }, + Err(e) => { + error!("Database error updating session: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + } + }, + Ok(None) => { + return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"}))).into_response(); + }, + Err(e) => { + error!("Database error fetching session: {:?}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response(); + } + } +} diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..b6442c8 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,59 @@ +use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey, TokenData}; +use serde::{Deserialize, Serialize}; +use chrono::{Utc, Duration}; +use std::env; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + // DID type shit + pub sub: String, + pub exp: usize, + pub iat: usize, + pub scope: String, + pub jti: String, +} + +pub fn create_access_token(did: &str) -> Result { + let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string()); + let expiration = Utc::now() + .checked_add_signed(Duration::minutes(15)) + .expect("valid timestamp") + .timestamp(); + + let claims = Claims { + sub: did.to_owned(), + exp: expiration as usize, + iat: Utc::now().timestamp() as usize, + scope: "access".to_string(), + jti: uuid::Uuid::new_v4().to_string(), + }; + + encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_ref())) +} + +pub fn create_refresh_token(did: &str) -> Result { + let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string()); + let expiration = Utc::now() + .checked_add_signed(Duration::days(7)) + .expect("valid timestamp") + .timestamp(); + + let claims = Claims { + sub: did.to_owned(), + exp: expiration as usize, + iat: Utc::now().timestamp() as usize, + scope: "refresh".to_string(), + jti: uuid::Uuid::new_v4().to_string(), + }; + + encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_ref())) +} + +pub fn verify_token(token: &str) -> Result, jsonwebtoken::errors::Error> { + let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string()); + decode::( + token, + &DecodingKey::from_secret(secret.as_ref()), + &Validation::default(), + ) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..627b446 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,73 @@ +mod api; +mod state; +mod auth; +mod repo; + +use axum::{ + extract::State, + routing::{get, post}, + Router, + Json, + response::IntoResponse, + http::StatusCode, +}; +use serde_json::json; +use std::net::SocketAddr; +use state::AppState; +use tracing::{info, error}; + +#[tokio::main] +async fn main() { + dotenvy::dotenv().ok(); + tracing_subscriber::fmt::init(); + + let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + .expect("Failed to connect to Postgres"); + + sqlx::migrate!("./migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + let state = AppState::new(pool); + + let app = Router::new() + .route("/health", get(health)) + .route("/xrpc/com.atproto.server.describeServer", get(describe_server)) + .route("/xrpc/com.atproto.server.createAccount", post(api::server::create_account)) + .route("/xrpc/com.atproto.server.createSession", post(api::server::create_session)) + .route("/xrpc/com.atproto.server.getSession", get(api::server::get_session)) + .route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session)) + .route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session)) + .route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record)) + .with_state(state); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + info!("listening on {}", addr); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +async fn health(State(state): State) -> impl IntoResponse { + match sqlx::query("SELECT 1").execute(&state.db).await { + Ok(_) => (StatusCode::OK, "OK"), + Err(e) => { + error!("Health check failed: {:?}", e); + (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable") + } + } +} + +async fn describe_server() -> impl IntoResponse { + let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string()); + let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); + + Json(json!({ + "availableUserDomains": domains + })) +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs new file mode 100644 index 0000000..e3b3c88 --- /dev/null +++ b/src/repo/mod.rs @@ -0,0 +1,94 @@ +use jacquard_repo::storage::BlockStore; +use jacquard_repo::error::RepoError; +use jacquard_repo::repo::CommitData; +use cid::Cid; +use sqlx::{PgPool, Row}; +use bytes::Bytes; +use sha2::{Sha256, Digest}; +use multihash::Multihash; + +#[derive(Clone)] +pub struct PostgresBlockStore { + pool: PgPool, +} + +impl PostgresBlockStore { + pub fn new(pool: PgPool) -> Self { + Self { pool } + } +} + +impl BlockStore for PostgresBlockStore { + async fn get(&self, cid: &Cid) -> Result, RepoError> { + let cid_bytes = cid.to_bytes(); + let row = sqlx::query("SELECT data FROM blocks WHERE cid = $1") + .bind(cid_bytes) + .fetch_optional(&self.pool) + .await + .map_err(|e| RepoError::storage(e))?; + + match row { + Some(row) => { + let data: Vec = row.get("data"); + Ok(Some(Bytes::from(data))) + }, + None => Ok(None), + } + } + + async fn put(&self, data: &[u8]) -> Result { + let mut hasher = Sha256::new(); + hasher.update(data); + let hash = hasher.finalize(); + let multihash = Multihash::wrap(0x12, &hash).unwrap(); + let cid = Cid::new_v1(0x71, multihash); + let cid_bytes = cid.to_bytes(); + + sqlx::query("INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING") + .bind(cid_bytes) + .bind(data) + .execute(&self.pool) + .await + .map_err(|e| RepoError::storage(e))?; + + Ok(cid) + } + + async fn has(&self, cid: &Cid) -> Result { + let cid_bytes = cid.to_bytes(); + let row = sqlx::query("SELECT 1 FROM blocks WHERE cid = $1") + .bind(cid_bytes) + .fetch_optional(&self.pool) + .await + .map_err(|e| RepoError::storage(e))?; + + Ok(row.is_some()) + } + + async fn put_many(&self, blocks: impl IntoIterator + Send) -> Result<(), RepoError> { + let blocks: Vec<_> = blocks.into_iter().collect(); + for (cid, data) in blocks { + let cid_bytes = cid.to_bytes(); + sqlx::query("INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING") + .bind(cid_bytes) + .bind(data.as_ref()) + .execute(&self.pool) + .await + .map_err(|e| RepoError::storage(e))?; + } + Ok(()) + } + + async fn get_many(&self, cids: &[Cid]) -> Result>, RepoError> { + let mut results = Vec::new(); + for cid in cids { + results.push(self.get(cid).await?); + } + Ok(results) + } + + async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> { + self.put_many(commit.blocks).await?; + Ok(()) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..de91669 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,15 @@ +use sqlx::PgPool; +use crate::repo::PostgresBlockStore; + +#[derive(Clone)] +pub struct AppState { + pub db: PgPool, + pub block_store: PostgresBlockStore, +} + +impl AppState { + pub fn new(db: PgPool) -> Self { + let block_store = PostgresBlockStore::new(db.clone()); + Self { db, block_store } + } +} diff --git a/tests/actor.rs b/tests/actor.rs new file mode 100644 index 0000000..b0a9b80 --- /dev/null +++ b/tests/actor.rs @@ -0,0 +1,36 @@ +mod common; +use common::*; +use reqwest::StatusCode; + +#[tokio::test] +async fn test_get_profile() { + let client = client(); + let params = [ + ("actor", AUTH_DID), + ]; + let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_search_actors() { + let client = client(); + let params = [ + ("q", "test"), + ("limit", "10"), + ]; + let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..2778057 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,101 @@ +use reqwest::{header, Client, StatusCode}; +use serde_json::{json, Value}; +use chrono::Utc; +#[allow(unused_imports)] +use std::collections::HashMap; +#[allow(unused_imports)] +use std::time::Duration; + +pub const BASE_URL: &str = "http://127.0.0.1:3000"; +#[allow(dead_code)] +pub const AUTH_TOKEN: &str = "test-token"; +#[allow(dead_code)] +pub const BAD_AUTH_TOKEN: &str = "bad-token"; +#[allow(dead_code)] +pub const AUTH_DID: &str = "did:plc:fake"; +#[allow(dead_code)] +pub const TARGET_DID: &str = "did:plc:target"; + +pub fn client() -> Client { + Client::new() +} + +#[allow(dead_code)] +pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value { + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + .header(header::CONTENT_TYPE, mime) + .bearer_auth(AUTH_TOKEN) + .body(data) + .send() + .await + .expect("Failed to send uploadBlob request"); + + assert_eq!(res.status(), StatusCode::OK, "Failed to upload blob"); + let body: Value = res.json().await.expect("Blob upload response was not JSON"); + body["blob"].clone() +} + + +#[allow(dead_code)] +pub async fn create_test_post( + client: &Client, + text: &str, + reply_to: Option +) -> (String, String, String) { + let collection = "app.bsky.feed.post"; + let mut record = json!({ + "$type": collection, + "text": text, + "createdAt": Utc::now().to_rfc3339() + }); + + if let Some(reply_obj) = reply_to { + record["reply"] = reply_obj; + } + + let payload = json!({ + "repo": AUTH_DID, + "collection": collection, + "record": record + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&payload) + .send() + .await + .expect("Failed to send createRecord"); + + assert_eq!(res.status(), StatusCode::OK, "Failed to create post record"); + let body: Value = res.json().await.expect("createRecord response was not JSON"); + + let uri = body["uri"].as_str().expect("Response had no URI").to_string(); + let cid = body["cid"].as_str().expect("Response had no CID").to_string(); + let rkey = uri.split('/').last().expect("URI was malformed").to_string(); + + (uri, cid, rkey) +} + +pub async fn create_account_and_login(client: &Client) -> (String, String) { + let handle = format!("user_{}", uuid::Uuid::new_v4()); + let payload = json!({ + "handle": handle, + "email": format!("{}@example.com", handle), + "password": "password" + }); + + let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + .json(&payload) + .send() + .await + .expect("Failed to create account"); + + if res.status() != StatusCode::OK { + panic!("Failed to create account: {:?}", res.text().await); + } + + let body: Value = res.json().await.expect("Invalid JSON"); + let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string(); + let did = body["did"].as_str().expect("No did").to_string(); + (access_jwt, did) +} diff --git a/tests/feed.rs b/tests/feed.rs new file mode 100644 index 0000000..c0de0f5 --- /dev/null +++ b/tests/feed.rs @@ -0,0 +1,53 @@ +mod common; +use common::*; +use reqwest::StatusCode; + +use std::collections::HashMap; + +#[tokio::test] +async fn test_get_timeline() { + let client = client(); + let params = [("limit", "30")]; + let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_get_author_feed() { + let client = client(); + let params = [ + ("actor", AUTH_DID), + ("limit", "30") + ]; + let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_get_post_thread() { + let client = client(); + let mut params = HashMap::new(); + params.insert("uri", "at://did:plc:other/app.bsky.feed.post/3k12345"); + params.insert("depth", "5"); + + let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} diff --git a/tests/graph.rs b/tests/graph.rs new file mode 100644 index 0000000..57d34c6 --- /dev/null +++ b/tests/graph.rs @@ -0,0 +1,68 @@ +mod common; +use common::*; +use reqwest::StatusCode; + +#[tokio::test] +async fn test_get_follows() { + let client = client(); + let params = [ + ("actor", AUTH_DID), + ]; + let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_get_followers() { + let client = client(); + let params = [ + ("actor", AUTH_DID), + ]; + let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_get_mutes() { + let client = client(); + let params = [ + ("limit", "25"), + ]; + let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +// User blocks, ie. not repo blocks ya know +async fn test_get_user_blocks() { + let client = client(); + let params = [ + ("limit", "25"), + ]; + let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} diff --git a/tests/identity.rs b/tests/identity.rs new file mode 100644 index 0000000..9766ed9 --- /dev/null +++ b/tests/identity.rs @@ -0,0 +1,18 @@ +mod common; +use common::*; +use reqwest::StatusCode; + +#[tokio::test] +async fn test_resolve_handle() { + let client = client(); + let params = [ + ("handle", "bsky.app"), + ]; + let res = client.get(format!("{}/xrpc/com.atproto.identity.resolveHandle", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs new file mode 100644 index 0000000..8380e77 --- /dev/null +++ b/tests/lifecycle.rs @@ -0,0 +1,936 @@ +mod common; +use common::*; + +use reqwest::StatusCode; +use serde_json::{json, Value}; +use chrono::Utc; +use std::time::Duration; + +use reqwest::Client; +#[allow(unused_imports)] +use std::collections::HashMap; + +#[tokio::test] +async fn test_post_crud_lifecycle() { + let client = client(); + let collection = "app.bsky.feed.post"; + + let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis()); + let now = Utc::now().to_rfc3339(); + + let original_text = "Hello from the lifecycle test!"; + let create_payload = json!({ + "repo": AUTH_DID, + "collection": collection, + "rkey": rkey, + "record": { + "$type": collection, + "text": original_text, + "createdAt": now + } + }); + + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&create_payload) + .send() + .await + .expect("Failed to send create request"); + + assert_eq!(create_res.status(), StatusCode::OK, "Failed to create record"); + let create_body: Value = create_res.json().await.expect("create response was not JSON"); + let uri = create_body["uri"].as_str().unwrap(); + + + let params = [ + ("repo", AUTH_DID), + ("collection", collection), + ("rkey", &rkey), + ]; + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send get request"); + + assert_eq!(get_res.status(), StatusCode::OK, "Failed to get record after create"); + let get_body: Value = get_res.json().await.expect("get response was not JSON"); + assert_eq!(get_body["uri"], uri); + assert_eq!(get_body["value"]["text"], original_text); + + + let updated_text = "This post has been updated."; + let update_payload = json!({ + "repo": AUTH_DID, + "collection": collection, + "rkey": rkey, + "record": { + "$type": collection, + "text": updated_text, + "createdAt": now + } + }); + + let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&update_payload) + .send() + .await + .expect("Failed to send update request"); + + assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record"); + + + let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send get-after-update request"); + + assert_eq!(get_updated_res.status(), StatusCode::OK, "Failed to get record after update"); + let get_updated_body: Value = get_updated_res.json().await.expect("get-updated response was not JSON"); + assert_eq!(get_updated_body["value"]["text"], updated_text, "Text was not updated"); + + + let delete_payload = json!({ + "repo": AUTH_DID, + "collection": collection, + "rkey": rkey + }); + + let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&delete_payload) + .send() + .await + .expect("Failed to send delete request"); + + assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record"); + + + let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send get-after-delete request"); + + assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Record was found, but it should be deleted"); +} + +#[tokio::test] +async fn test_post_with_image_lifecycle() { + let client = client(); + + let now_str = Utc::now().to_rfc3339(); + let fake_image_data = format!("This is a fake PNG for test at {}", now_str); + + let image_blob = upload_test_blob( + &client, + Box::leak(fake_image_data.into_boxed_str()), + "image/png" + ).await; + + let blob_ref = image_blob["ref"].clone(); + assert!(blob_ref.is_object(), "Blob ref is not an object"); + + + let collection = "app.bsky.feed.post"; + let rkey = format!("e2e_image_post_{}", Utc::now().timestamp_millis()); + + let create_payload = json!({ + "repo": AUTH_DID, + "collection": collection, + "rkey": rkey, + "record": { + "$type": collection, + "text": "Check out this image!", + "createdAt": Utc::now().to_rfc3339(), + "embed": { + "$type": "app.bsky.embed.images", + "images": [ + { + "image": image_blob, + "alt": "A test image" + } + ] + } + } + }); + + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&create_payload) + .send() + .await + .expect("Failed to create image post"); + + assert_eq!(create_res.status(), StatusCode::OK, "Failed to create post with image"); + + + let params = [ + ("repo", AUTH_DID), + ("collection", collection), + ("rkey", &rkey), + ]; + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to get image post"); + + assert_eq!(get_res.status(), StatusCode::OK, "Failed to get image post"); + let get_body: Value = get_res.json().await.expect("get image post was not JSON"); + + let embed_image = &get_body["value"]["embed"]["images"][0]["image"]; + assert!(embed_image.is_object(), "Embedded image is missing"); + assert_eq!(embed_image["ref"], blob_ref, "Embedded blob ref does not match uploaded ref"); +} + +#[tokio::test] +async fn test_graph_lifecycle_follow_unfollow() { + let client = client(); + let collection = "app.bsky.graph.follow"; + + let create_payload = json!({ + "repo": AUTH_DID, + "collection": collection, + // "rkey" is omitted, server will generate it right? + "record": { + "$type": collection, + "subject": TARGET_DID, + "createdAt": Utc::now().to_rfc3339() + } + }); + + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&create_payload) + .send() + .await + .expect("Failed to send follow createRecord"); + + assert_eq!(create_res.status(), StatusCode::OK, "Failed to create follow record"); + let create_body: Value = create_res.json().await.expect("create follow response was not JSON"); + let follow_uri = create_body["uri"].as_str().expect("Response had no URI"); + + let rkey = follow_uri.split('/').last().expect("URI was malformed"); + + + let params_get_follows = [ + ("actor", AUTH_DID), + ]; + let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) + .query(¶ms_get_follows) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send getFollows"); + + assert_eq!(get_follows_res.status(), StatusCode::OK, "getFollows did not return 200"); + let get_follows_body: Value = get_follows_res.json().await.expect("getFollows response was not JSON"); + + let follows_list = get_follows_body["follows"].as_array().expect("follows key was not an array"); + let is_following = follows_list.iter().any(|actor| { + actor["did"].as_str() == Some(TARGET_DID) + }); + + assert!(is_following, "getFollows list did not contain the target DID"); + + + let delete_payload = json!({ + "repo": AUTH_DID, + "collection": collection, + "rkey": rkey + }); + + let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&delete_payload) + .send() + .await + .expect("Failed to send unfollow deleteRecord"); + + assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record"); + + + let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) + .query(¶ms_get_follows) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send getFollows after delete"); + + assert_eq!(get_unfollowed_res.status(), StatusCode::OK, "getFollows (after delete) did not return 200"); + let get_unfollowed_body: Value = get_unfollowed_res.json().await.expect("getFollows (after delete) was not JSON"); + + let follows_list_after = get_unfollowed_body["follows"].as_array().expect("follows key was not an array"); + let is_still_following = follows_list_after.iter().any(|actor| { + actor["did"].as_str() == Some(TARGET_DID) + }); + + assert!(!is_still_following, "getFollows list *still* contains the target DID after unfollow"); +} + +#[tokio::test] +async fn test_list_records_pagination() { + let client = client(); + let collection = "app.bsky.feed.post"; + let mut created_rkeys = Vec::new(); + + for i in 0..3 { + let rkey = format!("e2e_pagination_{}", Utc::now().timestamp_millis()); + let payload = json!({ + "repo": AUTH_DID, + "collection": collection, + "rkey": rkey, + "record": { + "$type": collection, + "text": format!("Pagination test post #{}", i), + "createdAt": Utc::now().to_rfc3339() + } + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&payload) + .send() + .await + .expect("Failed to create pagination post"); + + assert_eq!(res.status(), StatusCode::OK, "Failed to create post for pagination test"); + created_rkeys.push(rkey); + tokio::time::sleep(Duration::from_millis(10)).await; + } + + let params_page1 = [ + ("repo", AUTH_DID), + ("collection", collection), + ("limit", "2"), + ]; + + let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) + .query(¶ms_page1) + .send() + .await + .expect("Failed to send listRecords (page 1)"); + + assert_eq!(page1_res.status(), StatusCode::OK, "listRecords (page 1) failed"); + let page1_body: Value = page1_res.json().await.expect("listRecords (page 1) was not JSON"); + + let page1_records = page1_body["records"].as_array().expect("records was not an array"); + assert_eq!(page1_records.len(), 2, "Page 1 did not return 2 records"); + + let cursor = page1_body["cursor"].as_str().expect("Page 1 did not have a cursor"); + + + let params_page2 = [ + ("repo", AUTH_DID), + ("collection", collection), + ("limit", "2"), + ("cursor", cursor), + ]; + + let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) + .query(¶ms_page2) + .send() + .await + .expect("Failed to send listRecords (page 2)"); + + assert_eq!(page2_res.status(), StatusCode::OK, "listRecords (page 2) failed"); + let page2_body: Value = page2_res.json().await.expect("listRecords (page 2) was not JSON"); + + let page2_records = page2_body["records"].as_array().expect("records was not an array"); + assert_eq!(page2_records.len(), 1, "Page 2 did not return 1 record"); + + assert!(page2_body["cursor"].is_null() || page2_body["cursor"].as_str().is_none(), "Page 2 should not have a cursor"); + + + for rkey in created_rkeys { + let delete_payload = json!({ + "repo": AUTH_DID, + "collection": collection, + "rkey": rkey + }); + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&delete_payload) + .send() + .await + .expect("Failed to cleanup pagination post"); + } +} + +#[tokio::test] +async fn test_reply_thread_lifecycle() { + let client = client(); + + let (root_uri, root_cid, root_rkey) = create_test_post( + &client, + "This is the root of the thread", + None + ).await; + + + let reply_ref = json!({ + "root": { "uri": root_uri.clone(), "cid": root_cid.clone() }, + "parent": { "uri": root_uri.clone(), "cid": root_cid.clone() } + }); + + let (reply_uri, _reply_cid, reply_rkey) = create_test_post( + &client, + "This is a reply!", + Some(reply_ref) + ).await; + + + let params = [ + ("uri", &root_uri), + ]; + let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send getPostThread"); + + assert_eq!(res.status(), StatusCode::OK, "getPostThread did not return 200"); + let body: Value = res.json().await.expect("getPostThread response was not JSON"); + + assert_eq!(body["thread"]["$type"], "app.bsky.feed.defs#threadViewPost"); + assert_eq!(body["thread"]["post"]["uri"], root_uri); + + let replies = body["thread"]["replies"].as_array().expect("replies was not an array"); + assert!(!replies.is_empty(), "Replies array is empty, but should contain the reply"); + + let found_reply = replies.iter().find(|r| { + r["post"]["uri"] == reply_uri + }); + + assert!(found_reply.is_some(), "Our specific reply was not found in the thread's replies"); + + + let collection = "app.bsky.feed.post"; + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey })) + .send().await.expect("Failed to delete reply"); + + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey })) + .send().await.expect("Failed to delete root post"); +} + +#[tokio::test] +async fn test_account_journey_lifecycle() { + let client = client(); + + let ts = Utc::now().timestamp_millis(); + let handle = format!("e2e-user-{}.test", ts); + let email = format!("e2e-user-{}@test.com", ts); + let password = "e2e-password-123"; + + let create_account_payload = json!({ + "handle": handle, + "email": email, + "password": password + }); + + let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + .json(&create_account_payload) + .send() + .await + .expect("Failed to send createAccount"); + + assert_eq!(create_res.status(), StatusCode::OK, "Failed to create account"); + let create_body: Value = create_res.json().await.expect("createAccount response was not JSON"); + + let new_did = create_body["did"].as_str().expect("Response had no DID").to_string(); + let _new_jwt = create_body["accessJwt"].as_str().expect("Response had no accessJwt").to_string(); + assert_eq!(create_body["handle"], handle); + + + let session_payload = json!({ + "identifier": handle, + "password": password + }); + + let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) + .json(&session_payload) + .send() + .await + .expect("Failed to send createSession"); + + assert_eq!(session_res.status(), StatusCode::OK, "Failed to create session"); + let session_body: Value = session_res.json().await.expect("createSession response was not JSON"); + + let session_jwt = session_body["accessJwt"].as_str().expect("Session response had no accessJwt").to_string(); + assert_eq!(session_body["did"], new_did); + + + let profile_payload = json!({ + "repo": new_did, + "collection": "app.bsky.actor.profile", + "rkey": "self", // The rkey for a profile is always "self" + "record": { + "$type": "app.bsky.actor.profile", + "displayName": "E2E Test User", + "description": "A user created by the e2e test suite." + } + }); + + let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(&session_jwt) + .json(&profile_payload) + .send() + .await + .expect("Failed to send putRecord for profile"); + + assert_eq!(profile_res.status(), StatusCode::OK, "Failed to create profile"); + + + let params_get_profile = [ + ("actor", &handle), + ]; + let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL)) + .query(¶ms_get_profile) + .send() + .await + .expect("Failed to send getProfile"); + + assert_eq!(get_profile_res.status(), StatusCode::OK, "getProfile did not return 200"); + let profile_body: Value = get_profile_res.json().await.expect("getProfile response was not JSON"); + + assert_eq!(profile_body["did"], new_did); + assert_eq!(profile_body["handle"], handle); + assert_eq!(profile_body["displayName"], "E2E Test User"); + + + let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL)) + .bearer_auth(&session_jwt) + .send() + .await + .expect("Failed to send deleteSession"); + + assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session"); + + + let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL)) + .bearer_auth(&session_jwt) + .send() + .await + .expect("Failed to send getSession"); + + assert_eq!(get_session_res.status(), StatusCode::UNAUTHORIZED, "Session was still valid after logout"); +} + +async fn setup_new_user(handle_prefix: &str) -> (String, String) { + let client = client(); + let ts = Utc::now().timestamp_millis(); + let handle = format!("{}-{}.test", handle_prefix, ts); + let email = format!("{}-{}@test.com", handle_prefix, ts); + let password = "e2e-password-123"; + + let create_account_payload = json!({ + "handle": handle, + "email": email, + "password": password + }); + let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + .json(&create_account_payload) + .send() + .await + .expect("setup_new_user: Failed to send createAccount"); + assert_eq!(create_res.status(), StatusCode::OK, "setup_new_user: Failed to create account"); + let create_body: Value = create_res.json().await.expect("setup_new_user: createAccount response was not JSON"); + + let new_did = create_body["did"].as_str().expect("setup_new_user: Response had no DID").to_string(); + let new_jwt = create_body["accessJwt"].as_str().expect("setup_new_user: Response had no accessJwt").to_string(); + + let profile_payload = json!({ + "repo": new_did.clone(), + "collection": "app.bsky.actor.profile", + "rkey": "self", + "record": { + "$type": "app.bsky.actor.profile", + "displayName": format!("E2E User {}", handle), + "description": "A user created by the e2e test suite." + } + }); + let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(&new_jwt) + .json(&profile_payload) + .send() + .await + .expect("setup_new_user: Failed to send putRecord for profile"); + assert_eq!(profile_res.status(), StatusCode::OK, "setup_new_user: Failed to create profile"); + + (new_did, new_jwt) +} + +async fn create_record_as( + client: &Client, + jwt: &str, + did: &str, + collection: &str, + record: Value, +) -> (String, String) { + let payload = json!({ + "repo": did, + "collection": collection, + "record": record + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + .bearer_auth(jwt) + .json(&payload) + .send() + .await + .expect("create_record_as: Failed to send createRecord"); + + assert_eq!(res.status(), StatusCode::OK, "create_record_as: Failed to create record"); + let body: Value = res.json().await.expect("create_record_as: response was not JSON"); + + let uri = body["uri"].as_str().expect("create_record_as: Response had no URI").to_string(); + let cid = body["cid"].as_str().expect("create_record_as: Response had no CID").to_string(); + (uri, cid) +} + +async fn delete_record_as( + client: &Client, + jwt: &str, + did: &str, + collection: &str, + rkey: &str, +) { + let payload = json!({ + "repo": did, + "collection": collection, + "rkey": rkey + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + .bearer_auth(jwt) + .json(&payload) + .send() + .await + .expect("delete_record_as: Failed to send deleteRecord"); + + assert_eq!(res.status(), StatusCode::OK, "delete_record_as: Failed to delete record"); +} + + +#[tokio::test] +async fn test_notification_lifecycle() { + let client = client(); + + let (user_a_did, user_a_jwt) = setup_new_user("user-a-notif").await; + let (user_b_did, user_b_jwt) = setup_new_user("user-b-notif").await; + + let (post_uri, post_cid) = create_record_as( + &client, + &user_a_jwt, + &user_a_did, + "app.bsky.feed.post", + json!({ + "$type": "app.bsky.feed.post", + "text": "A post to be notified about", + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + let post_ref = json!({ "uri": post_uri, "cid": post_cid }); + + let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) + .bearer_auth(&user_a_jwt) + .send().await.expect("getUnreadCount 1 failed"); + let count_body_1: Value = count_res_1.json().await.expect("count 1 not json"); + assert_eq!(count_body_1["count"], 0, "Initial unread count was not 0"); + + create_record_as( + &client, &user_b_jwt, &user_b_did, + "app.bsky.graph.follow", + json!({ + "$type": "app.bsky.graph.follow", + "subject": user_a_did, + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + create_record_as( + &client, &user_b_jwt, &user_b_did, + "app.bsky.feed.like", + json!({ + "$type": "app.bsky.feed.like", + "subject": post_ref, + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + create_record_as( + &client, &user_b_jwt, &user_b_did, + "app.bsky.feed.post", + json!({ + "$type": "app.bsky.feed.post", + "text": "This is a reply!", + "reply": { "root": post_ref.clone(), "parent": post_ref.clone() }, + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + + tokio::time::sleep(Duration::from_millis(500)).await; + + let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) + .bearer_auth(&user_a_jwt) + .send().await.expect("getUnreadCount 2 failed"); + let count_body_2: Value = count_res_2.json().await.expect("count 2 not json"); + assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions"); + + let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL)) + .bearer_auth(&user_a_jwt) + .send().await.expect("listNotifications failed"); + let list_body: Value = list_res.json().await.expect("list not json"); + + let notifs = list_body["notifications"].as_array().expect("notifications not array"); + assert_eq!(notifs.len(), 3, "Notification list did not have 3 items"); + + let has_follow = notifs.iter().any(|n| n["reason"] == "follow" && n["author"]["did"] == user_b_did); + let has_like = notifs.iter().any(|n| n["reason"] == "like" && n["author"]["did"] == user_b_did); + let has_reply = notifs.iter().any(|n| n["reason"] == "reply" && n["author"]["did"] == user_b_did); + + assert!(has_follow, "Notification list missing 'follow'"); + assert!(has_like, "Notification list missing 'like'"); + assert!(has_reply, "Notification list missing 'reply'"); + + let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) + .bearer_auth(&user_a_jwt) + .send().await.expect("getUnreadCount 3 failed"); + let count_body_3: Value = count_res_3.json().await.expect("count 3 not json"); + assert_eq!(count_body_3["count"], 0, "Unread count was not 0 after list"); +} + + +#[tokio::test] +async fn test_mute_lifecycle_filters_feed() { + let client = client(); + + let (user_a_did, user_a_jwt) = setup_new_user("user-a-mute").await; + let (user_b_did, user_b_jwt) = setup_new_user("user-b-mute").await; + + let (post_uri, _) = create_record_as( + &client, + &user_b_jwt, + &user_b_did, + "app.bsky.feed.post", + json!({ + "$type": "app.bsky.feed.post", + "text": "A post from User B", + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + + let feed_params_1 = [("actor", &user_b_did)]; + let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) + .query(&feed_params_1) + .bearer_auth(&user_a_jwt) + .send().await.expect("getAuthorFeed 1 failed"); + let feed_body_1: Value = feed_res_1.json().await.expect("feed 1 not json"); + + let feed_1 = feed_body_1["feed"].as_array().expect("feed 1 not array"); + let found_post_1 = feed_1.iter().any(|p| p["post"]["uri"] == post_uri); + assert!(found_post_1, "User B's post was not in their feed before mute"); + + let (mute_uri, _) = create_record_as( + &client, &user_a_jwt, &user_a_did, + "app.bsky.graph.mute", + json!({ + "$type": "app.bsky.graph.mute", + "subject": user_b_did, + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + let mute_rkey = mute_uri.split('/').last().unwrap(); + + let feed_params_2 = [("actor", &user_b_did)]; + let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) + .query(&feed_params_2) + .bearer_auth(&user_a_jwt) + .send().await.expect("getAuthorFeed 2 failed"); + let feed_body_2: Value = feed_res_2.json().await.expect("feed 2 not json"); + + let feed_2 = feed_body_2["feed"].as_array().expect("feed 2 not array"); + assert!(feed_2.is_empty(), "User B's feed was not empty after mute"); + + delete_record_as( + &client, &user_a_jwt, &user_a_did, + "app.bsky.graph.mute", + mute_rkey, + ).await; + + let feed_params_3 = [("actor", &user_b_did)]; + let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) + .query(&feed_params_3) + .bearer_auth(&user_a_jwt) + .send().await.expect("getAuthorFeed 3 failed"); + let feed_body_3: Value = feed_res_3.json().await.expect("feed 3 not json"); + + let feed_3 = feed_body_3["feed"].as_array().expect("feed 3 not array"); + let found_post_3 = feed_3.iter().any(|p| p["post"]["uri"] == post_uri); + assert!(found_post_3, "User B's post did not reappear after unmute"); +} + + +#[tokio::test] +async fn test_record_update_conflict_lifecycle() { + let client = client(); + + let (user_did, user_jwt) = setup_new_user("user-conflict").await; + + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + .query(&[ + ("repo", &user_did), + ("collection", &"app.bsky.actor.profile".to_string()), + ("rkey", &"self".to_string()), + ]) + .send().await.expect("getRecord failed"); + let get_body: Value = get_res.json().await.expect("getRecord not json"); + let cid_v1 = get_body["cid"].as_str().expect("Profile v1 had no CID").to_string(); + + let update_payload_v2 = json!({ + "repo": user_did, + "collection": "app.bsky.actor.profile", + "rkey": "self", + "record": { + "$type": "app.bsky.actor.profile", + "displayName": "Updated Name (v2)" + }, + "swapCommit": cid_v1 // <-- Correctly point to v1 + }); + let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(&user_jwt) + .json(&update_payload_v2) + .send().await.expect("putRecord v2 failed"); + assert_eq!(update_res_v2.status(), StatusCode::OK, "v2 update failed"); + let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); + let cid_v2 = update_body_v2["cid"].as_str().expect("v2 response had no CID").to_string(); + + let update_payload_v3_stale = json!({ + "repo": user_did, + "collection": "app.bsky.actor.profile", + "rkey": "self", + "record": { + "$type": "app.bsky.actor.profile", + "displayName": "Stale Update (v3)" + }, + "swapCommit": cid_v1 + }); + let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(&user_jwt) + .json(&update_payload_v3_stale) + .send().await.expect("putRecord v3 (stale) failed"); + + assert_eq!( + update_res_v3_stale.status(), + StatusCode::CONFLICT, + "Stale update did not cause a 409 Conflict" + ); + + let update_payload_v3_good = json!({ + "repo": user_did, + "collection": "app.bsky.actor.profile", + "rkey": "self", + "record": { + "$type": "app.bsky.actor.profile", + "displayName": "Good Update (v3)" + }, + "swapCommit": cid_v2 // <-- Correct + }); + let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(&user_jwt) + .json(&update_payload_v3_good) + .send().await.expect("putRecord v3 (good) failed"); + + assert_eq!(update_res_v3_good.status(), StatusCode::OK, "v3 (good) update failed"); +} + + +#[tokio::test] +async fn test_complex_thread_deletion_lifecycle() { + let client = client(); + + let (user_a_did, user_a_jwt) = setup_new_user("user-a-thread").await; + let (user_b_did, user_b_jwt) = setup_new_user("user-b-thread").await; + let (user_c_did, user_c_jwt) = setup_new_user("user-c-thread").await; + + let (p1_uri, p1_cid) = create_record_as( + &client, &user_a_jwt, &user_a_did, + "app.bsky.feed.post", + json!({ + "$type": "app.bsky.feed.post", + "text": "P1 (Root)", + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + let p1_ref = json!({ "uri": p1_uri.clone(), "cid": p1_cid.clone() }); + + let (p2_uri, p2_cid) = create_record_as( + &client, &user_b_jwt, &user_b_did, + "app.bsky.feed.post", + json!({ + "$type": "app.bsky.feed.post", + "text": "P2 (Reply)", + "reply": { "root": p1_ref.clone(), "parent": p1_ref.clone() }, + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + let p2_ref = json!({ "uri": p2_uri.clone(), "cid": p2_cid.clone() }); + let p2_rkey = p2_uri.split('/').last().unwrap().to_string(); + + let (p3_uri, _) = create_record_as( + &client, &user_c_jwt, &user_c_did, + "app.bsky.feed.post", + json!({ + "$type": "app.bsky.feed.post", + "text": "P3 (Grandchild)", + "reply": { "root": p1_ref.clone(), "parent": p2_ref.clone() }, + "createdAt": Utc::now().to_rfc3339() + }), + ).await; + + let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) + .query(&[("uri", &p1_uri)]) + .bearer_auth(&user_a_jwt) + .send().await.expect("getThread 1 failed"); + let thread_body_1: Value = thread_res_1.json().await.expect("thread 1 not json"); + + let p1_replies = thread_body_1["thread"]["replies"].as_array().unwrap(); + assert_eq!(p1_replies.len(), 1, "P1 should have 1 reply"); + assert_eq!(p1_replies[0]["post"]["uri"], p2_uri, "P1's reply is not P2"); + + let p2_replies = p1_replies[0]["replies"].as_array().unwrap(); + assert_eq!(p2_replies.len(), 1, "P2 should have 1 reply"); + assert_eq!(p2_replies[0]["post"]["uri"], p3_uri, "P2's reply is not P3"); + + delete_record_as( + &client, &user_b_jwt, &user_b_did, + "app.bsky.feed.post", + &p2_rkey, + ).await; + + let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) + .query(&[("uri", &p1_uri)]) + .bearer_auth(&user_a_jwt) + .send().await.expect("getThread 2 failed"); + let thread_body_2: Value = thread_res_2.json().await.expect("thread 2 not json"); + + let p1_replies_2 = thread_body_2["thread"]["replies"].as_array().unwrap(); + assert_eq!(p1_replies_2.len(), 1, "P1 should still have 1 reply (the deleted one)"); + + let deleted_post = &p1_replies_2[0]; + assert_eq!( + deleted_post["$type"], "app.bsky.feed.defs#notFoundPost", + "P2 did not appear as a notFoundPost" + ); + assert_eq!(deleted_post["uri"], p2_uri, "notFoundPost URI does not match P2"); + + let p3_reply = deleted_post["replies"].as_array().unwrap(); + assert_eq!(p3_reply.len(), 1, "notFoundPost should still have P3 as a reply"); + assert_eq!(p3_reply[0]["post"]["uri"], p3_uri, "The reply to the deleted post is not P3"); +} diff --git a/tests/notification.rs b/tests/notification.rs new file mode 100644 index 0000000..ced79a4 --- /dev/null +++ b/tests/notification.rs @@ -0,0 +1,31 @@ +mod common; +use common::*; +use reqwest::StatusCode; + +#[tokio::test] +async fn test_list_notifications() { + let client = client(); + let params = [ + ("limit", "30"), + ]; + let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL)) + .query(¶ms) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_get_unread_count() { + let client = client(); + let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} diff --git a/tests/repo.rs b/tests/repo.rs new file mode 100644 index 0000000..512cf32 --- /dev/null +++ b/tests/repo.rs @@ -0,0 +1,354 @@ +mod common; +use common::*; + +use reqwest::{header, StatusCode}; +use serde_json::{json, Value}; +use chrono::Utc; + +#[tokio::test] +#[ignore] +async fn test_get_record() { + let client = client(); + let params = [ + ("repo", "did:plc:12345"), + ("collection", "app.bsky.actor.profile"), + ("rkey", "self"), + ]; + + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert_eq!(body["value"]["$type"], "app.bsky.actor.profile"); +} + +#[tokio::test] +#[ignore] +async fn test_get_record_not_found() { + let client = client(); + let params = [ + ("repo", "did:plc:12345"), + ("collection", "app.bsky.feed.post"), + ("rkey", "nonexistent"), + ]; + + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::NOT_FOUND); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert_eq!(body["error"], "NotFound"); +} + +#[tokio::test] +#[ignore] +async fn test_upload_blob_no_auth() { + let client = client(); + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + .header(header::CONTENT_TYPE, "text/plain") + .body("no auth") + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert_eq!(body["error"], "AuthenticationFailed"); +} + +#[tokio::test] +#[ignore] +async fn test_upload_blob_success() { + let client = client(); + let (token, _) = create_account_and_login(&client).await; + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + .header(header::CONTENT_TYPE, "text/plain") + .bearer_auth(token) + .body("This is our blob data") + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert!(body["blob"]["ref"]["$link"].as_str().is_some()); +} + +#[tokio::test] +#[ignore] +async fn test_put_record_no_auth() { + let client = client(); + let payload = json!({ + "repo": "did:plc:123", + "collection": "app.bsky.feed.post", + "rkey": "fake", + "record": {} + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .json(&payload) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert_eq!(body["error"], "AuthenticationFailed"); +} + +#[tokio::test] +#[ignore] +async fn test_put_record_success() { + let client = client(); + let (token, did) = create_account_and_login(&client).await; + let now = Utc::now().to_rfc3339(); + let payload = json!({ + "repo": did, + "collection": "app.bsky.feed.post", + "rkey": "e2e_test_post", + "record": { + "$type": "app.bsky.feed.post", + "text": "Hello from the e2e test script!", + "createdAt": now + } + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(token) + .json(&payload) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert!(body.get("uri").is_some()); + assert!(body.get("cid").is_some()); +} + +#[tokio::test] +#[ignore] +async fn test_get_record_missing_params() { + let client = client(); + // Missing `collection` and `rkey` + let params = [ + ("repo", "did:plc:12345"), + ]; + + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send request"); + + // This will fail (get 404) until the handler validates query params + assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for missing params"); +} + +#[tokio::test] +#[ignore] +async fn test_upload_blob_bad_token() { + let client = client(); + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + .header(header::CONTENT_TYPE, "text/plain") + .bearer_auth(BAD_AUTH_TOKEN) + .body("This is our blob data") + .send() + .await + .expect("Failed to send request"); + + // This *should* pass if the auth stub is working correctly + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert_eq!(body["error"], "AuthenticationFailed"); +} + +#[tokio::test] +#[ignore] +async fn test_put_record_mismatched_repo() { + let client = client(); + let (token, _) = create_account_and_login(&client).await; + let now = Utc::now().to_rfc3339(); + let payload = json!({ + "repo": "did:plc:OTHER-USER", // This does NOT match AUTH_DID + "collection": "app.bsky.feed.post", + "rkey": "e2e_test_post", + "record": { + "$type": "app.bsky.feed.post", + "text": "Hello from the e2e test script!", + "createdAt": now + } + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(token) + .json(&payload) + .send() + .await + .expect("Failed to send request"); + + // This will fail (get 200) until handler validates repo matches auth + assert_eq!(res.status(), StatusCode::FORBIDDEN, "Expected 403 for mismatched repo and auth"); +} + +#[tokio::test] +#[ignore] +async fn test_put_record_invalid_schema() { + let client = client(); + let (token, did) = create_account_and_login(&client).await; + let now = Utc::now().to_rfc3339(); + let payload = json!({ + "repo": did, + "collection": "app.bsky.feed.post", + "rkey": "e2e_test_invalid", + "record": { + "$type": "app.bsky.feed.post", + // "text" field is missing, this is invalid + "createdAt": now + } + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + .bearer_auth(token) + .json(&payload) + .send() + .await + .expect("Failed to send request"); + + // This will fail (get 200) until handler validates record schema + assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid record schema"); +} + +#[tokio::test] +#[ignore] +async fn test_upload_blob_unsupported_mime_type() { + let client = client(); + let (token, _) = create_account_and_login(&client).await; + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + .header(header::CONTENT_TYPE, "application/xml") + .bearer_auth(token) + .body("not an image") + .send() + .await + .expect("Failed to send request"); + + // This will fail (get 200) until handler validates mime type + assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for unsupported mime type"); +} + +#[tokio::test] +async fn test_list_records() { + let client = client(); + let (_, did) = create_account_and_login(&client).await; + let params = [ + ("repo", did.as_str()), + ("collection", "app.bsky.feed.post"), + ("limit", "10"), + ]; + let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_delete_record() { + let client = client(); + let (token, did) = create_account_and_login(&client).await; + let payload = json!({ + "repo": did, + "collection": "app.bsky.feed.post", + "rkey": "some_post_to_delete" + }); + let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + .bearer_auth(token) + .json(&payload) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_describe_repo() { + let client = client(); + let (_, did) = create_account_and_login(&client).await; + let params = [ + ("repo", did.as_str()), + ]; + let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_create_record_success_with_generated_rkey() { + let client = client(); + let (token, did) = create_account_and_login(&client).await; + let payload = json!({ + "repo": did, + "collection": "app.bsky.feed.post", + "record": { + "$type": "app.bsky.feed.post", + "text": "Hello, world!", + "createdAt": "2025-12-02T12:00:00Z" + } + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + .json(&payload) + .bearer_auth(token) // Assuming auth is required + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Response was not valid JSON"); + let uri = body["uri"].as_str().unwrap(); + assert!(uri.starts_with(&format!("at://{}/app.bsky.feed.post/", did))); + // assert_eq!(body["cid"], "bafyreihy"); // CID is now real +} + +#[tokio::test] +async fn test_create_record_success_with_provided_rkey() { + let client = client(); + let (token, did) = create_account_and_login(&client).await; + let rkey = "custom-rkey"; + let payload = json!({ + "repo": did, + "collection": "app.bsky.feed.post", + "rkey": rkey, + "record": { + "$type": "app.bsky.feed.post", + "text": "Hello, world!", + "createdAt": "2025-12-02T12:00:00Z" + } + }); + + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + .json(&payload) + .bearer_auth(token) // Assuming auth is required + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert_eq!(body["uri"], format!("at://{}/app.bsky.feed.post/{}", did, rkey)); + // assert_eq!(body["cid"], "bafyreihy"); // CID is now real +} diff --git a/tests/server.rs b/tests/server.rs new file mode 100644 index 0000000..e2bb839 --- /dev/null +++ b/tests/server.rs @@ -0,0 +1,164 @@ +mod common; +use common::*; + +use reqwest::StatusCode; +use serde_json::{json, Value}; + +#[tokio::test] +async fn test_health() { + let client = client(); + let res = client.get(format!("{}/health", BASE_URL)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.text().await.unwrap(), "OK"); +} + +#[tokio::test] +async fn test_describe_server() { + let client = client(); + let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", BASE_URL)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert!(body.get("availableUserDomains").is_some()); +} + +#[tokio::test] +async fn test_create_session() { + let client = client(); + + let handle = format!("user_{}", uuid::Uuid::new_v4()); + let payload = json!({ + "handle": handle, + "email": format!("{}@example.com", handle), + "password": "password" + }); + let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + .json(&payload) + .send() + .await; + + let payload = json!({ + "identifier": handle, + "password": "password" + }); + + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) + .json(&payload) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Response was not valid JSON"); + assert!(body.get("accessJwt").is_some()); +} + +#[tokio::test] +async fn test_create_session_missing_identifier() { + let client = client(); + let payload = json!({ + "password": "password" + }); + + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) + .json(&payload) + .send() + .await + .expect("Failed to send request"); + + assert!(res.status() == StatusCode::BAD_REQUEST || res.status() == StatusCode::UNPROCESSABLE_ENTITY, + "Expected 400 or 422 for missing identifier, got {}", res.status()); +} + +#[tokio::test] +async fn test_create_account_invalid_handle() { + let client = client(); + let payload = json!({ + "handle": "invalid!handle.com", + "email": "test@example.com", + "password": "password" + }); + + let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + .json(&payload) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid handle chars"); +} + +#[tokio::test] +async fn test_get_session() { + let client = client(); + let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn test_refresh_session() { + let client = client(); + + let handle = format!("refresh_user_{}", uuid::Uuid::new_v4()); + let payload = json!({ + "handle": handle, + "email": format!("{}@example.com", handle), + "password": "password" + }); + let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + .json(&payload) + .send() + .await; + + let login_payload = json!({ + "identifier": handle, + "password": "password" + }); + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) + .json(&login_payload) + .send() + .await + .expect("Failed to login"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Invalid JSON"); + let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string(); + let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string(); + + let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", BASE_URL)) + .bearer_auth(&refresh_jwt) + .send() + .await + .expect("Failed to refresh"); + + assert_eq!(res.status(), StatusCode::OK); + let body: Value = res.json().await.expect("Invalid JSON"); + assert!(body["accessJwt"].as_str().is_some()); + assert!(body["refreshJwt"].as_str().is_some()); + assert_ne!(body["accessJwt"].as_str().unwrap(), access_jwt); + assert_ne!(body["refreshJwt"].as_str().unwrap(), refresh_jwt); +} + +#[tokio::test] +async fn test_delete_session() { + let client = client(); + let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL)) + .bearer_auth(AUTH_TOKEN) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); +} diff --git a/tests/sync.rs b/tests/sync.rs new file mode 100644 index 0000000..30095eb --- /dev/null +++ b/tests/sync.rs @@ -0,0 +1,34 @@ +mod common; +use common::*; +use reqwest::StatusCode; + +#[tokio::test] +async fn test_get_repo() { + let client = client(); + let params = [ + ("did", AUTH_DID), + ]; + let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_get_blocks() { + let client = client(); + let params = [ + ("did", AUTH_DID), + // "cids" would be a list of CIDs + ]; + let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", BASE_URL)) + .query(¶ms) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(res.status(), StatusCode::OK); +}