From 7e3558d776133b23c29d7f9edca9a542c1be58cd Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Wed, 26 Jun 2024 23:05:17 +0300
Subject: [PATCH] chore: Initialize the project
Signed-off-by: Awiteb
---
.dockerignore | 1 +
.gitignore | 3 +
Cargo.lock | 4461 +++++++++++++++++
Cargo.toml | 30 +
crates/oxidetalis/Cargo.toml | 75 +
crates/oxidetalis/Dockerfile | 15 +
crates/oxidetalis/src/database/mod.rs | 21 +
crates/oxidetalis/src/database/user.rs | 60 +
crates/oxidetalis/src/errors.rs | 81 +
crates/oxidetalis/src/extensions.rs | 91 +
crates/oxidetalis/src/main.rs | 77 +
crates/oxidetalis/src/middlewares/mod.rs | 58 +
.../oxidetalis/src/middlewares/public_key.rs | 32 +
.../oxidetalis/src/middlewares/signature.rs | 86 +
crates/oxidetalis/src/routes/mod.rs | 162 +
crates/oxidetalis/src/routes/user.rs | 88 +
crates/oxidetalis/src/schemas/mod.rs | 71 +
crates/oxidetalis/src/schemas/user.rs | 27 +
crates/oxidetalis/src/utils.rs | 95 +
crates/oxidetalis_config/Cargo.toml | 71 +
crates/oxidetalis_config/README.md | 25 +
crates/oxidetalis_config/src/commandline.rs | 98 +
crates/oxidetalis_config/src/defaults.rs | 109 +
crates/oxidetalis_config/src/lib.rs | 247 +
crates/oxidetalis_config/src/serde_with.rs | 63 +
crates/oxidetalis_config/src/types.rs | 126 +
crates/oxidetalis_core/Cargo.toml | 74 +
crates/oxidetalis_core/src/cipher.rs | 224 +
crates/oxidetalis_core/src/lib.rs | 33 +
crates/oxidetalis_core/src/types/cipher.rs | 213 +
.../oxidetalis_core/src/types/impl_serde.rs | 99 +
crates/oxidetalis_core/src/types/mod.rs | 29 +
crates/oxidetalis_core/src/types/size.rs | 125 +
crates/oxidetalis_entities/Cargo.toml | 60 +
crates/oxidetalis_entities/README.md | 16 +
crates/oxidetalis_entities/src/lib.rs | 23 +
crates/oxidetalis_entities/src/prelude.rs | 41 +
crates/oxidetalis_entities/src/users.rs | 36 +
crates/oxidetalis_migrations/Cargo.toml | 61 +
crates/oxidetalis_migrations/README.md | 52 +
.../src/create_users_table.rs | 66 +
crates/oxidetalis_migrations/src/lib.rs | 33 +
42 files changed, 7458 insertions(+)
create mode 100644 .dockerignore
create mode 100644 .gitignore
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
create mode 100644 crates/oxidetalis/Cargo.toml
create mode 100644 crates/oxidetalis/Dockerfile
create mode 100644 crates/oxidetalis/src/database/mod.rs
create mode 100644 crates/oxidetalis/src/database/user.rs
create mode 100644 crates/oxidetalis/src/errors.rs
create mode 100644 crates/oxidetalis/src/extensions.rs
create mode 100644 crates/oxidetalis/src/main.rs
create mode 100644 crates/oxidetalis/src/middlewares/mod.rs
create mode 100644 crates/oxidetalis/src/middlewares/public_key.rs
create mode 100644 crates/oxidetalis/src/middlewares/signature.rs
create mode 100644 crates/oxidetalis/src/routes/mod.rs
create mode 100644 crates/oxidetalis/src/routes/user.rs
create mode 100644 crates/oxidetalis/src/schemas/mod.rs
create mode 100644 crates/oxidetalis/src/schemas/user.rs
create mode 100644 crates/oxidetalis/src/utils.rs
create mode 100644 crates/oxidetalis_config/Cargo.toml
create mode 100644 crates/oxidetalis_config/README.md
create mode 100644 crates/oxidetalis_config/src/commandline.rs
create mode 100644 crates/oxidetalis_config/src/defaults.rs
create mode 100644 crates/oxidetalis_config/src/lib.rs
create mode 100644 crates/oxidetalis_config/src/serde_with.rs
create mode 100644 crates/oxidetalis_config/src/types.rs
create mode 100644 crates/oxidetalis_core/Cargo.toml
create mode 100644 crates/oxidetalis_core/src/cipher.rs
create mode 100644 crates/oxidetalis_core/src/lib.rs
create mode 100644 crates/oxidetalis_core/src/types/cipher.rs
create mode 100644 crates/oxidetalis_core/src/types/impl_serde.rs
create mode 100644 crates/oxidetalis_core/src/types/mod.rs
create mode 100644 crates/oxidetalis_core/src/types/size.rs
create mode 100644 crates/oxidetalis_entities/Cargo.toml
create mode 100644 crates/oxidetalis_entities/README.md
create mode 100644 crates/oxidetalis_entities/src/lib.rs
create mode 100644 crates/oxidetalis_entities/src/prelude.rs
create mode 100644 crates/oxidetalis_entities/src/users.rs
create mode 100644 crates/oxidetalis_migrations/Cargo.toml
create mode 100644 crates/oxidetalis_migrations/README.md
create mode 100644 crates/oxidetalis_migrations/src/create_users_table.rs
create mode 100644 crates/oxidetalis_migrations/src/lib.rs
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+/target
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fb10b84
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/target
+# Ignore the server configration file (Used for local development only)
+config.toml
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..7983c05
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,4461 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "getrandom",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "aliasable"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+
+[[package]]
+name = "async-lock"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+dependencies = [
+ "event-listener 5.3.1",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[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.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "backtrace"
+version = "0.3.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base58"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "bigdecimal"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[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 = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "borsh"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed"
+dependencies = [
+ "borsh-derive",
+ "cfg_aliases",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "syn_derive",
+]
+
+[[package]]
+name = "brotli"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "4.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d"
+dependencies = [
+ "jobserver",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
+[[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 = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "aes-gcm",
+ "base64 0.22.1",
+ "hmac",
+ "percent-encoding",
+ "rand",
+ "sha2",
+ "subtle",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "typenum",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive-new"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[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 = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "hkdf",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "enumflags2"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
+dependencies = [
+ "enumflags2_derive",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "etag"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b3d0661a2ccddc26cba0b834e9b717959ed6fdd76c7129ee159c170a875bf44"
+dependencies = [
+ "str-buf",
+ "xxhash-rust",
+]
+
+[[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 = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "5.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
+dependencies = [
+ "event-listener 5.3.1",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
+[[package]]
+name = "ff"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "flume"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+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.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "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",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
+name = "gimli"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash 0.7.8",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash 0.8.11",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "headers"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
+dependencies = [
+ "base64 0.21.7",
+ "bytes",
+ "headers-core",
+ "http",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
+dependencies = [
+ "http",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
+dependencies = [
+ "futures-util",
+ "http",
+ "hyper",
+ "hyper-util",
+ "log",
+ "rustls 0.23.10",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.5",
+ "serde",
+]
+
+[[package]]
+name = "inherent"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "inventory"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
+
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "jobserver"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "jsonwebtoken"
+version = "9.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f"
+dependencies = [
+ "base64 0.21.7",
+ "js-sys",
+ "pem",
+ "ring",
+ "serde",
+ "serde_json",
+ "simple_asn1",
+]
+
+[[package]]
+name = "k256"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b"
+dependencies = [
+ "cfg-if",
+ "elliptic-curve",
+]
+
+[[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.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "logcall"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb377687ad730d661a29ee17ca44644d388c72f0d8a83d69a75744a6041b1c3"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime-infer"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91caed19dd472bc88bcd063571df18153529d49301a1918f4cf37f42332bee2e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "moka"
+version = "0.12.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e0d88686dc561d743b40de8269b26eaf0dc58781bde087b0984646602021d08"
+dependencies = [
+ "async-lock",
+ "async-trait",
+ "crossbeam-channel",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "event-listener 5.3.1",
+ "futures-util",
+ "once_cell",
+ "parking_lot",
+ "quanta",
+ "rustc_version",
+ "smallvec",
+ "tagptr",
+ "thiserror",
+ "triomphe",
+ "uuid",
+]
+
+[[package]]
+name = "multer"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http",
+ "httparse",
+ "memchr",
+ "mime",
+ "spin",
+ "version_check",
+]
+
+[[package]]
+name = "multimap"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "openssl"
+version = "0.10.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "ordered-float"
+version = "3.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "ouroboros"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954"
+dependencies = [
+ "aliasable",
+ "ouroboros_macro",
+ "static_assertions",
+]
+
+[[package]]
+name = "ouroboros_macro"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "oxidetalis"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "derive-new",
+ "log",
+ "logcall",
+ "oxidetalis_config",
+ "oxidetalis_core",
+ "oxidetalis_entities",
+ "oxidetalis_migrations",
+ "pretty_env_logger",
+ "salvo",
+ "sea-orm",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "oxidetalis_config"
+version = "0.1.0"
+dependencies = [
+ "base58",
+ "clap",
+ "derivative",
+ "log",
+ "oxidetalis_core",
+ "salvo-oapi",
+ "salvo_core",
+ "serde",
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "oxidetalis_core"
+version = "0.1.0"
+dependencies = [
+ "aes",
+ "base58",
+ "cbc",
+ "hex",
+ "hmac",
+ "k256",
+ "log",
+ "logcall",
+ "rand",
+ "salvo-oapi",
+ "salvo_core",
+ "serde",
+ "sha2",
+ "thiserror",
+]
+
+[[package]]
+name = "oxidetalis_entities"
+version = "0.1.0"
+dependencies = [
+ "sea-orm",
+]
+
+[[package]]
+name = "oxidetalis_migrations"
+version = "0.1.0"
+dependencies = [
+ "sea-orm",
+ "sea-orm-migration",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.2",
+ "smallvec",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pem"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
+dependencies = [
+ "base64 0.22.1",
+ "serde",
+]
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "pretty_env_logger"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
+dependencies = [
+ "env_logger",
+ "log",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
+dependencies = [
+ "toml_edit 0.21.1",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quanta"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5"
+dependencies = [
+ "crossbeam-utils",
+ "libc",
+ "once_cell",
+ "raw-cpuid",
+ "wasi",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+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 = "raw-cpuid"
+version = "11.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.7",
+ "regex-syntax 0.8.4",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.4",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile 2.1.2",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rust-embed"
+version = "8.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.68",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a"
+dependencies = [
+ "arrayvec",
+ "borsh",
+ "bytes",
+ "num-traits",
+ "rand",
+ "rkyv",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags 2.6.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
+dependencies = [
+ "ring",
+ "rustls-webpki 0.101.7",
+ "sct",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki 0.102.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile 2.1.2",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64 0.21.7",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64 0.22.1",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "salvo"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "591a41ac90e952ca622b6d012129336a5aca71dd1ba03d05fd1ab4b3ee476125"
+dependencies = [
+ "salvo-jwt-auth",
+ "salvo-oapi",
+ "salvo-proxy",
+ "salvo-rate-limiter",
+ "salvo_core",
+ "salvo_extra",
+]
+
+[[package]]
+name = "salvo-jwt-auth"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2140ece82476b670589f1ca90639277e5be9d3a7780b1b15cb801b6851a2de6"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "http-body-util",
+ "hyper-rustls",
+ "hyper-util",
+ "jsonwebtoken",
+ "salvo_core",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "salvo-oapi"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db73bd4f5cb788d7ae1cf40e0123cc1b16db70f53568f151cd89e8f797e23a4a"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "chrono",
+ "futures-util",
+ "http",
+ "indexmap",
+ "inventory",
+ "mime-infer",
+ "once_cell",
+ "parking_lot",
+ "regex",
+ "rust-embed",
+ "rust_decimal",
+ "salvo-oapi-macros",
+ "salvo_core",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "smallvec",
+ "thiserror",
+ "time",
+ "tokio",
+ "tracing",
+ "ulid",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "salvo-oapi-macros"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5949e8880aed053da8f120ca6bc1a771c58d03bfaa147c49113943c12592227e"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "regex",
+ "salvo-serde-util",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "salvo-proxy"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddc8a85fff7503a3fc0673ea5e32f656c76147d345ea2b7ab5e9dff5e1b8cd0b"
+dependencies = [
+ "fastrand",
+ "futures-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "percent-encoding",
+ "reqwest",
+ "salvo_core",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "salvo-rate-limiter"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdea460c6b4b09a1f01ea74fa43dbf2da0f52944463cea651f6612769de964eb"
+dependencies = [
+ "moka",
+ "salvo_core",
+ "serde",
+ "time",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "salvo-serde-util"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170e19c27303e855beb58a28226712b3ede149a4a019098747b123bbbb222d48"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "salvo_core"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e0a64876b439b2e5176459f5159be6131321a11554db1cfd34204580b9f5b12"
+dependencies = [
+ "async-trait",
+ "base64 0.22.1",
+ "brotli",
+ "bytes",
+ "cookie",
+ "encoding_rs",
+ "enumflags2",
+ "flate2",
+ "form_urlencoded",
+ "futures-channel",
+ "futures-util",
+ "headers",
+ "http",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "indexmap",
+ "mime",
+ "mime-infer",
+ "multer",
+ "multimap",
+ "native-tls",
+ "nix",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "pin-project",
+ "rand",
+ "regex",
+ "salvo_macros",
+ "serde",
+ "serde-xml-rs",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-rustls",
+ "tokio-util",
+ "tracing",
+ "url",
+ "zstd",
+]
+
+[[package]]
+name = "salvo_extra"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca626969c04dca7acbfe2fc10a1d44d597c6b032eeb2ee21204c2aad240aff19"
+dependencies = [
+ "base64 0.22.1",
+ "etag",
+ "futures-util",
+ "hyper",
+ "pin-project",
+ "salvo_core",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tokio-tungstenite",
+ "tracing",
+ "ulid",
+]
+
+[[package]]
+name = "salvo_macros"
+version = "0.68.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea5a8f4a082a91529f085bc53bdad4e7e0fbe419f7188f5dd6265acdaf990876"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "salvo-serde-util",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "sea-bae"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "sea-orm"
+version = "0.12.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8814e37dc25de54398ee62228323657520b7f29713b8e238649385dbe473ee0"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "bigdecimal",
+ "chrono",
+ "futures",
+ "log",
+ "ouroboros",
+ "rust_decimal",
+ "sea-orm-macros",
+ "sea-query",
+ "sea-query-binder",
+ "serde",
+ "serde_json",
+ "sqlx",
+ "strum",
+ "thiserror",
+ "time",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "sea-orm-macros"
+version = "0.12.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e115c6b078e013aa963cc2d38c196c2c40b05f03d0ac872fe06b6e0d5265603"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "sea-bae",
+ "syn 2.0.68",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sea-orm-migration"
+version = "0.12.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8269bc6ff71afd6b78aa4333ac237a69eebd2cdb439036291e64fb4b8db23c"
+dependencies = [
+ "async-trait",
+ "futures",
+ "sea-orm",
+ "sea-schema",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "sea-query"
+version = "0.30.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4166a1e072292d46dc91f31617c2a1cdaf55a8be4b5c9f4bf2ba248e3ac4999b"
+dependencies = [
+ "bigdecimal",
+ "chrono",
+ "derivative",
+ "inherent",
+ "ordered-float",
+ "rust_decimal",
+ "sea-query-derive",
+ "serde_json",
+ "time",
+ "uuid",
+]
+
+[[package]]
+name = "sea-query-binder"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9"
+dependencies = [
+ "bigdecimal",
+ "chrono",
+ "rust_decimal",
+ "sea-query",
+ "serde_json",
+ "sqlx",
+ "time",
+ "uuid",
+]
+
+[[package]]
+name = "sea-query-derive"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a82fcb49253abcb45cdcb2adf92956060ec0928635eb21b4f7a6d8f25ab0bc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "thiserror",
+]
+
+[[package]]
+name = "sea-schema"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d148608012d25222442d1ebbfafd1228dbc5221baf4ec35596494e27a2394e"
+dependencies = [
+ "futures",
+ "sea-query",
+ "sea-schema-derive",
+]
+
+[[package]]
+name = "sea-schema-derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f686050f76bffc4f635cda8aea6df5548666b830b52387e8bc7de11056d11e"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
+dependencies = [
+ "bitflags 2.6.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
+
+[[package]]
+name = "serde"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-xml-rs"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782"
+dependencies = [
+ "log",
+ "serde",
+ "thiserror",
+ "xml-rs",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[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.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+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 = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
+
+[[package]]
+name = "simple_asn1"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[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 = "sqlformat"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f"
+dependencies = [
+ "nom",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
+dependencies = [
+ "ahash 0.8.11",
+ "atoi",
+ "bigdecimal",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener 2.5.3",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashlink",
+ "hex",
+ "indexmap",
+ "log",
+ "memchr",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "rust_decimal",
+ "rustls 0.21.12",
+ "rustls-pemfile 1.0.4",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "thiserror",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "uuid",
+ "webpki-roots",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck 0.4.1",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn 1.0.109",
+ "tempfile",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
+dependencies = [
+ "atoi",
+ "base64 0.21.7",
+ "bigdecimal",
+ "bitflags 2.6.0",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "rust_decimal",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
+dependencies = [
+ "atoi",
+ "base64 0.21.7",
+ "bigdecimal",
+ "bitflags 2.6.0",
+ "byteorder",
+ "chrono",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "num-bigint",
+ "once_cell",
+ "rand",
+ "rust_decimal",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
+dependencies = [
+ "atoi",
+ "chrono",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "sqlx-core",
+ "time",
+ "tracing",
+ "url",
+ "urlencoding",
+ "uuid",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "str-buf"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceb97b7225c713c2fd4db0153cb6b3cab244eb37900c3f634ed4d43310d8c34"
+
+[[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 = "strum"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn_derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tagptr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.3.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "time-macros"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82"
+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.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
+dependencies = [
+ "rustls 0.23.10",
+ "rustls-pki-types",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.22.14",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow 0.6.13",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "matchers",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+]
+
+[[package]]
+name = "triomphe"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369"
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "log",
+ "rand",
+ "thiserror",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "ulid"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259"
+dependencies = [
+ "getrandom",
+ "rand",
+ "web-time",
+]
+
+[[package]]
+name = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
+
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
+dependencies = [
+ "getrandom",
+ "serde",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[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.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.25.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
+
+[[package]]
+name = "whoami"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
+dependencies = [
+ "redox_syscall 0.4.1",
+ "wasite",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
+
+[[package]]
+name = "xxhash-rust"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03"
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zstd"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.11+zstd.1.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..98d8fc7
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,30 @@
+[workspace]
+members = ["crates/*"]
+resolver = "2"
+
+[workspace.package]
+authors = ["OxideTalis Developers "]
+readme = "README.md"
+repository = "https://git.4rs.nl/oxidetalis/oxidetalis"
+version = "0.1.0"
+rust-version = "1.76.0"
+
+[workspace.dependencies]
+# Local crates
+oxidetalis_core = { path = "crates/oxidetalis_core" }
+oxidetalis_config = { path = "crates/oxidetalis_config" }
+oxidetalis_migrations = { path = "crates/oxidetalis_migrations" }
+oxidetalis_entities = { path = "crates/oxidetalis_entities" }
+# Shered dependencies
+base58 = "0.2.0"
+serde = "1.0.203"
+thiserror = "1.0.61"
+log = "0.4.21"
+logcall = "0.1.9"
+chrono = "0.4.38"
+sea-orm = { version = "0.12.15", features = ["with-chrono", "macros"] }
+salvo_core = { version = "0.68.3", default-features = false }
+salvo-oapi = { version = "0.68.3", features = ["rapidoc","redoc","scalar","swagger-ui"] }
+
+[profile.release]
+strip = true # Automatically strip symbols from the binary.
diff --git a/crates/oxidetalis/Cargo.toml b/crates/oxidetalis/Cargo.toml
new file mode 100644
index 0000000..035cf8b
--- /dev/null
+++ b/crates/oxidetalis/Cargo.toml
@@ -0,0 +1,75 @@
+[package]
+name = "oxidetalis"
+description = "OxideTalis Messaging Protocol homeserver"
+edition = "2021"
+license = "AGPL-3.0-or-later"
+authors.workspace = true
+readme.workspace = true
+repository.workspace = true
+version.workspace = true
+rust-version.workspace = true
+
+
+[dependencies]
+oxidetalis_core = { workspace = true }
+oxidetalis_config = { workspace = true }
+oxidetalis_entities = { workspace = true }
+oxidetalis_migrations = { workspace = true }
+log = { workspace = true }
+logcall = { workspace = true }
+sea-orm = { workspace = true }
+serde = { workspace = true }
+thiserror = { workspace = true }
+chrono = { workspace = true }
+salvo = { version = "0.68.2", features = ["affix", "logging", "native-tls", "oapi", "rate-limiter", "websocket"] }
+tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
+derive-new = "0.6.0"
+pretty_env_logger = "0.5.0"
+serde_json = "1.0.117"
+
+[lints.rust]
+unsafe_code = "deny"
+missing_docs = "warn"
+
+
+[lints.clippy]
+wildcard_imports = "deny"
+manual_let_else = "deny"
+match_bool = "deny"
+match_on_vec_items = "deny"
+or_fun_call = "deny"
+panic = "deny"
+unwrap_used = "deny"
+
+missing_assert_message = "warn"
+missing_const_for_fn = "warn"
+missing_errors_doc = "warn"
+absolute_paths = "warn"
+cast_lossless = "warn"
+clone_on_ref_ptr = "warn"
+cloned_instead_of_copied = "warn"
+dbg_macro = "warn"
+default_trait_access = "warn"
+empty_enum_variants_with_brackets = "warn"
+empty_line_after_doc_comments = "warn"
+empty_line_after_outer_attr = "warn"
+empty_structs_with_brackets = "warn"
+enum_glob_use = "warn"
+equatable_if_let = "warn"
+explicit_iter_loop = "warn"
+filetype_is_file = "warn"
+filter_map_next = "warn"
+flat_map_option = "warn"
+float_cmp = "warn"
+format_push_string = "warn"
+future_not_send = "warn"
+if_not_else = "warn"
+if_then_some_else_none = "warn"
+implicit_clone = "warn"
+inconsistent_struct_constructor = "warn"
+iter_filter_is_ok = "warn"
+iter_filter_is_some = "warn"
+iter_not_returning_iterator = "warn"
+manual_is_variant_and = "warn"
+option_if_let_else = "warn"
+option_option = "warn"
diff --git a/crates/oxidetalis/Dockerfile b/crates/oxidetalis/Dockerfile
new file mode 100644
index 0000000..de75af1
--- /dev/null
+++ b/crates/oxidetalis/Dockerfile
@@ -0,0 +1,15 @@
+FROM rust:1.70.0-slim-bullseye as builder
+
+WORKDIR /builder
+
+COPY ./ ./
+
+RUN cargo build --release
+
+FROM debian:bullseye-slim
+
+WORKDIR /app
+
+COPY --from=builder /builder/target/release/oxidetalis .
+
+CMD ["./oxidetalis"]
diff --git a/crates/oxidetalis/src/database/mod.rs b/crates/oxidetalis/src/database/mod.rs
new file mode 100644
index 0000000..a9578ae
--- /dev/null
+++ b/crates/oxidetalis/src/database/mod.rs
@@ -0,0 +1,21 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! Database utilities for the OxideTalis homeserver.
+
+mod user;
+
+pub use user::*;
diff --git a/crates/oxidetalis/src/database/user.rs b/crates/oxidetalis/src/database/user.rs
new file mode 100644
index 0000000..7f52764
--- /dev/null
+++ b/crates/oxidetalis/src/database/user.rs
@@ -0,0 +1,60 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! Functions for interacting with the user table in the database.
+
+use logcall::logcall;
+use oxidetalis_core::types::PublicKey;
+use oxidetalis_entities::prelude::*;
+use sea_orm::DatabaseConnection;
+
+use crate::errors::{ApiError, ApiResult};
+
+pub trait UserTableExt {
+ /// Returns true if there is users in the database
+ async fn users_exists_in_database(&self) -> ApiResult;
+ /// Register new user
+ async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ApiResult<()>;
+}
+
+impl UserTableExt for DatabaseConnection {
+ #[logcall]
+ async fn users_exists_in_database(&self) -> ApiResult {
+ UserEntity::find()
+ .one(self)
+ .await
+ .map_err(Into::into)
+ .map(|u| u.is_some())
+ }
+
+ #[logcall]
+ async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ApiResult<()> {
+ if let Err(err) = (UserActiveModel {
+ public_key: Set(public_key.to_string()),
+ is_admin: Set(is_admin),
+ ..Default::default()
+ })
+ .save(self)
+ .await
+ {
+ if let Some(SqlErr::UniqueConstraintViolation(_)) = err.sql_err() {
+ return Err(ApiError::DuplicatedUser);
+ }
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/oxidetalis/src/errors.rs b/crates/oxidetalis/src/errors.rs
new file mode 100644
index 0000000..ed419ad
--- /dev/null
+++ b/crates/oxidetalis/src/errors.rs
@@ -0,0 +1,81 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use salvo::{
+ http::StatusCode,
+ oapi::{Components as OapiComponents, EndpointOutRegister, Operation as OapiOperation},
+ Response,
+ Scribe,
+};
+
+use crate::{routes::write_json_body, schemas::MessageSchema};
+
+/// Result type of the homeserver
+#[allow(clippy::absolute_paths)]
+pub(crate) type Result = std::result::Result;
+#[allow(clippy::absolute_paths)]
+pub type ApiResult = std::result::Result;
+
+/// The homeserver errors
+#[derive(Debug, thiserror::Error)]
+pub(crate) enum Error {
+ #[error("Database Error: {0}")]
+ Database(#[from] sea_orm::DbErr),
+ #[error("{0}")]
+ Configuration(#[from] oxidetalis_config::Error),
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum ApiError {
+ /// Error from the database (500 Internal Server Error)
+ #[error("Internal server error")]
+ SeaOrm(#[from] sea_orm::DbErr),
+ /// The server registration is closed (403 Forbidden)
+ #[error("Server registration is closed")]
+ RegistrationClosed,
+ /// The entered public key is already registered (400 Bad Request)
+ #[error("The entered public key is already registered")]
+ DuplicatedUser,
+ /// The user enterd tow different public keys
+ /// one in the header and other in the request body
+ /// (400 Bad Request)
+ #[error("TODO")]
+ TwoDifferentKeys,
+}
+
+impl ApiError {
+ /// Status code of the error
+ pub const fn status_code(&self) -> StatusCode {
+ match self {
+ Self::SeaOrm(_) => StatusCode::INTERNAL_SERVER_ERROR,
+ Self::RegistrationClosed => StatusCode::FORBIDDEN,
+ Self::DuplicatedUser | Self::TwoDifferentKeys => StatusCode::BAD_REQUEST,
+ }
+ }
+}
+
+impl EndpointOutRegister for ApiError {
+ fn register(_: &mut OapiComponents, _: &mut OapiOperation) {}
+}
+
+impl Scribe for ApiError {
+ fn render(self, res: &mut Response) {
+ log::error!("Error: {self}");
+
+ res.status_code(self.status_code());
+ write_json_body(res, MessageSchema::new(self.to_string()));
+ }
+}
diff --git a/crates/oxidetalis/src/extensions.rs b/crates/oxidetalis/src/extensions.rs
new file mode 100644
index 0000000..ad91c2d
--- /dev/null
+++ b/crates/oxidetalis/src/extensions.rs
@@ -0,0 +1,91 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::sync::Arc;
+
+use chrono::Utc;
+use oxidetalis_config::Config;
+use salvo::Depot;
+use sea_orm::DatabaseConnection;
+
+use crate::{routes::DEPOT_NONCE_CACHE_SIZE, NonceCache};
+
+/// Extension trait for the Depot.
+pub trait DepotExt {
+ /// Returns the database connection
+ fn db_conn(&self) -> &DatabaseConnection;
+ /// Returns the server configuration
+ fn config(&self) -> &Config;
+ /// Retutns the nonce cache
+ fn nonce_cache(&self) -> &NonceCache;
+ /// Returns the size of the nonce cache
+ fn nonce_cache_size(&self) -> &usize;
+}
+
+/// Extension trait for the nonce cache.
+pub trait NonceCacheExt {
+ /// Add a nonce to the cache, returns `true` if the nonce is added, `false`
+ /// if the nonce is already exist in the cache.
+ fn add_nonce(&self, nonce: &[u8; 16], limit: &usize) -> bool;
+}
+
+impl DepotExt for Depot {
+ fn db_conn(&self) -> &DatabaseConnection {
+ self.obtain::>()
+ .expect("Database connection not found")
+ }
+
+ fn config(&self) -> &Config {
+ self.obtain::>().expect("Config not found")
+ }
+
+ fn nonce_cache(&self) -> &NonceCache {
+ self.obtain::>()
+ .expect("Nonce cache not found")
+ }
+
+ fn nonce_cache_size(&self) -> &usize {
+ let s: &Arc = self
+ .get(DEPOT_NONCE_CACHE_SIZE)
+ .expect("Nonce cache size not found");
+ s.as_ref()
+ }
+}
+
+impl NonceCacheExt for &NonceCache {
+ fn add_nonce(&self, nonce: &[u8; 16], limit: &usize) -> bool {
+ let mut cache = self.lock().expect("Nonce cache lock poisoned, aborting...");
+ let now = Utc::now().timestamp();
+ cache.retain(|_, time| (now - *time) < 30);
+
+ if &cache.len() >= limit {
+ log::warn!("Nonce cache limit reached, clearing 10% of the cache");
+ let num_to_remove = limit / 10;
+ let keys: Vec<[u8; 16]> = cache.keys().copied().collect();
+ for key in keys.iter().take(num_to_remove) {
+ cache.remove(key);
+ }
+ }
+
+ // We can use insert directly, but it's will update the value if the key is
+ // already exist so we need to check if the key is already exist or not
+ if cache.contains_key(nonce) {
+ return false;
+ }
+ cache.insert(*nonce, now);
+ true
+ }
+}
diff --git a/crates/oxidetalis/src/main.rs b/crates/oxidetalis/src/main.rs
new file mode 100644
index 0000000..b770e18
--- /dev/null
+++ b/crates/oxidetalis/src/main.rs
@@ -0,0 +1,77 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+#![doc = include_str!("../../../README.md")]
+#![warn(missing_docs, unsafe_code)]
+
+use std::{collections::HashMap, process::ExitCode, sync::Mutex};
+
+use oxidetalis_config::{CliArgs, Parser};
+use oxidetalis_migrations::MigratorTrait;
+use salvo::{conn::TcpListener, Listener, Server};
+
+mod database;
+mod errors;
+mod extensions;
+mod middlewares;
+mod routes;
+mod schemas;
+mod utils;
+
+/// Nonce cache type, used to store nonces for a certain amount of time
+pub type NonceCache = Mutex>;
+
+async fn try_main() -> errors::Result<()> {
+ pretty_env_logger::init_timed();
+
+ log::info!("Parsing configuration");
+ let config = oxidetalis_config::Config::load(CliArgs::parse())?;
+ log::info!("Configuration parsed successfully");
+ log::info!("Connecting to the database");
+ let connection = sea_orm::Database::connect(utils::postgres_url(&config.postgresdb)).await?;
+ log::info!("Connected to the database successfully");
+ oxidetalis_migrations::Migrator::up(&connection, None).await?;
+ log::info!("Migrations applied successfully");
+
+ let local_addr = format!("{}:{}", config.server.host, config.server.port);
+ let acceptor = TcpListener::new(&local_addr).bind().await;
+ log::info!("Server listening on http://{local_addr}");
+ if config.openapi.enable {
+ log::info!(
+ "The openapi schema is available at http://{local_addr}{}",
+ config.openapi.path
+ );
+ log::info!(
+ "The openapi viewer is available at http://{local_addr}{}",
+ config.openapi.viewer_path
+ );
+ }
+ log::info!("Server version: {}", env!("CARGO_PKG_VERSION"));
+ Server::new(acceptor)
+ .serve(routes::service(connection, &config))
+ .await;
+ Ok(())
+}
+
+#[tokio::main]
+async fn main() -> ExitCode {
+ if let Err(err) = try_main().await {
+ eprintln!("{err}");
+ log::error!("{err}");
+ return ExitCode::FAILURE;
+ }
+ ExitCode::SUCCESS
+}
diff --git a/crates/oxidetalis/src/middlewares/mod.rs b/crates/oxidetalis/src/middlewares/mod.rs
new file mode 100644
index 0000000..959b892
--- /dev/null
+++ b/crates/oxidetalis/src/middlewares/mod.rs
@@ -0,0 +1,58 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! Middlewares for the OxideTalis homeserver.
+
+use salvo::{
+ handler,
+ http::{header, HeaderValue, StatusCode},
+ FlowCtrl,
+ Request,
+ Response,
+};
+
+mod public_key;
+mod signature;
+
+pub use public_key::*;
+pub use signature::*;
+
+use crate::{routes::write_json_body, schemas::MessageSchema};
+
+/// Add server headers to the response and request.
+#[handler]
+pub async fn add_server_headers(req: &mut Request, res: &mut Response) {
+ let res_headers = res.headers_mut();
+ let req_headers = req.headers_mut();
+ res_headers.insert(
+ header::CONTENT_TYPE,
+ HeaderValue::from_static("application/json"),
+ );
+ // Insert the accept header for salvo, so it returns JSON if there is error
+ req_headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
+}
+
+/// Write an errror message in the response
+pub fn write_error(
+ res: &mut Response,
+ ctrl: &mut FlowCtrl,
+ message: String,
+ status_code: StatusCode,
+) {
+ res.status_code(status_code);
+ write_json_body(res, MessageSchema::new(message));
+ ctrl.skip_rest();
+}
diff --git a/crates/oxidetalis/src/middlewares/public_key.rs b/crates/oxidetalis/src/middlewares/public_key.rs
new file mode 100644
index 0000000..939dc63
--- /dev/null
+++ b/crates/oxidetalis/src/middlewares/public_key.rs
@@ -0,0 +1,32 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! Request sender public key middleware.
+
+use salvo::{handler, http::StatusCode, FlowCtrl, Request, Response};
+
+use crate::utils;
+
+/// Middleware to check the public key of the request sender.
+///
+/// If the public key is valid, the request will be passed to the next handler.
+/// Otherwise, a 401 Unauthorized response will be returned.
+#[handler]
+pub async fn public_key_check(req: &mut Request, res: &mut Response, ctrl: &mut FlowCtrl) {
+ if let Err(err) = utils::extract_public_key(req) {
+ super::write_error(res, ctrl, err.to_string(), StatusCode::UNAUTHORIZED)
+ }
+}
diff --git a/crates/oxidetalis/src/middlewares/signature.rs b/crates/oxidetalis/src/middlewares/signature.rs
new file mode 100644
index 0000000..b75dfbe
--- /dev/null
+++ b/crates/oxidetalis/src/middlewares/signature.rs
@@ -0,0 +1,86 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! Request signature middleware.
+
+use salvo::{
+ handler,
+ http::{Body, StatusCode},
+ Depot,
+ FlowCtrl,
+ Request,
+ Response,
+};
+
+use crate::{extensions::DepotExt, utils};
+
+/// Middleware to check the signature of the request.
+///
+/// If the signature is valid, the request will be passed to the next handler.
+/// Otherwise, a 401 Unauthorized response will be returned.
+#[handler]
+pub async fn signature_check(
+ req: &mut Request,
+ res: &mut Response,
+ depot: &mut Depot,
+ ctrl: &mut FlowCtrl,
+) {
+ const UNAUTHORIZED: StatusCode = StatusCode::UNAUTHORIZED;
+ let mut write_err =
+ |message: &str, status_code| super::write_error(res, ctrl, message.to_owned(), status_code);
+
+ if req.body().is_end_stream() {
+ write_err(
+ "Request body is empty, the signature need a signed body",
+ UNAUTHORIZED,
+ );
+ return;
+ }
+ let json_body = match req.parse_json::().await {
+ Ok(j) => j.to_string(),
+ Err(err) => {
+ write_err(&err.to_string(), UNAUTHORIZED);
+ return;
+ }
+ };
+ let signature = match utils::extract_signature(req) {
+ Ok(s) => s,
+ Err(err) => {
+ write_err(&err.to_string(), UNAUTHORIZED);
+ return;
+ }
+ };
+
+ let sender_public_key = match utils::extract_public_key(req) {
+ Ok(k) => k,
+ Err(err) => {
+ write_err(&err.to_string(), UNAUTHORIZED);
+ return;
+ }
+ };
+
+ if !utils::is_valid_nonce(&signature, depot.nonce_cache(), depot.nonce_cache_size())
+ || !utils::is_valid_signature(
+ &sender_public_key,
+ &depot.config().server.private_key,
+ &signature,
+ json_body.as_bytes(),
+ )
+ {
+ write_err("Invalid signature", UNAUTHORIZED);
+ return;
+ }
+}
diff --git a/crates/oxidetalis/src/routes/mod.rs b/crates/oxidetalis/src/routes/mod.rs
new file mode 100644
index 0000000..47fddb8
--- /dev/null
+++ b/crates/oxidetalis/src/routes/mod.rs
@@ -0,0 +1,162 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+use std::{env, mem};
+
+use oxidetalis_config::Config;
+use salvo::http::ResBody;
+use salvo::oapi::{Info, License};
+use salvo::rate_limiter::{BasicQuota, FixedGuard, MokaStore, RateLimiter, RemoteIpIssuer};
+use salvo::{catcher::Catcher, logging::Logger, prelude::*};
+
+use crate::schemas::MessageSchema;
+use crate::{middlewares, NonceCache};
+
+mod user;
+
+/// Size of each entry in the nonce cache
+pub(crate) const NONCE_ENTRY_SIZE: usize = mem::size_of::<[u8; 16]>() + mem::size_of::();
+/// Size of the hashmap itself without the entrys (48 bytes)
+pub(crate) const HASH_MAP_SIZE: usize = mem::size_of::>();
+/// Name of the nonce cache size in the depot
+pub(crate) const DEPOT_NONCE_CACHE_SIZE: &str = "NONCE_CACHE_SIZE";
+
+pub fn write_json_body(res: &mut Response, json_body: impl serde::Serialize) {
+ res.write_body(serde_json::to_string(&json_body).expect("Json serialization can't be fail"))
+ .ok();
+}
+
+#[handler]
+async fn handle404(res: &mut Response, ctrl: &mut FlowCtrl) {
+ if res.status_code == Some(StatusCode::NOT_FOUND) {
+ write_json_body(res, MessageSchema::new("Not Found".to_owned()));
+ ctrl.skip_rest();
+ }
+}
+
+#[handler]
+async fn handle_server_errors(res: &mut Response, ctrl: &mut FlowCtrl) {
+ log::info!("New response catched: {res:#?}");
+ if matches!(res.status_code, Some(status) if !status.is_success()) {
+ if res.status_code == Some(StatusCode::TOO_MANY_REQUESTS) {
+ write_json_body(
+ res,
+ MessageSchema::new("Too many requests, please try again later".to_owned()),
+ );
+ ctrl.skip_rest();
+ } else if let ResBody::Error(err) = &res.body {
+ log::error!("Error: {err}");
+ write_json_body(
+ res,
+ MessageSchema::new(format!(
+ "{}, {}: {}",
+ err.name,
+ err.brief.trim_end_matches('.'),
+ err.cause
+ .as_deref()
+ .map_or_else(|| "".to_owned(), ToString::to_string)
+ .trim_end_matches('.')
+ .split(':')
+ .last()
+ .unwrap_or_default()
+ .trim()
+ )),
+ );
+ ctrl.skip_rest();
+ } else {
+ log::warn!("Unknown error uncatched: {res:#?}");
+ }
+ } else {
+ log::warn!("Unknown response uncatched: {res:#?}");
+ }
+}
+
+/// Hoop a middleware if the condation is true
+fn hoop_if(router: Router, middleware: impl Handler, condation: bool) -> Router {
+ if condation {
+ router.hoop(middleware)
+ } else {
+ router
+ }
+}
+
+/// Create the ratelimit middleware
+fn ratelimiter(
+ config: &Config,
+) -> RateLimiter, RemoteIpIssuer, BasicQuota> {
+ RateLimiter::new(
+ FixedGuard::new(),
+ MokaStore::::new(),
+ RemoteIpIssuer,
+ BasicQuota::set_seconds(config.ratelimit.limit, config.ratelimit.period_secs as i64),
+ )
+ .add_headers(true)
+}
+
+/// Create openapi and its viewer, and unshift them
+fn route_openapi(config: &Config, router: Router) -> Router {
+ if config.openapi.enable {
+ let openapi = OpenApi::new(&config.openapi.title, env!("CARGO_PKG_VERSION"))
+ .info(
+ Info::new(&config.openapi.title, env!("CARGO_PKG_VERSION"))
+ .license(
+ License::new("AGPL-3.0-or-later").url("https://gnu.org/licenses/agpl-3.0"),
+ )
+ .description(&config.openapi.description),
+ )
+ .merge_router(&router);
+ let router = router
+ .unshift(openapi.into_router(&config.openapi.path))
+ .unshift(config.openapi.viewer.into_router(config));
+ return router;
+ }
+ router
+}
+
+pub fn service(conn: sea_orm::DatabaseConnection, config: &Config) -> Service {
+ let nonce_cache_size = config.server.nonce_cache_size.as_bytes();
+ let nonce_cache: NonceCache = Mutex::new(HashMap::with_capacity(
+ (nonce_cache_size - HASH_MAP_SIZE) / NONCE_ENTRY_SIZE,
+ ));
+ log::info!(
+ "Nonce cache created with a capacity of {} ({})",
+ (nonce_cache_size - HASH_MAP_SIZE) / NONCE_ENTRY_SIZE,
+ config.server.nonce_cache_size
+ );
+
+ let router = Router::new()
+ .push(Router::with_path("user").push(user::route()))
+ .hoop(middlewares::add_server_headers)
+ .hoop(Logger::new())
+ .hoop(
+ affix::inject(Arc::new(conn))
+ .insert(DEPOT_NONCE_CACHE_SIZE, Arc::new(nonce_cache_size))
+ .inject(Arc::new(config.clone()))
+ .inject(Arc::new(nonce_cache)),
+ );
+
+ let router = hoop_if(router, ratelimiter(config), config.ratelimit.enable);
+ let router = route_openapi(config, router);
+
+ Service::new(router).catcher(
+ Catcher::default()
+ .hoop(middlewares::add_server_headers)
+ .hoop(handle404)
+ .hoop(handle_server_errors),
+ )
+}
diff --git a/crates/oxidetalis/src/routes/user.rs b/crates/oxidetalis/src/routes/user.rs
new file mode 100644
index 0000000..ccefe73
--- /dev/null
+++ b/crates/oxidetalis/src/routes/user.rs
@@ -0,0 +1,88 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! REST API endpoints for user management
+
+use oxidetalis_core::types::{PublicKey, Signature};
+use salvo::{
+ http::StatusCode,
+ oapi::{endpoint, extract::JsonBody},
+ Depot,
+ Request,
+ Router,
+ Writer,
+};
+
+use crate::{
+ database::UserTableExt,
+ errors::{ApiError, ApiResult},
+ extensions::DepotExt,
+ middlewares,
+ schemas::{EmptySchema, MessageSchema, RegisterUserBody},
+ utils,
+};
+
+#[endpoint(
+ operation_id = "register",
+ tags("User"),
+ responses(
+ (status_code = 201, description = "User registered"),
+ (status_code = 403, description = "Server registration is closed", content_type = "application/json", body = MessageSchema),
+ (status_code = 400, description = "The public key in the header is not the same as the key in the body", content_type = "application/json", body = MessageSchema),
+ (status_code = 400, description = "The entered public key is already registered", content_type = "application/json", body = MessageSchema),
+ (status_code = 401, description = "The entered signature is invalid", content_type = "application/json", body = MessageSchema),
+ (status_code = 401, description = "The entered public key is invalid", content_type = "application/json", body = MessageSchema),
+ (status_code = 429, description = "Too many requests", content_type = "application/json", body = MessageSchema),
+ (status_code = 500, description = "Internal server error", content_type = "application/json", body = MessageSchema),
+ ),
+ parameters(
+ ("X-OTMP-SIGNATURE" = Signature, Header, description = "Signature of the request"),
+ ("X-OTMP-PUBLIC" = PublicKey, Header, description = "Public key of the sender"),
+ ),
+)]
+pub async fn register(
+ body: JsonBody,
+ req: &Request,
+ depot: &mut Depot,
+) -> ApiResult {
+ let body = body.into_inner();
+ let db = depot.db_conn();
+ let config = depot.config();
+
+ if utils::extract_public_key(req).expect("Public key should be checked in the middleware")
+ != body.public_key
+ {
+ return Err(ApiError::TwoDifferentKeys);
+ }
+
+ if !db.users_exists_in_database().await? {
+ db.register_user(&body.public_key, true).await?;
+ } else if config.register.enable {
+ db.register_user(&body.public_key, false).await?;
+ } else {
+ return Err(ApiError::RegistrationClosed);
+ }
+
+ Ok(EmptySchema::new(StatusCode::CREATED))
+}
+
+/// The route of the endpoints of this module
+pub fn route() -> Router {
+ Router::new()
+ .push(Router::with_path("register").post(register))
+ .hoop(middlewares::public_key_check)
+ .hoop(middlewares::signature_check)
+}
diff --git a/crates/oxidetalis/src/schemas/mod.rs b/crates/oxidetalis/src/schemas/mod.rs
new file mode 100644
index 0000000..a068073
--- /dev/null
+++ b/crates/oxidetalis/src/schemas/mod.rs
@@ -0,0 +1,71 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use salvo::{
+ http::{header, StatusCode},
+ oapi::{
+ Components as OapiComponents,
+ EndpointOutRegister,
+ Operation as OapiOperation,
+ ToSchema,
+ },
+ Response,
+ Scribe,
+};
+use serde::{Deserialize, Serialize};
+
+mod user;
+
+pub use user::*;
+
+/// Json message schema, used for returning messages to the client, the message
+/// must be human readable.
+///
+/// # Example
+/// ```json
+/// {
+/// "message": "Message"
+/// }
+/// ```
+#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, derive_new::new)]
+#[salvo(schema(name = MessageSchema, example = json!(MessageSchema::new("Message".to_owned()))))]
+pub struct MessageSchema {
+ #[salvo(schema(example = "Message"))]
+ message: String,
+}
+
+/// Empty schema, used for returning empty responses.
+#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
+#[salvo(schema(name = EmptySchema))]
+pub struct EmptySchema(u16);
+
+impl EmptySchema {
+ /// Returns empty schema with the given status code
+ pub fn new(code: StatusCode) -> Self {
+ Self(code.as_u16())
+ }
+}
+
+impl EndpointOutRegister for EmptySchema {
+ fn register(_components: &mut OapiComponents, _operation: &mut OapiOperation) {}
+}
+
+impl Scribe for EmptySchema {
+ fn render(self, res: &mut Response) {
+ res.status_code(StatusCode::from_u16(self.0).expect("Is correct, from new function"));
+ res.headers_mut().remove(header::CONTENT_TYPE);
+ }
+}
diff --git a/crates/oxidetalis/src/schemas/user.rs b/crates/oxidetalis/src/schemas/user.rs
new file mode 100644
index 0000000..321ce28
--- /dev/null
+++ b/crates/oxidetalis/src/schemas/user.rs
@@ -0,0 +1,27 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use oxidetalis_core::{cipher::K256Secret, types::PublicKey};
+use salvo::oapi::ToSchema;
+use serde::{Deserialize, Serialize};
+
+/// The schema for the user registration request
+#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, derive_new::new)]
+#[salvo(schema(name = RegisterUserBody, example = json!(RegisterUserBody::new(K256Secret::new().pubkey()))))]
+pub struct RegisterUserBody {
+ /// The public key of the user
+ pub public_key: PublicKey,
+}
diff --git a/crates/oxidetalis/src/utils.rs b/crates/oxidetalis/src/utils.rs
new file mode 100644
index 0000000..76f10c1
--- /dev/null
+++ b/crates/oxidetalis/src/utils.rs
@@ -0,0 +1,95 @@
+// OxideTalis Messaging Protocol homeserver implementation
+// Copyright (C) 2024 OxideTalis Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::str::FromStr;
+
+use chrono::Utc;
+use logcall::logcall;
+use oxidetalis_config::Postgres;
+use oxidetalis_core::{
+ cipher::K256Secret,
+ types::{PrivateKey, PublicKey, Signature},
+ PUBLIC_KEY_HEADER,
+ SIGNATURE_HEADER,
+};
+use salvo::Request;
+
+use crate::{extensions::NonceCacheExt, NonceCache};
+
+/// Returns the postgres database url
+#[logcall]
+pub(crate) fn postgres_url(db_config: &Postgres) -> String {
+ format!(
+ "postgres://{}:{}@{}:{}/{}",
+ db_config.user,
+ db_config.password,
+ db_config.host.as_str(),
+ db_config.port,
+ db_config.name
+ )
+}
+
+/// Returns true if the given nonce a valid nonce.
+pub(crate) fn is_valid_nonce(
+ signature: &Signature,
+ nonce_cache: &NonceCache,
+ nonce_cache_limit: &usize,
+) -> bool {
+ let new_timestamp = Utc::now()
+ .timestamp()
+ .checked_sub(u64::from_be_bytes(*signature.timestamp()) as i64)
+ .is_some_and(|n| n <= 20);
+ let unused_nonce = new_timestamp && nonce_cache.add_nonce(signature.nonce(), nonce_cache_limit);
+ new_timestamp && unused_nonce
+}
+
+/// Returns true if the given signature is valid.
+pub(crate) fn is_valid_signature(
+ signer: &PublicKey,
+ private_key: &PrivateKey,
+ signature: &Signature,
+ data: &[u8],
+) -> bool {
+ K256Secret::from_privkey(private_key).verify(data, signature, signer)
+}
+
+/// Extract the sender public key from the request
+///
+/// Returns the public key of the sender extracted from the request, or the
+/// reason why it failed.
+pub(crate) fn extract_public_key(req: &Request) -> Result {
+ req.headers()
+ .get(PUBLIC_KEY_HEADER)
+ .map(|v| {
+ PublicKey::from_str(v.to_str().map_err(|err| err.to_string())?)
+ .map_err(|err| err.to_string())
+ })
+ .ok_or_else(|| "The public key is missing".to_owned())?
+}
+
+/// Extract the signature from the request
+///
+/// Returns the signature extracted from the request, or the reason why it
+/// failed.
+pub(crate) fn extract_signature(req: &Request) -> Result {
+ req.headers()
+ .get(SIGNATURE_HEADER)
+ .map(|v| {
+ Signature::from_str(v.to_str().map_err(|err| err.to_string())?)
+ .map_err(|err| err.to_string())
+ })
+ .ok_or_else(|| "The signature is missing".to_owned())?
+}
diff --git a/crates/oxidetalis_config/Cargo.toml b/crates/oxidetalis_config/Cargo.toml
new file mode 100644
index 0000000..3b6b05f
--- /dev/null
+++ b/crates/oxidetalis_config/Cargo.toml
@@ -0,0 +1,71 @@
+[package]
+name = "oxidetalis_config"
+description = "A library for managing configurations of Oxidetalis homeserver"
+edition = "2021"
+license = "MIT"
+authors.workspace = true
+readme.workspace = true
+repository.workspace = true
+version.workspace = true
+rust-version.workspace = true
+
+
+[dependencies]
+oxidetalis_core = { workspace = true }
+thiserror = { workspace = true }
+serde = { workspace = true }
+log = { workspace = true }
+salvo_core = { workspace = true }
+salvo-oapi = { workspace = true }
+clap = { version = "4.5.7", features = ["derive", "env"] }
+base58 = "0.2.0"
+toml = "0.8.14"
+derivative = "2.2.0"
+
+
+[lints.rust]
+unsafe_code = "deny"
+missing_docs = "warn"
+
+[lints.clippy]
+wildcard_imports = "deny"
+manual_let_else = "deny"
+match_bool = "deny"
+match_on_vec_items = "deny"
+or_fun_call = "deny"
+panic = "deny"
+unwrap_used = "deny"
+
+missing_assert_message = "warn"
+missing_const_for_fn = "warn"
+missing_errors_doc = "warn"
+absolute_paths = "warn"
+cast_lossless = "warn"
+clone_on_ref_ptr = "warn"
+cloned_instead_of_copied = "warn"
+dbg_macro = "warn"
+default_trait_access = "warn"
+empty_enum_variants_with_brackets = "warn"
+empty_line_after_doc_comments = "warn"
+empty_line_after_outer_attr = "warn"
+empty_structs_with_brackets = "warn"
+enum_glob_use = "warn"
+equatable_if_let = "warn"
+explicit_iter_loop = "warn"
+filetype_is_file = "warn"
+filter_map_next = "warn"
+flat_map_option = "warn"
+float_cmp = "warn"
+format_push_string = "warn"
+future_not_send = "warn"
+if_not_else = "warn"
+if_then_some_else_none = "warn"
+implicit_clone = "warn"
+inconsistent_struct_constructor = "warn"
+indexing_slicing = "warn"
+iter_filter_is_ok = "warn"
+iter_filter_is_some = "warn"
+iter_not_returning_iterator = "warn"
+manual_is_variant_and = "warn"
+option_if_let_else = "warn"
+option_option = "warn"
diff --git a/crates/oxidetalis_config/README.md b/crates/oxidetalis_config/README.md
new file mode 100644
index 0000000..c72965a
--- /dev/null
+++ b/crates/oxidetalis_config/README.md
@@ -0,0 +1,25 @@
+# Oxidetalis configurations
+A library for managing configurations of Oxidetalis homeserver.
+
+## Key Features
+- **Load and write configurations**: Load configurations from a file and write
+ configurations to a file.
+- **Multiple configuration entries**: The configurations are collected from CLI
+ arguments, environment variables, and configuration files.
+- **Configuration validation**: Validate the configurations before using them.
+- **Configuration defaults**: Set default values for configurations.
+
+## Must to know
+- The configurations are loaded in the following order (from highest priority to
+ lowest priority)
+ 1. Command-line options
+ 2. Environment variables
+ 3. Configuration file
+ 4. Default values (or ask you to provide the value)
+- The configurations are written to the configuration file every time you run
+ the server, even if you don't change any configuration. This is to ensure that
+ the configuration file is always up-to-date.
+
+
+## License
+This crate is licensed under the MIT license.
diff --git a/crates/oxidetalis_config/src/commandline.rs b/crates/oxidetalis_config/src/commandline.rs
new file mode 100644
index 0000000..021172e
--- /dev/null
+++ b/crates/oxidetalis_config/src/commandline.rs
@@ -0,0 +1,98 @@
+// OxideTalis homeserver configurations
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! Command-line arguments parser
+
+use std::{net::IpAddr, path::PathBuf};
+
+use clap::Parser;
+use oxidetalis_core::types::Size;
+
+use crate::{types::OpenApiViewer, IpOrUrl};
+
+#[derive(Parser)]
+#[clap(version)]
+/// Command-line arguments for the Oxidetalis server.
+pub struct CliArgs {
+ /// Path to the configuration file, toml format.
+ #[clap(long, env = "OXIDETALIS_CONFIG")]
+ pub config: PathBuf,
+ /// Server name, for example, `example.com`.
+ #[clap(long, env = "OXIDETALIS_SERVER_NAME")]
+ pub server_name: Option,
+ /// Local IP address to bind the server to.
+ #[clap(long, env = "OXIDETALIS_SERVER_HOST")]
+ pub server_host: Option,
+ /// Port to bind the server to.
+ #[clap(long, env = "OXIDETALIS_SERVER_PORT")]
+ pub server_port: Option,
+ /// Nonce cache size
+ ///
+ /// e.g. "50B", "300KB", "1MB", "1GB"
+ #[clap(long, env = "OXIDETALIS_SERVER_NONCE_CACHE_SIZE")]
+ pub server_nonce_cache_size: Option,
+ /// Enable or disable user registration.
+ #[clap(long, env = "OXIDETALIS_REGISTER_ENABLE")]
+ pub register_enable: Option,
+ /// Hostname or IP address of the PostgreSQL database.
+ #[clap(long, env = "OXIDETALIS_DB_HOST")]
+ pub postgres_host: Option,
+ /// Port number of the PostgreSQL database.
+ #[clap(long, env = "OXIDETALIS_DB_PORT")]
+ pub postgres_port: Option,
+ /// Username for the PostgreSQL database.
+ #[clap(long, env = "OXIDETALIS_DB_USER")]
+ pub postgres_user: Option,
+ /// Password for the PostgreSQL database.
+ #[clap(long, env = "OXIDETALIS_DB_PASSWORD")]
+ pub postgres_password: Option,
+ /// Name of the PostgreSQL database.
+ #[clap(long, env = "OXIDETALIS_DB_NAME")]
+ pub postgres_name: Option,
+ /// Enable or disable rate limiting.
+ #[clap(long, env = "OXIDETALIS_RATELIMIT_ENABLE")]
+ pub ratelimit_enable: Option,
+ /// Maximum number of requests allowed within a given time period for rate
+ /// limiting.
+ #[clap(long, env = "OXIDETALIS_RATELIMIT_LIMIT")]
+ pub ratelimit_limit: Option,
+ /// Time period in seconds for rate limiting.
+ #[clap(long, env = "OXIDETALIS_RATELIMIT_PREIOD")]
+ pub ratelimit_preiod: Option,
+ /// Enable or disable OpenAPI documentation generation.
+ #[clap(long, env = "OXIDETALIS_OPENAPI_ENABLE")]
+ pub openapi_enable: Option,
+ /// Title for the OpenAPI documentation.
+ #[clap(long, env = "OXIDETALIS_OPENAPI_TITLE")]
+ pub openapi_title: Option,
+ /// Description for the OpenAPI documentation.
+ #[clap(long, env = "OXIDETALIS_OPENAPI_DESCRIPTION")]
+ pub openapi_description: Option,
+ /// Path to serve the OpenAPI documentation.
+ #[clap(long, env = "OXIDETALIS_OPENAPI_PATH")]
+ pub openapi_path: Option,
+ /// OpenAPI viewer to use for rendering the documentation.
+ #[clap(long, env = "OXIDETALIS_OPENAPI_VIEWER")]
+ pub openapi_viewer: Option,
+ /// Path to the OpenAPI viewer HTML file.
+ #[clap(long, env = "OXIDETALIS_OPENAPI_VIEWER_PATH")]
+ pub openapi_viewer_path: Option,
+}
diff --git a/crates/oxidetalis_config/src/defaults.rs b/crates/oxidetalis_config/src/defaults.rs
new file mode 100644
index 0000000..57047c4
--- /dev/null
+++ b/crates/oxidetalis_config/src/defaults.rs
@@ -0,0 +1,109 @@
+// OxideTalis homeserver configurations
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! The config defaults value
+
+/// Server default configs
+pub(crate) mod server {
+ use std::net::{IpAddr, Ipv4Addr};
+
+ use oxidetalis_core::{
+ cipher::K256Secret,
+ types::{PrivateKey, Size},
+ };
+
+ pub fn name() -> String {
+ "example.com".to_owned()
+ }
+ pub const fn host() -> IpAddr {
+ IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
+ }
+ pub const fn port() -> u16 {
+ 3873
+ }
+ pub fn private_key() -> PrivateKey {
+ K256Secret::new().privkey()
+ }
+ pub const fn nonce_cache_size() -> Size {
+ Size::MB(1)
+ }
+}
+
+/// Ratelimit default configs
+pub(crate) mod ratelimit {
+
+ pub const fn limit() -> usize {
+ 1500
+ }
+ pub const fn period_secs() -> usize {
+ 60
+ }
+}
+
+/// OpenApi default configs
+pub(crate) mod openapi {
+ use crate::types;
+
+ pub fn title() -> String {
+ "Oxidetalis homeserver".to_owned()
+ }
+ pub fn description() -> String {
+ "OxideTalis Messaging Protocol homeserver".to_owned()
+ }
+ pub fn path() -> String {
+ "/openapi.json".to_owned()
+ }
+ pub const fn viewer() -> types::OpenApiViewer {
+ types::OpenApiViewer::SwaggerUi
+ }
+ pub fn viewer_path() -> String {
+ "/swagger-ui".to_owned()
+ }
+}
+
+/// Postgres default configs
+pub(crate) mod postgres {
+ use std::str::FromStr;
+
+ pub fn user() -> String {
+ "oxidetalis".to_owned()
+ }
+ pub fn password() -> String {
+ "oxidetalis".to_owned()
+ }
+ pub fn host() -> crate::IpOrUrl {
+ crate::IpOrUrl::from_str("localhost").expect("Is a valid localhost")
+ }
+ pub fn name() -> String {
+ "oxidetalis_db".to_owned()
+ }
+ pub const fn port() -> u16 {
+ 5432
+ }
+}
+
+pub(crate) const fn bool_true() -> bool {
+ true
+}
+
+pub(crate) const fn bool_false() -> bool {
+ false
+}
diff --git a/crates/oxidetalis_config/src/lib.rs b/crates/oxidetalis_config/src/lib.rs
new file mode 100644
index 0000000..700b8bb
--- /dev/null
+++ b/crates/oxidetalis_config/src/lib.rs
@@ -0,0 +1,247 @@
+// OxideTalis homeserver configurations
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#![doc = include_str!("../README.md")]
+
+use std::{fs, io::Error as IoError, net::IpAddr, path::Path};
+
+use derivative::Derivative;
+use oxidetalis_core::types::{PrivateKey, Size};
+use serde::{Deserialize, Serialize};
+use toml::{de::Error as TomlDeError, ser::Error as TomlSerError};
+
+mod commandline;
+mod defaults;
+mod serde_with;
+mod types;
+
+pub use clap::Parser;
+pub use commandline::CliArgs;
+pub use types::*;
+
+/// Configuration errors
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error("IO: {0}")]
+ IO(#[from] IoError),
+ #[error("Toml error: {0}")]
+ DeToml(#[from] TomlDeError),
+ #[error("Toml error: {0}")]
+ SeToml(#[from] TomlSerError),
+ #[error("Missing required option `--{0}`")]
+ RequiredConfiguration(String),
+}
+
+/// Server startup configuration
+#[derive(Deserialize, Serialize, Derivative, Clone)]
+#[derivative(Default)]
+#[serde(default)]
+pub struct Server {
+ /// Name of the server, for example, `example.com`
+ #[derivative(Default(value = "defaults::server::name()"))]
+ pub server_name: String,
+ /// Host that the server will listen in
+ #[derivative(Default(value = "defaults::server::host()"))]
+ pub host: IpAddr,
+ /// Port that the server will listen in
+ #[derivative(Default(value = "defaults::server::port()"))]
+ pub port: u16,
+ /// Server keypair
+ #[derivative(Default(value = "defaults::server::private_key()"))]
+ pub private_key: PrivateKey,
+ /// Nonce cache limit
+ #[derivative(Default(value = "defaults::server::nonce_cache_size()"))]
+ pub nonce_cache_size: Size,
+}
+
+/// Registration config
+#[derive(Debug, Deserialize, Serialize, Derivative, Clone)]
+#[derivative(Default)]
+#[serde(default)]
+pub struct Register {
+ /// Whether to enable the registration or not
+ #[derivative(Default(value = "defaults::bool_false()"))]
+ pub enable: bool,
+}
+
+/// Database configuration
+#[derive(Debug, Deserialize, Serialize, Derivative, Clone)]
+#[derivative(Default)]
+#[serde(default)]
+pub struct Postgres {
+ /// Username
+ #[derivative(Default(value = "defaults::postgres::user()"))]
+ pub user: String,
+ /// User password
+ #[derivative(Default(value = "defaults::postgres::password()"))]
+ pub password: String,
+ /// Database host
+ #[derivative(Default(value = "defaults::postgres::host()"))]
+ pub host: IpOrUrl,
+ /// Database port
+ #[derivative(Default(value = "defaults::postgres::port()"))]
+ pub port: u16,
+ /// Database name
+ #[derivative(Default(value = "defaults::postgres::name()"))]
+ pub name: String,
+}
+
+/// Ratelimit configuration
+#[derive(Debug, Deserialize, Serialize, Derivative, Clone)]
+#[derivative(Default)]
+#[serde(default)]
+pub struct Ratelimit {
+ /// Whether to enable the ratelimit or not
+ #[derivative(Default(value = "defaults::bool_true()"))]
+ pub enable: bool,
+ /// The limit of requests.
+ #[derivative(Default(value = "defaults::ratelimit::limit()"))]
+ pub limit: usize,
+ /// The period of requests.
+ #[derivative(Default(value = "defaults::ratelimit::period_secs()"))]
+ pub period_secs: usize,
+}
+
+/// OpenApi configuration
+#[derive(Debug, Deserialize, Serialize, Derivative, Clone)]
+#[derivative(Default)]
+#[serde(default)]
+pub struct OpenApi {
+ /// Whether to enable the openapi or not
+ #[derivative(Default(value = "defaults::bool_false()"))]
+ pub enable: bool,
+ /// Title of the openapi
+ #[derivative(Default(value = "defaults::openapi::title()"))]
+ pub title: String,
+ /// Description of the openapi
+ #[derivative(Default(value = "defaults::openapi::description()"))]
+ pub description: String,
+ /// Location to serve openapi json in
+ #[derivative(Default(value = "defaults::openapi::path()"))]
+ #[serde(deserialize_with = "serde_with::deserialize_url_path")]
+ pub path: String,
+ /// The openapi viewer
+ #[derivative(Default(value = "defaults::openapi::viewer()"))]
+ pub viewer: types::OpenApiViewer,
+ /// Location to server the viewer in
+ #[derivative(Default(value = "defaults::openapi::viewer_path()"))]
+ #[serde(deserialize_with = "serde_with::deserialize_url_path")]
+ pub viewer_path: String,
+}
+
+#[derive(Deserialize, Serialize, Default, Clone)]
+/// Oxidetalis homeserver configurations
+pub struct Config {
+ /// Server configuration (server startup configuration)
+ #[serde(default)]
+ pub server: Server,
+ /// Server registration configuration
+ #[serde(default)]
+ pub register: Register,
+ /// Database configuration
+ pub postgresdb: Postgres,
+ /// Ratelimit configuration
+ #[serde(default)]
+ pub ratelimit: Ratelimit,
+ /// OpenApi configuration
+ #[serde(default)]
+ pub openapi: OpenApi,
+}
+
+/// Check if required new configuration options are provided
+fn check_required_new_config(args: &CliArgs) -> Result<(), Error> {
+ log::info!("Checking the required options for the new configuration");
+ if args.server_name.is_none() {
+ return Err(Error::RequiredConfiguration("server-name".to_owned()));
+ }
+ Ok(())
+}
+
+impl Config {
+ /// Load the config from toml file and command-line options
+ ///
+ /// The priority is:
+ /// 1. Command-line options
+ /// 2. Environment variables
+ /// 3. Configuration file
+ /// 4. Default values (or ask you to provide the value)
+ ///
+ /// ## Errors
+ /// - Failed to read the config file
+ /// - Invalid toml file
+ pub fn load(args: CliArgs) -> Result {
+ let mut config = if args.config.exists() {
+ log::info!("Loading configuration from {}", args.config.display());
+ toml::from_str(&fs::read_to_string(&args.config)?)?
+ } else {
+ log::info!("Configuration file not found, creating a new one");
+ check_required_new_config(&args)?;
+ if let Some(parent) = args.config.parent() {
+ if !parent.exists() {
+ fs::create_dir_all(parent)?;
+ }
+ }
+ Config::default()
+ };
+
+ assign_option(&mut config.server.server_name, args.server_name);
+ assign_option(&mut config.server.host, args.server_host);
+ assign_option(&mut config.server.port, args.server_port);
+ assign_option(
+ &mut config.server.nonce_cache_size,
+ args.server_nonce_cache_size,
+ );
+ assign_option(&mut config.register.enable, args.register_enable);
+ assign_option(&mut config.postgresdb.host, args.postgres_host);
+ assign_option(&mut config.postgresdb.port, args.postgres_port);
+ assign_option(&mut config.postgresdb.user, args.postgres_user);
+ assign_option(&mut config.postgresdb.password, args.postgres_password);
+ assign_option(&mut config.postgresdb.name, args.postgres_name);
+ assign_option(&mut config.ratelimit.enable, args.ratelimit_enable);
+ assign_option(&mut config.ratelimit.limit, args.ratelimit_limit);
+ assign_option(&mut config.ratelimit.period_secs, args.ratelimit_preiod);
+ assign_option(&mut config.openapi.enable, args.openapi_enable);
+ assign_option(&mut config.openapi.title, args.openapi_title);
+ assign_option(&mut config.openapi.description, args.openapi_description);
+ assign_option(&mut config.openapi.path, args.openapi_path);
+ assign_option(&mut config.openapi.viewer, args.openapi_viewer);
+ assign_option(&mut config.openapi.viewer_path, args.openapi_viewer_path);
+
+ config.write(&args.config)?;
+ Ok(config)
+ }
+
+ /// Write the configs to the config file
+ ///
+ /// ## Errors
+ /// - Failed to write to the config file
+ pub fn write(&self, config_file: impl AsRef) -> Result<(), Error> {
+ fs::write(config_file, toml::to_string_pretty(self)?)?;
+ Ok(())
+ }
+}
+
+/// Assign the command-line option to the config if it is not None
+fn assign_option(config: &mut T, arg: Option) {
+ if let Some(value) = arg {
+ *config = value
+ }
+}
diff --git a/crates/oxidetalis_config/src/serde_with.rs b/crates/oxidetalis_config/src/serde_with.rs
new file mode 100644
index 0000000..725d7c8
--- /dev/null
+++ b/crates/oxidetalis_config/src/serde_with.rs
@@ -0,0 +1,63 @@
+// OxideTalis homeserver configurations
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! Serialize and deserialize some oxidetalis configurations
+
+use serde::{de::Error as DeError, Deserialize, Deserializer};
+
+/// Serialize and deserialze the string of IpOrUrl struct
+pub(crate) mod ip_or_url {
+ use std::str::FromStr;
+
+ use serde::{de::Error as DeError, Deserialize, Deserializer, Serializer};
+
+ use crate::IpOrUrl;
+
+ pub fn serialize(value: &str, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(value)
+ }
+
+ pub fn deserialize<'de, D>(de: D) -> Result
+ where
+ D: Deserializer<'de>,
+ {
+ Ok(IpOrUrl::from_str(&String::deserialize(de)?)
+ .map_err(DeError::custom)?
+ .as_str()
+ .to_owned())
+ }
+}
+
+pub fn deserialize_url_path<'de, D>(de: D) -> Result
+where
+ D: Deserializer<'de>,
+{
+ let url_path = String::deserialize(de)?;
+ if !url_path.starts_with('/') || url_path.ends_with('/') {
+ return Err(DeError::custom(
+ "Invalid url path, must start with `/` and not ends with `/`",
+ ));
+ }
+ Ok(url_path)
+}
diff --git a/crates/oxidetalis_config/src/types.rs b/crates/oxidetalis_config/src/types.rs
new file mode 100644
index 0000000..fe82322
--- /dev/null
+++ b/crates/oxidetalis_config/src/types.rs
@@ -0,0 +1,126 @@
+// OxideTalis homeserver configurations
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! Oxidetalis config types
+
+use std::{net::IpAddr, str::FromStr};
+
+use salvo_oapi::{rapidoc::RapiDoc, redoc::ReDoc, scalar::Scalar, swagger_ui::SwaggerUi};
+use serde::{Deserialize, Serialize};
+
+/// OpenApi viewers, the viewers that can be used to view the OpenApi
+/// documentation
+#[derive(Debug, Clone, Deserialize, Serialize, clap::ValueEnum)]
+#[serde(rename_all = "PascalCase")]
+pub enum OpenApiViewer {
+ /// Redoc viewer
+ RapiDoc,
+ /// Redoc viewer
+ ReDoc,
+ /// Scalar viewer
+ Scalar,
+ /// Swagger-UI viewer
+ SwaggerUi,
+}
+
+impl OpenApiViewer {
+ /// Create a router for the viewer
+ pub fn into_router(&self, config: &crate::Config) -> salvo_core::Router {
+ let spec_url = config.openapi.path.clone();
+ let title = config.openapi.title.clone();
+ let description = config.openapi.description.clone();
+
+ match self {
+ OpenApiViewer::RapiDoc => {
+ RapiDoc::new(spec_url)
+ .title(title)
+ .description(description)
+ .into_router(&config.openapi.viewer_path)
+ }
+ OpenApiViewer::ReDoc => {
+ ReDoc::new(spec_url)
+ .title(title)
+ .description(description)
+ .into_router(&config.openapi.viewer_path)
+ }
+ OpenApiViewer::Scalar => {
+ Scalar::new(spec_url)
+ .title(title)
+ .description(description)
+ .into_router(&config.openapi.viewer_path)
+ }
+ OpenApiViewer::SwaggerUi => {
+ SwaggerUi::new(spec_url)
+ .title(title)
+ .description(description)
+ .into_router(&config.openapi.viewer_path)
+ }
+ }
+ }
+}
+
+/// Type hold url or ip (used for database host)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct IpOrUrl(#[serde(with = "crate::serde_with::ip_or_url")] String);
+
+impl Default for IpOrUrl {
+ fn default() -> Self {
+ IpOrUrl("localhost".to_owned())
+ }
+}
+
+impl IpOrUrl {
+ /// Returns &str ip or url
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+impl FromStr for IpOrUrl {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result {
+ Ok(IpOrUrl(
+ if let Ok(res) = IpAddr::from_str(s).map(|i| i.to_string()) {
+ res
+ } else {
+ validate_domain(s)?
+ },
+ ))
+ }
+}
+
+fn validate_domain(domain: &str) -> Result {
+ if domain != "localhost" {
+ let subs = domain.split('.');
+ for sub in subs {
+ let length = sub.chars().count();
+ if !sub.chars().all(|c| c.is_alphanumeric() || c == '-')
+ || sub.starts_with('-')
+ || sub.ends_with('-')
+ || (length > 0 && length <= 64)
+ {
+ return Err("Invalid domain name".to_owned());
+ }
+ }
+ }
+ Ok(domain.to_owned())
+}
diff --git a/crates/oxidetalis_core/Cargo.toml b/crates/oxidetalis_core/Cargo.toml
new file mode 100644
index 0000000..00c8746
--- /dev/null
+++ b/crates/oxidetalis_core/Cargo.toml
@@ -0,0 +1,74 @@
+[package]
+name = "oxidetalis_core"
+description = "OxideTalis server core"
+edition = "2021"
+license = "MIT"
+authors.workspace = true
+readme.workspace = true
+repository.workspace = true
+version.workspace = true
+rust-version.workspace = true
+
+
+[dependencies]
+base58 = { workspace = true }
+thiserror = { workspace = true }
+salvo_core = { workspace = true }
+salvo-oapi = { workspace = true }
+serde = { workspace = true }
+log = { workspace = true }
+logcall = { workspace = true }
+cbc = { version = "0.1.2", features = ["alloc", "std"] }
+k256 = { version = "0.13.3", default-features = false, features = ["ecdh"] }
+rand = { version = "0.8.5", default-features = false, features = ["std_rng", "std"] }
+aes = "0.8.4"
+hex = "0.4.3"
+hmac = "0.12.1"
+sha2 = "0.10.8"
+
+
+[lints.rust]
+unsafe_code = "deny"
+missing_docs = "warn"
+
+[lints.clippy]
+wildcard_imports = "deny"
+manual_let_else = "deny"
+match_bool = "deny"
+match_on_vec_items = "deny"
+or_fun_call = "deny"
+panic = "deny"
+unwrap_used = "deny"
+
+missing_assert_message = "warn"
+missing_const_for_fn = "warn"
+missing_errors_doc = "warn"
+absolute_paths = "warn"
+cast_lossless = "warn"
+clone_on_ref_ptr = "warn"
+cloned_instead_of_copied = "warn"
+dbg_macro = "warn"
+default_trait_access = "warn"
+empty_enum_variants_with_brackets = "warn"
+empty_line_after_doc_comments = "warn"
+empty_line_after_outer_attr = "warn"
+empty_structs_with_brackets = "warn"
+enum_glob_use = "warn"
+equatable_if_let = "warn"
+explicit_iter_loop = "warn"
+filetype_is_file = "warn"
+filter_map_next = "warn"
+flat_map_option = "warn"
+float_cmp = "warn"
+format_push_string = "warn"
+future_not_send = "warn"
+if_not_else = "warn"
+if_then_some_else_none = "warn"
+implicit_clone = "warn"
+inconsistent_struct_constructor = "warn"
+iter_filter_is_ok = "warn"
+iter_filter_is_some = "warn"
+iter_not_returning_iterator = "warn"
+manual_is_variant_and = "warn"
+option_if_let_else = "warn"
+option_option = "warn"
diff --git a/crates/oxidetalis_core/src/cipher.rs b/crates/oxidetalis_core/src/cipher.rs
new file mode 100644
index 0000000..00c2f9e
--- /dev/null
+++ b/crates/oxidetalis_core/src/cipher.rs
@@ -0,0 +1,224 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! The `cipher` module contains the encryption and decryption functions for the
+//! OxideTalis protocol.
+
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
+use hmac::Mac;
+use k256::{
+ ecdh::diffie_hellman,
+ elliptic_curve::sec1::ToEncodedPoint,
+ FieldBytes,
+ NonZeroScalar,
+ PublicKey,
+};
+use logcall::logcall;
+use rand::{thread_rng, RngCore};
+
+use crate::types::{
+ PrivateKey as CorePrivateKey,
+ PublicKey as CorePublicKey,
+ Signature as CoreSignature,
+};
+
+/// The errors that can occur during in the cipher module.
+#[derive(Debug, thiserror::Error)]
+pub enum CipherError {
+ /// The public key is invalid.
+ #[error("Invalid Public Key")]
+ InvalidPublicKey,
+ /// The private key is invalid.
+ #[error("Invalid Private Key")]
+ InvalidPrivateKey,
+ /// The signature is invalid
+ #[error("Invalid signature")]
+ InvalidSignature,
+
+ /// A decryption error
+ #[error("Decryption Error")]
+ Decryption,
+ /// Invalid base58 string
+ #[error("Invalid base58 string `{0}`")]
+ InvalidBase58(String),
+ /// Invalid hex string
+ #[error("Invalid hex string `{0}`")]
+ InvalidHex(String),
+}
+#[allow(clippy::absolute_paths)]
+type Result = std::result::Result;
+type Aes256CbcEnc = cbc::Encryptor;
+type Aes256CbcDec = cbc::Decryptor;
+type HmacSha256 = hmac::Hmac;
+
+/// An wrapper around the k256 crate to provide a simple API for ecdh key
+/// exchange and keypair generation.
+pub struct K256Secret {
+ /// The private key scalar
+ scalar: NonZeroScalar,
+ /// The public key
+ public_key: PublicKey,
+}
+
+impl From for K256Secret {
+ fn from(scalar: NonZeroScalar) -> Self {
+ Self {
+ public_key: PublicKey::from_secret_scalar(&scalar),
+ scalar,
+ }
+ }
+}
+
+impl K256Secret {
+ /// Generate a new random keypair, using the system random number generator.
+ #[allow(clippy::new_without_default)]
+ pub fn new() -> Self {
+ Self::from(NonZeroScalar::random(&mut rand::thread_rng()))
+ }
+
+ /// Restore a keypair from a private key.
+ pub fn from_privkey(private_key: &CorePrivateKey) -> Self {
+ Self::from(
+ Option::::from(NonZeroScalar::from_repr(*FieldBytes::from_slice(
+ private_key.as_bytes(),
+ )))
+ .expect("The private key is correct"),
+ )
+ }
+
+ /// Returns the public key.
+ pub fn pubkey(&self) -> CorePublicKey {
+ CorePublicKey::try_from(
+ <[u8; 33]>::try_from(self.public_key.to_encoded_point(true).as_bytes())
+ .expect("The length is correct"),
+ )
+ .expect("Is correct public key")
+ }
+
+ /// Returns the private key.
+ pub fn privkey(&self) -> CorePrivateKey {
+ CorePrivateKey::try_from(<[u8; 32]>::from(FieldBytes::from(self.scalar)))
+ .expect("Correct private key")
+ }
+
+ /// Compute the shared secret with the given public key.
+ pub fn shared_secret(&self, with: &CorePublicKey) -> [u8; 32] {
+ let mut secret_buf = [0u8; 32];
+ diffie_hellman(
+ self.scalar,
+ PublicKey::from_sec1_bytes(with.as_bytes())
+ .expect("Correct public key")
+ .as_affine(),
+ )
+ .extract::(None)
+ .expand(&[], &mut secret_buf)
+ .expect("The buffer size is correct");
+
+ secret_buf
+ }
+
+ /// Encrypt a data with the shared secret.
+ ///
+ /// The data is encrypted using AES-256-CBC with a random IV (last 16 bytes
+ /// of the ciphertext).
+ pub fn encrypt_data(&self, encrypt_to: &CorePublicKey, data: &[u8]) -> Vec {
+ let mut iv = [0u8; 16];
+ thread_rng().fill_bytes(&mut iv);
+
+ let mut ciphertext =
+ Aes256CbcEnc::new(self.shared_secret(encrypt_to).as_slice().into(), &iv.into())
+ .encrypt_padded_vec_mut::(data);
+ ciphertext.extend(&iv);
+ ciphertext
+ }
+
+ /// Decrypt a data with the shared secret.
+ ///
+ /// The data is decrypted using AES-256-CBC with the IV being the last 16
+ /// bytes of the ciphertext.
+ ///
+ /// ## Errors
+ /// - If the data less then 16 bytes.
+ /// - If the iv less then 16 bytes.
+ /// - Falid to decrypt the data (invalid encrypted data)
+ pub fn decrypt_data(&self, decrypt_from: &CorePublicKey, data: &[u8]) -> Result> {
+ let (ciphertext, iv) =
+ data.split_at(data.len().checked_sub(16).ok_or(CipherError::Decryption)?);
+
+ if iv.len() != 16 {
+ return Err(CipherError::Decryption);
+ }
+
+ Aes256CbcDec::new(
+ self.shared_secret(decrypt_from).as_slice().into(),
+ iv.into(),
+ )
+ .decrypt_padded_vec_mut::(ciphertext)
+ .map_err(|_| CipherError::Decryption)
+ }
+
+ /// Sign a data with the shared secret.
+ ///
+ /// The signature is exiplained in the OTMP specification.
+ #[logcall]
+ pub fn sign(&self, data: &[u8], sign_to: &CorePublicKey) -> CoreSignature {
+ let mut time_and_nonce = [0u8; 24];
+ time_and_nonce[0..=7].copy_from_slice(
+ &SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("SystemTime before UNIX EPOCH!")
+ .as_secs()
+ .to_be_bytes(),
+ );
+ thread_rng().fill_bytes(&mut time_and_nonce[8..=23]);
+
+ let mut hmac_secret = [0u8; 56];
+ hmac_secret[0..=31].copy_from_slice(&self.shared_secret(sign_to));
+ hmac_secret[32..=55].copy_from_slice(&time_and_nonce);
+ let mut signature = [0u8; 56];
+ signature[0..=31].copy_from_slice(&hmac_sha256(data, &hmac_secret));
+ signature[32..=55].copy_from_slice(&time_and_nonce);
+
+ CoreSignature::from(signature)
+ }
+
+ /// Verify a signature with the shared secret.
+ ///
+ /// Note:
+ /// The time and the nonce will not be checked here
+ #[logcall]
+ pub fn verify(&self, data: &[u8], signature: &CoreSignature, signer: &CorePublicKey) -> bool {
+ let mut hmac_secret = [0u8; 56];
+ hmac_secret[0..=31].copy_from_slice(&self.shared_secret(signer));
+ hmac_secret[32..=39].copy_from_slice(signature.timestamp());
+ hmac_secret[40..=55].copy_from_slice(signature.nonce());
+
+ &hmac_sha256(data, &hmac_secret) == signature.hmac_output()
+ }
+}
+
+fn hmac_sha256(data: &[u8], secret: &[u8]) -> [u8; 32] {
+ let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
+ mac.update(data);
+ mac.finalize().into_bytes().into()
+}
diff --git a/crates/oxidetalis_core/src/lib.rs b/crates/oxidetalis_core/src/lib.rs
new file mode 100644
index 0000000..af4e532
--- /dev/null
+++ b/crates/oxidetalis_core/src/lib.rs
@@ -0,0 +1,33 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! The core library for the OxideTalis homeserver implementation.
+
+pub mod cipher;
+pub mod types;
+
+/// The header name for the signature. The signature is a hex encoded string.
+pub const SIGNATURE_HEADER: &str = "X-OTMP-SIGNATURE";
+/// The header name of the request sender public key. The public key is a base58
+/// encoded string.
+pub const PUBLIC_KEY_HEADER: &str = "X-OTMP-PUBLIC";
+/// Server name header name
+pub const SERVER_NAME_HEADER: &str = "X-OTMP-SERVER";
diff --git a/crates/oxidetalis_core/src/types/cipher.rs b/crates/oxidetalis_core/src/types/cipher.rs
new file mode 100644
index 0000000..e7285df
--- /dev/null
+++ b/crates/oxidetalis_core/src/types/cipher.rs
@@ -0,0 +1,213 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+use std::{fmt, str::FromStr};
+
+use base58::{FromBase58, ToBase58};
+use salvo_oapi::{
+ schema::{
+ Schema as OapiSchema,
+ SchemaFormat as OapiSchemaFormat,
+ SchemaType as OapiSchemaType,
+ },
+ ToSchema,
+};
+
+use crate::cipher::CipherError;
+
+/// Correct length except message
+const CORRECT_LENGTH: &str = "The length is correct";
+
+/// K256 public key
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct PublicKey([u8; 33]);
+
+/// K256 private key
+#[derive(Clone, Copy)]
+pub struct PrivateKey([u8; 32]);
+
+/// OTMP signature
+#[derive(Clone, Copy, Debug)]
+pub struct Signature {
+ hmac_output: [u8; 32],
+ timestamp: [u8; 8],
+ nonce: [u8; 16],
+}
+
+impl PublicKey {
+ /// Returns the public key as bytes
+ pub const fn as_bytes(&self) -> &[u8; 33] {
+ &self.0
+ }
+}
+
+impl PrivateKey {
+ /// Returns the private key as bytes
+ pub const fn as_bytes(&self) -> &[u8; 32] {
+ &self.0
+ }
+}
+
+impl Signature {
+ /// Returns the hmac output from the signature
+ pub const fn hmac_output(&self) -> &[u8; 32] {
+ &self.hmac_output
+ }
+
+ /// Returns the timestamp from the signature
+ pub const fn timestamp(&self) -> &[u8; 8] {
+ &self.timestamp
+ }
+
+ /// Returns the nonce from the signature
+ pub const fn nonce(&self) -> &[u8; 16] {
+ &self.nonce
+ }
+
+ /// Returns the signature as bytes
+ pub fn as_bytes(&self) -> [u8; 56] {
+ let mut sig = [0u8; 56];
+ sig[0..=31].copy_from_slice(&self.hmac_output);
+ sig[32..=39].copy_from_slice(&self.timestamp);
+ sig[40..=55].copy_from_slice(&self.nonce);
+ sig
+ }
+}
+
+/// Public key to base58 string
+impl fmt::Display for PublicKey {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0.to_base58())
+ }
+}
+
+/// Public key to base58 string
+impl fmt::Display for PrivateKey {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0.to_base58())
+ }
+}
+
+/// Signature to hex string
+impl fmt::Display for Signature {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", hex::encode(self.as_bytes()))
+ }
+}
+
+/// Public key from base58 string
+impl FromStr for PublicKey {
+ type Err = CipherError;
+
+ fn from_str(s: &str) -> Result {
+ let public_key = s
+ .from_base58()
+ .map_err(|_| CipherError::InvalidBase58(s.to_owned()))?;
+ if public_key.len() != 33 {
+ return Err(CipherError::InvalidPublicKey);
+ }
+ Self::try_from(<[u8; 33]>::try_from(public_key).expect(CORRECT_LENGTH))
+ }
+}
+
+/// Private key from base58 string
+impl FromStr for PrivateKey {
+ type Err = CipherError;
+
+ fn from_str(s: &str) -> Result {
+ let private_key = s
+ .from_base58()
+ .map_err(|_| CipherError::InvalidBase58(s.to_owned()))?;
+ if private_key.len() != 32 {
+ return Err(CipherError::InvalidPrivateKey);
+ }
+
+ Self::try_from(<[u8; 32]>::try_from(private_key).expect(CORRECT_LENGTH))
+ }
+}
+
+/// Signature from hex string
+impl FromStr for Signature {
+ type Err = CipherError;
+
+ fn from_str(s: &str) -> Result {
+ let signature = hex::decode(s).map_err(|_| CipherError::InvalidHex(s.to_owned()))?;
+ if signature.len() != 56 {
+ return Err(CipherError::InvalidSignature);
+ }
+ Ok(Signature::from(
+ <[u8; 56]>::try_from(signature).expect(CORRECT_LENGTH),
+ ))
+ }
+}
+
+impl TryFrom<[u8; 33]> for PublicKey {
+ type Error = CipherError;
+
+ fn try_from(public_key: [u8; 33]) -> Result {
+ if k256::PublicKey::from_sec1_bytes(&public_key).is_err() {
+ return Err(CipherError::InvalidPublicKey);
+ }
+ Ok(Self(public_key))
+ }
+}
+
+impl TryFrom<[u8; 32]> for PrivateKey {
+ type Error = CipherError;
+
+ fn try_from(private_key: [u8; 32]) -> Result {
+ if k256::NonZeroScalar::from_repr(*k256::FieldBytes::from_slice(&private_key))
+ .is_none()
+ .into()
+ {
+ return Err(CipherError::InvalidPrivateKey);
+ }
+ Ok(Self(private_key))
+ }
+}
+
+impl From<[u8; 56]> for Signature {
+ fn from(signature: [u8; 56]) -> Self {
+ Self {
+ hmac_output: signature[0..=31].try_into().expect(CORRECT_LENGTH),
+ timestamp: signature[32..=39].try_into().expect(CORRECT_LENGTH),
+ nonce: signature[40..=55].try_into().expect(CORRECT_LENGTH),
+ }
+ }
+}
+
+impl ToSchema for PublicKey {
+ fn to_schema(_components: &mut salvo_oapi::Components) -> salvo_oapi::RefOr {
+ salvo_oapi::Object::new()
+ .schema_type(OapiSchemaType::String)
+ .format(OapiSchemaFormat::Custom("base58".to_owned()))
+ .into()
+ }
+}
+
+impl ToSchema for Signature {
+ fn to_schema(_components: &mut salvo_oapi::Components) -> salvo_oapi::RefOr {
+ salvo_oapi::Object::new()
+ .schema_type(OapiSchemaType::String)
+ .format(OapiSchemaFormat::Custom("hex".to_owned()))
+ .into()
+ }
+}
diff --git a/crates/oxidetalis_core/src/types/impl_serde.rs b/crates/oxidetalis_core/src/types/impl_serde.rs
new file mode 100644
index 0000000..2f787dc
--- /dev/null
+++ b/crates/oxidetalis_core/src/types/impl_serde.rs
@@ -0,0 +1,99 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+use base58::FromBase58;
+use serde::{de::Error as DeError, Deserialize, Serialize};
+
+use super::{PrivateKey, PublicKey, Signature};
+
+impl<'de> Deserialize<'de> for PrivateKey {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let private_key = String::deserialize(deserializer)?
+ .from_base58()
+ .map_err(|_| DeError::custom("Invalid base58"))?;
+
+ Self::try_from(
+ <[u8; 32]>::try_from(private_key)
+ .map_err(|_| DeError::custom("Invalid private key length, must be 32 bytes"))?,
+ )
+ .map_err(|_| DeError::custom("Invalid private key"))
+ }
+}
+
+impl Serialize for PrivateKey {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.to_string().as_str())
+ }
+}
+
+impl<'de> Deserialize<'de> for PublicKey {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let public_key = String::deserialize(deserializer)?
+ .from_base58()
+ .map_err(|_| DeError::custom("Invalid base58"))?;
+
+ Self::try_from(
+ <[u8; 33]>::try_from(public_key)
+ .map_err(|_| DeError::custom("Invalid public key length, must be 33 bytes"))?,
+ )
+ .map_err(|_| DeError::custom("Invalid public key"))
+ }
+}
+
+impl Serialize for PublicKey {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.to_string().as_str())
+ }
+}
+
+impl<'de> Deserialize<'de> for Signature {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let signature = hex::decode(String::deserialize(deserializer)?)
+ .map_err(|_| DeError::custom("Invalid hex string"))?;
+ Ok(Self::from(<[u8; 56]>::try_from(signature).map_err(
+ |_| DeError::custom("Invalid signature length, must be 56 bytes"),
+ )?))
+ }
+}
+
+impl Serialize for Signature {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.to_string().as_str())
+ }
+}
diff --git a/crates/oxidetalis_core/src/types/mod.rs b/crates/oxidetalis_core/src/types/mod.rs
new file mode 100644
index 0000000..a5a28ec
--- /dev/null
+++ b/crates/oxidetalis_core/src/types/mod.rs
@@ -0,0 +1,29 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! Oxidetalis server types
+
+mod cipher;
+mod impl_serde;
+mod size;
+
+pub use cipher::*;
+pub use size::*;
diff --git a/crates/oxidetalis_core/src/types/size.rs b/crates/oxidetalis_core/src/types/size.rs
new file mode 100644
index 0000000..f292142
--- /dev/null
+++ b/crates/oxidetalis_core/src/types/size.rs
@@ -0,0 +1,125 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! Size type. Used to represent sizes in bytes, kilobytes, megabytes, and
+//! gigabytes.
+
+use std::{fmt, str::FromStr};
+
+use logcall::logcall;
+use serde::{de::Error as DeError, Deserialize, Serialize};
+
+/// Size type. Used to represent sizes in bytes, kilobytes, megabytes, and
+/// gigabytes.
+#[derive(Copy, Clone, Debug)]
+pub enum Size {
+ /// Byte
+ B(usize),
+ /// Kilobyte
+ KB(usize),
+ /// Megabyte
+ MB(usize),
+ /// Gigabyte
+ GB(usize),
+}
+
+impl Size {
+ /// Returns the size in bytes, regardless of the unit
+ pub const fn as_bytes(&self) -> usize {
+ match self {
+ Size::B(n) => *n,
+ Size::KB(n) => *n * 1e+3 as usize,
+ Size::MB(n) => *n * 1e+6 as usize,
+ Size::GB(n) => *n * 1e+9 as usize,
+ }
+ }
+
+ /// Returns the unit name of the size (e.g. `B`, `KB`, `MB`, `GB`)
+ pub const fn unit_name(&self) -> &'static str {
+ match self {
+ Size::B(_) => "B",
+ Size::KB(_) => "KB",
+ Size::MB(_) => "MB",
+ Size::GB(_) => "GB",
+ }
+ }
+
+ /// Returns the size in the unit (e.g. `2MB` -> `2`, `2GB` -> `2`)
+ pub const fn size(&self) -> usize {
+ match self {
+ Size::B(n) | Size::KB(n) | Size::MB(n) | Size::GB(n) => *n,
+ }
+ }
+}
+
+impl fmt::Display for Size {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}{}", self.size(), self.unit_name())
+ }
+}
+
+impl FromStr for Size {
+ type Err = String;
+
+ #[logcall]
+ fn from_str(s: &str) -> Result {
+ let Some(first_alpha) = s.find(|c: char| c.is_alphabetic()) else {
+ return Err("Missing unit, e.g. `2MB`".to_owned());
+ };
+
+ let (size, unit) = s.split_at(first_alpha);
+ let Ok(size) = size.parse() else {
+ return Err(format!("Invalid size `{size}`"));
+ };
+ Ok(match unit {
+ "B" => Self::B(size),
+ "KB" => Self::KB(size),
+ "MB" => Self::MB(size),
+ "GB" => Self::GB(size),
+ unknown_unit => {
+ return Err(format!(
+ "Unsupported unit `{unknown_unit}`, supported units are `B`, `KB`, `MB`, `GB`"
+ ));
+ }
+ })
+ }
+}
+
+impl<'de> Deserialize<'de> for Size {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ String::deserialize(deserializer)?
+ .as_str()
+ .parse()
+ .map_err(DeError::custom)
+ }
+}
+
+impl Serialize for Size {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.to_string().as_str())
+ }
+}
diff --git a/crates/oxidetalis_entities/Cargo.toml b/crates/oxidetalis_entities/Cargo.toml
new file mode 100644
index 0000000..b8f6e33
--- /dev/null
+++ b/crates/oxidetalis_entities/Cargo.toml
@@ -0,0 +1,60 @@
+[package]
+name = "oxidetalis_entities"
+description = "Database entities for the Oxidetalis homeserver"
+edition = "2021"
+license = "MIT"
+authors.workspace = true
+readme.workspace = true
+repository.workspace = true
+version.workspace = true
+rust-version.workspace = true
+
+
+[dependencies]
+sea-orm = {workspace = true }
+
+[lints.rust]
+unsafe_code = "deny"
+
+[lints.clippy]
+wildcard_imports = "deny"
+manual_let_else = "deny"
+match_bool = "deny"
+match_on_vec_items = "deny"
+or_fun_call = "deny"
+panic = "deny"
+unwrap_used = "deny"
+
+missing_assert_message = "warn"
+missing_const_for_fn = "warn"
+missing_errors_doc = "warn"
+absolute_paths = "warn"
+cast_lossless = "warn"
+clone_on_ref_ptr = "warn"
+cloned_instead_of_copied = "warn"
+dbg_macro = "warn"
+default_trait_access = "warn"
+empty_enum_variants_with_brackets = "warn"
+empty_line_after_doc_comments = "warn"
+empty_line_after_outer_attr = "warn"
+empty_structs_with_brackets = "warn"
+enum_glob_use = "warn"
+equatable_if_let = "warn"
+explicit_iter_loop = "warn"
+filetype_is_file = "warn"
+filter_map_next = "warn"
+flat_map_option = "warn"
+float_cmp = "warn"
+format_push_string = "warn"
+future_not_send = "warn"
+if_not_else = "warn"
+if_then_some_else_none = "warn"
+implicit_clone = "warn"
+inconsistent_struct_constructor = "warn"
+indexing_slicing = "warn"
+iter_filter_is_ok = "warn"
+iter_filter_is_some = "warn"
+iter_not_returning_iterator = "warn"
+manual_is_variant_and = "warn"
+option_if_let_else = "warn"
+option_option = "warn"
diff --git a/crates/oxidetalis_entities/README.md b/crates/oxidetalis_entities/README.md
new file mode 100644
index 0000000..47cd9d2
--- /dev/null
+++ b/crates/oxidetalis_entities/README.md
@@ -0,0 +1,16 @@
+# Oxidetalis database entities
+
+This crate contains the database entities for the Oxidetalis homeserver, using
+SeaORM.
+
+## Must to know
+- Don't import sea_orm things in another crates, import the entities and sea_orm
+ things from this crate, from `prelude` module.
+
+## How to write a new entity
+Check the [SeaORM
+documentation](https://www.sea-ql.org/SeaORM/docs/generate-entity/entity-structure/)
+for more information about how to write entities.
+
+## License
+This crate is licensed under the MIT license.
diff --git a/crates/oxidetalis_entities/src/lib.rs b/crates/oxidetalis_entities/src/lib.rs
new file mode 100644
index 0000000..21ae991
--- /dev/null
+++ b/crates/oxidetalis_entities/src/lib.rs
@@ -0,0 +1,23 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+pub mod prelude;
+pub mod users;
diff --git a/crates/oxidetalis_entities/src/prelude.rs b/crates/oxidetalis_entities/src/prelude.rs
new file mode 100644
index 0000000..037a7e4
--- /dev/null
+++ b/crates/oxidetalis_entities/src/prelude.rs
@@ -0,0 +1,41 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+pub use sea_orm::{
+ ActiveModelTrait,
+ ColumnTrait,
+ EntityTrait,
+ IntoActiveModel,
+ Order,
+ PaginatorTrait,
+ QueryFilter,
+ QueryOrder,
+ QuerySelect,
+ Set,
+ SqlErr,
+};
+
+pub use super::users::{
+ ActiveModel as UserActiveModel,
+ Column as UserColumn,
+ Entity as UserEntity,
+ Model as UserModel,
+};
diff --git a/crates/oxidetalis_entities/src/users.rs b/crates/oxidetalis_entities/src/users.rs
new file mode 100644
index 0000000..1bae627
--- /dev/null
+++ b/crates/oxidetalis_entities/src/users.rs
@@ -0,0 +1,36 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+#[sea_orm(table_name = "users")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: i32,
+ pub public_key: String,
+ pub is_admin: bool,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/oxidetalis_migrations/Cargo.toml b/crates/oxidetalis_migrations/Cargo.toml
new file mode 100644
index 0000000..c0725e1
--- /dev/null
+++ b/crates/oxidetalis_migrations/Cargo.toml
@@ -0,0 +1,61 @@
+[package]
+name = "oxidetalis_migrations"
+description = "Database migrations for the Oxidetalis homeserver"
+edition = "2021"
+license = "MIT"
+authors.workspace = true
+readme.workspace = true
+repository.workspace = true
+version.workspace = true
+rust-version.workspace = true
+
+
+[dependencies]
+sea-orm = { workspace = true }
+sea-orm-migration = { version = "0.12.15", default-features = false, features = ["runtime-tokio-rustls", "sqlx-postgres"] }
+
+[lints.rust]
+unsafe_code = "deny"
+
+[lints.clippy]
+wildcard_imports = "deny"
+manual_let_else = "deny"
+match_bool = "deny"
+match_on_vec_items = "deny"
+or_fun_call = "deny"
+panic = "deny"
+unwrap_used = "deny"
+
+missing_assert_message = "warn"
+missing_const_for_fn = "warn"
+missing_errors_doc = "warn"
+absolute_paths = "warn"
+cast_lossless = "warn"
+clone_on_ref_ptr = "warn"
+cloned_instead_of_copied = "warn"
+dbg_macro = "warn"
+default_trait_access = "warn"
+empty_enum_variants_with_brackets = "warn"
+empty_line_after_doc_comments = "warn"
+empty_line_after_outer_attr = "warn"
+empty_structs_with_brackets = "warn"
+enum_glob_use = "warn"
+equatable_if_let = "warn"
+explicit_iter_loop = "warn"
+filetype_is_file = "warn"
+filter_map_next = "warn"
+flat_map_option = "warn"
+float_cmp = "warn"
+format_push_string = "warn"
+future_not_send = "warn"
+if_not_else = "warn"
+if_then_some_else_none = "warn"
+implicit_clone = "warn"
+inconsistent_struct_constructor = "warn"
+indexing_slicing = "warn"
+iter_filter_is_ok = "warn"
+iter_filter_is_some = "warn"
+iter_not_returning_iterator = "warn"
+manual_is_variant_and = "warn"
+option_if_let_else = "warn"
+option_option = "warn"
diff --git a/crates/oxidetalis_migrations/README.md b/crates/oxidetalis_migrations/README.md
new file mode 100644
index 0000000..c2f2764
--- /dev/null
+++ b/crates/oxidetalis_migrations/README.md
@@ -0,0 +1,52 @@
+# Oxidetalis database migrations
+
+This crate contains the database migrations for the Oxidetalis homeserver, using
+SeaORM.
+
+## How to run the migrations
+The migrations are run when the server starts. The server will check if the
+database is up-to-date and run the migrations if needed. So, you don't need to
+run the migrations manually.
+
+## How to create a new migration
+The migrations will saved in the database, so SeaORM will track the migrations,
+and you don't need to worry about the migration files, just write the migration
+and SeaORM will take care of the rest.
+
+To create a new migration, you need to create a new migration file in the `src`
+directory. You can name the file anything you want, for example,
+`create_users_table.rs`. The file should contain the migration code, you can
+take this as a template:
+```rust
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ // Here you can write the migration code, the `manager` can do anything you want.
+
+ // When the homeserver starts, it will run the `up` function for each migration that is not run yet.
+ }
+}
+
+#[derive(DeriveIden)]
+enum TableName {
+ Table, // Required for the table name
+ Id, // Required for the primary key
+ // Add more columns here
+d}
+```
+
+> [!NOTE] Don't write the `down` function, I prefer to do each migration in a
+> separate migration file, so you don't need to write the `down` function. If you
+> want to delete a table later, you can create a new migration file that deletes
+> the table.
+
+After you write the migration code, you need to add the migration to the
+`src/lib.rs` file.
+
+## License
+This crate is licensed under the MIT license.
diff --git a/crates/oxidetalis_migrations/src/create_users_table.rs b/crates/oxidetalis_migrations/src/create_users_table.rs
new file mode 100644
index 0000000..71e771e
--- /dev/null
+++ b/crates/oxidetalis_migrations/src/create_users_table.rs
@@ -0,0 +1,66 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .create_table(
+ Table::create()
+ .table(Users::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(Users::Id)
+ .integer()
+ .not_null()
+ .auto_increment()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(Users::PublicKey)
+ .string()
+ .not_null()
+ .unique_key(),
+ )
+ .col(
+ ColumnDef::new(Users::IsAdmin)
+ .boolean()
+ .not_null()
+ .default(false),
+ )
+ .to_owned(),
+ )
+ .await
+ }
+}
+
+#[derive(DeriveIden)]
+enum Users {
+ Table,
+ Id,
+ PublicKey,
+ IsAdmin,
+}
diff --git a/crates/oxidetalis_migrations/src/lib.rs b/crates/oxidetalis_migrations/src/lib.rs
new file mode 100644
index 0000000..83f94c0
--- /dev/null
+++ b/crates/oxidetalis_migrations/src/lib.rs
@@ -0,0 +1,33 @@
+// OxideTalis Messaging Protocol homeserver core implementation
+// Copyright (c) 2024 OxideTalis Developers
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+pub use sea_orm_migration::prelude::*;
+
+mod create_users_table;
+
+pub struct Migrator;
+
+#[async_trait::async_trait]
+impl MigratorTrait for Migrator {
+ fn migrations() -> Vec> {
+ vec![Box::new(create_users_table::Migration)]
+ }
+}