From 4a85efc2704b09a8e1f448caa623182e9a91d449 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 9 Apr 2026 14:55:37 +0200 Subject: [PATCH] first --- .gitignore | 2 + README.md | 28 +- backend/Cargo.lock | 3014 +++++++++++++++++ backend/Cargo.toml | 24 + .../20260409120000_initial/down.sql | 18 + .../migrations/20260409120000_initial/up.sql | 190 ++ backend/src/app_state.rs | 113 + backend/src/config.rs | 82 + backend/src/db/mod.rs | 29 + backend/src/error.rs | 69 + backend/src/lib.rs | 43 + backend/src/main.rs | 6 + backend/src/routes/health.rs | 27 + backend/src/routes/mod.rs | 12 + backend/src/routes/session.rs | 24 + backend/src/telemetry.rs | 15 + backend/tests/api_smoke.rs | 46 + contracts/localization/en.json | 40 + contracts/localization/sv.json | 40 + contracts/openapi/openapi.yaml | 584 ++++ docs/analytics-taxonomy.md | 106 + docs/architecture.md | 50 + docs/localization-catalog.md | 52 + docs/relational-schema.md | 192 ++ docs/state-machines.md | 31 + fixtures/README.md | 9 + infra/docker-compose.yml | 19 + mobile/android-app/README.md | 15 + mobile/ios-app/README.md | 13 + scripts/README.md | 3 + 30 files changed, 4895 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 backend/Cargo.lock create mode 100644 backend/Cargo.toml create mode 100644 backend/migrations/20260409120000_initial/down.sql create mode 100644 backend/migrations/20260409120000_initial/up.sql create mode 100644 backend/src/app_state.rs create mode 100644 backend/src/config.rs create mode 100644 backend/src/db/mod.rs create mode 100644 backend/src/error.rs create mode 100644 backend/src/lib.rs create mode 100644 backend/src/main.rs create mode 100644 backend/src/routes/health.rs create mode 100644 backend/src/routes/mod.rs create mode 100644 backend/src/routes/session.rs create mode 100644 backend/src/telemetry.rs create mode 100644 backend/tests/api_smoke.rs create mode 100644 contracts/localization/en.json create mode 100644 contracts/localization/sv.json create mode 100644 contracts/openapi/openapi.yaml create mode 100644 docs/analytics-taxonomy.md create mode 100644 docs/architecture.md create mode 100644 docs/localization-catalog.md create mode 100644 docs/relational-schema.md create mode 100644 docs/state-machines.md create mode 100644 fixtures/README.md create mode 100644 infra/docker-compose.yml create mode 100644 mobile/android-app/README.md create mode 100644 mobile/ios-app/README.md create mode 100644 scripts/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6438f1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +build/ diff --git a/README.md b/README.md index 175ce60..3aaff43 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,29 @@ # Hermes -Hermes is the internal name of this project +Hermes is a native iOS and Android prototype for a sports-betting research study. +The app is designed to feel premium, fast, and clear while keeping the server +authoritative for timing, odds, settlement, and analytics. + +## Goals + +- Native UI on iOS and Android +- English and Swedish from day one +- Research-first flow, not real-money betting +- Structured analytics and audit logging +- Stable backend and API contracts for iteration + +## Repo Layout + +- `backend/` Rust API and migrations +- `contracts/` OpenAPI and localization contracts +- `docs/` architecture, schema, and state-machine docs +- `infra/` local environment and deployment assets +- `mobile/` native app scaffolds +- `fixtures/` sample media and test data +- `scripts/` helper automation + +## Status + +The repository has been initialized with the first planning and backend +foundation artifacts. The remaining work is to fill out the domain modules and +native clients. diff --git a/backend/Cargo.lock b/backend/Cargo.lock new file mode 100644 index 0000000..c5d04af --- /dev/null +++ b/backend/Cargo.lock @@ -0,0 +1,3014 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[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.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[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 = "config" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermes-backend" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "config", + "redis", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +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 0.2.17", +] + +[[package]] +name = "redis" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37ec3fd44bea2ec947ba6cc7634d7999a6590aca7c35827c250bc0de502bda6" +dependencies = [ + "arc-swap", + "combine", + "itoa", + "num-bigint", + "percent-encoding", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-ini" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink 0.10.0", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.8.4", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..d0d6aee --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "hermes-backend" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +axum = { version = "0.8", features = ["json"] } +chrono = { version = "0.4", features = ["serde", "clock"] } +config = "0.14" +redis = "0.28" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-rustls", "uuid", "chrono"] } +thiserror = "2" +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "sync"] } +tower = { version = "0.5", features = ["make"] } +tower-http = { version = "0.6", features = ["trace"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +uuid = { version = "1", features = ["serde", "v4"] } +validator = { version = "0.19", features = ["derive"] } + +[dev-dependencies] diff --git a/backend/migrations/20260409120000_initial/down.sql b/backend/migrations/20260409120000_initial/down.sql new file mode 100644 index 0000000..6cf5647 --- /dev/null +++ b/backend/migrations/20260409120000_initial/down.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS audit_log_attributes; +DROP TABLE IF EXISTS audit_logs; +DROP TABLE IF EXISTS analytics_event_attributes; +DROP TABLE IF EXISTS analytics_events; +DROP TABLE IF EXISTS analytics_event_types; +DROP TABLE IF EXISTS localization_values; +DROP TABLE IF EXISTS localization_keys; +DROP TABLE IF EXISTS experiment_assignments; +DROP TABLE IF EXISTS settlements; +DROP TABLE IF EXISTS bet_intents; +DROP TABLE IF EXISTS outcome_odds; +DROP TABLE IF EXISTS odds_versions; +DROP TABLE IF EXISTS outcomes; +DROP TABLE IF EXISTS markets; +DROP TABLE IF EXISTS event_media; +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS sessions; +DROP TABLE IF EXISTS users; diff --git a/backend/migrations/20260409120000_initial/up.sql b/backend/migrations/20260409120000_initial/up.sql new file mode 100644 index 0000000..47b850a --- /dev/null +++ b/backend/migrations/20260409120000_initial/up.sql @@ -0,0 +1,190 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE users ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + external_ref text NOT NULL UNIQUE, + created_at timestamptz NOT NULL DEFAULT now(), + preferred_language text NOT NULL, + device_platform text NOT NULL +); + +CREATE TABLE sessions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + started_at timestamptz NOT NULL DEFAULT now(), + ended_at timestamptz, + experiment_variant text NOT NULL, + app_version text NOT NULL, + device_model text, + os_version text, + locale_code text NOT NULL +); + +CREATE TABLE events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + sport_type text NOT NULL, + source_ref text NOT NULL UNIQUE, + title_en text NOT NULL, + title_sv text NOT NULL, + status text NOT NULL, + preview_start_ms bigint NOT NULL, + preview_end_ms bigint NOT NULL, + reveal_start_ms bigint NOT NULL, + reveal_end_ms bigint NOT NULL, + lock_at timestamptz NOT NULL, + settle_at timestamptz NOT NULL +); + +CREATE TABLE event_media ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + event_id uuid NOT NULL REFERENCES events(id) ON DELETE CASCADE, + media_type text NOT NULL, + hls_master_url text NOT NULL, + poster_url text, + duration_ms bigint NOT NULL, + preview_start_ms bigint NOT NULL, + preview_end_ms bigint NOT NULL, + reveal_start_ms bigint NOT NULL, + reveal_end_ms bigint NOT NULL, + UNIQUE (event_id, media_type) +); + +CREATE TABLE markets ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + event_id uuid NOT NULL REFERENCES events(id) ON DELETE CASCADE, + question_key text NOT NULL, + market_type text NOT NULL, + status text NOT NULL, + lock_at timestamptz NOT NULL, + settlement_rule_key text NOT NULL +); + +CREATE TABLE outcomes ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + market_id uuid NOT NULL REFERENCES markets(id) ON DELETE CASCADE, + outcome_code text NOT NULL, + label_key text NOT NULL, + sort_order integer NOT NULL, + UNIQUE (market_id, outcome_code) +); + +CREATE TABLE odds_versions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + market_id uuid NOT NULL REFERENCES markets(id) ON DELETE CASCADE, + version_no integer NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + is_current boolean NOT NULL DEFAULT false, + UNIQUE (market_id, version_no) +); + +CREATE UNIQUE INDEX odds_versions_current_idx + ON odds_versions (market_id) + WHERE is_current; + +CREATE TABLE outcome_odds ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + odds_version_id uuid NOT NULL REFERENCES odds_versions(id) ON DELETE CASCADE, + outcome_id uuid NOT NULL REFERENCES outcomes(id) ON DELETE CASCADE, + decimal_odds numeric(10,4) NOT NULL, + fractional_num integer NOT NULL, + fractional_den integer NOT NULL, + UNIQUE (odds_version_id, outcome_id) +); + +CREATE TABLE bet_intents ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id), + session_id uuid NOT NULL REFERENCES sessions(id), + event_id uuid NOT NULL REFERENCES events(id), + market_id uuid NOT NULL REFERENCES markets(id), + outcome_id uuid NOT NULL REFERENCES outcomes(id), + idempotency_key text NOT NULL UNIQUE, + client_sent_at timestamptz NOT NULL, + server_received_at timestamptz NOT NULL DEFAULT now(), + accepted boolean NOT NULL, + acceptance_code text NOT NULL, + accepted_odds_version_id uuid REFERENCES odds_versions(id) +); + +CREATE TABLE settlements ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + market_id uuid NOT NULL UNIQUE REFERENCES markets(id) ON DELETE CASCADE, + settled_at timestamptz NOT NULL, + winning_outcome_id uuid NOT NULL REFERENCES outcomes(id) +); + +CREATE TABLE experiment_assignments ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + session_id uuid NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + variant text NOT NULL, + assigned_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (session_id) +); + +CREATE TABLE localization_keys ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + key_name text NOT NULL UNIQUE, + description text NOT NULL +); + +CREATE TABLE localization_values ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + localization_key_id uuid NOT NULL REFERENCES localization_keys(id) ON DELETE CASCADE, + locale_code text NOT NULL, + text_value text NOT NULL, + UNIQUE (localization_key_id, locale_code) +); + +CREATE TABLE analytics_event_types ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + event_name text NOT NULL UNIQUE, + description text NOT NULL +); + +CREATE TABLE analytics_events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + analytics_event_type_id uuid NOT NULL REFERENCES analytics_event_types(id), + session_id uuid NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + occurred_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE analytics_event_attributes ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + analytics_event_id uuid NOT NULL REFERENCES analytics_events(id) ON DELETE CASCADE, + attribute_key text NOT NULL, + attribute_value text NOT NULL +); + +CREATE TABLE audit_logs ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + created_at timestamptz NOT NULL DEFAULT now(), + actor_type text NOT NULL, + actor_id uuid, + action_name text NOT NULL, + target_type text NOT NULL, + target_id uuid, + trace_id text NOT NULL, + note text +); + +CREATE TABLE audit_log_attributes ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + audit_log_id uuid NOT NULL REFERENCES audit_logs(id) ON DELETE CASCADE, + attribute_key text NOT NULL, + attribute_value text NOT NULL +); + +CREATE INDEX sessions_user_id_idx ON sessions (user_id); +CREATE INDEX sessions_started_at_idx ON sessions (started_at); +CREATE INDEX events_status_idx ON events (status); +CREATE INDEX markets_event_id_idx ON markets (event_id); +CREATE INDEX markets_lock_at_idx ON markets (lock_at); +CREATE INDEX bet_intents_session_id_idx ON bet_intents (session_id); +CREATE INDEX bet_intents_market_id_idx ON bet_intents (market_id); +CREATE INDEX bet_intents_idempotency_key_idx ON bet_intents (idempotency_key); +CREATE INDEX analytics_events_session_id_idx ON analytics_events (session_id); +CREATE INDEX analytics_events_user_id_idx ON analytics_events (user_id); +CREATE INDEX analytics_events_type_id_idx ON analytics_events (analytics_event_type_id); +CREATE INDEX audit_logs_created_at_idx ON audit_logs (created_at); diff --git a/backend/src/app_state.rs b/backend/src/app_state.rs new file mode 100644 index 0000000..ab9eb22 --- /dev/null +++ b/backend/src/app_state.rs @@ -0,0 +1,113 @@ +use std::{sync::Arc, time::Instant}; + +use chrono::{DateTime, Utc}; +use redis::Client as RedisClient; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use tokio::sync::RwLock; +use uuid::Uuid; + +use crate::{config::AppConfig, error::AppError}; + +#[derive(Clone)] +pub struct AppState { + pub config: AppConfig, + pub started_at: Instant, + pub database_pool: Option, + pub redis_client: Option, + current_user_id: Uuid, + session: Arc>>, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SessionStartRequest { + pub locale_code: Option, + pub device_platform: Option, + pub device_model: Option, + pub os_version: Option, + pub app_version: Option, + pub experiment_variant: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SessionSnapshot { + pub session_id: Uuid, + pub user_id: Uuid, + pub started_at: DateTime, + pub ended_at: Option>, + pub experiment_variant: String, + pub app_version: String, + pub device_model: Option, + pub os_version: Option, + pub locale_code: String, + pub device_platform: String, +} + +impl AppState { + pub fn new( + config: AppConfig, + database_pool: Option, + redis_client: Option, + ) -> Self { + Self { + config, + started_at: Instant::now(), + database_pool, + redis_client, + current_user_id: Uuid::new_v4(), + session: Arc::new(RwLock::new(None)), + } + } + + pub async fn start_session(&self, request: SessionStartRequest) -> SessionSnapshot { + let session = SessionSnapshot { + session_id: Uuid::new_v4(), + user_id: self.current_user_id, + started_at: Utc::now(), + ended_at: None, + experiment_variant: request + .experiment_variant + .unwrap_or_else(|| self.config.default_experiment_variant.clone()), + app_version: request + .app_version + .unwrap_or_else(|| self.config.app_version.clone()), + device_model: request.device_model, + os_version: request.os_version, + locale_code: request + .locale_code + .unwrap_or_else(|| self.config.default_locale.clone()), + device_platform: request + .device_platform + .unwrap_or_else(|| self.config.default_device_platform.clone()), + }; + + *self.session.write().await = Some(session.clone()); + session + } + + pub async fn current_session(&self) -> Option { + self.session.read().await.clone() + } + + pub async fn end_session(&self) -> Result { + let mut guard = self.session.write().await; + let mut session = guard + .clone() + .ok_or_else(|| AppError::not_found("No active session"))?; + session.ended_at = Some(Utc::now()); + *guard = Some(session.clone()); + Ok(session) + } + + pub fn database_ready(&self) -> bool { + self.database_pool.is_some() + } + + pub fn redis_ready(&self) -> bool { + self.redis_client.is_some() + } + + pub fn uptime_ms(&self) -> u128 { + self.started_at.elapsed().as_millis() + } +} diff --git a/backend/src/config.rs b/backend/src/config.rs new file mode 100644 index 0000000..fa3ab0d --- /dev/null +++ b/backend/src/config.rs @@ -0,0 +1,82 @@ +use std::net::SocketAddr; + +use anyhow::Context; +use serde::Deserialize; +use validator::Validate; + +#[derive(Clone, Debug, Deserialize, Validate)] +pub struct AppConfig { + #[validate(length(min = 1))] + pub service_name: String, + #[validate(length(min = 1))] + pub environment: String, + pub bind_addr: SocketAddr, + #[validate(length(min = 1))] + pub app_version: String, + #[validate(length(min = 1))] + pub default_locale: String, + #[validate(length(min = 1))] + pub default_device_platform: String, + #[validate(length(min = 1))] + pub default_experiment_variant: String, + #[validate(length(min = 1))] + pub log_level: String, + pub stream_base_url: Option, + pub cdn_base_url: Option, + #[validate(length(min = 1))] + pub analytics_mode: String, + #[validate(length(min = 1))] + pub experiment_config: String, + #[validate(length(min = 1))] + pub localization_source: String, + pub database_url: Option, + pub redis_url: Option, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + service_name: "hermes-backend".to_string(), + environment: "local".to_string(), + bind_addr: "127.0.0.1:3000".parse().expect("valid bind address"), + app_version: "0.1.0-dev".to_string(), + default_locale: "en".to_string(), + default_device_platform: "ios".to_string(), + default_experiment_variant: "control".to_string(), + log_level: "info".to_string(), + stream_base_url: None, + cdn_base_url: None, + analytics_mode: "relational".to_string(), + experiment_config: "study-default".to_string(), + localization_source: "contracts/localization".to_string(), + database_url: None, + redis_url: None, + } + } +} + +impl AppConfig { + pub fn load() -> anyhow::Result { + let settings = config::Config::builder() + .set_default("service_name", "hermes-backend")? + .set_default("environment", "local")? + .set_default("bind_addr", "127.0.0.1:3000")? + .set_default("app_version", "0.1.0-dev")? + .set_default("default_locale", "en")? + .set_default("default_device_platform", "ios")? + .set_default("default_experiment_variant", "control")? + .set_default("log_level", "info")? + .set_default("analytics_mode", "relational")? + .set_default("experiment_config", "study-default")? + .set_default("localization_source", "contracts/localization")? + .add_source(config::Environment::with_prefix("HERMES").separator("__")) + .build() + .context("failed to load Hermes configuration")?; + + let config: Self = settings + .try_deserialize() + .context("failed to deserialize Hermes configuration")?; + config.validate().context("invalid Hermes configuration")?; + Ok(config) + } +} diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs new file mode 100644 index 0000000..81ecf9c --- /dev/null +++ b/backend/src/db/mod.rs @@ -0,0 +1,29 @@ +use std::time::Duration; + +use anyhow::Context; +use redis::Client as RedisClient; +use sqlx::{postgres::PgPoolOptions, PgPool}; + +pub async fn connect_postgres(database_url: Option<&str>) -> anyhow::Result> { + match database_url { + Some(url) if !url.trim().is_empty() => { + let pool = PgPoolOptions::new() + .max_connections(5) + .acquire_timeout(Duration::from_secs(5)) + .connect(url) + .await + .with_context(|| format!("failed to connect to postgres at {url}"))?; + Ok(Some(pool)) + } + _ => Ok(None), + } +} + +pub fn connect_redis(redis_url: Option<&str>) -> anyhow::Result> { + match redis_url { + Some(url) if !url.trim().is_empty() => { + Ok(Some(RedisClient::open(url).context("failed to open redis client")?)) + } + _ => Ok(None), + } +} diff --git a/backend/src/error.rs b/backend/src/error.rs new file mode 100644 index 0000000..d94d137 --- /dev/null +++ b/backend/src/error.rs @@ -0,0 +1,69 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::Serialize; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("{0}")] + BadRequest(String), + #[error("{0}")] + NotFound(String), + #[error("{0}")] + Conflict(String), + #[error("{0}")] + ServiceUnavailable(String), + #[error("{0}")] + Internal(String), +} + +#[derive(Debug, Serialize)] +struct ErrorResponse { + code: &'static str, + message: String, +} + +impl AppError { + pub fn bad_request(message: impl Into) -> Self { + Self::BadRequest(message.into()) + } + + pub fn not_found(message: impl Into) -> Self { + Self::NotFound(message.into()) + } + + fn status_code(&self) -> StatusCode { + match self { + Self::BadRequest(_) => StatusCode::BAD_REQUEST, + Self::NotFound(_) => StatusCode::NOT_FOUND, + Self::Conflict(_) => StatusCode::CONFLICT, + Self::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE, + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn code(&self) -> &'static str { + match self { + Self::BadRequest(_) => "bad_request", + Self::NotFound(_) => "not_found", + Self::Conflict(_) => "conflict", + Self::ServiceUnavailable(_) => "service_unavailable", + Self::Internal(_) => "internal_error", + } + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let status = self.status_code(); + let body = Json(ErrorResponse { + code: self.code(), + message: self.to_string(), + }); + + (status, body).into_response() + } +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs new file mode 100644 index 0000000..fda2e55 --- /dev/null +++ b/backend/src/lib.rs @@ -0,0 +1,43 @@ +#![forbid(unsafe_code)] + +pub mod app_state; +pub mod config; +pub mod db; +pub mod error; +pub mod routes; +pub mod telemetry; + +use axum::Router; +use axum::Extension; +use tower_http::trace::TraceLayer; + +use crate::{app_state::AppState, config::AppConfig}; + +pub fn build_router(state: AppState) -> Router { + routes::router() + .layer(Extension(state)) + .layer(TraceLayer::new_for_http()) +} + +pub async fn run() -> anyhow::Result<()> { + let config = AppConfig::load()?; + telemetry::init(&config.log_level); + + tracing::info!( + service = %config.service_name, + environment = %config.environment, + version = %config.app_version, + "starting Hermes backend" + ); + + let database_pool = db::connect_postgres(config.database_url.as_deref()).await?; + let redis_client = db::connect_redis(config.redis_url.as_deref())?; + + let state = AppState::new(config.clone(), database_pool, redis_client); + let app = build_router(state); + let listener = tokio::net::TcpListener::bind(config.bind_addr).await?; + + tracing::info!(addr = %config.bind_addr, "listening"); + axum::serve(listener, app).await?; + Ok(()) +} diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..edcb18a --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + hermes_backend::run().await +} diff --git a/backend/src/routes/health.rs b/backend/src/routes/health.rs new file mode 100644 index 0000000..7db59f0 --- /dev/null +++ b/backend/src/routes/health.rs @@ -0,0 +1,27 @@ +use axum::{extract::Extension, Json}; +use serde::Serialize; + +use crate::app_state::AppState; + +#[derive(Serialize)] +pub struct HealthResponse { + pub status: &'static str, + pub service_name: String, + pub environment: String, + pub version: String, + pub uptime_ms: u128, + pub database_ready: bool, + pub redis_ready: bool, +} + +pub async fn handler(Extension(state): Extension) -> Json { + Json(HealthResponse { + status: "ok", + service_name: state.config.service_name.clone(), + environment: state.config.environment.clone(), + version: state.config.app_version.clone(), + uptime_ms: state.uptime_ms(), + database_ready: state.database_ready(), + redis_ready: state.redis_ready(), + }) +} diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs new file mode 100644 index 0000000..f14de35 --- /dev/null +++ b/backend/src/routes/mod.rs @@ -0,0 +1,12 @@ +pub mod health; +pub mod session; + +use axum::{routing::{get, post}, Router}; + +pub fn router() -> Router { + Router::new() + .route("/health", get(health::handler)) + .route("/api/v1/session/start", post(session::start)) + .route("/api/v1/session/end", post(session::end)) + .route("/api/v1/session/me", get(session::me)) +} diff --git a/backend/src/routes/session.rs b/backend/src/routes/session.rs new file mode 100644 index 0000000..51b43de --- /dev/null +++ b/backend/src/routes/session.rs @@ -0,0 +1,24 @@ +use axum::{extract::Extension, http::StatusCode, Json}; + +use crate::{app_state::{AppState, SessionSnapshot, SessionStartRequest}, error::AppError}; + +pub async fn start( + Extension(state): Extension, + Json(payload): Json, +) -> Result<(StatusCode, Json), AppError> { + let session = state.start_session(payload).await; + Ok((StatusCode::CREATED, Json(session))) +} + +pub async fn end(Extension(state): Extension) -> Result, AppError> { + let session = state.end_session().await?; + Ok(Json(session)) +} + +pub async fn me(Extension(state): Extension) -> Result, AppError> { + let session = state + .current_session() + .await + .ok_or_else(|| AppError::not_found("No active session"))?; + Ok(Json(session)) +} diff --git a/backend/src/telemetry.rs b/backend/src/telemetry.rs new file mode 100644 index 0000000..6d47671 --- /dev/null +++ b/backend/src/telemetry.rs @@ -0,0 +1,15 @@ +use tracing_subscriber::EnvFilter; + +pub fn init(filter: &str) { + let filter = EnvFilter::try_new(filter).unwrap_or_else(|_| EnvFilter::new("info")); + let subscriber = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_target(true) + .with_thread_ids(true) + .with_thread_names(true) + .compact() + .finish(); + + tracing::subscriber::set_global_default(subscriber) + .expect("failed to install tracing subscriber"); +} diff --git a/backend/tests/api_smoke.rs b/backend/tests/api_smoke.rs new file mode 100644 index 0000000..87e7cba --- /dev/null +++ b/backend/tests/api_smoke.rs @@ -0,0 +1,46 @@ +use axum::{body::{to_bytes, Body}, http::{Request, StatusCode}}; +use hermes_backend::{app_state::AppState, build_router, config::AppConfig}; +use tower::ServiceExt; + +#[tokio::test] +async fn health_returns_ok() { + let app = build_router(AppState::new(AppConfig::default(), None, None)); + + let response = app + .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + +#[tokio::test] +async fn session_start_and_me_work() { + let app = build_router(AppState::new(AppConfig::default(), None, None)); + + let response = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/v1/session/start") + .header("content-type", "application/json") + .body(Body::from("{}")) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["locale_code"], "en"); + + let response = app + .oneshot(Request::builder().uri("/api/v1/session/me").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} diff --git a/contracts/localization/en.json b/contracts/localization/en.json new file mode 100644 index 0000000..d2e9bbc --- /dev/null +++ b/contracts/localization/en.json @@ -0,0 +1,40 @@ +{ + "app.name": "Hermes", + "common.continue": "Continue", + "common.cancel": "Cancel", + "common.close": "Close", + "common.retry": "Retry", + "common.loading": "Loading", + "common.error_title": "Something went wrong", + "common.ok": "OK", + "onboarding.title": "Study intro", + "onboarding.subtitle": "Watch the clip, make your choice before lock, then see the reveal.", + "onboarding.consent_title": "Consent", + "onboarding.consent_body": "This prototype is for research and does not use real money.", + "onboarding.language_title": "Choose language", + "onboarding.start_session": "Start session", + "feed.next_round_title": "Next round", + "feed.next_round_body": "A new clip is ready for review.", + "feed.watch_preview": "Watch preview", + "feed.round_ready": "Round ready", + "round.countdown_label": "Lock in", + "round.locked_label": "Locked", + "round.selection_prompt": "Choose an outcome before lock.", + "round.selection_confirmed": "Selection accepted", + "round.selection_submitting": "Submitting selection", + "round.odds_label": "Odds", + "reveal.title": "Reveal", + "reveal.subtitle": "See what happened next.", + "result.title": "Result", + "result.user_selection": "Your selection", + "result.outcome": "Outcome", + "result.next_round": "Next round", + "settings.title": "Settings", + "settings.language": "Language", + "settings.haptics": "Haptics", + "settings.analytics": "Analytics", + "errors.generic": "Please try again.", + "errors.network": "Network error. Check your connection.", + "errors.playback": "Video playback failed.", + "errors.session_expired": "Session expired. Please start again." +} diff --git a/contracts/localization/sv.json b/contracts/localization/sv.json new file mode 100644 index 0000000..ab4ea11 --- /dev/null +++ b/contracts/localization/sv.json @@ -0,0 +1,40 @@ +{ + "app.name": "Hermes", + "common.continue": "Fortsätt", + "common.cancel": "Avbryt", + "common.close": "Stäng", + "common.retry": "Försök igen", + "common.loading": "Laddar", + "common.error_title": "Något gick fel", + "common.ok": "OK", + "onboarding.title": "Studieintro", + "onboarding.subtitle": "Titta på klippet, gör ditt val före låsning och se sedan avslöjandet.", + "onboarding.consent_title": "Samtycke", + "onboarding.consent_body": "Denna prototyp är för forskning och använder inte riktiga pengar.", + "onboarding.language_title": "Välj språk", + "onboarding.start_session": "Starta session", + "feed.next_round_title": "Nästa runda", + "feed.next_round_body": "Ett nytt klipp är klart för granskning.", + "feed.watch_preview": "Titta på förhandsklipp", + "feed.round_ready": "Rundan är klar", + "round.countdown_label": "Låsning om", + "round.locked_label": "Låst", + "round.selection_prompt": "Välj ett utfall före låsning.", + "round.selection_confirmed": "Valet accepterat", + "round.selection_submitting": "Skickar val", + "round.odds_label": "Odds", + "reveal.title": "Avslöjande", + "reveal.subtitle": "Se vad som hände sedan.", + "result.title": "Resultat", + "result.user_selection": "Ditt val", + "result.outcome": "Utfall", + "result.next_round": "Nästa runda", + "settings.title": "Inställningar", + "settings.language": "Språk", + "settings.haptics": "Haptik", + "settings.analytics": "Analys", + "errors.generic": "Försök igen.", + "errors.network": "Nätverksfel. Kontrollera anslutningen.", + "errors.playback": "Videouppspelningen misslyckades.", + "errors.session_expired": "Sessionen har gått ut. Starta igen." +} diff --git a/contracts/openapi/openapi.yaml b/contracts/openapi/openapi.yaml new file mode 100644 index 0000000..90b9074 --- /dev/null +++ b/contracts/openapi/openapi.yaml @@ -0,0 +1,584 @@ +openapi: 3.1.0 +info: + title: Hermes API + version: 0.1.0 + description: Native betting study prototype API. +servers: + - url: http://localhost:3000 +paths: + /health: + get: + summary: Health check + responses: + '200': + description: Service health + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + /api/v1/session/start: + post: + summary: Start a session + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SessionStartRequest' + responses: + '201': + description: Session started + content: + application/json: + schema: + $ref: '#/components/schemas/SessionResponse' + /api/v1/session/end: + post: + summary: End the current session + responses: + '200': + description: Session ended + content: + application/json: + schema: + $ref: '#/components/schemas/SessionResponse' + /api/v1/session/me: + get: + summary: Inspect the current session + responses: + '200': + description: Current session snapshot + content: + application/json: + schema: + $ref: '#/components/schemas/SessionResponse' + /api/v1/feed/next: + get: + summary: Fetch the next round + responses: + '200': + description: Next event payload + content: + application/json: + schema: + $ref: '#/components/schemas/Event' + /api/v1/events/{event_id}: + get: + summary: Fetch an event + parameters: + - name: event_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Event + content: + application/json: + schema: + $ref: '#/components/schemas/Event' + /api/v1/events/{event_id}/manifest: + get: + summary: Fetch the event manifest + parameters: + - name: event_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Manifest + content: + application/json: + schema: + $ref: '#/components/schemas/EventManifest' + /api/v1/events/{event_id}/markets: + get: + summary: List markets for an event + parameters: + - name: event_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Markets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Market' + /api/v1/markets/{market_id}/odds/current: + get: + summary: Fetch current odds for a market + parameters: + - name: market_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Current odds + content: + application/json: + schema: + $ref: '#/components/schemas/OddsVersion' + /api/v1/stream: + get: + summary: Live odds and state stream + responses: + '200': + description: Server-sent events stream + content: + text/event-stream: + schema: + type: string + /api/v1/bets/intent: + post: + summary: Submit a bet intent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BetIntentRequest' + responses: + '201': + description: Bet intent result + content: + application/json: + schema: + $ref: '#/components/schemas/BetIntentResponse' + /api/v1/bets/{bet_intent_id}: + get: + summary: Fetch a bet intent + parameters: + - name: bet_intent_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Bet intent + content: + application/json: + schema: + $ref: '#/components/schemas/BetIntentResponse' + /api/v1/events/{event_id}/result: + get: + summary: Fetch event result + parameters: + - name: event_id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Result payload + content: + application/json: + schema: + $ref: '#/components/schemas/Settlement' + /api/v1/analytics/batch: + post: + summary: Ingest analytics batch + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AnalyticsBatchRequest' + responses: + '202': + description: Accepted + /api/v1/experiments/config: + get: + summary: Fetch experiment config + responses: + '200': + description: Experiment config + content: + application/json: + schema: + $ref: '#/components/schemas/ExperimentConfig' + /api/v1/localization/{locale_code}: + get: + summary: Fetch a localization bundle + parameters: + - name: locale_code + in: path + required: true + schema: + type: string + enum: [en, sv] + responses: + '200': + description: Localization bundle + content: + application/json: + schema: + $ref: '#/components/schemas/LocalizationBundle' + /api/v1/admin/events: + post: + summary: Create an event + responses: + '201': + description: Created + /api/v1/admin/markets: + post: + summary: Create a market + responses: + '201': + description: Created + /api/v1/admin/odds: + post: + summary: Publish odds + responses: + '201': + description: Created + /api/v1/admin/settlements: + post: + summary: Publish a settlement + responses: + '201': + description: Created +components: + schemas: + HealthResponse: + type: object + required: [status, service_name, environment, version, uptime_ms, database_ready, redis_ready] + properties: + status: + type: string + service_name: + type: string + environment: + type: string + version: + type: string + uptime_ms: + type: integer + database_ready: + type: boolean + redis_ready: + type: boolean + SessionStartRequest: + type: object + properties: + locale_code: + type: string + device_platform: + type: string + device_model: + type: string + os_version: + type: string + app_version: + type: string + SessionResponse: + type: object + required: [session_id, user_id, started_at, experiment_variant, app_version, locale_code, device_platform] + properties: + session_id: + type: string + format: uuid + user_id: + type: string + format: uuid + started_at: + type: string + format: date-time + ended_at: + type: string + format: date-time + nullable: true + experiment_variant: + type: string + app_version: + type: string + device_model: + type: string + nullable: true + os_version: + type: string + nullable: true + locale_code: + type: string + device_platform: + type: string + Event: + type: object + required: [id, sport_type, source_ref, title_en, title_sv, status, lock_at, settle_at] + properties: + id: + type: string + format: uuid + sport_type: + type: string + source_ref: + type: string + title_en: + type: string + title_sv: + type: string + status: + type: string + preview_start_ms: + type: integer + preview_end_ms: + type: integer + reveal_start_ms: + type: integer + reveal_end_ms: + type: integer + lock_at: + type: string + format: date-time + settle_at: + type: string + format: date-time + EventManifest: + type: object + properties: + event: + $ref: '#/components/schemas/Event' + media: + type: array + items: + $ref: '#/components/schemas/EventMedia' + markets: + type: array + items: + $ref: '#/components/schemas/Market' + EventMedia: + type: object + required: [id, event_id, media_type, hls_master_url, duration_ms] + properties: + id: + type: string + format: uuid + event_id: + type: string + format: uuid + media_type: + type: string + hls_master_url: + type: string + poster_url: + type: string + nullable: true + duration_ms: + type: integer + preview_start_ms: + type: integer + preview_end_ms: + type: integer + reveal_start_ms: + type: integer + reveal_end_ms: + type: integer + Market: + type: object + required: [id, event_id, question_key, market_type, status, lock_at, settlement_rule_key] + properties: + id: + type: string + format: uuid + event_id: + type: string + format: uuid + question_key: + type: string + market_type: + type: string + status: + type: string + lock_at: + type: string + format: date-time + settlement_rule_key: + type: string + outcomes: + type: array + items: + $ref: '#/components/schemas/Outcome' + Outcome: + type: object + required: [id, market_id, outcome_code, label_key, sort_order] + properties: + id: + type: string + format: uuid + market_id: + type: string + format: uuid + outcome_code: + type: string + label_key: + type: string + sort_order: + type: integer + OddsVersion: + type: object + required: [id, market_id, version_no, created_at, is_current] + properties: + id: + type: string + format: uuid + market_id: + type: string + format: uuid + version_no: + type: integer + created_at: + type: string + format: date-time + is_current: + type: boolean + odds: + type: array + items: + $ref: '#/components/schemas/OutcomeOdds' + OutcomeOdds: + type: object + required: [id, odds_version_id, outcome_id, decimal_odds, fractional_num, fractional_den] + properties: + id: + type: string + format: uuid + odds_version_id: + type: string + format: uuid + outcome_id: + type: string + format: uuid + decimal_odds: + type: number + fractional_num: + type: integer + fractional_den: + type: integer + BetIntentRequest: + type: object + required: [session_id, event_id, market_id, outcome_id, idempotency_key, client_sent_at] + properties: + session_id: + type: string + format: uuid + event_id: + type: string + format: uuid + market_id: + type: string + format: uuid + outcome_id: + type: string + format: uuid + idempotency_key: + type: string + client_sent_at: + type: string + format: date-time + BetIntentResponse: + type: object + required: [id, accepted, acceptance_code, server_received_at] + properties: + id: + type: string + format: uuid + accepted: + type: boolean + acceptance_code: + type: string + accepted_odds_version_id: + type: string + format: uuid + nullable: true + server_received_at: + type: string + format: date-time + Settlement: + type: object + required: [id, market_id, settled_at, winning_outcome_id] + properties: + id: + type: string + format: uuid + market_id: + type: string + format: uuid + settled_at: + type: string + format: date-time + winning_outcome_id: + type: string + format: uuid + AnalyticsBatchRequest: + type: object + required: [events] + properties: + events: + type: array + items: + $ref: '#/components/schemas/AnalyticsEventInput' + AnalyticsEventInput: + type: object + required: [event_name, occurred_at] + properties: + event_name: + type: string + occurred_at: + type: string + format: date-time + attributes: + type: array + items: + $ref: '#/components/schemas/AttributeInput' + AttributeInput: + type: object + required: [key, value] + properties: + key: + type: string + value: + type: string + ExperimentConfig: + type: object + properties: + variant: + type: string + feature_flags: + type: object + additionalProperties: + type: boolean + LocalizationBundle: + type: object + required: [locale_code, values] + properties: + locale_code: + type: string + values: + type: object + additionalProperties: + type: string + ErrorResponse: + type: object + required: [code, message] + properties: + code: + type: string + message: + type: string diff --git a/docs/analytics-taxonomy.md b/docs/analytics-taxonomy.md new file mode 100644 index 0000000..5ce1611 --- /dev/null +++ b/docs/analytics-taxonomy.md @@ -0,0 +1,106 @@ +# Analytics Taxonomy + +## Rules + +- All analytics events are structured +- No JSONB payloads +- Events live in `analytics_events` +- Attributes live in `analytics_event_attributes` + +## Event Groups + +### Lifecycle + +- `app_opened` +- `app_backgrounded` +- `app_closed` +- `session_started` +- `session_ended` + +### Onboarding + +- `language_selected` +- `consent_viewed` +- `consent_accepted` + +### Feed and Round Load + +- `feed_viewed` +- `round_card_viewed` +- `round_loaded` +- `event_manifest_received` +- `preview_prefetch_started` +- `preview_prefetch_completed` + +### Playback + +- `preview_started` +- `preview_paused` +- `preview_resumed` +- `preview_completed` +- `reveal_started` +- `reveal_completed` +- `playback_error` +- `stream_reconnected` + +### Timing and Odds + +- `countdown_visible` +- `countdown_warning_threshold_hit` +- `odds_panel_viewed` +- `odds_version_received` +- `odds_changed` + +### Selection and Settlement + +- `outcome_focused` +- `outcome_selected` +- `selection_submitted` +- `selection_accepted` +- `selection_rejected` +- `duplicate_selection_attempt` +- `market_locked` +- `result_viewed` +- `next_round_requested` + +### UI and Input + +- `screen_viewed` +- `cta_pressed` +- `gesture_swipe` +- `gesture_tap` +- `gesture_cancelled` +- `haptic_triggered` + +### Localization and Errors + +- `localization_bundle_loaded` +- `locale_changed` +- `network_error` + +## Common Attributes + +- `screen_name` +- `event_id` +- `market_id` +- `outcome_id` +- `odds_version_id` +- `countdown_ms_remaining` +- `locale_code` +- `experiment_variant` +- `network_type` +- `device_orientation` +- `playback_position_ms` +- `latency_ms` + +## Research Metrics + +- Decision latency +- Round completion rate +- Missed rounds +- Post-lock selection attempts +- Replay desire +- Exit rate +- Control vs modern comparison +- Locale effects +- Device platform effects diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..ec21bf5 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,50 @@ +# Architecture + +## System Shape + +Hermes uses three layers: + +- Native client apps for iOS and Android +- A Rust backend API that owns timing and state +- PostgreSQL plus Valkey for durable and short-lived data + +## Client Responsibilities + +- Render the study experience +- Play preview and reveal video +- Handle gestures and haptics +- Show odds and lock timing +- Keep local session state +- Prefetch media +- Sync clocks with the server +- Capture structured analytics +- Load English and Swedish strings from localization assets + +## Backend Responsibilities + +- Auth and session binding +- Event feed and manifests +- Markets and odds distribution +- Bet intent validation and acceptance +- Settlement and audit logging +- Experiment assignment +- Localization bundle serving +- Analytics ingestion +- Admin fixture publishing + +## Core Rules + +- The server decides lock time and settlement +- Clients send intent, not fairness decisions +- Video playback must not block overlay updates +- Client and server state machines must match +- Localization is mandatory for every user-facing string +- Analytics and audit data stay relational + +## Repo Order + +1. Documents and contracts +2. Backend foundation +3. Domain modules and API coverage +4. Fixture and admin workflows +5. Native iOS and Android apps diff --git a/docs/localization-catalog.md b/docs/localization-catalog.md new file mode 100644 index 0000000..364918e --- /dev/null +++ b/docs/localization-catalog.md @@ -0,0 +1,52 @@ +# Localization Catalog + +## Rules + +- Every user-facing string must be localizable +- Stable key names only +- English is the fallback locale +- Swedish must be present at launch +- Keys are mirrored in `contracts/localization/en.json` and `contracts/localization/sv.json` + +## Current Keys + +| Key | Purpose | +| --- | --- | +| `app.name` | App name | +| `common.continue` | Continue action | +| `common.cancel` | Cancel action | +| `common.close` | Close action | +| `common.retry` | Retry action | +| `common.loading` | Loading state | +| `common.error_title` | Error title | +| `common.ok` | OK action | +| `onboarding.title` | Study intro title | +| `onboarding.subtitle` | Study intro body | +| `onboarding.consent_title` | Consent title | +| `onboarding.consent_body` | Consent body | +| `onboarding.language_title` | Language picker title | +| `onboarding.start_session` | Start session CTA | +| `feed.next_round_title` | Next round title | +| `feed.next_round_body` | Next round body | +| `feed.watch_preview` | Watch preview CTA | +| `feed.round_ready` | Round ready label | +| `round.countdown_label` | Countdown label | +| `round.locked_label` | Locked label | +| `round.selection_prompt` | Selection prompt | +| `round.selection_confirmed` | Selection accepted | +| `round.selection_submitting` | Selection submitting | +| `round.odds_label` | Odds label | +| `reveal.title` | Reveal title | +| `reveal.subtitle` | Reveal subtitle | +| `result.title` | Result title | +| `result.user_selection` | User selection label | +| `result.outcome` | Outcome label | +| `result.next_round` | Next round CTA | +| `settings.title` | Settings title | +| `settings.language` | Language setting | +| `settings.haptics` | Haptics setting | +| `settings.analytics` | Analytics setting | +| `errors.generic` | Generic error copy | +| `errors.network` | Network error copy | +| `errors.playback` | Playback error copy | +| `errors.session_expired` | Session expired copy | diff --git a/docs/relational-schema.md b/docs/relational-schema.md new file mode 100644 index 0000000..2224796 --- /dev/null +++ b/docs/relational-schema.md @@ -0,0 +1,192 @@ +# Relational Schema + +## Rules + +- UUID primary keys +- `timestamptz` for all timestamps +- No JSONB columns +- Child tables for attributes and event payloads +- Foreign keys wherever possible + +## Core Tables + +### users + +- `id` +- `external_ref` +- `created_at` +- `preferred_language` +- `device_platform` + +### sessions + +- `id` +- `user_id` +- `started_at` +- `ended_at` +- `experiment_variant` +- `app_version` +- `device_model` +- `os_version` +- `locale_code` + +### events + +- `id` +- `sport_type` +- `source_ref` +- `title_en` +- `title_sv` +- `status` +- `preview_start_ms` +- `preview_end_ms` +- `reveal_start_ms` +- `reveal_end_ms` +- `lock_at` +- `settle_at` + +### event_media + +- `id` +- `event_id` +- `media_type` +- `hls_master_url` +- `poster_url` +- `duration_ms` +- `preview_start_ms` +- `preview_end_ms` +- `reveal_start_ms` +- `reveal_end_ms` + +### markets + +- `id` +- `event_id` +- `question_key` +- `market_type` +- `status` +- `lock_at` +- `settlement_rule_key` + +### outcomes + +- `id` +- `market_id` +- `outcome_code` +- `label_key` +- `sort_order` + +### odds_versions + +- `id` +- `market_id` +- `version_no` +- `created_at` +- `is_current` + +### outcome_odds + +- `id` +- `odds_version_id` +- `outcome_id` +- `decimal_odds` +- `fractional_num` +- `fractional_den` + +### bet_intents + +- `id` +- `user_id` +- `session_id` +- `event_id` +- `market_id` +- `outcome_id` +- `idempotency_key` +- `client_sent_at` +- `server_received_at` +- `accepted` +- `acceptance_code` +- `accepted_odds_version_id` + +### settlements + +- `id` +- `market_id` +- `settled_at` +- `winning_outcome_id` + +### experiment_assignments + +- `id` +- `user_id` +- `session_id` +- `variant` +- `assigned_at` + +### localization_keys + +- `id` +- `key_name` +- `description` + +### localization_values + +- `id` +- `localization_key_id` +- `locale_code` +- `text_value` + +### analytics_event_types + +- `id` +- `event_name` +- `description` + +### analytics_events + +- `id` +- `analytics_event_type_id` +- `session_id` +- `user_id` +- `occurred_at` + +### analytics_event_attributes + +- `id` +- `analytics_event_id` +- `attribute_key` +- `attribute_value` + +### audit_logs + +- `id` +- `created_at` +- `actor_type` +- `actor_id` +- `action_name` +- `target_type` +- `target_id` +- `trace_id` +- `note` + +### audit_log_attributes + +- `id` +- `audit_log_id` +- `attribute_key` +- `attribute_value` + +## Indexes + +- `sessions.user_id` +- `sessions.started_at` +- `events.status` +- `markets.event_id` +- `markets.lock_at` +- `bet_intents.session_id` +- `bet_intents.market_id` +- `bet_intents.idempotency_key` +- `analytics_events.session_id` +- `analytics_events.user_id` +- `analytics_events.analytics_event_type_id` +- `audit_logs.created_at` diff --git a/docs/state-machines.md b/docs/state-machines.md new file mode 100644 index 0000000..8ef0a32 --- /dev/null +++ b/docs/state-machines.md @@ -0,0 +1,31 @@ +# State Machines + +## Event Lifecycle + +`scheduled -> prefetch_ready -> preview_open -> locking -> locked -> reveal_open -> settled -> archived` + +## Client Round State + +`idle -> prefetching -> ready -> preview_playing -> selection_pending -> selection_submitting -> selection_accepted -> locked -> reveal_playing -> result_visible -> transitioning -> error` + +## Bet Acceptance State + +`received -> validated -> accepted` + +Rejection states: + +- `rejected_too_late` +- `rejected_invalid_market` +- `rejected_invalid_session` +- `rejected_duplicate` + +Terminal state: + +- `settled` + +## Notes + +- Locking is server authoritative +- Clients may display synced countdowns, but acceptance is decided by the server +- Selection confirmation must be visually unambiguous +- The locked state freezes the odds display for the user diff --git a/fixtures/README.md b/fixtures/README.md new file mode 100644 index 0000000..2fab9aa --- /dev/null +++ b/fixtures/README.md @@ -0,0 +1,9 @@ +# Fixtures + +Reserved for sample videos, manifests, and test data. + +Planned subdirectories: + +- `videos/` +- `manifests/` +- `test-data/` diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..41f9019 --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,19 @@ +services: + postgres: + image: postgres:16 + environment: + POSTGRES_DB: hermes + POSTGRES_USER: hermes + POSTGRES_PASSWORD: hermes + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + + valkey: + image: valkey/valkey:8 + ports: + - "6379:6379" + +volumes: + postgres-data: diff --git a/mobile/android-app/README.md b/mobile/android-app/README.md new file mode 100644 index 0000000..7df3b7e --- /dev/null +++ b/mobile/android-app/README.md @@ -0,0 +1,15 @@ +# Android App + +Native Jetpack Compose client scaffold for the Hermes study app. + +Planned structure: + +- `app/` +- `core/` +- `data/` +- `domain/` +- `feature/` +- `benchmark/` +- `androidTest/` + +This directory is reserved for the phase 6 native Android implementation. diff --git a/mobile/ios-app/README.md b/mobile/ios-app/README.md new file mode 100644 index 0000000..4acd44a --- /dev/null +++ b/mobile/ios-app/README.md @@ -0,0 +1,13 @@ +# iOS App + +Native SwiftUI client scaffold for the Hermes study app. + +Planned structure: + +- `App/` +- `Core/` +- `Features/` +- `Resources/` +- `Tests/` + +This directory is reserved for the phase 5 native iOS implementation. diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..8043f99 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,3 @@ +# Scripts + +Reserved for repo automation and data-loading helpers.