From 4711fe85477d21a78893bd6ba399571a992e7b65 Mon Sep 17 00:00:00 2001 From: kaichao Date: Thu, 11 Jun 2026 16:55:32 +0800 Subject: [PATCH] feat: http server for account and keypackages storage (#2) * feat: http server for account and keypackages storage * chore: rename package * feat: change to sqlx * smoke test script * chore: use account pub in code * fix dockerfile --- .dockerignore | 7 + .gitignore | 10 + Cargo.lock | 2354 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 37 + Dockerfile | 44 + README.md | 221 +++- examples/smoke_test.rs | 129 +++ migrations/0001_init.sql | 18 + src/handlers.rs | 255 +++++ src/main.rs | 98 ++ src/store.rs | 316 +++++ 11 files changed, 3488 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 examples/smoke_test.rs create mode 100644 migrations/0001_init.sql create mode 100644 src/handlers.rs create mode 100644 src/main.rs create mode 100644 src/store.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fd87951 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +target +*.db +*.db-shm +*.db-wal +.git +.gitignore +.DS_Store diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5604a2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Rust build artifacts +/target + +# Local SQLite databases created at runtime +*.db +*.db-shm +*.db-wal + +# Editor / OS noise +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2bc4819 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2354 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[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", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +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 = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chat-store" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "base64", + "clap", + "ed25519-dalek", + "hex", + "reqwest", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[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 = "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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[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 = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +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", +] + +[[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 = "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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[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", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[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 = "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 = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[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", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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 = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[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.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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 = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.8.1", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[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.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[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.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[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-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", + "smallvec", + "zeroize", +] + +[[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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[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 0.5.18", + "smallvec", + "windows-link", +] + +[[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 = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[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_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", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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_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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.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 = "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", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[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", +] + +[[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", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-sqlite", + "syn", + "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", + "bitflags", + "byteorder", + "bytes", + "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", + "rsa", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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 = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[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.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +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", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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 = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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 = "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 = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0dc0376 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +# Standalone single-crate workspace: keeps this repo independent of any parent +# Cargo workspace it might be checked out beneath. +[workspace] + +[package] +name = "chat-store" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "chat-store" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" +axum = "0.7" +base64 = "0.22" +clap = { version = "4", features = ["derive"] } +ed25519-dalek = "2.2.0" +hex = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +# SQLite via sqlx with a bundled libsqlite3 — no sqlcipher/OpenSSL, no system libs. +sqlx = { version = "0.8", default-features = false, features = [ + "runtime-tokio", + "sqlite", + "macros", + "migrate", +] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "time"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[dev-dependencies] +# `smoke_test` example only talks to a localhost HTTP server, so no TLS backend +# (and no OpenSSL) is pulled in. +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b0c6088 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# syntax=docker/dockerfile:1 + +######################################## +# Build stage +######################################## +FROM rust:1-bookworm AS builder + +WORKDIR /app + +# Build dependencies first against a stub binary so the (slow) dependency +# compilation — bundled libsqlite3 and the async stack — is cached and only +# re-runs when Cargo.toml/Cargo.lock change. +COPY Cargo.toml Cargo.lock ./ +RUN mkdir src \ + && echo "fn main() {}" > src/main.rs \ + && cargo build --release --locked \ + && rm -rf src target/release/deps/chat_store* target/release/chat-store + +# Now build the real binary; dependency artifacts above are reused. `migrations/` +# is needed at build time too — `sqlx::migrate!()` embeds the .sql files. +COPY src ./src +COPY migrations ./migrations +RUN cargo build --release --locked --bin chat-store + +######################################## +# Runtime stage +######################################## +FROM debian:bookworm-slim AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/chat-store /usr/local/bin/chat-store + +# Matches the default --bind 0.0.0.0:8080. +EXPOSE 8080 + +# Persist the SQLite database on a volume rather than the container layer. +VOLUME ["/data"] +ENV RUST_LOG=info + +ENTRYPOINT ["chat-store"] +CMD ["--bind", "0.0.0.0:8080", "--db", "/data/chat-store.db"] diff --git a/README.md b/README.md index 5cf73d4..90d83c5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,222 @@ # Chat Store -For persistence of group chat users' key package. +Persistence for group-chat users' key packages — the **chat-store** HTTP service +(formerly `keypackage-registry`), extracted from +[libchat](https://github.com/logos-messaging/libchat) so it can be deployed on +its own. + +Standalone HTTP service that caches MLS KeyPackages keyed by **`device_id`**, so a +client can fetch a contact's keypackage without an out-of-band exchange. +Throwaway by design: scheduled to be replaced by a λLEZ-based service in v0.3, so +it intentionally has no overlap with the rest of libchat (axum + rusqlite only). + +`device_id` is the hex-encoded 32-byte Ed25519 verifying key of a device. + +It also runs a minimal **account service**: one signed blob per **`account_pub`** +mapping an Account to its set of device (LocalIdentity) public keys, so clients +can invite every LocalIdentity of an account. `account_pub` is the hex-encoded +32-byte Ed25519 AccountAddress verifying key. See +[Account device-list endpoints](#account-device-list-endpoints). + +## Trust model + +A bundle is an opaque **payload** plus its **signature**, published under a +**`device_id`** (the hex of the device's 32-byte Ed25519 verifying key). +The signed bytes and the wire bytes are identical, so a verifier checks the +signature over exactly what it received, no reconstruction. + +The **server treats `payload` as a black box**: it never decodes it. It only +verifies that `signature` over the payload bytes is valid under `device_id`'s +key, then stores it. A valid signature is proof-of-possession — only the holder +of `device_id`'s key can publish under it — so an adversary can't publish under +a `device_id` it doesn't control, and junk is dropped before storage. The server +is not a trusted authority, so **consumers MUST also verify on retrieve**, and a +valid signature does not prove the device is authorized for any account (that +binding arrives with λLEZ in v0.3). + +Consumers define the payload layout. Today it is: + +```text +payload = timestamp_ms_le[8] || key_package[..] +``` + +Fixed-width field first with the variable `key_package` last makes it parse +exactly one way — no delimiter, even though `key_package` is arbitrary bytes. + +## Building & running + +```bash +cargo build --release +./target/release/chat-store # binds 0.0.0.0:8080, db ./chat-store.db +``` + +| Flag | Default | Description | +|------|---------|-------------| +| `--bind ` | `0.0.0.0:8080` | HTTP bind address | +| `--db ` | `chat-store.db` | SQLite database path | +| `--max-per-identity ` | `100` | Bundles retained per `device_id` | +| `--retention-days ` | `30` | Drop bundles older than this | +| `--prune-interval-secs ` | `3600` | How often the prune task runs | + +Logs via `RUST_LOG` (default `info`). + +## Docker + +```bash +# Build the image +docker build -t chat-store . + +# Run it, persisting the SQLite db on a named volume and exposing port 8080 +docker run --rm -p 8080:8080 -v chat-store-data:/data chat-store +``` + +The image runs the binary with `--bind 0.0.0.0:8080 --db /data/chat-store.db` +by default; override the `CMD` to change flags, e.g.: + +```bash +docker run --rm -p 9000:9000 -v chat-store-data:/data chat-store \ + --bind 0.0.0.0:9000 --db /data/registry.db --retention-days 14 +``` + +## API + +### `POST /v0/keypackage` + +```json +{ + "device_id": "hex(32-byte ed25519 verifying key)", + "payload": "base64(opaque signed bytes)", + "signature": "base64(64-byte ed25519 signature over payload)" +} +``` + +The server verifies `signature` over the (opaque) `payload` bytes under +`device_id`'s key before storing, keyed by `device_id`. It does not decode +`payload`. Returns `204` on success, `400` on malformed input or a signature +that fails to verify. + +### `GET /v0/keypackage/{device_id}` + +Returns the most recently submitted bundle for that `device_id`, or `404`: + +```json +{ + "payload": "base64(...)", + "signature": "base64(64-byte ed25519 signature)" +} +``` + +Consumers verify `signature` over the `payload` bytes using the key recovered +from `device_id`, then read `key_package` out of the payload. A bundle that +fails verification must be treated as not found. + +## Account device-list endpoints + +The account service stores **exactly one blob per `account_pub`** mapping an +Account to its LocalIdentity device keys. Same trust model as keypackages: the +server verifies `signature` over `payload` under `account_pub`'s key +(proof-of-possession), and consumers MUST re-verify on retrieve. Clients encode +a lamport-timestamped list of device public keys in `payload`; the rest of the +payload stays opaque to the server. + +> Anti-replay: the server reads the lamport from the (signature-verified) +> `payload` and replaces the stored bundle only when the incoming lamport is +> strictly higher, returning `409` otherwise. Because the lamport is covered by +> the account signature it cannot be forged, so a replayed older-but-still-valid +> bundle cannot downgrade the device list, nor refresh the retention clock. +> Consumers should still compare lamports themselves as defence in depth. + +### `POST /v0/account` + +Upsert the device-list bundle for an account; replaces any previous value. + +```json +{ + "account_pub": "hex(32-byte ed25519 AccountAddress verifying key)", + "payload": "base64(opaque signed bytes: lamport-ts + device pubkeys)", + "signature": "base64(64-byte ed25519 signature over payload by the account key)" +} +``` + +Returns `204` on success, `400` on malformed input or a signature that fails to +verify, and `409` when the bundle's lamport is not newer than the stored one +(replay / stale publish). + +### `GET /v0/account/{account_pub}` + +Returns the stored bundle for that account, or `404`: + +```json +{ + "payload": "base64(...)", + "signature": "base64(64-byte ed25519 signature)", + "updated_at": 1700000000000 +} +``` + +`updated_at` is the server's last-upsert time in Unix ms. Consumers verify +`signature` over `payload` under `account_pub`'s key, then decode the device list. + +## Storage & retention + +Two SQLite tables: `keypackages` keyed by `device_id`, and `account_bundles` +(one row per `account_pub`). A background task runs every `--prune-interval-secs`, +dropping keypackage bundles older than `--retention-days` (keeping at most +`--max-per-identity` per `device_id`) and dropping account bundles not refreshed +within `--retention-days`. The schema is an internal detail and may change. + +## Smoke test + +The quickest end-to-end check is the bundled [`smoke_test`](examples/smoke_test.rs) +example. It generates throwaway Ed25519 keys, signs and publishes a keypackage and +an account bundle, fetches both back, and confirms the replay guard: + +```bash +# Terminal 1 — start a server with a fresh db +cargo run -- --bind 127.0.0.1:8080 --db tmp/chat-store.db + +# Terminal 2 — run the example against it (defaults to http://127.0.0.1:8080) +cargo run --example smoke_test +# or point it elsewhere: +cargo run --example smoke_test -- http://127.0.0.1:8080 +``` + +Expected output: + +```text +POST /v0/keypackage -> 204 No Content (expect 204) +GET /v0/keypackage/ -> 200 OK (expect 200) {"payload":...,"signature":...} +POST /v0/account -> 204 No Content (expect 204) +GET /v0/account/ -> 200 OK (expect 200) {"payload":...,"signature":...,"updated_at":...} +POST /v0/account (replay) -> 409 Conflict (expect 409) +``` + +You can also exercise it with the real `chat-cli` (which lives in the +[libchat](https://github.com/logos-messaging/libchat) repo) against a running +server: + +```bash +# In this repo: start the server on a test port with a fresh db +cargo run -- --bind 127.0.0.1:18080 --db tmp/registry.db + +# In a libchat checkout: register two identities (--smoketest exits after registering) +cargo build -p chat-cli +./target/debug/chat-cli --name alice --transport file --data tmp/alice \ + --registry-url http://127.0.0.1:18080 --smoketest # exits 0 on success +./target/debug/chat-cli --name bob --transport file --data tmp/bob \ + --registry-url http://127.0.0.1:18080 --smoketest + +# Confirm both bundles landed +sqlite3 tmp/registry.db "SELECT substr(device_id,1,12), length(payload) FROM keypackages;" +``` + +A non-zero exit from `chat-cli` means the server rejected the submission — e.g. +the signature failed verification. `GET /v0/keypackage/{device_id}` returns `200` +for a registered device and `404` otherwise. + +## Lifecycle + +Exists to unblock contact-by-id flows on testnet; removed once λLEZ-based +discovery lands in v0.3. The seam is the `RegistrationService` trait in libchat +(`core/conversations/src/service_traits.rs`) — swapping implementations does not +touch the chat protocol. diff --git a/examples/smoke_test.rs b/examples/smoke_test.rs new file mode 100644 index 0000000..92738e7 --- /dev/null +++ b/examples/smoke_test.rs @@ -0,0 +1,129 @@ +//! End-to-end smoke test for the chat-store HTTP API. +//! +//! Generates throwaway Ed25519 keys, signs a keypackage bundle and an account +//! device-list bundle, POSTs both, then GETs them back. The server verifies the +//! signature over the exact payload bytes, so the bundles must be properly +//! signed — this example does that for you. +//! +//! Run it against a live server: +//! +//! ```text +//! cargo run -- --bind 127.0.0.1:8080 --db tmp/chat-store.db # terminal 1 +//! cargo run --example smoke_test # terminal 2 +//! cargo run --example smoke_test -- http://host:port # custom target +//! ``` + +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Result; +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64; +use ed25519_dalek::{Signer, SigningKey}; +use reqwest::blocking::Client; +use serde_json::json; + +/// Domain-separation prefix the server expects on every account bundle payload. +/// Must match `BUNDLE_DOMAIN` in `src/store.rs` (and libchat's account_directory). +const ACCOUNT_BUNDLE_DOMAIN: &[u8] = b"libchat:account-device-bundle\0"; + +fn main() -> Result<()> { + let base = std::env::args() + .nth(1) + .unwrap_or_else(|| "http://127.0.0.1:8080".to_string()); + let base = base.trim_end_matches('/'); + let client = Client::new(); + + println!("Testing chat-store at {base}"); + test_keypackage(&client, base)?; + test_account(&client, base)?; + Ok(()) +} + +fn test_keypackage(client: &Client, base: &str) -> Result<()> { + let key = signing_key(1); + let device_id = pub_hex(&key); + + // The keypackage payload is opaque to the server: any bytes work as long as + // the signature matches. Real clients put `timestamp_ms_le[8] || key_package`. + let mut payload = 0u64.to_le_bytes().to_vec(); + payload.extend_from_slice(b"hello-keypackage"); + + let resp = client + .post(format!("{base}/v0/keypackage")) + .json(&json!({ + "device_id": device_id, + "payload": BASE64.encode(&payload), + "signature": BASE64.encode(key.sign(&payload).to_bytes()), + })) + .send()?; + println!("POST /v0/keypackage -> {} (expect 204)", resp.status()); + + let resp = client + .get(format!("{base}/v0/keypackage/{device_id}")) + .send()?; + println!( + "GET /v0/keypackage/ -> {} (expect 200) {}", + resp.status(), + resp.text()? + ); + Ok(()) +} + +fn test_account(client: &Client, base: &str) -> Result<()> { + let key = signing_key(2); + let account_pub = pub_hex(&key); + + // The account payload is NOT arbitrary: it must start with the domain prefix, + // a version byte, and an 8-byte little-endian lamport so the server can run + // its replay check. Device pubkeys would follow, but stay opaque to the server. + let lamport: u64 = 1; + let mut payload = ACCOUNT_BUNDLE_DOMAIN.to_vec(); + payload.push(1); // version + payload.extend_from_slice(&lamport.to_le_bytes()); + + let body = json!({ + "account_pub": account_pub, + "payload": BASE64.encode(&payload), + "signature": BASE64.encode(key.sign(&payload).to_bytes()), + }); + + let resp = client + .post(format!("{base}/v0/account")) + .json(&body) + .send()?; + println!("POST /v0/account -> {} (expect 204)", resp.status()); + + let resp = client.get(format!("{base}/v0/account/{account_pub}")).send()?; + println!( + "GET /v0/account/ -> {} (expect 200) {}", + resp.status(), + resp.text()? + ); + + // Re-posting the same lamport must be rejected as a stale replay. + let resp = client + .post(format!("{base}/v0/account")) + .json(&body) + .send()?; + println!("POST /v0/account (replay) -> {} (expect 409)", resp.status()); + Ok(()) +} + +/// A throwaway Ed25519 signing key seeded from the current time (plus a salt so +/// the keypackage and account keys differ), so repeated runs use fresh ids. +fn signing_key(salt: u8) -> SigningKey { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .to_le_bytes(); + let mut seed = [0u8; 32]; + for (i, b) in seed.iter_mut().enumerate() { + *b = nanos[i % nanos.len()] ^ salt ^ (i as u8); + } + SigningKey::from_bytes(&seed) +} + +fn pub_hex(key: &SigningKey) -> String { + hex::encode(key.verifying_key().to_bytes()) +} diff --git a/migrations/0001_init.sql b/migrations/0001_init.sql new file mode 100644 index 0000000..a484841 --- /dev/null +++ b/migrations/0001_init.sql @@ -0,0 +1,18 @@ +-- KeyPackage bundles: history per device, newest read back on fetch. +CREATE TABLE keypackages ( + device_id TEXT NOT NULL, + received_at INTEGER NOT NULL, + payload BLOB NOT NULL, + signature BLOB NOT NULL, + PRIMARY KEY (device_id, received_at) +); + +-- Account device-list bundles: exactly one row per account; newer upserts +-- replace the existing row (compare-and-swap on the payload's lamport). Keyed by +-- the hex-encoded account verifying key. +CREATE TABLE account_bundles ( + account_pub TEXT NOT NULL PRIMARY KEY, + updated_at INTEGER NOT NULL, + payload BLOB NOT NULL, + signature BLOB NOT NULL +); diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..d2e400c --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,255 @@ +use std::sync::Arc; + +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64; +use ed25519_dalek::{Signature, VerifyingKey}; +use serde::{Deserialize, Serialize}; + +use crate::store::{Store, StoredAccountBundle, StoredKeyPackageBundle}; + +#[derive(Debug, Deserialize)] +pub struct SubmitRequest { + /// Hex of the 32-byte Ed25519 device verifying key. Used to verify the + /// signature and as the storage/lookup key. `payload` stays opaque. + pub device_id: String, + /// base64 of the signed payload. Opaque to the server — it never decodes it. + pub payload: String, + /// base64 of the 64-byte Ed25519 signature over `payload`. Verifying it + /// under `device_id`'s key is proof-of-possession: only the holder of that + /// key can publish under this `device_id`. + pub signature: String, +} + +#[derive(Debug, Serialize)] +pub struct FetchResponse { + /// base64 of the stored payload; consumers verify `signature` over it. + pub payload: String, + pub signature: String, +} + +#[derive(Debug, Serialize)] +struct ErrorBody { + error: String, +} + +pub fn router(store: Arc) -> Router { + Router::new() + .route("/v0/keypackage", post(submit)) + .route("/v0/keypackage/:device_id", get(fetch)) + .route("/v0/account", post(submit_account)) + .route("/v0/account/:account_pub", get(fetch_account)) + .with_state(store) +} + +async fn submit( + State(store): State>, + Json(req): Json, +) -> Result { + // Verify proof-of-possession before persisting. `payload` is opaque — the + // server only checks that `signature` over the received payload bytes is + // valid under `device_id`'s key. A valid signature means the submitter holds + // that key. This rejects junk early (DoS mitigation); consumers still verify + // on retrieve, the server is not a trusted authority. + let device_pubkey: [u8; 32] = hex::decode(&req.device_id) + .ok() + .and_then(|b| b.try_into().ok()) + .ok_or_else(|| ApiError::bad("device_id: must be hex of a 32-byte key"))?; + let payload = BASE64 + .decode(&req.payload) + .map_err(|_| ApiError::bad("payload: not valid base64"))?; + let signature: [u8; 64] = BASE64 + .decode(&req.signature) + .ok() + .and_then(|b| b.try_into().ok()) + .ok_or_else(|| ApiError::bad("signature: must be base64 of 64 bytes"))?; + + let verifying_key = VerifyingKey::from_bytes(&device_pubkey) + .map_err(|_| ApiError::bad("device_id: not a valid ed25519 key"))?; + verifying_key + .verify_strict(&payload, &Signature::from_bytes(&signature)) + .map_err(|_| ApiError::bad("signature: verification failed"))?; + + store + .insert( + &req.device_id, + &StoredKeyPackageBundle { + payload, + signature: signature.to_vec(), + }, + ) + .await + .map_err(ApiError::internal)?; + Ok(StatusCode::NO_CONTENT) +} + +async fn fetch( + State(store): State>, + Path(device_id): Path, +) -> Result, ApiError> { + let Some(bundle) = store + .latest(&device_id) + .await + .map_err(ApiError::internal)? + else { + return Err(ApiError::not_found("no keypackage for device")); + }; + Ok(Json(FetchResponse { + payload: BASE64.encode(&bundle.payload), + signature: BASE64.encode(&bundle.signature), + })) +} + +/// Request body for publishing a signed device-list bundle under an account. +/// +/// The `payload` is intentionally opaque to the server. Clients are expected +/// to encode a lamport-timestamped list of device (LocalIdentity) Ed25519 +/// public keys inside it so that consumers can detect stale bundles. The server +/// only verifies that `signature` is a valid Ed25519 signature over `payload` +/// made by the key identified by `account_pub`. +#[derive(Debug, Deserialize)] +pub struct SubmitAccountRequest { + /// Hex of the 32-byte Ed25519 account (AccountAddress) verifying key. + /// Acts as both the storage key and the verification key. + pub account_pub: String, + /// base64 of the opaque signed payload (lamport-ts + device pubkeys, etc.). + pub payload: String, + /// base64 of the 64-byte Ed25519 signature over `payload` made by the + /// account key. Proof-of-possession: only the account holder can publish. + pub signature: String, +} + +#[derive(Debug, Serialize)] +pub struct FetchAccountResponse { + /// base64 of the stored payload. + pub payload: String, + /// base64 of the 64-byte Ed25519 signature. + pub signature: String, + /// Unix timestamp (ms) of the last successful upsert. + pub updated_at: i64, +} + +/// `POST /v0/account` — upsert a signed device-list bundle for an account. +/// +/// The server verifies the Ed25519 signature and then stores exactly one blob +/// per `account_pub`, replacing any previous value. Clients should re-publish +/// whenever they add or rotate LocalIdentities. +async fn submit_account( + State(store): State>, + Json(req): Json, +) -> Result { + let account_pubkey: [u8; 32] = hex::decode(&req.account_pub) + .ok() + .and_then(|b| b.try_into().ok()) + .ok_or_else(|| ApiError::bad("account_pub: must be hex of a 32-byte key"))?; + let payload = BASE64 + .decode(&req.payload) + .map_err(|_| ApiError::bad("payload: not valid base64"))?; + let signature: [u8; 64] = BASE64 + .decode(&req.signature) + .ok() + .and_then(|b| b.try_into().ok()) + .ok_or_else(|| ApiError::bad("signature: must be base64 of 64 bytes"))?; + + let verifying_key = VerifyingKey::from_bytes(&account_pubkey) + .map_err(|_| ApiError::bad("account_pub: not a valid ed25519 key"))?; + verifying_key + .verify_strict(&payload, &Signature::from_bytes(&signature)) + .map_err(|_| ApiError::bad("signature: verification failed"))?; + + // Read the bundle's lamport so the store can reject replays. Safe to trust: + // the signature over `payload` was just verified, so the lamport can't be + // forged without the account key. + let lamport = crate::store::payload_lamport(&payload) + .ok_or_else(|| ApiError::bad("payload: too short to contain a lamport header"))?; + + let applied = store + .upsert_account( + &req.account_pub, + lamport, + &StoredAccountBundle { + payload, + signature: signature.to_vec(), + updated_at: 0, // filled in by store + }, + ) + .await + .map_err(ApiError::internal)?; + if !applied { + return Err(ApiError::conflict( + "stale bundle: lamport is not newer than the stored one", + )); + } + Ok(StatusCode::NO_CONTENT) +} + +/// `GET /v0/account/:account_pub` — fetch the device-list bundle for an account. +/// +/// Returns the latest published bundle so consumers can verify the +/// account signature and decode the list of LocalIdentity keys themselves. +async fn fetch_account( + State(store): State>, + Path(account_pub): Path, +) -> Result, ApiError> { + let Some(bundle) = store + .get_account(&account_pub) + .await + .map_err(ApiError::internal)? + else { + return Err(ApiError::not_found("no account bundle for account_pub")); + }; + Ok(Json(FetchAccountResponse { + payload: BASE64.encode(&bundle.payload), + signature: BASE64.encode(&bundle.signature), + updated_at: bundle.updated_at, + })) +} + +struct ApiError { + status: StatusCode, + message: String, +} + +impl ApiError { + fn bad(msg: impl Into) -> Self { + Self { + status: StatusCode::BAD_REQUEST, + message: msg.into(), + } + } + fn not_found(msg: impl Into) -> Self { + Self { + status: StatusCode::NOT_FOUND, + message: msg.into(), + } + } + fn conflict(msg: impl Into) -> Self { + Self { + status: StatusCode::CONFLICT, + message: msg.into(), + } + } + fn internal(err: E) -> Self { + tracing::error!("internal: {err}"); + Self { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: "internal error".into(), + } + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + ( + self.status, + Json(ErrorBody { + error: self.message, + }), + ) + .into_response() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..79d23f6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,98 @@ +//! Testnet KeyPackage Registry HTTP service. +//! +//! Throwaway service for issue #110 — replaced by λLEZ in v0.3. Intentionally +//! self-contained: depends only on axum + sqlite + ed25519, no libchat core. +//! +//! Wire: +//! POST /v0/keypackage — submit a signed keypackage bundle +//! GET /v0/keypackage/{device_id} — fetch the latest stored keypackage bundle +//! POST /v0/account — upsert a signed account device-list bundle +//! GET /v0/account/{account_pub} — fetch the account device-list bundle + +mod handlers; +mod store; + +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, Result}; +use clap::Parser; +use tracing_subscriber::EnvFilter; + +use store::Store; + +#[derive(Parser, Debug)] +#[command(name = "chat-store", about = "Testnet Chat Store (KeyPackage + account directory)")] +struct Cli { + /// Address to bind the HTTP server. + #[arg(long, default_value = "0.0.0.0:8080")] + bind: SocketAddr, + + /// SQLite database path. + #[arg(long, default_value = "chat-store.db")] + db: PathBuf, + + /// Maximum number of keypackage bundles retained per device_id. + #[arg(long, default_value_t = 100)] + max_per_identity: usize, + + /// Retention window in days; older bundles are pruned. + #[arg(long, default_value_t = 30)] + retention_days: u64, + + /// How often the prune task runs. + #[arg(long, default_value_t = 3600)] + prune_interval_secs: u64, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + let cli = Cli::parse(); + + let store = Arc::new( + Store::open(&cli.db) + .await + .context("failed to open store")?, + ); + + let prune_store = store.clone(); + let max_per_id = cli.max_per_identity; + let retention = Duration::from_secs(cli.retention_days * 24 * 3600); + let interval = Duration::from_secs(cli.prune_interval_secs); + tokio::spawn(async move { + let mut ticker = tokio::time::interval(interval); + loop { + ticker.tick().await; + if let Err(e) = prune_store.prune_key_packages(max_per_id, retention).await { + tracing::warn!("prune (keypackages) failed: {e}"); + } + if let Err(e) = prune_store.prune_accounts(retention).await { + tracing::warn!("prune (accounts) failed: {e}"); + } + } + }); + + let app = handlers::router(store); + let listener = tokio::net::TcpListener::bind(cli.bind) + .await + .with_context(|| format!("failed to bind {}", cli.bind))?; + tracing::info!("chat-store listening on {}", cli.bind); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .context("server error")?; + Ok(()) +} + +async fn shutdown_signal() { + let _ = tokio::signal::ctrl_c().await; + tracing::info!("shutdown signal received"); +} diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..77607c9 --- /dev/null +++ b/src/store.rs @@ -0,0 +1,316 @@ +use std::path::Path; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::{Context, Result}; +use sqlx::SqlitePool; +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; + +pub struct Store { + pool: SqlitePool, +} + +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct StoredKeyPackageBundle { + /// The canonical signed payload, stored verbatim and returned as-is so + /// consumers verify over the exact bytes that were signed. + pub payload: Vec, + /// 64-byte Ed25519 signature over `payload`. Opaque to the server. + pub signature: Vec, +} + +/// A signed bundle associating an account with its set of device (LocalIdentity) +/// public keys. The server stores exactly one blob per `account_pub`; a newer +/// bundle replaces the old one only when its lamport is strictly higher (see +/// [`Store::upsert_account`]). `payload` is otherwise opaque to the server: it +/// encodes a lamport-timestamped list of device pubkeys signed by the account +/// key so that consumers can verify the full device set. +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct StoredAccountBundle { + /// The canonical signed payload, returned verbatim so consumers can verify + /// the account signature over the exact bytes. + pub payload: Vec, + /// 64-byte Ed25519 signature over `payload` made by the account key. + pub signature: Vec, + /// Unix timestamp (ms) of the last upsert, stored for pruning. + pub updated_at: i64, +} + +impl Store { + pub async fn open(path: &Path) -> Result { + let is_memory = path == Path::new(":memory:"); + + let mut options = SqliteConnectOptions::new() + .create_if_missing(true) + .busy_timeout(Duration::from_secs(5)); + + if is_memory { + options = options.filename(":memory:"); + } else { + // Create the db's parent directory if the caller pointed at a nested + // path (e.g. `tmp/registry.db`); SQLite won't create it and errors + // with "unable to open database file" otherwise. + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent) + .with_context(|| format!("create db directory {}", parent.display()))?; + } + options = options.filename(path).journal_mode(SqliteJournalMode::Wal); + } + + // A shared in-memory database only lives as long as a connection is open, + // so pin the `:memory:` pool to a single reused connection; file-backed + // pools can fan out across requests and the prune task. + let pool = SqlitePoolOptions::new() + .max_connections(if is_memory { 1 } else { 5 }) + .connect_with(options) + .await + .context("open sqlite")?; + + sqlx::migrate!() + .run(&pool) + .await + .context("run migrations")?; + + Ok(Self { pool }) + } + + pub async fn insert(&self, device_id: &str, bundle: &StoredKeyPackageBundle) -> Result<()> { + let received_at = now_ms() as i64; + sqlx::query( + "INSERT INTO keypackages (device_id, received_at, payload, signature) + VALUES (?, ?, ?, ?)", + ) + .bind(device_id) + .bind(received_at) + .bind(&bundle.payload) + .bind(&bundle.signature) + .execute(&self.pool) + .await?; + Ok(()) + } + + /// Returns the most recently received bundle for `device_id`. Scope A: the + /// chat layer consumes one bundle per device. When multi-keypackage fanout + /// lands, switch this to return a `Vec`. + pub async fn latest(&self, device_id: &str) -> Result> { + let row = sqlx::query_as::<_, StoredKeyPackageBundle>( + "SELECT payload, signature FROM keypackages + WHERE device_id = ? + ORDER BY received_at DESC + LIMIT 1", + ) + .bind(device_id) + .fetch_optional(&self.pool) + .await?; + Ok(row) + } + + /// Upsert the signed device-list bundle for `account_pub`. The server stores + /// exactly one blob per account. + /// + /// Anti-replay: `lamport` is the monotonic version read from `bundle.payload` + /// (already signature-verified by the handler, so a forged value can't slip + /// past — the signature wouldn't match). The stored bundle is replaced only + /// when `lamport` is strictly greater than the one currently on file. A + /// replayed older-but-still-valid bundle therefore can't downgrade the device + /// list, and `updated_at` (the retention clock) is only bumped on a real + /// update so a replay can't keep a stale bundle alive past retention. + /// + /// Returns `true` when the bundle was stored, `false` when it was rejected as + /// stale. The compare-and-swap runs inside a transaction so a concurrent + /// publish can't interleave the read with the write. The `updated_at` field of + /// `bundle` is ignored; the store stamps the row with the current time. + pub async fn upsert_account( + &self, + account_pub: &str, + lamport: u64, + bundle: &StoredAccountBundle, + ) -> Result { + let updated_at = now_ms() as i64; + + let mut tx = self.pool.begin().await?; + let existing_lamport = sqlx::query_scalar::<_, Vec>( + "SELECT payload FROM account_bundles WHERE account_pub = ?", + ) + .bind(account_pub) + .fetch_optional(&mut *tx) + .await? + .and_then(|payload| payload_lamport(&payload)); + if let Some(stored) = existing_lamport + && lamport <= stored + { + // Dropping `tx` rolls the (read-only) transaction back. + return Ok(false); + } + sqlx::query( + "INSERT INTO account_bundles (account_pub, updated_at, payload, signature) + VALUES (?, ?, ?, ?) + ON CONFLICT(account_pub) DO UPDATE SET + updated_at = excluded.updated_at, + payload = excluded.payload, + signature = excluded.signature", + ) + .bind(account_pub) + .bind(updated_at) + .bind(&bundle.payload) + .bind(&bundle.signature) + .execute(&mut *tx) + .await?; + tx.commit().await?; + Ok(true) + } + + /// Returns the stored bundle for `account_pub`, or `None` if unknown. + pub async fn get_account(&self, account_pub: &str) -> Result> { + let row = sqlx::query_as::<_, StoredAccountBundle>( + "SELECT payload, signature, updated_at FROM account_bundles + WHERE account_pub = ?", + ) + .bind(account_pub) + .fetch_optional(&self.pool) + .await?; + Ok(row) + } + + /// Drops account bundles that have not been refreshed within `retention`. + pub async fn prune_accounts(&self, retention: Duration) -> Result<()> { + let cutoff_ms = now_ms().saturating_sub(retention.as_millis() as u64) as i64; + sqlx::query("DELETE FROM account_bundles WHERE updated_at < ?") + .bind(cutoff_ms) + .execute(&self.pool) + .await?; + Ok(()) + } + + /// Drops bundles older than `retention` and keeps at most + /// `max_per_identity` per `device_id` — each device's history is bounded + /// independently. + pub async fn prune_key_packages( + &self, + max_per_identity: usize, + retention: Duration, + ) -> Result<()> { + let cutoff_ms = now_ms().saturating_sub(retention.as_millis() as u64) as i64; + sqlx::query("DELETE FROM keypackages WHERE received_at < ?") + .bind(cutoff_ms) + .execute(&self.pool) + .await?; + sqlx::query( + "DELETE FROM keypackages + WHERE rowid IN ( + SELECT rowid FROM ( + SELECT rowid, + ROW_NUMBER() OVER ( + PARTITION BY device_id + ORDER BY received_at DESC + ) AS rn + FROM keypackages + ) + WHERE rn > ? + )", + ) + .bind(max_per_identity as i64) + .execute(&self.pool) + .await?; + Ok(()) + } +} + +/// Domain-separation prefix on every account-device-bundle payload. Must stay in +/// sync with `account_directory::BUNDLE_DOMAIN` in the conversations crate; this +/// throwaway service deliberately has no libchat-core dependency, so the constant +/// is duplicated here rather than imported. +const BUNDLE_DOMAIN: &[u8] = b"libchat:account-device-bundle\0"; + +/// Extract the lamport version from a bundle payload without otherwise +/// interpreting it. The canonical layout (owned by the conversations crate's +/// `encode_bundle_payload`) is `domain | version:u8 | lamport:u64 LE | …`, so the +/// lamport sits in the 8 bytes right after the domain prefix and version byte. +/// Returns `None` when the domain prefix is absent or the payload is too short to +/// contain a header — the handler treats either as a malformed request. +pub fn payload_lamport(payload: &[u8]) -> Option { + payload + .strip_prefix(BUNDLE_DOMAIN)? + .get(1..9) + .map(|b| u64::from_le_bytes(b.try_into().expect("1..9 is 8 bytes"))) +} + +fn now_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Minimal stand-in for a real bundle payload: the domain prefix plus the + /// header fields the server reads (`version:u8 | lamport:u64 LE`), no device + /// keys needed. + fn payload_with_lamport(lamport: u64) -> Vec { + let mut p = BUNDLE_DOMAIN.to_vec(); + p.push(1u8); // version + p.extend_from_slice(&lamport.to_le_bytes()); + p + } + + fn bundle(lamport: u64) -> StoredAccountBundle { + StoredAccountBundle { + payload: payload_with_lamport(lamport), + signature: vec![0u8; 64], + updated_at: 0, + } + } + + async fn upsert(store: &Store, account: &str, lamport: u64) -> bool { + store + .upsert_account(account, lamport, &bundle(lamport)) + .await + .unwrap() + } + + #[tokio::test] + async fn rejects_replayed_or_stale_lamport() { + let store = Store::open(Path::new(":memory:")).await.unwrap(); + + // First publish is always accepted. + assert!(upsert(&store, "acct", 5).await); + // A strictly higher lamport replaces it. + assert!(upsert(&store, "acct", 6).await); + // Re-publishing the same lamport (a replay) is rejected. + assert!(!upsert(&store, "acct", 6).await); + // An older lamport (a downgrade) is rejected. + assert!(!upsert(&store, "acct", 4).await); + + // The stored bundle is still the newest one accepted. + let stored = store.get_account("acct").await.unwrap().unwrap(); + assert_eq!(payload_lamport(&stored.payload), Some(6)); + } + + #[tokio::test] + async fn stale_publish_does_not_refresh_retention_clock() { + let store = Store::open(Path::new(":memory:")).await.unwrap(); + assert!(upsert(&store, "acct", 9).await); + let after_first = store.get_account("acct").await.unwrap().unwrap().updated_at; + + // A rejected (stale) publish must not bump updated_at, so a replay can't + // keep a stale bundle alive past the retention window. + assert!(!upsert(&store, "acct", 9).await); + let after_replay = store.get_account("acct").await.unwrap().unwrap().updated_at; + assert_eq!(after_first, after_replay); + } + + #[test] + fn payload_lamport_requires_domain_and_full_header() { + assert_eq!(payload_lamport(&payload_with_lamport(42)), Some(42)); + // Missing the domain prefix → unparseable. + assert_eq!(payload_lamport(&[1u8, 0, 0, 0, 0, 0, 0, 0, 0]), None); + // Has the domain but is too short for version + u64 → unparseable. + let mut short = BUNDLE_DOMAIN.to_vec(); + short.push(1u8); + assert_eq!(payload_lamport(&short), None); + } +}