commit aa1a7fa572e317925b08b97d54caee3d2c1d8835 Author: Bendik Aagaard Lynghaug Date: Wed Jul 29 19:38:40 2026 +0200 Initial commit: content-driven onboarding portal Leptos/Axum app that renders a Question/Alternative/Feature schema loaded from a sibling content repo (portal-content). Kanidm OIDC login, content-driven authorization (Question.qualifies), a generic NATS KV-backed resource + state-transition mechanism (no bespoke "applicant" concept baked into the runtime - it's all content), a SHA-256 DAG chain tying submissions and decisions together, and the "YES - Rasterized Lines" piece (ported from the live uhhm.no site) as the landing hero. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..65aa92f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5042 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "any_spawner" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d" +dependencies = [ + "futures", + "thiserror 2.0.19", + "tokio", + "wasm-bindgen-futures", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-nats" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76433c4de73442daedb3a59e991d94e85c14ebfc33db53dfcd347a21cd6ef4f8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "memchr", + "nkeys", + "nuid", + "once_cell", + "pin-project", + "portable-atomic", + "rand 0.8.7", + "regex", + "ring", + "rustls-native-certs 0.7.3", + "rustls-pemfile", + "rustls-webpki 0.102.8", + "serde", + "serde_json", + "serde_nanos", + "serde_repr", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tokio-websockets", + "tracing", + "tryhard", + "url", +] + +[[package]] +name = "async-once-cell" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.119", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-credential-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "aws-runtime" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.140.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9660cf991e512fbe6094f1041ff3d3282bc5aeeeb6184e8de437ec2de024a10" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.2", + "http-body 1.1.0", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.11.0", + "tracing", + "url", +] + +[[package]] +name = "aws-sigv4" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.2", + "p256", + "percent-encoding", + "sha2 0.11.0", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67ecd999972b58e67cab052f5129906c08c25883bd0788ceefc55ef97d61307" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1 0.11.0", + "sha2 0.11.0", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9381123ab62d20c13082b151f30f962a3b112b727345394536dfa39a482944" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635d23afda0a6ab48d666c4d447c4873e8d1e83518a2be2093122397e50b838e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.15", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.11.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.9", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.42", + "rustls-native-certs 0.8.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.2", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.2", +] + +[[package]] +name = "aws-smithy-types" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "hyper 1.11.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1 0.10.7", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "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 1.4.2", + "http-body 1.1.0", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[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 = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "codee" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "config" +version = "0.15.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139" +dependencies = [ + "convert_case 0.6.0", + "pathdiff", + "serde_core", + "toml", + "winnow", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-str" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_str_slice_concat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" + +[[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 = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case_extras" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589c70f0faf8aa9d17787557d5eae854d7755cac50f5c3d12c81d3d57661cebb" +dependencies = [ + "convert_case 0.11.0", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin 0.10.1", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "either_of" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5060e0a4cbf26a87550792688ade88e6b8aec9208613631a7a363bda7bc2d4cd" +dependencies = [ + "paste", + "pin-project-lite", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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 = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "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", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 1.4.2", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "guardian" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.2", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "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 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "html-escape" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c1ff2d1cbf39efe5af0900ced8a069b5e61557a17544eb0c4a50239937389e" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http 1.4.2", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.1.0", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[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 = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hydration_context" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbbeb23ee808258cef2c5585ff0dc8e41da21a8dde943f6b290da153a042a96" +dependencies = [ + "futures", + "js-sys", + "or_poisoned", + "pin-project-lite", + "serde", + "throw_error", + "wasm-bindgen", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.15", + "http 1.4.2", + "http-body 1.1.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.2", + "hyper 1.11.0", + "hyper-util", + "rustls 0.23.42", + "rustls-native-certs 0.8.4", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "hyper 1.11.0", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[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 = "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "leptos" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705e2951f3688e0c4f66bbb7a2702282782dcee716971dbd6209c2619d272479" +dependencies = [ + "any_spawner", + "base64 0.22.1", + "cfg-if", + "either_of", + "futures", + "getrandom 0.4.3", + "hydration_context", + "leptos_config", + "leptos_dom", + "leptos_hot_reload", + "leptos_macro", + "leptos_server", + "oco_ref", + "or_poisoned", + "paste", + "rand 0.9.5", + "reactive_graph", + "rustc-hash", + "rustc_version", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn", + "slotmap", + "tachys", + "thiserror 2.0.19", + "throw_error", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_split_helpers", + "web-sys", +] + +[[package]] +name = "leptos_axum" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513e2dd5fe81512b53304429b9ca77cceb954d23af88921c0c33119380f07cbe" +dependencies = [ + "any_spawner", + "axum", + "futures", + "hydration_context", + "leptos", + "leptos_integration_utils", + "leptos_macro", + "leptos_meta", + "leptos_router", + "or_poisoned", + "server_fn", + "tachys", + "tokio", + "tower", + "tower-http", +] + +[[package]] +name = "leptos_config" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c06f751315bccc0d193fab302ac01d25bcfcd97474d4676440e7e3250dc3fc3" +dependencies = [ + "config", + "regex", + "serde", + "thiserror 2.0.19", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35742e9ed8f8aaf9e549b454c68a7ac0992536e06856365639b111f72ab07884" +dependencies = [ + "js-sys", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2a0f220c8a5ef3c51199dfb9cdd702bc0eb80d52fbe70c7890adfaaae8a4b1" +dependencies = [ + "anyhow", + "camino", + "indexmap 2.14.0", + "or_poisoned", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "leptos_integration_utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c097f89cd9aa606297672f56fa5bdda09f01609a9f4eefaccdbb5ab5afea4279" +dependencies = [ + "futures", + "hydration_context", + "leptos", + "leptos_config", + "leptos_meta", + "leptos_router", + "reactive_graph", +] + +[[package]] +name = "leptos_macro" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de6e8da9d4f1a7b74b447b317d590ebabb38709f588f7ee20564b773ccbcce" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.11.0", + "convert_case_extras", + "html-escape", + "itertools 0.14.0", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "rustc_version", + "server_fn_macro", + "syn 2.0.119", + "uuid", +] + +[[package]] +name = "leptos_meta" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c3efe657b4c55ed2e078922786ffe20acfb71767c3dd913767b09a35c75c890" +dependencies = [ + "futures", + "indexmap 2.14.0", + "leptos", + "or_poisoned", + "send_wrapper", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "318c035b5995324c2f60900978ac67da3b21e6062a6e904f8b93b38eddc4e3a2" +dependencies = [ + "any_spawner", + "either_of", + "futures", + "gloo-net", + "js-sys", + "leptos", + "leptos_router_macro", + "or_poisoned", + "percent-encoding", + "reactive_graph", + "rustc_version", + "send_wrapper", + "tachys", + "thiserror 2.0.19", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router_macro" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409c0bd99f986c3cfa1a4db2443c835bc602ded1a12784e22ecb28c3ed5a2ae2" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "leptos_server" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da974775c5ccbb6bd64be7f53f75e8321542e28f21563a416574dbe4d5447eae" +dependencies = [ + "any_spawner", + "base64 0.22.1", + "codee", + "futures", + "hydration_context", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "serde", + "serde_json", + "server_fn", + "tachys", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[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", + "serde", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.4.2", + "httparse", + "memchr", + "mime", + "spin 0.9.9", + "version_check", +] + +[[package]] +name = "next_tuple" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" + +[[package]] +name = "nkeys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf" +dependencies = [ + "data-encoding", + "ed25519", + "ed25519-dalek", + "getrandom 0.2.17", + "log", + "rand 0.8.7", + "signatory", +] + +[[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 = "nuid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" +dependencies = [ + "rand 0.8.7", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.7", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "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 = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.17", + "http 1.4.2", + "rand 0.8.7", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "oco_ref" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" +dependencies = [ + "serde", + "thiserror 2.0.19", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac 0.12.1", + "http 1.4.2", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.7", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2 0.10.9", + "subtle", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[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 = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portal" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-nats", + "aws-sdk-s3", + "axum", + "base64 0.22.1", + "chrono", + "console_error_panic_hook", + "dotenvy", + "futures", + "js-sys", + "leptos", + "leptos_axum", + "leptos_meta", + "leptos_router", + "openidconnect", + "serde", + "serde_json", + "serde_yaml", + "sha2 0.10.9", + "tokio", + "tower", + "tower-http", + "tower-sessions", + "tracing", + "tracing-subscriber", + "url", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-error-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 2.0.119", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "version_check", + "yansi", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.42", + "socket2 0.6.5", + "thiserror 2.0.19", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls 0.23.42", + "rustls-pki-types", + "slab", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.5", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[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 = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "reactive_graph" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c5a025366836190c7030e883cc2bcd9e384ff555336e3c7954741ca411b177" +dependencies = [ + "any_spawner", + "async-lock", + "futures", + "guardian", + "hydration_context", + "indexmap 2.14.0", + "or_poisoned", + "paste", + "pin-project-lite", + "rustc-hash", + "rustc_version", + "send_wrapper", + "serde", + "slotmap", + "thiserror 2.0.19", + "web-sys", +] + +[[package]] +name = "reactive_stores" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c30fd35b7d299c591293bb69fed47a703eb2703b1cff0493e78b16ed007e5382" +dependencies = [ + "guardian", + "indexmap 2.14.0", + "itertools 0.14.0", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores_macro", + "rustc-hash", + "send_wrapper", +] + +[[package]] +name = "reactive_stores_macro" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68072edd607edd30b9ebf57d984ba45d8ab8809e598d0f6046278373fb76a5a0" +dependencies = [ + "convert_case 0.11.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "hyper 1.11.0", + "hyper-rustls 0.27.9", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.42", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstml" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" +dependencies = [ + "derive-where", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.119", + "syn_derive", + "thiserror 2.0.19", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_nanos" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985" +dependencies = [ + "serde", +] + +[[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_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.19", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "server_fn" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8559dd05af1b5b7e363a150616589d5a88af5187273f7f331ba0dae8922812" +dependencies = [ + "axum", + "base64 0.22.1", + "bytes", + "const-str", + "const_format", + "futures", + "gloo-net", + "http 1.4.2", + "http-body-util", + "hyper 1.11.0", + "inventory", + "js-sys", + "or_poisoned", + "pin-project-lite", + "rustc_version", + "rustversion", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.19", + "throw_error", + "tokio", + "tower", + "tower-layer", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1295b54815397d30d986b63f93cfd515fa86d5e528e0bb589ce9d530502f9e0f" +dependencies = [ + "const_format", + "convert_case 0.11.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00" +dependencies = [ + "server_fn_macro", + "syn 2.0.119", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signatory" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" +dependencies = [ + "pkcs8", + "rand_core 0.6.4", + "signature", + "zeroize", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tachys" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92ba81187437cc5df4281f2326a2e13cc81e8f96448292d1112388e2025ca66" +dependencies = [ + "any_spawner", + "async-trait", + "const_str_slice_concat", + "drain_filter_polyfill", + "either_of", + "erased", + "futures", + "html-escape", + "indexmap 2.14.0", + "itertools 0.14.0", + "js-sys", + "next_tuple", + "oco_ref", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores", + "rustc-hash", + "rustc_version", + "send_wrapper", + "slotmap", + "throw_error", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "throw_error" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[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.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.5", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.42", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "http 1.4.2", + "httparse", + "rand 0.8.7", + "ring", + "rustls-native-certs 0.8.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[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-cookies" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" +dependencies = [ + "axum-core", + "cookie", + "futures-util", + "http 1.4.2", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tower-sessions" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3" +dependencies = [ + "async-trait", + "http 1.4.2", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.22.1", + "futures", + "http 1.4.2", + "parking_lot", + "rand 0.8.7", + "serde", + "serde_json", + "thiserror 2.0.19", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + +[[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 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tryhard" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.2", + "httparse", + "log", + "rand 0.9.5", + "sha1 0.10.7", + "thiserror 2.0.19", +] + +[[package]] +name = "typed-builder" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[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.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm_split_helpers" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab578aae2fe2916edaea06843187d50f87b0965622da0ceef648edca27b385ba" +dependencies = [ + "async-once-cell", + "wasm_split_macros", +] + +[[package]] +name = "wasm_split_macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e653af7ee4a9ef0fce481a9ec6f43cb78de20d0cdb4f4f5862e1dc6e407e6c8" +dependencies = [ + "base16", + "quote", + "sha2 0.10.9", + "syn 2.0.119", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-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 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[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 = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[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 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..64cd5e9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,111 @@ +[package] +name = "portal" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +leptos = { version = "0.8" } +leptos_meta = { version = "0.8" } +leptos_router = { version = "0.8" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# --- server only --- +leptos_axum = { version = "0.8", optional = true } +axum = { version = "0.8", features = ["multipart"], optional = true } +aws-sdk-s3 = { version = "1", optional = true } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"], optional = true } +tower = { version = "0.5", optional = true } +tower-http = { version = "0.6", features = ["fs"], optional = true } +tower-sessions = { version = "0.14", optional = true } +async-nats = { version = "0.38", optional = true } +url = { version = "2", optional = true } +serde_yaml = { version = "0.9", optional = true } +sha2 = { version = "0.10", optional = true } +openidconnect = { version = "4", optional = true } +# For pulling the `groups` custom claim out of the already-verified ID +# token's raw JWT payload - see server/oidc.rs::extract_groups_claim. +base64 = { version = "0.22", optional = true } +chrono = { version = "0.4", features = ["serde"], optional = true } +uuid = { version = "1", features = ["v4"], optional = true } +dotenvy = { version = "0.15", optional = true } +anyhow = { version = "1", optional = true } +futures = { version = "0.3", optional = true } +tracing = { version = "0.1", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } + +# --- browser only --- +wasm-bindgen = { version = "0.2", optional = true } +wasm-bindgen-futures = { version = "0.4", optional = true } +js-sys = { version = "0.3", optional = true } +console_error_panic_hook = { version = "0.1", optional = true } +web-sys = { version = "0.3", features = [ + "Blob", + "File", + "FileList", + "FormData", + "HtmlInputElement", + "Request", + "RequestInit", + "Response", + "Window", +], optional = true } + +[features] +default = [] +hydrate = [ + "leptos/hydrate", + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", + "dep:js-sys", + "dep:console_error_panic_hook", + "dep:web-sys", +] +ssr = [ + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", + "dep:leptos_axum", + "dep:axum", + "dep:aws-sdk-s3", + "dep:tokio", + "dep:tower", + "dep:tower-http", + "dep:tower-sessions", + "dep:async-nats", + "dep:url", + "dep:serde_yaml", + "dep:sha2", + "dep:openidconnect", + "dep:base64", + "dep:chrono", + "dep:uuid", + "dep:dotenvy", + "dep:anyhow", + "dep:futures", + "dep:tracing", + "dep:tracing-subscriber", +] + +[profile.wasm-release] +inherits = "release" +opt-level = 'z' +lto = true +codegen-units = 1 + +[package.metadata.leptos] +output-name = "portal" +site-root = "target/site" +site-pkg-dir = "pkg" +style-file = "style/main.css" +assets-dir = "public" +site-addr = "127.0.0.1:3010" +reload-port = 3011 +bin-features = ["ssr"] +bin-default-features = false +lib-features = ["hydrate"] +lib-default-features = false +lib-profile-release = "wasm-release" diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..a8bf722 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/fonts/quicksand-semibold-latin-ext.woff2 b/public/fonts/quicksand-semibold-latin-ext.woff2 new file mode 100644 index 0000000..c7abeeb Binary files /dev/null and b/public/fonts/quicksand-semibold-latin-ext.woff2 differ diff --git a/public/fonts/quicksand-semibold-latin.woff2 b/public/fonts/quicksand-semibold-latin.woff2 new file mode 100644 index 0000000..df7256d Binary files /dev/null and b/public/fonts/quicksand-semibold-latin.woff2 differ diff --git a/public/fonts/quicksand-semibold-vietnamese.woff2 b/public/fonts/quicksand-semibold-vietnamese.woff2 new file mode 100644 index 0000000..a96d672 Binary files /dev/null and b/public/fonts/quicksand-semibold-vietnamese.woff2 differ diff --git a/src/answers.rs b/src/answers.rs new file mode 100644 index 0000000..db39be6 --- /dev/null +++ b/src/answers.rs @@ -0,0 +1,179 @@ +//! Durable, queryable storage for submitted answers - the KV-backed +//! counterpart to the ephemeral `events::AnswerSubmitted` every +//! submission already publishes. The runtime here has no notion of +//! "applicant" or any other specific workflow: an `Alternative.record_as` +//! names a NATS KV bucket (nothing more); on submit, the response gets +//! wrapped in the same envelope (`Answer`) regardless of what content +//! produced it. `resource::get_resource` reads it back generically, and +//! `transition_answer` below moves an answer between states named in +//! content (`ResourceSpec.transitions`), never states hardcoded here. +//! Whatever workflow this ends up powering - applicant review, order +//! triage, comment moderation - is entirely a property of some +//! `portal-content` YAML, not of this file. + +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +/// An answer's starting state - content decides what it may transition +/// to (`ResourceSpec.transitions`), never this file. +pub const OPEN_STATE: &str = "open"; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Answer { + /// The submission's chain hash - stable, unique, and already what + /// ties this answer back into the DAG a transition later extends. + pub id: String, + pub question_id: String, + pub alternative: String, + /// The submitted form fields, verbatim - the runtime never inspects + /// these keys, only content (via its own copy/labels) gives them + /// meaning. + pub responses: serde_json::Value, + pub state: String, + pub submitted_ms: i64, + #[serde(default)] + pub decided_ms: Option, + #[serde(default)] + pub decided_by: Option, +} + +/// Writes a submitted answer into `bucket`, creating it on first use - +/// there's no fixed, pre-declared list of buckets, since content can +/// name any. +#[cfg(feature = "ssr")] +pub async fn store_answer( + js: &async_nats::jetstream::Context, + bucket: &str, + id: String, + question_id: &str, + alternative: &str, + responses: &serde_json::Value, + submitted_ms: i64, +) -> anyhow::Result<()> { + let store = match js.get_key_value(bucket).await { + Ok(store) => store, + Err(_) => { + js.create_key_value(async_nats::jetstream::kv::Config { + bucket: bucket.to_string(), + ..Default::default() + }) + .await? + } + }; + let answer = Answer { + id: id.clone(), + question_id: question_id.to_string(), + alternative: alternative.to_string(), + responses: responses.clone(), + state: OPEN_STATE.to_string(), + submitted_ms, + decided_ms: None, + decided_by: None, + }; + let data = serde_json::to_vec(&answer)?; + store.put(id, data.into()).await?; + Ok(()) +} + +/// Moves a stored answer from `open` to one of the target states its +/// own resource spec declares (`ResourceSpec.transitions`) - `to` is +/// checked against that content-declared allow-list, never accepted as +/// an arbitrary string. Requires a signed-in member of the resource's +/// `requires_group`, regardless of whether the resource is otherwise +/// `public` - reads can be public, mutations always need identity. +#[server] +pub async fn transition_answer( + question_id: String, + feature_name: String, + item_id: String, + to: String, +) -> Result<(), ServerFnError> { + use crate::auth::{User, SESSION_USER_KEY}; + use crate::chain::hash_node; + use crate::events::{emit_answer_submitted, AnswerSubmitted}; + use crate::server::AppState; + + let state = expect_context::(); + let question = state + .questions + .get(&question_id) + .ok_or_else(|| ServerFnError::new("unknown question"))?; + let feature = question + .alternatives + .iter() + .flat_map(|a| &a.features) + .find(|f| f.name == feature_name) + .ok_or_else(|| ServerFnError::new("unknown feature"))?; + let resource = feature + .resource + .as_ref() + .ok_or_else(|| ServerFnError::new("feature has no resource"))?; + let transition = resource + .transitions + .iter() + .find(|t| t.to == to) + .ok_or_else(|| ServerFnError::new("no such transition"))? + .clone(); + let group = resource + .requires_group + .as_deref() + .ok_or_else(|| ServerFnError::new("resource has no transitions available"))?; + + let session: tower_sessions::Session = leptos_axum::extract().await?; + let user = session + .get::(SESSION_USER_KEY) + .await + .map_err(|e| ServerFnError::new(e.to_string()))? + .ok_or_else(|| ServerFnError::new("not signed in"))?; + if !user.groups.iter().any(|g| g == group) { + return Err(ServerFnError::new("not authorized")); + } + + let store = state + .jetstream + .get_key_value(&resource.bucket) + .await + .map_err(|e| ServerFnError::new(format!("resource bucket unavailable: {e}")))?; + let bytes = store + .get(&item_id) + .await + .map_err(|e| ServerFnError::new(e.to_string()))? + .ok_or_else(|| ServerFnError::new("unknown answer"))?; + let mut answer: Answer = + serde_json::from_slice(&bytes).map_err(|e| ServerFnError::new(e.to_string()))?; + if answer.state != OPEN_STATE { + return Err(ServerFnError::new("already decided")); + } + + let decided_ms = chrono::Utc::now().timestamp_millis(); + answer.state = to.clone(); + answer.decided_ms = Some(decided_ms); + answer.decided_by = Some(user.username.clone()); + + let data = serde_json::to_vec(&answer).map_err(|e| ServerFnError::new(e.to_string()))?; + store + .put(&item_id, data.into()) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + + // Extend the DAG: the decision is a child node of the answer's own + // submission hash, published the same way any other answer is - + // question_id/alternative come from this call's own arguments and + // the content-declared transition label, never a hardcoded value. + let decision_payload = serde_json::json!({ "to": to, "item": item_id, "by": user.username }); + let parent_hashes = vec![item_id.clone()]; + let chain_hash = hash_node(&question_id, &parent_hashes, &decision_payload, decided_ms); + let event = AnswerSubmitted { + chain_hash, + parent_hashes, + question_id, + alternative: transition.label, + responses: decision_payload, + timestamp_ms: decided_ms, + }; + emit_answer_submitted(&state.nats, &event) + .await + .map_err(|e| ServerFnError::new(format!("nats publish failed: {e}")))?; + + Ok(()) +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..78edd5e --- /dev/null +++ b/src/app.rs @@ -0,0 +1,862 @@ +use leptos::prelude::*; +use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; +use leptos_router::{ + components::{Route, Router, Routes}, + hooks::{use_location, use_navigate, use_query_map}, + path, NavigateOptions, +}; +use serde::{Deserialize, Serialize}; + +use crate::answers::{Answer, TransitionAnswer}; +use crate::auth::{current_user, User}; +use crate::content::{is_qualified, Alternative, Question, Transition}; +use crate::resource::get_resource; + +/// The visible site name/wordmark - "portal" is just this codebase's +/// working title, not necessarily what any given deployment is called. +/// Baked in at *build* time (`option_env!`, not `std::env::var`) so the +/// ssr and hydrate targets - compiled separately - end up with the +/// identical value without a runtime round-trip or context-passing +/// between them; set `SITE_NAME` for both `cargo leptos` invocations +/// (ssr and hydrate) to change it, e.g. `SITE_NAME=uhhm`. CSS uppercases +/// the wordmark for display regardless of the source casing here. +const SITE_NAME: &str = match option_env!("SITE_NAME") { + Some(name) => name, + None => "portal", +}; + +pub fn shell(options: LeptosOptions) -> impl IntoView { + view! { + + + + + + + + + + + + + + + } +} + +#[component] +pub fn App() -> impl IntoView { + provide_meta_context(); + + view! { + + + <Router> + <Routes fallback=|| view! { <NotFound/> }> + <Route path=path!("") view=QuestionPage/> + <Route path=path!("/*any") view=QuestionPage/> + </Routes> + </Router> + } +} + +#[component] +fn QuestionPage() -> impl IntoView { + let location = use_location(); + let query = use_query_map(); + let path = Memo::new(move |_| { + let p = location.pathname.get(); + if p.is_empty() { + "/".to_string() + } else { + p + } + }); + let parent_hash = Memo::new(move |_| query.with(|q| q.get("chain"))); + + let question = Resource::new(move || path.get(), get_question); + let user = Resource::new(|| (), |_| current_user()); + + view! { + <Suspense fallback=|| { + view! { + <main class="loading"> + <p>"loading…"</p> + </main> + } + }> + {move || { + // Both read unconditionally (not nested inside the match + // below) so Suspense tracks both from the first pass, + // rather than only discovering `user` once `question` + // has already resolved. + let user_res = user.get(); + let question_res = question.get(); + question_res + .map(|res| match res { + Ok(Some(q)) => { + let current = user_res.and_then(|r| r.ok()).flatten(); + view! { + <QuestionView + question=q + parent_hash=parent_hash.get() + user=current + /> + } + .into_any() + } + _ => view! { <NotFound/> }.into_any(), + }) + }} + </Suspense> + } +} + +#[component] +fn QuestionView( + question: Question, + parent_hash: Option<String>, + user: Option<User>, +) -> impl IntoView { + let question_id = question.id.clone(); + + // Generic: any question with `qualifies` set renders this same gate + // instead of its alternatives - "/review" isn't a special case, it's + // just a question that happens to have `qualifies` set. + if !is_qualified(user.as_ref(), &question) { + let signed_in = user.is_some(); + return view! { + <Hero + title=question.name.clone() + description=question.description.clone() + show_yes=question_id == "/" + /> + <div class="alternatives"> + <section class="alt-card gate-card"> + {if signed_in { + view! { <p>"This part of the site is for organizational owners — sign in with that account to take a look."</p> } + .into_any() + } else { + view! { + <a class="alt-submit" href="/auth/login" rel="external"> + "Sign in" + </a> + } + .into_any() + }} + </section> + </div> + } + .into_any(); + } + + view! { + <Hero + title=question.name.clone() + description=question.description.clone() + show_yes=question_id == "/" + /> + <div class="alternatives"> + <For + each=move || question.alternatives.clone() + key=|a| a.name.clone() + children=move |alt: Alternative| { + view! { + <AlternativeCard + question_id=question_id.clone() + alternative=alt + parent_hash=parent_hash.clone() + /> + } + } + /> + </div> + } + .into_any() +} + +// Binds the actual "YES - Rasterized Lines" piece live at uhhm.no +// (ported to `public/yes.js`, an ES module now instead of a page-owning +// script) as a typed JS class - the idiomatic wasm-bindgen way to drive +// an existing JS module from Rust, rather than re-implementing canvas +// animation logic here. +#[cfg(feature = "hydrate")] +mod yes { + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "/yes.js")] + extern "C" { + #[wasm_bindgen(js_name = RasterizedYES)] + pub type RasterizedYes; + + #[wasm_bindgen(constructor, js_class = "RasterizedYES")] + pub fn new() -> RasterizedYes; + + #[wasm_bindgen(method)] + pub fn stop(this: &RasterizedYes); + } +} + +#[component] +fn Hero(title: String, description: String, show_yes: bool) -> impl IntoView { + // Only the landing page gets the full interactive piece - it's the + // one page this is actually "the" hero for; other pages (thank-you + // pages, /review) get the plain dark header below. Starts the + // animation once the canvas elements exist (post-mount, via + // Effect), and explicitly stops the requestAnimationFrame loop on + // unmount - the original page-owning script never needed this since + // navigating away meant a full document unload, which doesn't + // happen in an SPA. + #[cfg(feature = "hydrate")] + if show_yes { + // `on_cleanup` requires Send + Sync (even single-threaded, wasm), + // which a JS-backed value never is - `StoredValue`'s `LocalStorage` + // variant is the established way around this (same pattern + // cnats' WebRTC call state uses for its own non-Send peer map). + let instance: StoredValue<Option<yes::RasterizedYes>, LocalStorage> = + StoredValue::new_local(None); + Effect::new(move |_| { + instance.set_value(Some(yes::RasterizedYes::new())); + }); + on_cleanup(move || { + instance.update_value(|opt| { + if let Some(inst) = opt.take() { + inst.stop(); + } + }); + }); + } + + view! { + <header class="hero" class:hero-yes=show_yes> + {show_yes + .then(|| { + view! { + <div class="hero-canvas" aria-hidden="true"> + <canvas id="lineCanvas"></canvas> + <canvas id="rasterCanvas"></canvas> + </div> + } + })} + <div class="hero-copy"> + <a class="wordmark" href="/"> + {SITE_NAME} + </a> + <h1>{title}</h1> + <p>{description}</p> + </div> + </header> + } +} + +#[component] +fn AlternativeCard( + question_id: String, + alternative: Alternative, + parent_hash: Option<String>, +) -> impl IntoView { + let has_action = alternative.action.is_some(); + + // File fields don't fit a live-typed RwSignal<String> - they get + // their own map of element refs, read (and uploaded) only at submit + // time, and are excluded from `field_map` below. + let mut field_map: std::collections::HashMap<String, RwSignal<String>> = + std::collections::HashMap::new(); + let mut file_refs: std::collections::HashMap<String, NodeRef<leptos::html::Input>> = + std::collections::HashMap::new(); + for feature in &alternative.features { + for req in &feature.requirements { + if req.kind == "file" { + file_refs.entry(req.name.clone()).or_insert_with(NodeRef::new); + } else { + field_map + .entry(req.name.clone()) + .or_insert_with(|| RwSignal::new(String::new())); + } + } + } + + // Prefixes field DOM ids with the alternative too, not just the + // question - two alternatives on the same page (e.g. both asking for + // "email") would otherwise mint the same id twice, which is invalid + // HTML and makes <label for=…> pick the wrong input. + let field_prefix = format!( + "{question_id}-{}", + alternative.name.to_lowercase().replace(' ', "-") + ); + + let submit = ServerAction::<SubmitAnswer>::new(); + let navigate = use_navigate(); + + let question_id_for_submit = question_id.clone(); + let alt_name_for_submit = alternative.name.clone(); + let parent_hash_for_submit = parent_hash.clone(); + let field_map_for_submit = field_map.clone(); + let file_refs_for_submit = file_refs.clone(); + let on_submit = move |ev: leptos::ev::SubmitEvent| { + ev.prevent_default(); + let field_map_for_submit = field_map_for_submit.clone(); + let file_refs_for_submit = file_refs_for_submit.clone(); + let question_id_for_submit = question_id_for_submit.clone(); + let alt_name_for_submit = alt_name_for_submit.clone(); + let parent_hash_for_submit = parent_hash_for_submit.clone(); + leptos::task::spawn_local(async move { + let mut map = serde_json::Map::new(); + for (name, sig) in field_map_for_submit.iter() { + map.insert(name.clone(), serde_json::Value::String(sig.get())); + } + // Only used under hydrate (file uploads are a browser-only + // concern); referenced unconditionally so an `ssr` build + // doesn't warn about it going unused. + let _ = &file_refs_for_submit; + + #[cfg(feature = "hydrate")] + for (name, file_ref) in file_refs_for_submit.iter() { + let Some(input) = file_ref.get_untracked() else { + continue; + }; + let Some(files) = input.files() else { continue }; + let Some(file) = files.get(0) else { continue }; + match upload_file(&file).await { + Ok(key) => { + map.insert(name.clone(), serde_json::Value::String(key)); + } + Err(e) => { + leptos::logging::error!("upload of {name} failed: {e:?}"); + } + } + } + + let responses_json = + serde_json::to_string(&serde_json::Value::Object(map)).unwrap_or_default(); + submit.dispatch(SubmitAnswer { + question_id: question_id_for_submit, + alternative: alt_name_for_submit, + parent_hash: parent_hash_for_submit, + responses_json, + }); + }); + }; + + Effect::new(move |_| { + if let Some(Ok(result)) = submit.value().get() { + if let Some(next) = result.next { + navigate( + &format!("{next}?chain={}", result.chain_hash), + NavigateOptions::default(), + ); + } + } + }); + + let button_label = alternative + .consequence + .first() + .cloned() + .unwrap_or_else(|| "Send".to_string()); + + view! { + <section class="alt-card"> + <h2>{alternative.name.clone()}</h2> + <p class="alt-description">{alternative.description.clone()}</p> + <For + each=move || alternative.encouragements.clone() + key=|e| e.clone() + children=move |e| view! { <p class="alt-encouragement">{e}</p> } + /> + <div class="features"> + <For + each={ + let features = alternative.features.clone(); + move || features.clone() + } + key=|f| f.name.clone() + children={ + let field_map = field_map.clone(); + let file_refs = file_refs.clone(); + let field_prefix = field_prefix.clone(); + let question_id = question_id.clone(); + move |feature| { + let field_map = field_map.clone(); + let file_refs = file_refs.clone(); + let field_prefix = field_prefix.clone(); + let question_id = question_id.clone(); + let resource = feature.resource.clone(); + let feature_name = feature.name.clone(); + view! { + <div class="feature"> + <Show when={ + let name = feature.name.clone(); + move || !name.is_empty() + }> + <h3>{feature.name.clone()}</h3> + </Show> + <Show when={ + let description = feature.description.clone(); + move || !description.is_empty() + }> + <p>{feature.description.clone()}</p> + </Show> + {resource.map(|spec| { + view! { + <ResourceFeature + question_id=question_id.clone() + feature_name=feature_name.clone() + transitions=spec.transitions.clone() + /> + } + })} + <For + each={ + let reqs = feature.requirements.clone(); + move || reqs.clone() + } + key=|r| r.name.clone() + children={ + let field_map = field_map.clone(); + let file_refs = file_refs.clone(); + let field_prefix = field_prefix.clone(); + move |req| { + let field_id = format!("{}-{}", field_prefix, req.name); + let label_for = field_id.clone(); + let label_text = view! { + <span class="field-label"> + {req.display_label()} + {(!req.optional).then(|| view! { <span class="required">"*"</span> })} + </span> + }; + + if req.kind == "file" { + let file_ref = file_refs.get(&req.name).copied().unwrap_or_else(NodeRef::new); + return view! { + <label class="field" for=label_for> + {label_text} + <input + id=field_id + type="file" + accept=req.accept.clone().unwrap_or_default() + multiple=req.multiple + required=!req.optional + node_ref=file_ref + /> + </label> + } + .into_any(); + } + + let sig = field_map.get(&req.name).copied().unwrap_or_else(|| RwSignal::new(String::new())); + view! { + <label class="field" for=label_for> + {label_text} + <Show + when={ + let kind = req.kind.clone(); + move || kind == "textarea" + } + fallback={ + let kind = req.kind.clone(); + let placeholder = req.placeholder.clone().unwrap_or_default(); + let field_id = field_id.clone(); + let required = !req.optional; + move || { + view! { + <input + id=field_id.clone() + type=kind.clone() + placeholder=placeholder.clone() + required=required + prop:value=move || sig.get() + on:input=move |ev| sig.set(event_target_value(&ev)) + /> + } + } + } + > + { + let field_id = field_id.clone(); + let placeholder = req.placeholder.clone().unwrap_or_default(); + let required = !req.optional; + view! { + <textarea + id=field_id.clone() + placeholder=placeholder.clone() + required=required + prop:value=move || sig.get() + on:input=move |ev| sig.set(event_target_value(&ev)) + ></textarea> + } + } + </Show> + </label> + } + .into_any() + } + } + /> + </div> + } + } + } + /> + </div> + {has_action + .then(|| { + view! { + <form on:submit=on_submit> + <button + type="submit" + class="alt-submit" + disabled=move || submit.pending().get() + > + {button_label.clone()} + </button> + </form> + } + })} + </section> + } +} + +/// Fetches and renders a `Feature`'s `resource` (`content::ResourceSpec`). +/// Fully generic: it has no idea what "/review" or "applicants" are - it +/// just fetches `question_id`/`feature_name`'s data and renders it +/// according to its own shape (see `ResourceValue`), with one button +/// per `transitions` entry when the data looks like a list of answers. +#[component] +fn ResourceFeature( + question_id: String, + feature_name: String, + transitions: Vec<Transition>, +) -> impl IntoView { + let data = Resource::new( + { + let question_id = question_id.clone(); + let feature_name = feature_name.clone(); + move || (question_id.clone(), feature_name.clone()) + }, + |(q, f)| get_resource(q, f), + ); + + view! { + <div class="resource"> + <Suspense fallback=|| view! { <p class="resource-loading">"loading…"</p> }> + {move || { + let question_id = question_id.clone(); + let feature_name = feature_name.clone(); + let transitions = transitions.clone(); + data.get() + .map(|res| match res { + Ok(value) => { + view! { + <ResourceValue + question_id=question_id + feature_name=feature_name + transitions=transitions + value=value + /> + } + .into_any() + } + Err(e) => { + view! { <p class="resource-error">{e.to_string()}</p> }.into_any() + } + }) + }} + </Suspense> + </div> + } +} + +/// Renders whatever `get_resource` returned, purely from its shape: an +/// array that parses as `answers::Answer` renders as a list (with +/// transition buttons, if any were declared); anything else renders as +/// a plain, read-only dump. No `kind`/render-mode tag anywhere - the +/// data decides. +#[component] +fn ResourceValue( + question_id: String, + feature_name: String, + transitions: Vec<Transition>, + value: serde_json::Value, +) -> impl IntoView { + if let serde_json::Value::Array(items) = &value { + if let Ok(answers) = serde_json::from_value::<Vec<Answer>>(value.clone()) { + let mut answers = answers; + answers.sort_by(|a, b| { + let open = |s: &str| s != crate::answers::OPEN_STATE; + open(&a.state).cmp(&open(&b.state)).then(b.submitted_ms.cmp(&a.submitted_ms)) + }); + return view! { + <div class="answer-list"> + <For + each=move || answers.clone() + key=|a| a.id.clone() + children={ + let question_id = question_id.clone(); + let feature_name = feature_name.clone(); + let transitions = transitions.clone(); + move |answer: Answer| { + view! { + <AnswerRow + question_id=question_id.clone() + feature_name=feature_name.clone() + transitions=transitions.clone() + answer=answer + /> + } + } + } + /> + </div> + } + .into_any(); + } + if items.is_empty() { + return view! { <p class="resource-empty">"Nothing here yet."</p> }.into_any(); + } + } + + view! { <pre class="resource-raw">{value.to_string()}</pre> }.into_any() +} + +#[component] +fn AnswerRow( + question_id: String, + feature_name: String, + transitions: Vec<Transition>, + answer: Answer, +) -> impl IntoView { + let is_open = answer.state == crate::answers::OPEN_STATE; + let responses = answer + .responses + .as_object() + .cloned() + .unwrap_or_default(); + let submitted_ms = answer.submitted_ms; + let state = answer.state.clone(); + let item_id = answer.id.clone(); + + let transition = ServerAction::<TransitionAnswer>::new(); + + view! { + <article class="answer-row" class:decided=move || !is_open> + <div class="answer-fields"> + <For + each={ + let responses = responses.clone(); + move || responses.clone().into_iter().collect::<Vec<_>>() + } + key=|(k, _)| k.clone() + children=move |(key, value)| { + let text = value.as_str().map(str::to_string).unwrap_or_else(|| value.to_string()); + view! { + <div class="answer-field"> + <span class="answer-field-key">{key}</span> + <span class="answer-field-value">{text}</span> + </div> + } + } + /> + </div> + <div class="answer-meta"> + <span class="answer-state">{state}</span> + <span class="answer-time">{format_ms(submitted_ms)}</span> + </div> + <Show when={ + let transitions = transitions.clone(); + move || is_open && !transitions.is_empty() + }> + <div class="answer-actions"> + <For + each={ + let transitions = transitions.clone(); + move || transitions.clone() + } + key=|t| t.to.clone() + children={ + let question_id = question_id.clone(); + let feature_name = feature_name.clone(); + let item_id = item_id.clone(); + move |t: Transition| { + let question_id = question_id.clone(); + let feature_name = feature_name.clone(); + let item_id = item_id.clone(); + let to = t.to.clone(); + view! { + <button + class="answer-action" + on:click=move |_| { + transition.dispatch(TransitionAnswer { + question_id: question_id.clone(), + feature_name: feature_name.clone(), + item_id: item_id.clone(), + to: to.clone(), + }); + } + disabled=move || transition.pending().get() + > + {t.label.clone()} + </button> + } + } + } + /> + </div> + </Show> + </article> + } +} + +fn format_ms(ms: i64) -> String { + #[cfg(feature = "ssr")] + { + use chrono::TimeZone; + chrono::Utc + .timestamp_millis_opt(ms) + .single() + .map(|t| t.format("%Y-%m-%d %H:%M UTC").to_string()) + .unwrap_or_else(|| ms.to_string()) + } + #[cfg(not(feature = "ssr"))] + { + ms.to_string() + } +} + +#[component] +fn NotFound() -> impl IntoView { + view! { + <main class="not-found"> + <h1>"Nothing here"</h1> + <a href="/">"back to the start"</a> + </main> + } +} + +#[server] +pub async fn get_question(path: String) -> Result<Option<Question>, ServerFnError> { + use crate::server::AppState; + let state = expect_context::<AppState>(); + Ok(state.questions.get(&path).cloned()) +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SubmitResult { + pub next: Option<String>, + pub chain_hash: String, +} + +#[server] +pub async fn submit_answer( + question_id: String, + alternative: String, + parent_hash: Option<String>, + responses_json: String, +) -> Result<SubmitResult, ServerFnError> { + use crate::answers::store_answer; + use crate::auth::{User, SESSION_USER_KEY}; + use crate::chain::hash_node; + use crate::content::is_qualified; + use crate::events::{emit_answer_submitted, AnswerSubmitted}; + use crate::server::AppState; + + let state = expect_context::<AppState>(); + let question = state + .questions + .get(&question_id) + .ok_or_else(|| ServerFnError::new("unknown question"))?; + + let session: tower_sessions::Session = leptos_axum::extract().await?; + let user = session + .get::<User>(SESSION_USER_KEY) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + if !is_qualified(user.as_ref(), question) { + return Err(ServerFnError::new("not authorized for this question")); + } + + let alt = question + .alternatives + .iter() + .find(|a| a.name == alternative) + .ok_or_else(|| ServerFnError::new("unknown alternative"))?; + let next = alt.action.clone(); + let record_as = alt.record_as.clone(); + + let responses: serde_json::Value = serde_json::from_str(&responses_json) + .map_err(|e| ServerFnError::new(format!("invalid responses: {e}")))?; + + let parent_hashes: Vec<String> = parent_hash.into_iter().collect(); + let timestamp_ms = chrono::Utc::now().timestamp_millis(); + let chain_hash = hash_node(&question_id, &parent_hashes, &responses, timestamp_ms); + + emit_answer_submitted( + &state.nats, + &AnswerSubmitted { + chain_hash: chain_hash.clone(), + parent_hashes, + question_id: question_id.clone(), + alternative: alternative.clone(), + responses: responses.clone(), + timestamp_ms, + }, + ) + .await + .map_err(|e| ServerFnError::new(format!("nats publish failed: {e}")))?; + + // Best-effort: a bucket-write hiccup shouldn't fail a submission the + // NATS event has already recorded. + if let Some(bucket) = &record_as { + if let Err(e) = store_answer( + &state.jetstream, + bucket, + chain_hash.clone(), + &question_id, + &alternative, + &responses, + timestamp_ms, + ) + .await + { + tracing::error!("failed to store answer in {bucket}: {e}"); + } + } + + Ok(SubmitResult { next, chain_hash }) +} + +/// Uploads one file to `POST /upload` and returns its stored object key +/// - the value a `type: file` requirement contributes to +/// `responses_json`, same as any other field. A plain `fetch`, not a +/// Leptos server fn: those aren't built for binary bodies, and the +/// server side is a raw Axum multipart handler (`src/upload.rs`) for +/// the same reason. +#[cfg(feature = "hydrate")] +async fn upload_file(file: &web_sys::File) -> Result<String, wasm_bindgen::JsValue> { + use wasm_bindgen::{JsCast, JsValue}; + use wasm_bindgen_futures::JsFuture; + use web_sys::{FormData, Request, RequestInit, Response}; + + let form = FormData::new()?; + form.append_with_blob("file", file)?; + + let opts = RequestInit::new(); + opts.set_method("POST"); + opts.set_body_opt_form_data(Some(&form)); + let request = Request::new_with_str_and_init("/upload", &opts)?; + + let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; + let resp: Response = JsFuture::from(window.fetch_with_request(&request)) + .await? + .dyn_into()?; + if !resp.ok() { + return Err(JsValue::from_str(&format!( + "upload failed with status {}", + resp.status() + ))); + } + let json = JsFuture::from(resp.json()?).await?; + js_sys::Reflect::get(&json, &JsValue::from_str("key"))? + .as_string() + .ok_or_else(|| JsValue::from_str("upload response had no key")) +} diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..2811336 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,30 @@ +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +/// The authenticated user, as established by the Kanidm OIDC flow and +/// stored in the server-side session. Ported from cnats' `auth.rs` - +/// same shape, same provider. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct User { + pub sub: String, + pub username: String, + pub display_name: String, + /// Kanidm group membership, from the `groups` OIDC claim (see + /// `oauth2 update-claim-map`). Fixed at login time - not re-checked + /// live, so a group change only takes effect on the next login. + #[serde(default)] + pub groups: Vec<String>, +} + +pub const SESSION_USER_KEY: &str = "user"; + +/// Returns the currently signed-in user, if any. +#[server] +pub async fn current_user() -> Result<Option<User>, ServerFnError> { + let session: tower_sessions::Session = leptos_axum::extract().await?; + let user = session + .get::<User>(SESSION_USER_KEY) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + Ok(user) +} diff --git a/src/chain.rs b/src/chain.rs new file mode 100644 index 0000000..264db66 --- /dev/null +++ b/src/chain.rs @@ -0,0 +1,35 @@ +//! SHA-256 hashed DAG chain, ported from dodrenett's lib/challenge.ts. +//! Kept deliberately plain: every submission hashes in its parent(s), but +//! nothing here produces more than one parent yet - that only starts +//! mattering once something (an LLM, or a human) posts follow-up +//! questions back into a visitor's chain instead of the fixed content in +//! ../portal-content. The DAG-shaped `parent_hashes` field exists so that +//! can slot in later without a schema change; the hashing itself doesn't +//! need to. +#![cfg(feature = "ssr")] + +use sha2::{Digest, Sha256}; + +/// Hashes one chain node from its question, its parent hash(es), the +/// submitted responses, and a timestamp. Parents are sorted first so the +/// hash doesn't depend on the order multiple parents happened to arrive +/// in. +pub fn hash_node( + question_id: &str, + parent_hashes: &[String], + responses: &serde_json::Value, + timestamp_ms: i64, +) -> String { + let mut parents = parent_hashes.to_vec(); + parents.sort(); + let payload = serde_json::json!({ + "questionId": question_id, + "parentHashes": parents, + "responses": responses, + "timestamp": timestamp_ms, + }); + let data = serde_json::to_vec(&payload).expect("json values always serialize"); + let mut hasher = Sha256::new(); + hasher.update(&data); + format!("{:x}", hasher.finalize()) +} diff --git a/src/content.rs b/src/content.rs new file mode 100644 index 0000000..212fa17 --- /dev/null +++ b/src/content.rs @@ -0,0 +1,168 @@ +use serde::{Deserialize, Serialize}; + +/// One page: a prompt plus the paths on from it. `id` doubles as the URL +/// path it's served at ("/" is the landing page). Loaded from a plain +/// YAML file per question in a content directory kept in its own git +/// repo (see ../portal-content) - editing content is a content-repo +/// commit, not a Rust rebuild. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Question { + pub id: String, + #[serde(default)] + pub route: Option<String>, + pub name: String, + #[serde(default)] + pub description: String, + /// Kanidm group required to view/submit this question - `None` means + /// open to anyone, matching every question today. Content-driven + /// on purpose: a gated page like "/review" is just a Question with + /// this set, not a bespoke Rust route. + #[serde(default)] + pub qualifies: Option<String>, + #[serde(default)] + pub alternatives: Vec<Alternative>, +} + +/// Whether `user` may view/submit `question`. `true` when the question +/// has no `qualifies` requirement. Mirrors `chat::is_authorized_for_room` +/// in cnats - same synchronous, I/O-free shape, same staleness tradeoff +/// (group membership is fixed at login, not re-checked live). +pub fn is_qualified(user: Option<&crate::auth::User>, question: &Question) -> bool { + match &question.qualifies { + None => true, + Some(group) => user.is_some_and(|u| u.groups.iter().any(|g| g == group)), + } +} + +/// One path through a question: a short pitch, an optional next question +/// to advance to on submit, and the form (via `features`) that collects +/// what's needed to get there. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Alternative { + pub name: String, + #[serde(default)] + pub description: String, + #[serde(default)] + pub action: Option<String>, + #[serde(default)] + pub consequence: Vec<String>, + #[serde(default)] + pub encouragements: Vec<String>, + #[serde(default)] + pub features: Vec<Feature>, + /// Names a NATS KV bucket to also durably store this submission + /// into, beyond the ephemeral NATS event every submission already + /// gets published as - just a bucket name, not a keyword the + /// runtime special-cases (see `src/answers.rs`). Content-driven + /// rather than matching on the alternative's name/copy, which would + /// silently break if the wording changes later. + #[serde(default)] + pub record_as: Option<String>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Feature { + pub name: String, + #[serde(default)] + pub description: String, + #[serde(default)] + pub requirements: Vec<Requirement>, + /// Live data this feature pulls in. Read-only unless `transitions` + /// is non-empty, in which case listed answers get one action button + /// per transition (see `src/resource.rs`, `src/answers.rs`). + #[serde(default)] + pub resource: Option<ResourceSpec>, +} + +/// Declares a NATS KV-backed live-data read. The bucket/key here are +/// only ever read server-side, from content the server itself loaded at +/// startup - `get_resource` (`src/resource.rs`) takes a question id + +/// feature name from the client, never a bucket/key directly, so a +/// visitor can never probe an arbitrary bucket by naming it. There is +/// no render-mode/"kind" tag here on purpose - what a resource displays +/// as follows from its data's own shape (a plain value, or a list of +/// `answers::Answer`s), not from a Rust-side special case per content +/// item. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ResourceSpec { + pub bucket: String, + /// A single item; omit to list the whole bucket. + #[serde(default)] + pub key: Option<String>, + /// Kanidm group required to read this resource. + #[serde(default)] + pub requires_group: Option<String>, + /// Must be explicitly set for an anonymous-readable resource - a + /// spec with neither this nor `requires_group` is unreachable by + /// design (fail closed, not fail open). Reads only - mutations + /// (`transitions` below) always require `requires_group` regardless + /// of this flag. + #[serde(default)] + pub public: bool, + /// States a listed answer may move to, and the button label for + /// each - empty means the resource is read-only. The allow-list a + /// `transition_answer` call is checked against, so a client can + /// never move an answer to a state content didn't declare. + #[serde(default)] + pub transitions: Vec<Transition>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Transition { + pub to: String, + pub label: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Requirement { + pub name: String, + #[serde(default)] + pub label: Option<String>, + #[serde(default)] + pub placeholder: Option<String>, + #[serde(default = "default_requirement_type", rename = "type")] + pub kind: String, + #[serde(default)] + pub optional: bool, + /// `type: file` only - accept multiple files. + #[serde(default)] + pub multiple: bool, + /// `type: file` only - HTML `accept` hint (UX only, not a security + /// boundary - the upload handler re-checks content-type itself). + #[serde(default)] + pub accept: Option<String>, +} + +fn default_requirement_type() -> String { + "text".to_string() +} + +impl Requirement { + pub fn display_label(&self) -> String { + self.label.clone().unwrap_or_else(|| self.name.clone()) + } +} + +/// Reads every `*.yaml` file in `dir` as a `Question`, keyed by its own +/// `id`. Runs once at startup; no hot-reload yet - restart the process +/// (or add a watcher later) to pick up content changes. +#[cfg(feature = "ssr")] +pub fn load_questions( + dir: &std::path::Path, +) -> anyhow::Result<std::collections::HashMap<String, Question>> { + let mut out = std::collections::HashMap::new(); + for entry in std::fs::read_dir(dir) + .map_err(|e| anyhow::anyhow!("reading content dir {}: {e}", dir.display()))? + { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("yaml") { + continue; + } + let raw = std::fs::read_to_string(&path)?; + let question: Question = serde_yaml::from_str(&raw) + .map_err(|e| anyhow::anyhow!("parsing {}: {e}", path.display()))?; + out.insert(question.id.clone(), question); + } + Ok(out) +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..364d2c3 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,30 @@ +//! Plain NATS event publishing - ported from dodrenett's lib/nats.ts +//! (AnswerSubmitted). Deliberately just a publish, no stream/consumer +//! bookkeeping here: whoever reads `portal.answers.submitted` today is a +//! person watching the subject; a future LLM-driven follow-up poster +//! reads the exact same subject and shape, so this doesn't need to +//! change shape when that shows up, only gain a second subscriber. +#![cfg(feature = "ssr")] + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AnswerSubmitted { + pub chain_hash: String, + pub parent_hashes: Vec<String>, + pub question_id: String, + pub alternative: String, + pub responses: serde_json::Value, + pub timestamp_ms: i64, +} + +pub const ANSWERS_SUBJECT: &str = "portal.answers.submitted"; + +pub async fn emit_answer_submitted( + nats: &async_nats::Client, + payload: &AnswerSubmitted, +) -> anyhow::Result<()> { + let data = serde_json::to_vec(payload)?; + nats.publish(ANSWERS_SUBJECT, data.into()).await?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d3e214f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,20 @@ +pub mod answers; +pub mod app; +pub mod auth; +pub mod chain; +pub mod content; +pub mod events; +pub mod resource; + +#[cfg(feature = "ssr")] +pub mod server; + +#[cfg(feature = "ssr")] +pub mod upload; + +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + console_error_panic_hook::set_once(); + leptos::mount::hydrate_body(app::App); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8491bb0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,144 @@ +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() -> anyhow::Result<()> { + use axum::{body::Body, extract::State, http::Request, response::IntoResponse, routing::{any, get, post}, Router}; + use leptos::prelude::*; + use leptos_axum::{generate_route_list, LeptosRoutes}; + use portal::app::{shell, App}; + use portal::content; + use portal::server::{oidc, AppState}; + use portal::upload::{self, Garage}; + use std::sync::Arc; + use tower_http::services::{ServeDir, ServeFile}; + use tower_sessions::{MemoryStore, SessionManagerLayer}; + + dotenvy::dotenv().ok(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info,portal=debug".into()), + ) + .init(); + + let conf = get_configuration(None)?; + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(App); + + let content_dir = std::env::var("CONTENT_DIR") + .unwrap_or_else(|_| "../portal-content/questions".to_string()); + let questions = content::load_questions(std::path::Path::new(&content_dir))?; + tracing::info!(count = questions.len(), dir = %content_dir, "loaded content"); + + let nats_url = + std::env::var("NATS_URL").unwrap_or_else(|_| "nats://127.0.0.1:4222".to_string()); + tracing::info!(%nats_url, "connecting to NATS"); + let parsed = url::Url::parse(&nats_url)?; + let mut nats_opts = async_nats::ConnectOptions::new(); + if !parsed.username().is_empty() { + nats_opts = nats_opts.user_and_password( + parsed.username().to_string(), + parsed.password().unwrap_or_default().to_string(), + ); + } + let nats = nats_opts.connect(&nats_url).await?; + + // No fixed, pre-declared list of buckets to initialize here - content + // names whatever bucket an alternative's `record_as` should write + // into, and `answers::store_answer` creates it on first use. + let jetstream = async_nats::jetstream::new(nats.clone()); + + let oidc_state = Arc::new(oidc::Oidc::from_env().await?); + + let garage = Garage::from_env(); + if garage.is_none() { + tracing::warn!("GARAGE_* env vars not set - file upload fields will fail closed"); + } + + let state = AppState { + leptos_options: leptos_options.clone(), + nats, + jetstream, + questions: std::sync::Arc::new(questions), + oidc: oidc_state, + garage, + }; + + // Dev-friendly defaults: in-memory sessions, secure cookies only when + // COOKIE_SECURE=true (set it behind TLS in production) - same + // defaults cnats uses. SameSite=Lax (tower-sessions defaults to + // Strict) because the OIDC callback is, by definition, a top-level + // GET arriving via a redirect *from* the IdP's origin - Strict + // withholds the cookie on exactly that request, breaking login + // whenever the app and Kanidm aren't under the same registrable + // domain (e.g. this app on 127.0.0.1 during local dev vs. Kanidm's + // real domain). Lax is the standard, correct setting for this + // pattern, not a workaround. + let cookie_secure = std::env::var("COOKIE_SECURE") + .map(|v| v == "true" || v == "1") + .unwrap_or(false); + let session_layer = SessionManagerLayer::new(MemoryStore::default()) + .with_secure(cookie_secure) + .with_same_site(tower_sessions::cookie::SameSite::Lax) + .with_name("portal_session"); + + async fn server_fn_handler( + State(state): State<AppState>, + request: Request<Body>, + ) -> impl IntoResponse { + leptos_axum::handle_server_fns_with_context( + move || provide_context(state.clone()), + request, + ) + .await + } + + // The app's own Leptos route list includes a root-level wildcard + // (`/*any`, since a question's `id` doubles as its URL path), which + // would otherwise swallow every request - including these static + // assets - before `.fallback()` ever got a chance to serve them. + // Registering them explicitly here lets axum's route matching pick + // the more specific match over the wildcard. + let pkg_dir = format!( + "{}/{}", + leptos_options.site_root, leptos_options.site_pkg_dir + ); + let favicon_path = format!("{}/favicon.svg", leptos_options.site_root); + let fonts_dir = format!("{}/fonts", leptos_options.site_root); + + let app = Router::new() + .route("/auth/login", get(oidc::login)) + .route("/auth/callback", get(oidc::callback)) + .route("/auth/logout", get(oidc::logout)) + .route("/api/{*fn_name}", any(server_fn_handler)) + .route("/upload", post(upload::upload)) + .nest_service("/pkg", ServeDir::new(pkg_dir)) + .nest_service("/fonts", ServeDir::new(fonts_dir)) + .route_service("/favicon.svg", ServeFile::new(favicon_path)) + .leptos_routes_with_context( + &state, + routes, + { + let state = state.clone(); + move || provide_context(state.clone()) + }, + { + let leptos_options = leptos_options.clone(); + move || shell(leptos_options.clone()) + }, + ) + .fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell)) + .layer(session_layer) + .with_state(state); + + tracing::info!("listening on http://{addr}"); + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app.into_make_service()).await?; + Ok(()) +} + +#[cfg(not(feature = "ssr"))] +fn main() { + // The browser build is a cdylib; this stub only exists so `cargo check` + // without features still succeeds. +} diff --git a/src/resource.rs b/src/resource.rs new file mode 100644 index 0000000..5ed242e --- /dev/null +++ b/src/resource.rs @@ -0,0 +1,89 @@ +//! Generic, authorized reads of NATS KV data declared in content +//! (`content::ResourceSpec`, on a `Feature`). The one property that +//! matters: the bucket/key a resource reads is never a client-supplied +//! parameter, only something the server resolves from its own +//! trusted, YAML-loaded content - the client only ever names a +//! question id + feature name, so it can't probe an arbitrary bucket +//! by just naming it. + +use leptos::prelude::*; + +/// Fetches the live data for `question_id`'s `feature_name` feature. +/// Fails closed: a resource with neither `public: true` nor +/// `requires_group` set is unreachable, not "open" by omission. +#[server] +pub async fn get_resource( + question_id: String, + feature_name: String, +) -> Result<serde_json::Value, ServerFnError> { + use crate::auth::{User, SESSION_USER_KEY}; + use crate::server::AppState; + + let state = expect_context::<AppState>(); + let question = state + .questions + .get(&question_id) + .ok_or_else(|| ServerFnError::new("unknown question"))?; + let feature = question + .alternatives + .iter() + .flat_map(|a| &a.features) + .find(|f| f.name == feature_name) + .ok_or_else(|| ServerFnError::new("unknown feature"))?; + let resource = feature + .resource + .as_ref() + .ok_or_else(|| ServerFnError::new("feature has no resource"))?; + + if !resource.public { + let group = resource + .requires_group + .as_deref() + .ok_or_else(|| ServerFnError::new("resource is not accessible"))?; + let session: tower_sessions::Session = leptos_axum::extract().await?; + let user = session + .get::<User>(SESSION_USER_KEY) + .await + .map_err(|e| ServerFnError::new(e.to_string()))? + .ok_or_else(|| ServerFnError::new("not signed in"))?; + if !user.groups.iter().any(|g| g == group) { + return Err(ServerFnError::new("not authorized")); + } + } + + let store = state + .jetstream + .get_key_value(&resource.bucket) + .await + .map_err(|e| ServerFnError::new(format!("resource bucket unavailable: {e}")))?; + + match &resource.key { + Some(key) => { + let bytes = store + .get(key) + .await + .map_err(|e| ServerFnError::new(e.to_string()))? + .ok_or_else(|| ServerFnError::new("resource key not found"))?; + serde_json::from_slice(&bytes).map_err(|e| ServerFnError::new(e.to_string())) + } + None => { + use futures::TryStreamExt; + let keys: Vec<String> = store + .keys() + .await + .map_err(|e| ServerFnError::new(e.to_string()))? + .try_collect() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + let mut items = Vec::new(); + for key in keys { + if let Ok(Some(bytes)) = store.get(&key).await { + if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes) { + items.push(value); + } + } + } + Ok(serde_json::Value::Array(items)) + } + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..78eea50 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,30 @@ +#![cfg(feature = "ssr")] + +pub mod oidc; + +use axum::extract::FromRef; +use leptos::prelude::LeptosOptions; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::content::Question; + +#[derive(Clone)] +pub struct AppState { + pub leptos_options: LeptosOptions, + pub nats: async_nats::Client, + /// JetStream context - source of every NATS KV bucket this app + /// reads/writes (applicants, and whatever a `ResourceSpec` names). + pub jetstream: async_nats::jetstream::Context, + pub questions: Arc<HashMap<String, Question>>, + pub oidc: Arc<oidc::Oidc>, + /// `None` when `GARAGE_*` env vars aren't set - uploads are the one + /// optional feature, everything else works without Garage. + pub garage: Option<crate::upload::Garage>, +} + +impl FromRef<AppState> for LeptosOptions { + fn from_ref(state: &AppState) -> Self { + state.leptos_options.clone() + } +} diff --git a/src/server/oidc.rs b/src/server/oidc.rs new file mode 100644 index 0000000..5ba4339 --- /dev/null +++ b/src/server/oidc.rs @@ -0,0 +1,251 @@ +//! OIDC authorization-code + PKCE flow against Kanidm. Ported from +//! cnats' `server/oidc.rs` - same provider, same flow, unchanged. +//! +//! Kanidm serves per-client OIDC discovery documents at +//! `<KANIDM_URL>/oauth2/openid/<client_id>/.well-known/openid-configuration`, +//! and enforces PKCE, so this module always sends a S256 challenge. + +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::Redirect, +}; +use openidconnect::{ + core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata}, + AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndpointMaybeSet, EndpointNotSet, + EndpointSet, IssuerUrl, Nonce, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, + TokenResponse, +}; +use serde::Deserialize; +use tower_sessions::Session; + +use crate::auth::{User, SESSION_USER_KEY}; + +use super::AppState; + +type OidcClient = CoreClient< + EndpointSet, // auth endpoint + EndpointNotSet, // device auth + EndpointNotSet, // introspection + EndpointNotSet, // revocation + EndpointMaybeSet, // token endpoint (from discovery) + EndpointMaybeSet, // userinfo endpoint (from discovery) +>; + +pub struct Oidc { + client: OidcClient, + http: openidconnect::reqwest::Client, +} + +const PKCE_KEY: &str = "oidc_pkce_verifier"; +const CSRF_KEY: &str = "oidc_csrf_state"; +const NONCE_KEY: &str = "oidc_nonce"; + +impl Oidc { + /// Discovers the provider and builds the client from environment: + /// `KANIDM_URL`, `OAUTH2_CLIENT_ID`, `OAUTH2_CLIENT_SECRET`, `PUBLIC_URL`. + pub async fn from_env() -> anyhow::Result<Self> { + let kanidm_url = require_env("KANIDM_URL")?; + let client_id = require_env("OAUTH2_CLIENT_ID")?; + let client_secret = require_env("OAUTH2_CLIENT_SECRET")?; + let public_url = require_env("PUBLIC_URL")?; + + let issuer = IssuerUrl::new(format!( + "{}/oauth2/openid/{}", + kanidm_url.trim_end_matches('/'), + client_id + ))?; + let redirect = RedirectUrl::new(format!( + "{}/auth/callback", + public_url.trim_end_matches('/') + ))?; + + // Never follow redirects when talking to the IdP (SSRF hygiene). + let http = openidconnect::reqwest::ClientBuilder::new() + .redirect(openidconnect::reqwest::redirect::Policy::none()) + .build()?; + + tracing::info!(issuer = %issuer.as_str(), "discovering OIDC provider"); + let metadata = CoreProviderMetadata::discover_async(issuer, &http).await?; + let client = CoreClient::from_provider_metadata( + metadata, + ClientId::new(client_id), + Some(ClientSecret::new(client_secret)), + ) + .set_redirect_uri(redirect); + + Ok(Self { client, http }) + } +} + +fn require_env(name: &str) -> anyhow::Result<String> { + std::env::var(name).map_err(|_| anyhow::anyhow!("missing required env var {name}")) +} + +type HandlerError = (StatusCode, String); + +fn internal(err: impl std::fmt::Display) -> HandlerError { + tracing::error!("oidc error: {err}"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "authentication failed; see server logs".to_string(), + ) +} + +/// GET /auth/login — stash PKCE/state/nonce in the session and bounce to Kanidm. +pub async fn login(State(state): State<AppState>, session: Session) -> Result<Redirect, HandlerError> { + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + let (auth_url, csrf_state, nonce) = state + .oidc + .client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scope(Scope::new("openid".to_string())) + .add_scope(Scope::new("profile".to_string())) + .add_scope(Scope::new("email".to_string())) + .set_pkce_challenge(pkce_challenge) + .url(); + + session + .insert(PKCE_KEY, pkce_verifier.secret()) + .await + .map_err(internal)?; + session + .insert(CSRF_KEY, csrf_state.secret()) + .await + .map_err(internal)?; + session + .insert(NONCE_KEY, nonce.secret()) + .await + .map_err(internal)?; + + Ok(Redirect::to(auth_url.as_str())) +} + +#[derive(Deserialize)] +pub struct CallbackParams { + code: String, + state: String, +} + +/// GET /auth/callback — verify state, exchange the code, verify the ID token, +/// and store the user in the session. +pub async fn callback( + State(state): State<AppState>, + session: Session, + Query(params): Query<CallbackParams>, +) -> Result<Redirect, HandlerError> { + let stored_csrf: Option<String> = session.remove(CSRF_KEY).await.map_err(internal)?; + let pkce_verifier: Option<String> = session.remove(PKCE_KEY).await.map_err(internal)?; + let nonce: Option<String> = session.remove(NONCE_KEY).await.map_err(internal)?; + + let (Some(stored_csrf), Some(pkce_verifier), Some(nonce)) = + (stored_csrf, pkce_verifier, nonce) + else { + return Err(( + StatusCode::BAD_REQUEST, + "no login in progress; start again at /auth/login".to_string(), + )); + }; + if params.state != stored_csrf { + return Err(( + StatusCode::BAD_REQUEST, + "state mismatch; start again at /auth/login".to_string(), + )); + } + + let oidc = &state.oidc; + let token_response = oidc + .client + .exchange_code(AuthorizationCode::new(params.code)) + .map_err(internal)? + .set_pkce_verifier(PkceCodeVerifier::new(pkce_verifier)) + .request_async(&oidc.http) + .await + .map_err(internal)?; + + let id_token = token_response + .id_token() + .ok_or_else(|| internal("provider returned no ID token"))?; + let claims = id_token + .claims(&oidc.client.id_token_verifier(), &Nonce::new(nonce)) + .map_err(internal)?; + + let username = claims + .preferred_username() + .map(|u| u.as_str().to_string()) + .or_else(|| claims.email().map(|e| e.as_str().to_string())) + .unwrap_or_else(|| claims.subject().as_str().to_string()); + let display_name = claims + .name() + .and_then(|n| n.get(None)) + .map(|n| n.as_str().to_string()) + .unwrap_or_else(|| username.clone()); + + // `groups` is a custom claim (Kanidm `oauth2 update-claim-map`), not + // something the Core* typed claims struct above knows about. The + // signature is already verified by `id_token.claims(...)` above, so + // re-reading the same payload's raw JSON for one more field is safe - + // just a plain field extraction, not a second verification step. + // IdToken's Serialize impl (not Display - it has none) produces the + // raw compact JWT string "header.payload.signature". + let groups = extract_groups_claim(&id_token); + + let user = User { + sub: claims.subject().as_str().to_string(), + username, + display_name, + groups, + }; + + // Rotate the session id on privilege change, then store the user. + session.cycle_id().await.map_err(internal)?; + session + .insert(SESSION_USER_KEY, &user) + .await + .map_err(internal)?; + + tracing::info!(user = %user.username, "signed in"); + Ok(Redirect::to("/")) +} + +/// GET /auth/logout — drop the session. +pub async fn logout(session: Session) -> Result<Redirect, HandlerError> { + session.flush().await.map_err(internal)?; + Ok(Redirect::to("/")) +} + +/// Pulls the `groups` custom claim (Kanidm `oauth2 update-claim-map`) out +/// of an ID token's raw JWT payload. `IdToken`'s `Serialize` impl (it has +/// no `Display`) produces the compact "header.payload.signature" string, +/// which is where this reads from - the signature itself is never +/// re-checked here, that already happened via `id_token.claims(...)` +/// before this is called. Defensive by design: any parse failure (no +/// claim, wrong shape) just yields no groups rather than failing login. +fn extract_groups_claim<T: serde::Serialize>(id_token: &T) -> Vec<String> { + use base64::Engine; + + let Ok(serde_json::Value::String(compact)) = serde_json::to_value(id_token) else { + return Vec::new(); + }; + let Some(payload_b64) = compact.split('.').nth(1) else { + return Vec::new(); + }; + let Ok(payload_bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload_b64) + else { + return Vec::new(); + }; + let Ok(payload) = serde_json::from_slice::<serde_json::Value>(&payload_bytes) else { + return Vec::new(); + }; + + payload + .get("groups") + .and_then(|g| g.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()) + .unwrap_or_default() +} diff --git a/src/upload.rs b/src/upload.rs new file mode 100644 index 0000000..05f468a --- /dev/null +++ b/src/upload.rs @@ -0,0 +1,113 @@ +//! Plain multipart upload → Garage (S3-compatible), for `type: file` +//! requirement fields. A raw Axum handler, not a Leptos server fn - +//! those aren't built for binary bodies. The stored object key becomes +//! that field's value in `responses_json` (`app.rs`'s file-input +//! widget), so nothing about `submit_answer`'s generic +//! `Map<String, String>` flow needs to know a file was involved. +#![cfg(feature = "ssr")] + +use aws_sdk_s3::config::{BehaviorVersion, Credentials, Region}; +use aws_sdk_s3::primitives::ByteStream; +use axum::extract::{Multipart, State}; +use axum::http::StatusCode; +use axum::Json; +use serde::Serialize; + +use crate::server::AppState; + +const MAX_UPLOAD_BYTES: usize = 20 * 1024 * 1024; + +#[derive(Clone)] +pub struct Garage { + client: aws_sdk_s3::Client, + bucket: String, +} + +impl Garage { + /// Builds a client from `GARAGE_S3_ENDPOINT`/`GARAGE_ACCESS_KEY`/ + /// `GARAGE_SECRET_KEY`/`GARAGE_UPLOADS_BUCKET` - same secret names + /// dodrenett's own Woodpecker pipeline already uses. `None` (not an + /// error) when unset, since uploads are optional: everything else + /// works without Garage configured. + pub fn from_env() -> Option<Self> { + let endpoint = std::env::var("GARAGE_S3_ENDPOINT") + .unwrap_or_else(|_| "http://garage:3900".to_string()); + let access_key = std::env::var("GARAGE_ACCESS_KEY").ok()?; + let secret_key = std::env::var("GARAGE_SECRET_KEY").ok()?; + let bucket = std::env::var("GARAGE_UPLOADS_BUCKET") + .unwrap_or_else(|_| "portal-attachments".to_string()); + + let credentials = Credentials::new(access_key, secret_key, None, None, "portal"); + let config = aws_sdk_s3::Config::builder() + .behavior_version(BehaviorVersion::latest()) + .region(Region::new("garage")) + .endpoint_url(endpoint) + .credentials_provider(credentials) + // Garage expects path-style bucket addressing, not the + // virtual-hosted `bucket.host` style AWS defaults to. + .force_path_style(true) + .build(); + Some(Self { + client: aws_sdk_s3::Client::from_conf(config), + bucket, + }) + } +} + +#[derive(Serialize)] +pub struct UploadResponse { + key: String, +} + +/// POST /upload - streams the first `file` field straight through to +/// Garage and hands back its object key. Server-side hygiene beyond the +/// HTML `accept` hint (a UX-only signal, not a boundary): a hard size +/// cap, and the upload is rejected outright if Garage isn't configured +/// rather than silently succeeding nowhere. +pub async fn upload( + State(state): State<AppState>, + mut multipart: Multipart, +) -> Result<Json<UploadResponse>, (StatusCode, String)> { + let garage = state.garage.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + "uploads are not configured".to_string(), + ))?; + + while let Some(field) = multipart + .next_field() + .await + .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))? + { + if field.name() != Some("file") { + continue; + } + let content_type = field + .content_type() + .unwrap_or("application/octet-stream") + .to_string(); + let filename = field.file_name().unwrap_or("upload").to_string(); + let bytes = field + .bytes() + .await + .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?; + if bytes.len() > MAX_UPLOAD_BYTES { + return Err((StatusCode::PAYLOAD_TOO_LARGE, "file too large".to_string())); + } + + let key = format!("{}-{filename}", uuid::Uuid::new_v4()); + garage + .client + .put_object() + .bucket(&garage.bucket) + .key(&key) + .body(ByteStream::from(bytes)) + .content_type(content_type) + .send() + .await + .map_err(|e| (StatusCode::BAD_GATEWAY, format!("upload failed: {e}")))?; + + return Ok(Json(UploadResponse { key })); + } + + Err((StatusCode::BAD_REQUEST, "no file field in request".to_string())) +} diff --git a/style/main.css b/style/main.css new file mode 100644 index 0000000..83cd9b0 --- /dev/null +++ b/style/main.css @@ -0,0 +1,327 @@ +/* Same self-hosted Quicksand used for headings on Gitea/the homepage + dashboard elsewhere in this stack (see + ~/repos/infrastructure/gitea-custom-header.tmpl) - reused as-is + (same files, same unicode-range split) rather than pulled from a + font CDN. */ +@font-face { + font-family: "Quicksand"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/fonts/quicksand-semibold-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "Quicksand"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/fonts/quicksand-semibold-latin-ext.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: "Quicksand"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/fonts/quicksand-semibold-vietnamese.woff2") format("woff2"); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} + +:root { + /* the one brand color - everything else is neutral. Tuned for + legibility against near-black, distinct from the YES canvas's own + cyan/magenta/yellow so UI chrome never fights with it. */ + --accent: #f0923c; + --accent-soft: rgba(240, 146, 60, 0.16); + + --paper: #0a0a0a; + --paper-raised: #161616; + --ink: #ededed; + --ink-dim: #9a9a9a; + --line: #2a2a2a; + + --heading: "Quicksand", var(--sans); + --sans: -apple-system, "SF Pro Text", ui-sans-serif, "Segoe UI", system-ui, sans-serif; + + --radius: 1.1rem; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + background: var(--paper); + color: var(--ink); + font-family: var(--sans); + font-size: 17px; + line-height: 1.55; +} + +h1, h2, h3 { + font-family: var(--heading); + font-weight: 600; + line-height: 1.2; + margin: 0 0 0.4em; +} + +p { margin: 0 0 0.9em; } + +a { color: var(--accent); } + +::selection { background: var(--accent-soft); color: var(--ink); } + +main.loading, main.not-found { + min-height: 60vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + text-align: center; + color: var(--ink-dim); +} + +/* hero */ + +.hero { + max-width: 780px; + margin: 0 auto; + padding: 4rem 1.5rem 2.5rem; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 1rem; +} + +.wordmark { + font-family: var(--heading); + font-size: 0.85rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--ink-dim); + text-decoration: none; +} + +.hero-copy h1 { font-size: 2.1rem; } + +.hero-copy p { + color: var(--ink-dim); + font-size: 1.05rem; + max-width: 46ch; +} + +/* the actual "YES - Rasterized Lines" piece live at uhhm.no (see + public/yes.js) - landing page only. Full-viewport so it's a real + moment, not a thumbnail; hero-copy overlays near the bottom rather + than interrupting the canvas. */ + +.hero-yes { + position: relative; + max-width: none; + width: 100%; + height: 100svh; + padding: 0; + justify-content: flex-end; + overflow: hidden; +} + +.hero-canvas { + position: absolute; + inset: 0; +} + +.hero-canvas canvas { + position: absolute; + top: 0; + left: 0; + cursor: pointer; +} + +.hero-canvas #rasterCanvas { + mix-blend-mode: overlay; + opacity: 0.5; +} + +.hero-yes .hero-copy { + position: relative; + z-index: 1; + padding: 0 1.5rem 3.5rem; + gap: 0.6rem; + text-shadow: 0 2px 24px rgba(0, 0, 0, 0.8); +} + +/* alternatives */ + +.alternatives { + max-width: 780px; + margin: 0 auto 5rem; + padding: 0 1.5rem; + display: grid; + gap: 1.25rem; +} + +.alt-card { + background: var(--paper-raised); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 1.75rem 1.9rem; +} + +.alt-card h2 { font-size: 1.3rem; } + +.alt-description { color: var(--ink-dim); } + +.alt-encouragement { + font-size: 0.9rem; + color: var(--accent); + font-style: italic; +} + +.features { display: grid; gap: 1.1rem; margin-top: 0.6rem; } + +.feature h3 { font-size: 1rem; margin-bottom: 0.15em; } +.feature p { color: var(--ink-dim); font-size: 0.95rem; } + +.field { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-top: 0.6rem; +} + +.field-label { + font-size: 0.85rem; + font-weight: 600; + color: var(--ink-dim); +} + +.required { color: var(--accent); margin-left: 0.2em; } + +input, textarea { + font: inherit; + color: var(--ink); + background: var(--paper); + border: 1px solid var(--line); + border-radius: 0.6rem; + padding: 0.65rem 0.8rem; + width: 100%; +} + +textarea { min-height: 5rem; resize: vertical; } + +input:focus, textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} + +.alt-submit { + margin-top: 1.4rem; + font-family: var(--sans); + font-weight: 600; + font-size: 0.95rem; + color: #0a0a0a; + background: var(--accent); + border: none; + border-radius: 0.7rem; + padding: 0.75rem 1.4rem; + cursor: pointer; + transition: filter 120ms, transform 120ms; +} + +.alt-submit:hover { filter: brightness(1.08); } +.alt-submit:active { transform: translateY(1px); } +.alt-submit:disabled { filter: grayscale(0.5) brightness(0.85); cursor: wait; } + +.gate-card { text-align: center; } + +/* resources */ + +.resource { margin-top: 0.6rem; } + +.resource-loading, .resource-empty { + color: var(--ink-dim); + font-size: 0.9rem; +} + +.resource-error { color: #ff6b6b; font-size: 0.9rem; } + +.resource-raw { + background: var(--paper); + border: 1px solid var(--line); + border-radius: 0.6rem; + padding: 0.8rem; + font-size: 0.82rem; + overflow-x: auto; + white-space: pre-wrap; +} + +.answer-list { display: grid; gap: 0.8rem; } + +.answer-row { + border: 1px solid var(--line); + border-radius: 0.8rem; + padding: 1rem 1.1rem; + display: grid; + gap: 0.6rem; +} + +.answer-row.decided { opacity: 0.55; } + +.answer-fields { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 0.5rem 1rem; +} + +.answer-field { display: flex; flex-direction: column; gap: 0.1rem; } + +.answer-field-key { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ink-dim); +} + +.answer-field-value { font-size: 0.92rem; word-break: break-word; } + +.answer-meta { + display: flex; + gap: 0.8rem; + align-items: center; + font-size: 0.78rem; + color: var(--ink-dim); +} + +.answer-state { + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; + color: var(--accent); +} + +.answer-actions { display: flex; gap: 0.6rem; } + +.answer-action { + font-family: var(--sans); + font-weight: 600; + font-size: 0.82rem; + color: var(--ink); + background: var(--paper); + border: 1px solid var(--line); + border-radius: 0.5rem; + padding: 0.4rem 0.8rem; + cursor: pointer; + transition: border-color 120ms, color 120ms; +} + +.answer-action:hover { border-color: var(--accent); color: var(--accent); } +.answer-action:disabled { opacity: 0.5; cursor: wait; } + +@media (max-width: 640px) { + .hero { padding: 3rem 1.25rem 2rem; } + .alt-card { padding: 1.4rem 1.3rem; } +} diff --git a/yes.js b/yes.js new file mode 100644 index 0000000..4a02419 --- /dev/null +++ b/yes.js @@ -0,0 +1,438 @@ +// Ported from ~/repos/webpage/content/visualize/ah.html ("YES - Rasterized +// Lines"), the piece currently live at uhhm.no - kept as the actual asset, +// not reinvented. Two changes from the original: exported as a class (no +// auto-init on `window load`, since Leptos controls when this mounts) and +// a `stop()` method that actually breaks the requestAnimationFrame loop - +// the original ran forever once started, fine for a static page that's +// the whole document, not fine in an SPA where this hero mounts/unmounts +// as you navigate. + +export class RasterizedYES { + constructor() { + this.rasterCanvas = document.getElementById('rasterCanvas'); + this.lineCanvas = document.getElementById('lineCanvas'); + this.rasterCtx = this.rasterCanvas.getContext('2d'); + this.lineCtx = this.lineCanvas.getContext('2d'); + + this.lines = []; + this.rasterData = null; + this.isActive = true; + this.destroyed = false; + this.time = 0; + + this.mouseX = 0.5; + this.mouseY = 0.5; + this.containmentStrength = 0.5; + this.wiggleAmount = 0.5; + + this.setupCanvas(); + this.setupMouseTracking(); + this.setupClickHandler(); + this.rasterizeText(); + this.initializeLines(); + this.animate(); + } + + stop() { + this.destroyed = true; + if (this._resizeHandler) { + window.removeEventListener('resize', this._resizeHandler); + } + } + + setupCanvas() { + const pixelRatio = window.devicePixelRatio || 1; + const width = window.innerWidth; + const height = window.innerHeight; + + this.rasterCanvas.style.width = width + 'px'; + this.rasterCanvas.style.height = height + 'px'; + this.lineCanvas.style.width = width + 'px'; + this.lineCanvas.style.height = height + 'px'; + + this.rasterCanvas.width = width * pixelRatio; + this.rasterCanvas.height = height * pixelRatio; + this.lineCanvas.width = width * pixelRatio; + this.lineCanvas.height = height * pixelRatio; + + this.rasterCtx.scale(pixelRatio, pixelRatio); + this.lineCtx.scale(pixelRatio, pixelRatio); + + this.displayWidth = width; + this.displayHeight = height; + this.pixelRatio = pixelRatio; + + if (!this._resizeHandler) { + this._resizeHandler = () => { + this.setupCanvas(); + this.rasterizeText(); + }; + window.addEventListener('resize', this._resizeHandler); + } + } + + setupMouseTracking() { + const updatePosition = (clientX, clientY) => { + this.mouseX = clientX / window.innerWidth; + this.mouseY = clientY / window.innerHeight; + this.containmentStrength = 0.1 + (this.mouseX * 0.9); + this.wiggleAmount = 0.1 + (this.mouseY * 1.9); + }; + + window.addEventListener('mousemove', (e) => { + updatePosition(e.clientX, e.clientY); + }); + + // Scoped to the canvas itself, not `window` - the original + // standalone page was the whole document, so preventDefault()ing + // touchmove globally was harmless (nothing else to scroll to). + // Embedded as a hero above a longer page, that same global + // handler silently blocks scrolling everywhere, not just over + // the canvas - this only intercepts touches that start there. + this.lineCanvas.addEventListener('touchmove', (e) => { + e.preventDefault(); + if (e.touches.length > 0) { + const touch = e.touches[0]; + updatePosition(touch.clientX, touch.clientY); + } + }, { passive: false }); + + this.lineCanvas.addEventListener('touchstart', (e) => { + e.preventDefault(); + if (e.touches.length > 0) { + const touch = e.touches[0]; + updatePosition(touch.clientX, touch.clientY); + } + }, { passive: false }); + + this.mouseX = 0.5; + this.mouseY = 0.5; + this.containmentStrength = 0.55; + this.wiggleAmount = 1.05; + } + + setupClickHandler() { + this.lineCanvas.addEventListener('click', () => { + this.restartAnimation(); + }); + } + + restartAnimation() { + this.time = 0; + this.lineCtx.fillStyle = '#0a0a0a'; + this.lineCtx.fillRect(0, 0, this.displayWidth, this.displayHeight); + this.initializeLines(); + this.isActive = true; + } + + rasterizeText() { + const width = this.displayWidth; + const height = this.displayHeight; + + const aspectRatio = width / height; + let fontSize; + if (aspectRatio < 1) { + fontSize = width * 0.4; + } else { + fontSize = height * 0.5; + } + + this.fontSize = fontSize; + + this.rasterCtx.font = `bold ${fontSize}px Arial, sans-serif`; + this.rasterCtx.textAlign = 'left'; + this.rasterCtx.textBaseline = 'middle'; + + const fullTextMetrics = this.rasterCtx.measureText('YES'); + const textWidth = fullTextMetrics.width; + const textStartX = (width - textWidth) / 2; + const textY = height / 2; + + const letters = ['Y', 'E', 'S']; + this.letterPositions = []; + let currentX = textStartX; + + for (let i = 0; i < letters.length; i++) { + const letterMetrics = this.rasterCtx.measureText(letters[i]); + this.letterPositions[i] = { + x: currentX, + y: textY, + width: letterMetrics.width, + centerX: currentX + letterMetrics.width / 2 + }; + currentX += letterMetrics.width; + } + + this.letterRasters = []; + + for (let i = 0; i < 3; i++) { + this.rasterCtx.fillStyle = '#111'; + this.rasterCtx.fillRect(0, 0, width, height); + + this.rasterCtx.fillStyle = '#ffffff'; + this.rasterCtx.fillText(letters[i], this.letterPositions[i].x, this.letterPositions[i].y); + + this.letterRasters[i] = this.rasterCtx.getImageData(0, 0, this.rasterCanvas.width, this.rasterCanvas.height); + } + + this.rasterCtx.fillStyle = '#111'; + this.rasterCtx.fillRect(0, 0, width, height); + this.rasterCtx.fillStyle = '#ffffff'; + this.rasterCtx.fillText('YES', textStartX, textY); + + this.rasterData = this.rasterCtx.getImageData(0, 0, this.rasterCanvas.width, this.rasterCanvas.height); + } + + isInSpecificLetter(x, y, letterIndex) { + const canvasX = x * this.pixelRatio; + const canvasY = y * this.pixelRatio; + + if (!this.letterRasters || !this.letterRasters[letterIndex] || + canvasX < 0 || canvasY < 0 || + canvasX >= this.rasterCanvas.width || canvasY >= this.rasterCanvas.height) { + return false; + } + + const index = (Math.floor(canvasY) * this.rasterCanvas.width + Math.floor(canvasX)) * 4; + const red = this.letterRasters[letterIndex].data[index]; + return red > 128; + } + + initializeLines() { + this.lines = []; + const numLines = 60; + + const letterCentroids = this.calculateLetterCentroids(); + + for (let i = 0; i < numLines; i++) { + const letterIndex = Math.floor(i / (numLines / 3)); + let startX, startY, centerX, centerY; + + if (letterCentroids[letterIndex]) { + centerX = letterCentroids[letterIndex].x; + centerY = letterCentroids[letterIndex].y; + + const startVariation = this.fontSize * 0.1; + startX = centerX + (Math.random() - 0.5) * startVariation; + startY = centerY + (Math.random() - 0.5) * startVariation; + + if (!this.isInSpecificLetter(startX, startY, letterIndex)) { + const nearestPoint = this.findNearestSpecificLetterPixel(startX, startY, letterIndex); + if (nearestPoint) { + startX = nearestPoint.x; + startY = nearestPoint.y; + } else { + startX = centerX; + startY = centerY; + } + } + } else { + const letterPos = this.letterPositions[letterIndex]; + startX = letterPos.centerX; + startY = letterPos.y; + centerX = startX; + centerY = startY; + } + + const colors = [ + 'hsl(180, 90%, 70%)', + 'hsl(300, 90%, 70%)', + 'hsl(60, 90%, 70%)' + ]; + + this.lines.push({ + relativeX: (startX - centerX) / this.fontSize, + relativeY: (startY - centerY) / this.fontSize, + prevRelativeX: (startX - centerX) / this.fontSize, + prevRelativeY: (startY - centerY) / this.fontSize, + angle: Math.random() * Math.PI * 2, + color: colors[letterIndex], + letterIndex: letterIndex, + lastSeenInside: { x: (startX - centerX) / this.fontSize, y: (startY - centerY) / this.fontSize }, + outsideDuration: 0 + }); + } + } + + calculateLetterCentroids() { + const centroids = []; + + for (let letterIndex = 0; letterIndex < 3; letterIndex++) { + let sumX = 0, sumY = 0, count = 0; + + const letterPos = this.letterPositions[letterIndex]; + const searchStartX = Math.max(0, letterPos.x - this.fontSize * 0.1); + const searchEndX = Math.min(this.displayWidth, letterPos.x + letterPos.width + this.fontSize * 0.1); + const searchStartY = Math.max(0, letterPos.y - this.fontSize * 0.6); + const searchEndY = Math.min(this.displayHeight, letterPos.y + this.fontSize * 0.6); + + const step = Math.max(1, Math.floor(this.fontSize * 0.02)); + for (let y = searchStartY; y <= searchEndY; y += step) { + for (let x = searchStartX; x <= searchEndX; x += step) { + if (this.isInSpecificLetter(x, y, letterIndex)) { + sumX += x; + sumY += y; + count++; + } + } + } + + if (count > 0) { + centroids[letterIndex] = { + x: sumX / count, + y: sumY / count + }; + } else { + centroids[letterIndex] = { + x: letterPos.centerX, + y: letterPos.y + }; + } + } + + return centroids; + } + + updateLines() { + this.time += 0.016; + if (!this.isActive) return; + + const letterCentroids = this.calculateLetterCentroids(); + + this.lines.forEach(line => { + line.prevRelativeX = line.relativeX; + line.prevRelativeY = line.relativeY; + + const centroid = letterCentroids[line.letterIndex]; + if (!centroid) return; + + const currentX = centroid.x + line.relativeX * this.fontSize; + const currentY = centroid.y + line.relativeY * this.fontSize; + + const currentlyInside = this.isInSpecificLetter(currentX, currentY, line.letterIndex); + + if (currentlyInside) { + line.outsideDuration = 0; + line.lastSeenInside = { x: line.relativeX, y: line.relativeY }; + } else { + line.outsideDuration++; + } + + const visionDistance = 0.08 * this.fontSize; + const centerX = currentX + Math.cos(line.angle) * visionDistance; + const centerY = currentY + Math.sin(line.angle) * visionDistance; + const leftX = currentX + Math.cos(line.angle - 0.4) * visionDistance; + const leftY = currentY + Math.sin(line.angle - 0.4) * visionDistance; + const rightX = currentX + Math.cos(line.angle + 0.4) * visionDistance; + const rightY = currentY + Math.sin(line.angle + 0.4) * visionDistance; + + const centerSees = this.isInSpecificLetter(centerX, centerY, line.letterIndex); + const leftSees = this.isInSpecificLetter(leftX, leftY, line.letterIndex); + const rightSees = this.isInSpecificLetter(rightX, rightY, line.letterIndex); + + let speed = 0.02; + + const attractionThreshold = Math.floor(15 + (1 - this.containmentStrength) * 45); + + if (line.outsideDuration > attractionThreshold) { + const targetX = line.lastSeenInside.x; + const targetY = line.lastSeenInside.y; + const deltaX = targetX - line.relativeX; + const deltaY = targetY - line.relativeY; + const angleToTarget = Math.atan2(deltaY, deltaX); + + let angleDiff = angleToTarget - line.angle; + while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; + while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; + + const baseAttraction = Math.min(0.4, line.outsideDuration / 80); + const attractionStrength = baseAttraction * this.containmentStrength; + line.angle += angleDiff * attractionStrength; + } + + if (centerSees) { + const baseWiggle = 0.15; + line.angle += (Math.random() - 0.5) * baseWiggle * this.wiggleAmount; + } else { + speed *= (0.3 + this.containmentStrength * 0.4); + + const baseTurnStrength = 0.3 + (this.containmentStrength * 0.4); + const randomTurnAmount = 0.2 * this.wiggleAmount; + + if (leftSees && !rightSees) { + line.angle -= baseTurnStrength + Math.random() * randomTurnAmount; + } else if (rightSees && !leftSees) { + line.angle += baseTurnStrength + Math.random() * randomTurnAmount; + } else { + const randomTurn = (Math.random() - 0.5) * (0.8 + this.wiggleAmount * 0.7); + line.angle += randomTurn; + } + } + + line.relativeX += Math.cos(line.angle) * speed; + line.relativeY += Math.sin(line.angle) * speed; + + line.relativeX = Math.max(-1.7, Math.min(1.7, line.relativeX)); + line.relativeY = Math.max(-1.7, Math.min(1.7, line.relativeY)); + }); + } + + findNearestSpecificLetterPixel(x, y, letterIndex) { + const searchRadius = this.fontSize * 0.08; + const step = Math.max(1, Math.floor(this.fontSize * 0.01)); + let nearestPoint = null; + let nearestDistance = Infinity; + + for (let dy = -searchRadius; dy <= searchRadius; dy += step) { + for (let dx = -searchRadius; dx <= searchRadius; dx += step) { + const testX = x + dx; + const testY = y + dy; + + if (this.isInSpecificLetter(testX, testY, letterIndex)) { + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < nearestDistance) { + nearestDistance = distance; + nearestPoint = { x: testX, y: testY }; + } + } + } + } + + return nearestPoint; + } + + draw() { + this.lineCtx.fillStyle = 'rgba(10, 10, 10, 0.03)'; + this.lineCtx.fillRect(0, 0, this.displayWidth, this.displayHeight); + + const letterCentroids = this.calculateLetterCentroids(); + + this.lines.forEach(line => { + const centroid = letterCentroids[line.letterIndex]; + if (!centroid) return; + + const currentX = centroid.x + line.relativeX * this.fontSize; + const currentY = centroid.y + line.relativeY * this.fontSize; + const prevX = centroid.x + line.prevRelativeX * this.fontSize; + const prevY = centroid.y + line.prevRelativeY * this.fontSize; + + if (prevX === currentX && prevY === currentY) return; + + this.lineCtx.strokeStyle = line.color.replace('70%)', '80%, 0.8)'); + this.lineCtx.lineWidth = this.fontSize * 0.003; + this.lineCtx.lineCap = 'round'; + + this.lineCtx.beginPath(); + this.lineCtx.moveTo(prevX, prevY); + this.lineCtx.lineTo(currentX, currentY); + this.lineCtx.stroke(); + }); + } + + animate() { + if (this.destroyed) return; + this.updateLines(); + this.draw(); + requestAnimationFrame(() => this.animate()); + } +}