From aa1a7fa572e317925b08b97d54caee3d2c1d8835 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Wed, 29 Jul 2026 19:38:40 +0200 Subject: [PATCH] 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. --- .gitignore | 1 + Cargo.lock | 5042 +++++++++++++++++ Cargo.toml | 111 + public/favicon.svg | 3 + .../fonts/quicksand-semibold-latin-ext.woff2 | Bin 0 -> 15320 bytes public/fonts/quicksand-semibold-latin.woff2 | Bin 0 -> 15864 bytes .../fonts/quicksand-semibold-vietnamese.woff2 | Bin 0 -> 5216 bytes src/answers.rs | 179 + src/app.rs | 862 +++ src/auth.rs | 30 + src/chain.rs | 35 + src/content.rs | 168 + src/events.rs | 30 + src/lib.rs | 20 + src/main.rs | 144 + src/resource.rs | 89 + src/server/mod.rs | 30 + src/server/oidc.rs | 251 + src/upload.rs | 113 + style/main.css | 327 ++ yes.js | 438 ++ 21 files changed, 7873 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 public/favicon.svg create mode 100644 public/fonts/quicksand-semibold-latin-ext.woff2 create mode 100644 public/fonts/quicksand-semibold-latin.woff2 create mode 100644 public/fonts/quicksand-semibold-vietnamese.woff2 create mode 100644 src/answers.rs create mode 100644 src/app.rs create mode 100644 src/auth.rs create mode 100644 src/chain.rs create mode 100644 src/content.rs create mode 100644 src/events.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/resource.rs create mode 100644 src/server/mod.rs create mode 100644 src/server/oidc.rs create mode 100644 src/upload.rs create mode 100644 style/main.css create mode 100644 yes.js 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 0000000000000000000000000000000000000000..c7abeeb979993ad0fe204e76ac96c3cb7b8bda43 GIT binary patch literal 15320 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 literal 0 HcmV?d00001 diff --git a/public/fonts/quicksand-semibold-latin.woff2 b/public/fonts/quicksand-semibold-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..df7256d3c88299b44c1074fab8a9b91882edd4de GIT binary patch literal 15864 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, + #[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()); + } +}