From fe4fd901dc00a310566bc1e6be8f9d803691c36d Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Thu, 25 Jun 2026 22:37:39 -0700 Subject: [PATCH] Add andref-ipfs-depot --- andref-ipfs-depot/.cargo/audit.toml | 15 + andref-ipfs-depot/Cargo.lock | 2123 ++++++++++++++++- andref-ipfs-depot/Cargo.toml | 22 +- andref-ipfs-depot/assets/app.css | 117 + andref-ipfs-depot/assets/app.js | 112 + andref-ipfs-depot/assets/index.html | 32 + andref-ipfs-depot/assets/invalid.html | 18 + andref-ipfs-depot/deny.toml | 24 +- andref-ipfs-depot/flake.lock | 94 + andref-ipfs-depot/flake.nix | 44 +- andref-ipfs-depot/src/assets.rs | 6 + andref-ipfs-depot/src/config.rs | 41 + andref-ipfs-depot/src/discord.rs | 73 + andref-ipfs-depot/src/ipfs.rs | 79 + andref-ipfs-depot/src/main.rs | 99 +- andref-ipfs-depot/src/state.rs | 26 + andref-ipfs-depot/src/tokens.rs | 134 ++ andref-ipfs-depot/src/web.rs | 164 ++ eight/per-domain/andref.app.nix | 11 + exports/whale/digests/andref-ipfs-depot.txt | 1 + flake-profiles/whale/flake.lock | 108 +- flake-profiles/whale/flake.nix | 4 + .../stage00/orion-system/main.jsonnet | 22 + milky-way/lib/andref-ipfs-depot.libsonnet | 164 ++ milky-way/lib/images.libsonnet | 7 + milky-way/lib/kubo.libsonnet | 41 +- secrets/k8s-config/k8s-secret-values.jsonnet | 6 +- venus/modules/nixos-darwin/sodium.nix | 8 + whale/outputs.nix | 32 + 29 files changed, 3590 insertions(+), 37 deletions(-) create mode 100644 andref-ipfs-depot/assets/app.css create mode 100644 andref-ipfs-depot/assets/app.js create mode 100644 andref-ipfs-depot/assets/index.html create mode 100644 andref-ipfs-depot/assets/invalid.html create mode 100644 andref-ipfs-depot/flake.lock create mode 100644 andref-ipfs-depot/src/assets.rs create mode 100644 andref-ipfs-depot/src/config.rs create mode 100644 andref-ipfs-depot/src/discord.rs create mode 100644 andref-ipfs-depot/src/ipfs.rs create mode 100644 andref-ipfs-depot/src/state.rs create mode 100644 andref-ipfs-depot/src/tokens.rs create mode 100644 andref-ipfs-depot/src/web.rs create mode 100644 exports/whale/digests/andref-ipfs-depot.txt create mode 100644 milky-way/lib/andref-ipfs-depot.libsonnet diff --git a/andref-ipfs-depot/.cargo/audit.toml b/andref-ipfs-depot/.cargo/audit.toml index d068ac3..3c027a3 100644 --- a/andref-ipfs-depot/.cargo/audit.toml +++ b/andref-ipfs-depot/.cargo/audit.toml @@ -2,3 +2,18 @@ [yanked] enabled = false # Warn for yanked crates in Cargo.lock (default: true) update_index = false # Auto-update the crates.io index (default: true) + +[advisories] +# All four are in rustls-webpki 0.102, pulled transitively only by serenity 0.12's websocket +# stack (tungstenite 0.21 -> tokio-rustls 0.25 -> rustls 0.22 -> rustls-webpki 0.102). reqwest +# (our actual upload path) already uses the patched rustls-webpki 0.103. The fixes exist only in +# 0.103, which serenity 0.12's pinned rustls 0.22 cannot use -- so these are unfixable here until +# serenity bumps rustls. They affect certificate/CRL validation on the gateway connection to +# Discord's own endpoint, not the file-upload path. Drop these when serenity ships a rustls-0.23 +# release. +ignore = [ + "RUSTSEC-2026-0049", + "RUSTSEC-2026-0098", + "RUSTSEC-2026-0099", + "RUSTSEC-2026-0104", +] diff --git a/andref-ipfs-depot/Cargo.lock b/andref-ipfs-depot/Cargo.lock index b3a9899..5bb1dd1 100644 --- a/andref-ipfs-depot/Cargo.lock +++ b/andref-ipfs-depot/Cargo.lock @@ -1,7 +1,2126 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "quick-start" +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "andref-ipfs-depot" version = "0.1.0" +dependencies = [ + "axum", + "reqwest", + "serde", + "serde_json", + "serenity", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" +dependencies = [ + "serde", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[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-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[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 = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-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", +] + +[[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", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls 0.23.41", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots 1.0.8", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[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 = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[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 = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "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 = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[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.41", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls 0.23.41", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[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 = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.41", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.8", +] + +[[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 = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[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 = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cow" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7bbbec7196bfde255ab54b65e34087c0849629280028238e67ee25d6a4b7da" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serenity" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bde37f42765dfdc34e2a039e0c84afbf79a3101c1941763b0beb816c2f17541" +dependencies = [ + "arrayvec", + "async-trait", + "base64", + "bitflags", + "bytes", + "flate2", + "futures", + "mime_guess", + "percent-encoding", + "reqwest", + "secrecy", + "serde", + "serde_cow", + "serde_json", + "time", + "tokio", + "tokio-tungstenite", + "tracing", + "typemap_rev", + "url", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "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 = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "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.41", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.6", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "typemap_rev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" + +[[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 = "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 = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +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 = "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", + "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.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[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 = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.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", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/andref-ipfs-depot/Cargo.toml b/andref-ipfs-depot/Cargo.toml index 3e2467c..f98f791 100644 --- a/andref-ipfs-depot/Cargo.toml +++ b/andref-ipfs-depot/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "quick-start" +name = "andref-ipfs-depot" version = "0.1.0" edition = "2021" license = "MIT" @@ -7,3 +7,23 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = { version = "0.8", features = ["multipart"] } +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "multipart", + "rustls-tls", +] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serenity = { version = "0.12", default-features = false, features = [ + "builder", + "client", + "gateway", + "http", + "model", + "rustls_backend", +] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +uuid = { version = "1", features = ["v4"] } diff --git a/andref-ipfs-depot/assets/app.css b/andref-ipfs-depot/assets/app.css new file mode 100644 index 0000000..5b59624 --- /dev/null +++ b/andref-ipfs-depot/assets/app.css @@ -0,0 +1,117 @@ +:root { + color-scheme: light dark; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + min-height: 100vh; + display: grid; + place-items: center; + font: 16px/1.5 system-ui, sans-serif; + background: #f4f4f5; + color: #18181b; +} +@media (prefers-color-scheme: dark) { + body { + background: #18181b; + color: #f4f4f5; + } +} +.card { + width: min(90vw, 28rem); + padding: 2rem; + border-radius: 0.75rem; + background: Canvas; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + text-align: center; +} +h1 { + margin: 0 0 0.25rem; + font-size: 1.4rem; +} +.hint { + margin: 0 0 1.5rem; + opacity: 0.6; + font-size: 0.85rem; +} +button { + font: inherit; + padding: 0.6rem 1.2rem; + margin: 0.25rem; + border: 1px solid #6366f1; + border-radius: 0.5rem; + background: #6366f1; + color: #fff; + cursor: pointer; +} +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} +#choose { + background: transparent; + color: #6366f1; +} +.filename { + display: block; + margin: 0.5rem 0; + font-size: 0.85rem; + opacity: 0.8; + word-break: break-all; +} +.status { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-top: 1rem; +} +.spinner { + width: 1rem; + height: 1rem; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +.progress { + height: 0.4rem; + margin-top: 0.75rem; + background: rgba(127, 127, 127, 0.25); + border-radius: 1rem; + overflow: hidden; +} +.progress-bar { + height: 100%; + width: 0; + background: #6366f1; + transition: width 0.15s ease; +} +.result { + margin-top: 1.25rem; + word-break: break-all; +} +.result a { + color: #6366f1; +} +.result img { + max-width: 100%; + margin-top: 0.75rem; + border-radius: 0.5rem; +} +.success { + color: #16a34a; +} +.error { + color: #dc2626; +} +.hidden { + display: none; +} diff --git a/andref-ipfs-depot/assets/app.js b/andref-ipfs-depot/assets/app.js new file mode 100644 index 0000000..fd3a805 --- /dev/null +++ b/andref-ipfs-depot/assets/app.js @@ -0,0 +1,112 @@ +// The upload token is the last path segment of /u/. +const token = location.pathname.split("/").pop(); + +const fileInput = document.getElementById("file"); +const choose = document.getElementById("choose"); +const submit = document.getElementById("submit"); +const filename = document.getElementById("filename"); +const statusBox = document.getElementById("status"); +const statusText = document.getElementById("status-text"); +const progressWrap = document.getElementById("progress-wrap"); +const progressBar = document.getElementById("progress-bar"); +const result = document.getElementById("result"); + +choose.addEventListener("click", () => fileInput.click()); + +fileInput.addEventListener("change", () => { + const f = fileInput.files[0]; + filename.textContent = f ? f.name : ""; + submit.disabled = !f; + result.className = "result hidden"; +}); + +submit.addEventListener("click", () => { + const file = fileInput.files[0]; + if (!file) return; + + submit.disabled = true; + choose.disabled = true; + result.className = "result hidden"; + showStatus("Uploading…"); + progressWrap.className = "progress"; + progressBar.style.width = "0"; + + const form = new FormData(); + form.append("file", file, file.name); + + // XMLHttpRequest (not fetch) gives real upload progress via upload.onprogress. + const xhr = new XMLHttpRequest(); + xhr.open("POST", `/api/upload/${token}`); + + xhr.upload.addEventListener("progress", (e) => { + if (!e.lengthComputable) return; + const pct = Math.round((e.loaded / e.total) * 100); + progressBar.style.width = pct + "%"; + statusText.textContent = `Uploading… ${pct}%`; + }); + + // Once the body is fully sent, the backend is hashing + pinning to IPFS. + xhr.upload.addEventListener("load", () => { + progressWrap.className = "progress hidden"; + showStatus("Pinning to IPFS…"); + }); + + xhr.addEventListener("load", () => { + hideStatus(); + choose.disabled = false; + if (xhr.status >= 200 && xhr.status < 300) { + const { url } = JSON.parse(xhr.responseText); + showResult(url); + } else { + submit.disabled = false; + showError(xhr.responseText || `Upload failed (${xhr.status})`); + } + }); + + xhr.addEventListener("error", () => { + hideStatus(); + choose.disabled = false; + submit.disabled = false; + showError("Network error"); + }); + + xhr.send(form); +}); + +function showStatus(text) { + statusText.textContent = text; + statusBox.className = "status"; +} + +function hideStatus() { + statusBox.className = "status hidden"; +} + +function showResult(url) { + result.className = "result success"; + const link = document.createElement("a"); + link.href = url; + link.textContent = url; + link.target = "_blank"; + link.rel = "noopener"; + const note = document.createElement("p"); + note.className = "hint"; + note.textContent = "Also posted to the channel."; + result.replaceChildren("Uploaded ✅", document.createElement("br"), link, note); + // Best-effort inline preview for images. + if (/\.(png|jpe?g|gif|webp|avif|svg)$/i.test(file_name())) { + const img = document.createElement("img"); + img.src = url; + img.alt = "preview"; + result.appendChild(img); + } +} + +function showError(msg) { + result.className = "result error"; + result.textContent = msg; +} + +function file_name() { + return fileInput.files[0] ? fileInput.files[0].name : ""; +} diff --git a/andref-ipfs-depot/assets/index.html b/andref-ipfs-depot/assets/index.html new file mode 100644 index 0000000..73a1abb --- /dev/null +++ b/andref-ipfs-depot/assets/index.html @@ -0,0 +1,32 @@ + + + + + + IPFS Depot + + + +
+

Upload to IPFS

+

This link is single-use and expires after 15 minutes.

+ + + + + + + + + + + +
+ + + diff --git a/andref-ipfs-depot/assets/invalid.html b/andref-ipfs-depot/assets/invalid.html new file mode 100644 index 0000000..4c625c9 --- /dev/null +++ b/andref-ipfs-depot/assets/invalid.html @@ -0,0 +1,18 @@ + + + + + + IPFS Depot + + + +
+

Link invalid or already used

+

+ Upload links are single-use and expire after 15 minutes. Run + /upload in Discord again to get a fresh one. +

+
+ + diff --git a/andref-ipfs-depot/deny.toml b/andref-ipfs-depot/deny.toml index ab17368..e23c22d 100644 --- a/andref-ipfs-depot/deny.toml +++ b/andref-ipfs-depot/deny.toml @@ -1,2 +1,24 @@ +# cargo-deny runs as part of `nix flake check` (the `*-deny` crane check). The dependency tree +# behind serenity + axum + reqwest(rustls) pulls in the usual permissive ecosystem licenses, so +# allow that standard set. ring declares its license only via a LICENSE file (no SPDX `license` +# field), so it needs a clarification (the hash is printed by cargo-deny if it ever drifts). [licenses] -allow = ["MIT"] +allow = [ + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "CC0-1.0", + "CDLA-Permissive-2.0", + "ISC", + "MIT", + "MPL-2.0", + "OpenSSL", + "Unicode-3.0", + "Zlib", +] +confidence-threshold = 0.9 + +[[licenses.clarify]] +name = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] diff --git a/andref-ipfs-depot/flake.lock b/andref-ipfs-depot/flake.lock new file mode 100644 index 0000000..209f0c0 --- /dev/null +++ b/andref-ipfs-depot/flake.lock @@ -0,0 +1,94 @@ +{ + "nodes": { + "advisory-db": { + "flake": false, + "locked": { + "lastModified": 1782213767, + "narHash": "sha256-di8OUljwKRpPOYzp4gR/YxX7cumWFyZHbRmZIg2ol7Y=", + "owner": "rustsec", + "repo": "advisory-db", + "rev": "49f543184c8992aaa3552f2730df0dbb7ec9d2fc", + "type": "github" + }, + "original": { + "owner": "rustsec", + "repo": "advisory-db", + "type": "github" + } + }, + "crane": { + "locked": { + "lastModified": 1781825982, + "narHash": "sha256-SlXKwIRIhrOSAcTjCB3ftPLzJWZStQIPS7J1FlZPnKk=", + "owner": "ipetkov", + "repo": "crane", + "rev": "469fd08d0bcf6926321fa973c6777fbc87785dd7", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1782175435, + "narHash": "sha256-EMzXKmnOtBQ2MnvpiNOm7E+kOMvdPrIKaeg52Tip2Uk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "89570f24e97e614aa34aa9ab1c927b6578a43775", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "advisory-db": "advisory-db", + "crane": "crane", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/andref-ipfs-depot/flake.nix b/andref-ipfs-depot/flake.nix index b3efe1c..a2d03ac 100644 --- a/andref-ipfs-depot/flake.nix +++ b/andref-ipfs-depot/flake.nix @@ -1,5 +1,5 @@ { - description = "Build a cargo project"; + description = "andref-ipfs-depot: Discord-gated IPFS upload depot"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; @@ -31,13 +31,29 @@ inherit (pkgs) lib; craneLib = crane.mkLib pkgs; - src = craneLib.cleanCargoSource ./.; + # Keep the embedded frontend assets (assets/*.html|css|js) in the build source -- + # cleanCargoSource strips non-Rust files and `include_str!("../assets/...")` would then + # fail to compile. + src = lib.cleanSourceWith { + src = ./.; + name = "source"; + filter = + path: type: + (builtins.match ".*/assets/.*" path != null) || (craneLib.filterCargoSources path type); + }; # Common arguments can be set here to avoid repeating them later commonArgs = { inherit src; strictDeps = true; + # reqwest's rustls TLS provider (aws-lc-rs) builds via cmake; ring's build script uses + # perl. Harmless if the resolved provider needs only one of them. + nativeBuildInputs = [ + pkgs.cmake + pkgs.perl + ]; + buildInputs = [ # Add additional build inputs here ] @@ -56,7 +72,7 @@ # Build the actual crate itself, reusing the dependency # artifacts from above. - my-crate = craneLib.buildPackage ( + andref-ipfs-depot = craneLib.buildPackage ( commonArgs // { inherit cargoArtifacts; @@ -66,7 +82,7 @@ { checks = { # Build the crate as part of `nix flake check` for convenience - inherit my-crate; + inherit andref-ipfs-depot; # Run clippy (and deny all warnings) on the crate source, # again, reusing the dependency artifacts from above. @@ -74,7 +90,7 @@ # Note that this is done as a separate derivation so that # we can block the CI if there are issues here, but not # prevent downstream consumers from building our crate by itself. - my-crate-clippy = craneLib.cargoClippy ( + andref-ipfs-depot-clippy = craneLib.cargoClippy ( commonArgs // { inherit cargoArtifacts; @@ -82,7 +98,7 @@ } ); - my-crate-doc = craneLib.cargoDoc ( + andref-ipfs-depot-doc = craneLib.cargoDoc ( commonArgs // { inherit cargoArtifacts; @@ -93,30 +109,30 @@ ); # Check formatting - my-crate-fmt = craneLib.cargoFmt { + andref-ipfs-depot-fmt = craneLib.cargoFmt { inherit src; }; - my-crate-toml-fmt = craneLib.taploFmt { + andref-ipfs-depot-toml-fmt = craneLib.taploFmt { src = pkgs.lib.sources.sourceFilesBySuffices src [ ".toml" ]; # taplo arguments can be further customized below as needed # taploExtraArgs = "--config ./taplo.toml"; }; # Audit dependencies - my-crate-audit = craneLib.cargoAudit { + andref-ipfs-depot-audit = craneLib.cargoAudit { inherit src advisory-db; }; # Audit licenses - my-crate-deny = craneLib.cargoDeny { + andref-ipfs-depot-deny = craneLib.cargoDeny { inherit src; }; # Run tests with cargo-nextest - # Consider setting `doCheck = false` on `my-crate` if you do not want + # Consider setting `doCheck = false` on `andref-ipfs-depot` if you do not want # the tests to run twice - my-crate-nextest = craneLib.cargoNextest ( + andref-ipfs-depot-nextest = craneLib.cargoNextest ( commonArgs // { inherit cargoArtifacts; @@ -128,11 +144,11 @@ }; packages = { - default = my-crate; + default = andref-ipfs-depot; }; apps.default = flake-utils.lib.mkApp { - drv = my-crate; + drv = andref-ipfs-depot; }; devShells.default = craneLib.devShell { diff --git a/andref-ipfs-depot/src/assets.rs b/andref-ipfs-depot/src/assets.rs new file mode 100644 index 0000000..bd1a4ca --- /dev/null +++ b/andref-ipfs-depot/src/assets.rs @@ -0,0 +1,6 @@ +//! The frontend, compiled into the binary so the container ships nothing but the executable. + +pub const INDEX_HTML: &str = include_str!("../assets/index.html"); +pub const INVALID_HTML: &str = include_str!("../assets/invalid.html"); +pub const APP_CSS: &str = include_str!("../assets/app.css"); +pub const APP_JS: &str = include_str!("../assets/app.js"); diff --git a/andref-ipfs-depot/src/config.rs b/andref-ipfs-depot/src/config.rs new file mode 100644 index 0000000..661ba4d --- /dev/null +++ b/andref-ipfs-depot/src/config.rs @@ -0,0 +1,41 @@ +//! Process configuration, read once from the environment at startup. + +/// All runtime configuration. Every field is required; `from_env` fails fast naming the missing +/// variable rather than letting the app start half-configured. +#[derive(Clone, Debug)] +pub struct Config { + /// Discord bot token (gateway + HTTP credential). + pub discord_bot_token: String, + /// The single guild the `/upload` command is registered in. + pub discord_guild_id: u64, + /// Base URL of the kubo RPC API, e.g. `http://kubo.default.svc.cluster.local:5001`. + pub kubo_rpc_base: String, + /// Bearer token, scoped in kubo's `API.Authorizations` to `/api/v0/add`. + pub kubo_rpc_token: String, + /// Subdomain-gateway base, e.g. `ipfs.andref.app`; links are `https://.`. + pub gateway_base_domain: String, + /// Public base URL of this app, e.g. `https://depot.andref.app`; the upload link is `/u/`. + pub app_base_url: String, + /// `host:port` the HTTP server binds, e.g. `0.0.0.0:8080`. + pub bind_addr: String, +} + +impl Config { + pub fn from_env() -> Result { + Ok(Self { + discord_bot_token: req("DISCORD_BOT_TOKEN")?, + discord_guild_id: req("DISCORD_GUILD_ID")? + .parse() + .map_err(|_| "DISCORD_GUILD_ID must be a u64".to_string())?, + kubo_rpc_base: req("KUBO_RPC_BASE")?, + kubo_rpc_token: req("KUBO_RPC_TOKEN")?, + gateway_base_domain: req("GATEWAY_BASE_DOMAIN")?, + app_base_url: req("APP_BASE_URL")?, + bind_addr: req("BIND_ADDR")?, + }) + } +} + +fn req(key: &str) -> Result { + std::env::var(key).map_err(|_| format!("missing required env var {key}")) +} diff --git a/andref-ipfs-depot/src/discord.rs b/andref-ipfs-depot/src/discord.rs new file mode 100644 index 0000000..81704ac --- /dev/null +++ b/andref-ipfs-depot/src/discord.rs @@ -0,0 +1,73 @@ +//! The Discord side: register the guild `/upload` command and, when invoked, mint a token and +//! reply (ephemerally) with the upload link. + +use serenity::all::{ + CommandInteraction, Context, CreateCommand, CreateInteractionResponse, + CreateInteractionResponseMessage, EventHandler, GuildId, Interaction, Ready, +}; +use serenity::async_trait; + +use crate::state::{AppState, AppStateKey}; + +pub struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, ctx: Context, ready: Ready) { + let state = app_state(&ctx).await; + let guild = GuildId::new(state.cfg.discord_guild_id); + // Guild-scoped commands register instantly (global ones take ~1h). set_commands replaces + // the guild's command set with exactly ours, so re-deploys stay idempotent. + let cmd = CreateCommand::new("upload").description("Get a link to upload a file to IPFS"); + match guild.set_commands(&ctx.http, vec![cmd]).await { + Ok(_) => tracing::info!( + "registered /upload in guild {} as {}", + guild.get(), + ready.user.name + ), + Err(e) => tracing::error!("failed to register /upload in guild {}: {e}", guild.get()), + } + } + + async fn interaction_create(&self, ctx: Context, interaction: Interaction) { + let Interaction::Command(command) = interaction else { + return; + }; + if command.data.name != "upload" { + return; + } + handle_upload(&ctx, &command).await; + } +} + +async fn handle_upload(ctx: &Context, command: &CommandInteraction) { + let state = app_state(ctx).await; + let token = state + .store + .issue(command.channel_id.get(), command.user.id.get()); + let link = format!( + "{}/u/{}", + state.cfg.app_base_url.trim_end_matches('/'), + token + ); + let msg = CreateInteractionResponseMessage::new() + .ephemeral(true) + .content(format!( + "Upload a file here (single-use link, expires in 15 min):\n{link}" + )); + if let Err(e) = command + .create_response(&ctx.http, CreateInteractionResponse::Message(msg)) + .await + { + tracing::error!("failed to reply to /upload: {e}"); + } +} + +async fn app_state(ctx: &Context) -> AppState { + ctx.data + .read() + .await + .get::() + .expect("AppState inserted into TypeMap at startup") + .clone() +} diff --git a/andref-ipfs-depot/src/ipfs.rs b/andref-ipfs-depot/src/ipfs.rs new file mode 100644 index 0000000..e6a44a5 --- /dev/null +++ b/andref-ipfs-depot/src/ipfs.rs @@ -0,0 +1,79 @@ +//! The one kubo RPC call we make: add a file (pinned) and turn its CID into a gateway URL. + +use serde::Deserialize; + +use crate::state::AppState; + +/// kubo's `/api/v0/add` reply for a single file (one JSON object per added file). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +struct AddResponse { + hash: String, +} + +#[derive(Debug)] +pub enum IpfsError { + Http(reqwest::Error), + /// kubo returned a body we could not parse as an add result. + BadResponse(String), +} + +impl std::fmt::Display for IpfsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IpfsError::Http(e) => write!(f, "kubo request failed: {e}"), + IpfsError::BadResponse(e) => write!(f, "unexpected kubo response: {e}"), + } + } +} + +impl std::error::Error for IpfsError {} + +impl From for IpfsError { + fn from(e: reqwest::Error) -> Self { + IpfsError::Http(e) + } +} + +/// Upload `bytes` to kubo, pinned, as a CIDv1 (base32 -- required so the CID is a DNS-safe +/// subdomain label). Returns the resulting CID. +pub async fn add(state: &AppState, filename: String, bytes: Vec) -> Result { + let part = reqwest::multipart::Part::bytes(bytes) + .file_name(filename) + .mime_str("application/octet-stream")?; + let form = reqwest::multipart::Form::new().part("file", part); + + let url = format!( + "{}/api/v0/add", + state.cfg.kubo_rpc_base.trim_end_matches('/') + ); + let body = state + .kubo + .post(url) + // pin=true so this NoFetch, pinned-only node serves + announces it; cid-version=1 so the + // CID is base32 and works as .ipfs.andref.app. + .query(&[("pin", "true"), ("cid-version", "1")]) + .bearer_auth(&state.cfg.kubo_rpc_token) + .multipart(form) + .send() + .await? + .error_for_status()? + .text() + .await?; + + // `add` streams newline-delimited JSON (one object per file, plus progress objects); the final + // non-empty line carries the root file's result. + let last = body + .lines() + .map(str::trim) + .rfind(|l| !l.is_empty()) + .ok_or_else(|| IpfsError::BadResponse("empty body".to_string()))?; + let parsed: AddResponse = + serde_json::from_str(last).map_err(|e| IpfsError::BadResponse(e.to_string()))?; + Ok(parsed.hash) +} + +/// `https://.` -- the direct subdomain-gateway link to the file. +pub fn gateway_url(state: &AppState, cid: &str) -> String { + format!("https://{}.{}", cid, state.cfg.gateway_base_domain) +} diff --git a/andref-ipfs-depot/src/main.rs b/andref-ipfs-depot/src/main.rs index e7a11a9..069b57f 100644 --- a/andref-ipfs-depot/src/main.rs +++ b/andref-ipfs-depot/src/main.rs @@ -1,3 +1,98 @@ -fn main() { - println!("Hello, world!"); +//! andref-ipfs-depot: a Discord-gated file uploader for a pinned-only kubo (IPFS) node. +//! +//! One process runs both the serenity Discord bot (which mints upload links) and an axum HTTP +//! server (which serves the upload page and pins files to kubo), sharing an in-memory token store. + +mod assets; +mod config; +mod discord; +mod ipfs; +mod state; +mod tokens; +mod web; + +use std::sync::Arc; + +use serenity::all::{Client, GatewayIntents}; +use serenity::gateway::ShardManager; + +use crate::config::Config; +use crate::discord::Handler; +use crate::state::{AppState, AppStateKey}; +use crate::tokens::TokenStore; + +type BoxError = Box; + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let cfg = Config::from_env().map_err(BoxError::from)?; + let bind_addr = cfg.bind_addr.clone(); + + // Slash-command interactions need no privileged (or any) gateway intents. + let mut client = Client::builder(&cfg.discord_bot_token, GatewayIntents::empty()) + .event_handler(Handler) + .await?; + + let state = AppState { + cfg: Arc::new(cfg), + kubo: reqwest::Client::new(), + discord: client.http.clone(), + store: Arc::new(TokenStore::new()), + }; + client + .data + .write() + .await + .insert::(state.clone()); + + let app = web::router(state); + let listener = tokio::net::TcpListener::bind(&bind_addr).await?; + tracing::info!("listening on {bind_addr}"); + + // Run the bot and the web server together; if either ends (error or shutdown) the process + // exits and k8s restarts the pod. axum gets a graceful SIGTERM/Ctrl-C shutdown so in-flight + // uploads finish, and the serenity shards are told to wind down on the same signal. + let shard_manager = client.shard_manager.clone(); + let serenity_fut = async move { client.start().await.map_err(BoxError::from) }; + let web_fut = async move { + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal(shard_manager)) + .await + .map_err(BoxError::from) + }; + + tokio::try_join!(serenity_fut, web_fut)?; + Ok(()) +} + +/// Resolve on SIGTERM (k8s pod stop) or Ctrl-C, then ask serenity to wind down its shards so both +/// halves stop together. +async fn shutdown_signal(shard_manager: Arc) { + let ctrl_c = async { + tokio::signal::ctrl_c().await.ok(); + }; + #[cfg(unix)] + let terminate = async { + if let Ok(mut sig) = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + { + sig.recv().await; + } + }; + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {} + _ = terminate => {} + } + tracing::info!("shutdown signal received"); + shard_manager.shutdown_all().await; } diff --git a/andref-ipfs-depot/src/state.rs b/andref-ipfs-depot/src/state.rs new file mode 100644 index 0000000..eafdd6f --- /dev/null +++ b/andref-ipfs-depot/src/state.rs @@ -0,0 +1,26 @@ +//! Shared application state, cloned into both the Discord event handler and the axum router. + +use std::sync::Arc; + +use serenity::http::Http; + +use crate::config::Config; +use crate::tokens::TokenStore; + +/// Cheap to clone: every field is an `Arc` (or `reqwest::Client`, which is itself `Arc`-backed). +#[derive(Clone)] +pub struct AppState { + pub cfg: Arc, + /// HTTP client for the kubo RPC. + pub kubo: reqwest::Client, + /// serenity's Discord HTTP client, so the upload handler can post the result to the channel. + pub discord: Arc, + pub store: Arc, +} + +/// serenity stores per-client data in a typed map; this key hands the handler its `AppState`. +pub struct AppStateKey; + +impl serenity::prelude::TypeMapKey for AppStateKey { + type Value = AppState; +} diff --git a/andref-ipfs-depot/src/tokens.rs b/andref-ipfs-depot/src/tokens.rs new file mode 100644 index 0000000..842434b --- /dev/null +++ b/andref-ipfs-depot/src/tokens.rs @@ -0,0 +1,134 @@ +//! In-memory, single-use, time-limited upload tokens. +//! +//! A token is minted by the Discord bot when a member runs `/upload`, embedded in the link the +//! member opens, and consumed exactly once by the upload handler. Consuming removes it (single +//! use) and checks the TTL. Expiry is lazy -- there is no reaper task; a stale token just lives in +//! the map until something tries to consume it (then it is removed). For a per-member upload tool +//! the leftover-entry memory is negligible, and the whole store is disposable across restarts (a +//! member simply re-runs `/upload`). + +use std::collections::HashMap; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +use uuid::Uuid; + +/// How long a minted link stays valid. +const TTL: Duration = Duration::from_secs(15 * 60); + +/// What a token authorizes: an upload whose result is announced back to the channel + member it +/// originated from. +#[derive(Clone, Copy, Debug)] +pub struct Pending { + pub channel_id: u64, + pub user_id: u64, + expiry: Instant, +} + +#[derive(Default)] +pub struct TokenStore { + inner: Mutex>, +} + +impl TokenStore { + pub fn new() -> Self { + Self::default() + } + + /// Mint a single-use token bound to the originating channel + member. + pub fn issue(&self, channel_id: u64, user_id: u64) -> String { + let token = Uuid::new_v4().simple().to_string(); + let pending = Pending { + channel_id, + user_id, + expiry: Instant::now() + TTL, + }; + self.inner.lock().unwrap().insert(token.clone(), pending); + token + } + + /// Consume a token: remove it (single use) and return its binding iff it had not expired. A + /// stale token is removed and reported invalid, so even a failed attempt burns it. + pub fn consume(&self, token: &str) -> Option { + let pending = self.inner.lock().unwrap().remove(token)?; + (Instant::now() < pending.expiry).then_some(pending) + } + + /// Check whether a token is currently usable (present and unexpired) WITHOUT consuming it -- + /// used to gate serving the upload page so a guessed/expired/already-used link shows an error + /// instead of the form. A token found expired is removed (lazy cleanup). The single-use + /// guarantee still rests on `consume`: a page served here is only spent when its upload POSTs. + pub fn is_valid(&self, token: &str) -> bool { + let mut map = self.inner.lock().unwrap(); + match map.get(token) { + Some(p) if Instant::now() < p.expiry => true, + Some(_) => { + map.remove(token); + false + } + None => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn issued_token_validates_then_is_single_use() { + let store = TokenStore::new(); + let token = store.issue(111, 222); + + // is_valid must NOT consume: a live link can be opened/refreshed repeatedly before upload. + assert!(store.is_valid(&token)); + assert!(store.is_valid(&token)); + + // First consume returns the binding it was issued with... + let pending = store + .consume(&token) + .expect("a freshly issued token must consume"); + assert_eq!(pending.channel_id, 111); + assert_eq!(pending.user_id, 222); + + // ...and the token is now spent: not valid, not consumable again (no reuse). + assert!(!store.is_valid(&token)); + assert!(store.consume(&token).is_none()); + } + + #[test] + fn guessed_token_is_rejected() { + let store = TokenStore::new(); + // A token that was never issued (a guessed link) is never valid or consumable. + assert!(!store.is_valid("hello")); + assert!(store.consume("hello").is_none()); + } + + #[test] + fn expired_token_is_rejected_and_swept() { + let store = TokenStore::new(); + let token = "already-expired".to_string(); + let already_expired = Pending { + channel_id: 1, + user_id: 2, + expiry: Instant::now(), // a later now() is strictly greater -> treated as expired + }; + store + .inner + .lock() + .unwrap() + .insert(token.clone(), already_expired); + + // is_valid rejects an expired token AND lazily sweeps it from the map. + assert!(!store.is_valid(&token)); + assert!(!store.inner.lock().unwrap().contains_key(&token)); + + // consume also rejects an expired token rather than handing back its binding. + store + .inner + .lock() + .unwrap() + .insert(token.clone(), already_expired); + assert!(store.consume(&token).is_none()); + } +} diff --git a/andref-ipfs-depot/src/web.rs b/andref-ipfs-depot/src/web.rs new file mode 100644 index 0000000..6c41c47 --- /dev/null +++ b/andref-ipfs-depot/src/web.rs @@ -0,0 +1,164 @@ +//! The HTTP surface: serves the upload page + assets, accepts the upload, and on success posts +//! the resulting link back to the originating Discord channel. + +use axum::extract::{DefaultBodyLimit, Multipart, Path, State}; +use axum::http::{header, StatusCode}; +use axum::response::{Html, IntoResponse, Response}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde::Serialize; +use serenity::all::ChannelId; + +use crate::assets; +use crate::ipfs; +use crate::state::AppState; + +/// Cap on a single upload. The endpoint is public (token-gated only) and the body is buffered in +/// memory, so this bound is what stops it being a memory-exhaustion DoS. +const MAX_UPLOAD_BYTES: usize = 100 * 1024 * 1024; + +pub fn router(state: AppState) -> Router { + Router::new() + .route("/healthz", get(healthz)) + .route("/u/{token}", get(serve_page)) + .route("/api/upload/{token}", post(upload)) + .route("/assets/app.css", get(serve_css)) + .route("/assets/app.js", get(serve_js)) + .layer(DefaultBodyLimit::max(MAX_UPLOAD_BYTES)) + .with_state(state) +} + +async fn healthz() -> &'static str { + "ok" +} + +/// The upload page. The token is validated (but NOT consumed -- a GET must not burn it) so a +/// guessed, expired, or already-used link shows an error page instead of the form. The token is +/// only spent when the upload POSTs. JS reads the token back from the URL. +async fn serve_page(State(state): State, Path(token): Path) -> Response { + if state.store.is_valid(&token) { + Html(assets::INDEX_HTML).into_response() + } else { + (StatusCode::NOT_FOUND, Html(assets::INVALID_HTML)).into_response() + } +} + +async fn serve_css() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/css; charset=utf-8")], + assets::APP_CSS, + ) +} + +async fn serve_js() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/javascript; charset=utf-8")], + assets::APP_JS, + ) +} + +#[derive(Serialize)] +struct UploadResult { + cid: String, + url: String, +} + +async fn upload( + State(state): State, + Path(token): Path, + mut multipart: Multipart, +) -> Result, (StatusCode, String)> { + // Consume the token BEFORE reading the body, so a replayed/expired link is rejected without + // streaming the file. This burns the token even on a later failure -- fine, the member just + // re-runs `/upload`. + let pending = state + .store + .consume(&token) + .ok_or((StatusCode::FORBIDDEN, "invalid or expired link".to_string()))?; + + let field = multipart + .next_field() + .await + .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))? + .ok_or((StatusCode::BAD_REQUEST, "no file field".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()))?; + + let cid = ipfs::add(&state, filename, bytes.to_vec()) + .await + .map_err(|e| { + tracing::error!("kubo add failed: {e}"); + (StatusCode::BAD_GATEWAY, "upload to IPFS failed".to_string()) + })?; + let url = ipfs::gateway_url(&state, &cid); + + // Announce the result in the channel the command was run in. A bare URL lets Discord unfurl / + // embed images, video, audio, etc. A failed post is logged but does not fail the upload -- the + // member already has the link in the page. + let content = format!("<@{}> uploaded: {url}", pending.user_id); + if let Err(e) = ChannelId::new(pending.channel_id) + .say(&state.discord, content) + .await + { + tracing::warn!( + "failed to post upload to channel {}: {e}", + pending.channel_id + ); + } + + Ok(Json(UploadResult { cid, url })) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::tokens::TokenStore; + use std::sync::Arc; + + fn test_state() -> AppState { + AppState { + cfg: Arc::new(Config { + discord_bot_token: "x".into(), + discord_guild_id: 0, + kubo_rpc_base: "http://localhost:5001".into(), + kubo_rpc_token: "x".into(), + gateway_base_domain: "ipfs.example".into(), + app_base_url: "https://example".into(), + bind_addr: "0.0.0.0:8080".into(), + }), + kubo: reqwest::Client::new(), + // Http::new does no I/O; it just holds the token for later requests. + discord: Arc::new(serenity::http::Http::new("test-token")), + store: Arc::new(TokenStore::new()), + } + } + + // A guessed link (token never issued) must not serve the upload form. + #[tokio::test] + async fn page_is_404_for_guessed_token() { + let state = test_state(); + let resp = serve_page(State(state), Path("hello".to_string())).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + // A live link serves the form once; after the upload spends it, the same link is rejected + // (no reuse on refresh). + #[tokio::test] + async fn page_serves_live_token_then_404s_after_use() { + let state = test_state(); + let token = state.store.issue(1, 2); + + let resp = serve_page(State(state.clone()), Path(token.clone())).await; + assert_eq!(resp.status(), StatusCode::OK); + + // Simulate the upload consuming the token. + assert!(state.store.consume(&token).is_some()); + + let resp = serve_page(State(state), Path(token)).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } +} diff --git a/eight/per-domain/andref.app.nix b/eight/per-domain/andref.app.nix index 0d388f1..7350796 100644 --- a/eight/per-domain/andref.app.nix +++ b/eight/per-domain/andref.app.nix @@ -106,6 +106,17 @@ value = "carless-drivers-ddns.andref.app."; }; + # Public web UI for the andref-ipfs-depot uploader (kubo, milky-way orion-system, + # lib/andref-ipfs-depot.libsonnet). CNAME to the home-IP DDNS target (same pattern as ipfs / + # test-traefik-acme) so Traefik on methanol serves it with a cert-manager cert. DNS-only (grey + # cloud); the /-ddns$/ rejectlist does not match this label. + "depot" = { + octodns.cloudflare = { auto-ttl = true; }; + ttl = 300; + type = "CNAME"; + value = "carless-drivers-ddns.andref.app."; + }; + old = { octodns.cloudflare = { auto-ttl = true; }; ttl = 300; diff --git a/exports/whale/digests/andref-ipfs-depot.txt b/exports/whale/digests/andref-ipfs-depot.txt new file mode 100644 index 0000000..32f75c6 --- /dev/null +++ b/exports/whale/digests/andref-ipfs-depot.txt @@ -0,0 +1 @@ +sha256:22bfa85fa3c70443427edb38a89cc4448dfd0e6e0064970cfe0356b09161e9b6 \ No newline at end of file diff --git a/flake-profiles/whale/flake.lock b/flake-profiles/whale/flake.lock index f964917..b9102e9 100644 --- a/flake-profiles/whale/flake.lock +++ b/flake-profiles/whale/flake.lock @@ -1,16 +1,83 @@ { "nodes": { + "advisory-db": { + "flake": false, + "locked": { + "lastModified": 1782213767, + "narHash": "sha256-di8OUljwKRpPOYzp4gR/YxX7cumWFyZHbRmZIg2ol7Y=", + "owner": "rustsec", + "repo": "advisory-db", + "rev": "49f543184c8992aaa3552f2730df0dbb7ec9d2fc", + "type": "github" + }, + "original": { + "owner": "rustsec", + "repo": "advisory-db", + "type": "github" + } + }, + "andref-ipfs-depot": { + "inputs": { + "advisory-db": "advisory-db", + "crane": "crane", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "path": "./../../andref-ipfs-depot", + "type": "path" + }, + "original": { + "path": "./../../andref-ipfs-depot", + "type": "path" + }, + "parent": [] + }, + "crane": { + "locked": { + "lastModified": 1781825982, + "narHash": "sha256-SlXKwIRIhrOSAcTjCB3ftPLzJWZStQIPS7J1FlZPnKk=", + "owner": "ipetkov", + "repo": "crane", + "rev": "469fd08d0bcf6926321fa973c6777fbc87785dd7", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1754968417, - "narHash": "sha256-j9Yue6jmpI79hJ3Ab3OPpsWP/WOYyGbtDJ8Xr3iBKyw=", + "lastModified": 1782175435, + "narHash": "sha256-EMzXKmnOtBQ2MnvpiNOm7E+kOMvdPrIKaeg52Tip2Uk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d53549102f8e7ba2e9cdd6788f2bbb09a3627c4c", + "rev": "89570f24e97e614aa34aa9ab1c927b6578a43775", "type": "github" }, "original": { "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -31,11 +98,27 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1754968417, + "narHash": "sha256-j9Yue6jmpI79hJ3Ab3OPpsWP/WOYyGbtDJ8Xr3iBKyw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d53549102f8e7ba2e9cdd6788f2bbb09a3627c4c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { - "nixpkgs": "nixpkgs", + "andref-ipfs-depot": "andref-ipfs-depot", + "nixpkgs": "nixpkgs_2", "nixpkgs-autobrr": "nixpkgs-autobrr", - "systems": "systems" + "systems": "systems_2" } }, "systems": { @@ -52,6 +135,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake-profiles/whale/flake.nix b/flake-profiles/whale/flake.nix index 2ec83c7..b5a64fd 100644 --- a/flake-profiles/whale/flake.nix +++ b/flake-profiles/whale/flake.nix @@ -6,6 +6,10 @@ # above still has 1.64.0. Isolated as its own input so bumping it to rebuild the patched # autobrr image doesn't churn the other whale images (mopidy/grand-central). inputs.nixpkgs-autobrr.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + # The andref-ipfs-depot crate (crane flake). whale consumes its built x86_64-linux binary and + # wraps it in a minimal image. Self-contained (its own pinned nixpkgs), so it doesn't follow + # whale's nixpkgs. + inputs.andref-ipfs-depot.url = "path:./../../andref-ipfs-depot"; inputs.systems.url = "github:nix-systems/default"; outputs = inputs: (import ./../../whale/outputs.nix) inputs; diff --git a/milky-way/environments/stage00/orion-system/main.jsonnet b/milky-way/environments/stage00/orion-system/main.jsonnet index 02fc5e8..e53fb03 100644 --- a/milky-way/environments/stage00/orion-system/main.jsonnet +++ b/milky-way/environments/stage00/orion-system/main.jsonnet @@ -29,6 +29,7 @@ local grandCentral = import 'milky-way/lib/grand-central.libsonnet'; local gluetunLeakTest = import 'milky-way/lib/gluetun-leak-test.libsonnet'; local kubo = import 'milky-way/lib/kubo.libsonnet'; local kuboTest = import 'milky-way/lib/kubo-test.libsonnet'; +local andrefIpfsDepot = import 'milky-way/lib/andref-ipfs-depot.libsonnet'; local testExampleWhaleImageDigest = import 'milky-way/lib/test-example-whale-image-digest.libsonnet'; local letsEncryptCloudflare = import 'milky-way/lib/letsencrypt-cloudflare.libsonnet'; local testTraefikAcme = import 'milky-way/lib/test-traefik-acme-ingress.libsonnet'; @@ -514,6 +515,8 @@ local pubkeys = import 'magic/common/public_keys.json'; kubo: kubo.new( testRpcToken = secrets.kubo.rpcTokenForTest, webuiRpcToken = secrets.kubo.rpcTokenForIpfsWebui, + // Third, least-privilege RPC grant (scoped to /api/v0/add) for the andref-ipfs-depot uploader. + depotRpcToken = secrets.kubo.rpcTokenForAndrefIpfsDepot, tailscaleHostname = "ipfs-webui", wireguardPrivateKey = wgConf.privateKeyOf(importstr 'milky-way/secrets/kubo-gluetun.conf'), vpnProvider = "protonvpn", @@ -535,6 +538,25 @@ local pubkeys = import 'magic/common/public_keys.json'; kuboService = this.kubo.service, ), + // andref-ipfs-depot: Discord-gated file uploader for the kubo pinned-mirror node + // (lib/andref-ipfs-depot.libsonnet). A guild member runs /upload, gets a single-use link, uploads + // a file via the public page, and the backend pins it to kubo (the scoped 'depot' RPC token above) + // and returns + posts back the subdomain-gateway link https://.ipfs.andref.app. The HTTP + // server is public (depot.andref.app, Traefik + cert-manager wildcard issuer); the bot is + // outbound-only. kubo's RPC is reached in-cluster via its Service (host + api port read from it). + andrefIpfsDepot: andrefIpfsDepot.new( + discordBotToken = secrets.discordBots.andrefIpfsDepot.token, + discordGuildId = secrets.discord.andref.guildId, + kuboRpcToken = secrets.kubo.rpcTokenForAndrefIpfsDepot, + kuboRpcBase = 'http://%s:%d' % [ + utils.domainOfService(this.kubo.service), + utils.associateObjectsByKey(this.kubo.service.spec.ports, 'name')['api'].port, + ], + publicHostname = "depot.andref.app", + gatewayBaseDomain = "ipfs.andref.app", + issuerName = activeLetsEncryptIssuerName, + ), + cilium: charts.cilium, traefikConfig: traefik.reconfigForCilium(), diff --git a/milky-way/lib/andref-ipfs-depot.libsonnet b/milky-way/lib/andref-ipfs-depot.libsonnet new file mode 100644 index 0000000..b463060 --- /dev/null +++ b/milky-way/lib/andref-ipfs-depot.libsonnet @@ -0,0 +1,164 @@ +local utils = import 'milky-way/lib/utils.libsonnet'; +local images = import 'milky-way/lib/images.libsonnet'; + +// andref-ipfs-depot: a Discord-gated file uploader for the pinned-only kubo node (lib/kubo.libsonnet). +// +// One whale-built Rust binary runs BOTH a serenity Discord bot and an axum HTTP server in one +// process (see andref-ipfs-depot/). A guild member runs `/upload`; the bot replies (ephemerally) +// with a single-use https:///u/ link; the page POSTs the file back; the +// backend pins it to kubo via the RPC `/api/v0/add?pin=true&cid-version=1` (holding a bearer token +// kubo's API.Authorizations scopes to just `/api/v0/add` -- see kubo.libsonnet's depotRpcToken) and +// returns the direct subdomain-gateway link https://., which the bot also +// posts back into the channel (a bare URL, so Discord auto-embeds images/video/etc.). +// +// Single process by design: the token-issuing bot and the token-validating HTTP handler share an +// in-memory token store, so they MUST be one process (a separate-services split would need a +// networked store). Hence replicas:1 + Recreate -- a second pod would hold a different store and a +// link issued by one would 403 on the other. The store is disposable (lost on restart; a member +// just re-runs `/upload`), so no PVC. +// +// Exposure: the bot is outbound-only (no ingress); the HTTP server is PUBLIC internet (members open +// the link from anywhere), so it gets a Traefik ingress + a cert-manager TLS cert (DNS-01, same +// pattern as kubo's gateway). The kubo RPC is reached in-cluster via kuboRpcBase (ClusterIP). +{ + new( + discordBotToken, // required -> Secret env DISCORD_BOT_TOKEN + discordGuildId, // required -> Secret env DISCORD_GUILD_ID (guild /upload is registered in) + kuboRpcToken, // required -> Secret env KUBO_RPC_TOKEN (kubo 'depot' grant, scoped to /api/v0/add) + kuboRpcBase, // required -> KUBO_RPC_BASE, e.g. http://kubo.default.svc.cluster.local:5001 + publicHostname, // required, NO default -> public host (e.g. depot.andref.app); cert SAN + Ingress host + issuerName, // required, NO default -> cert-manager ClusterIssuer the TLS cert is issued from + gatewayBaseDomain='ipfs.andref.app', // -> GATEWAY_BASE_DOMAIN; result links are https://. (kubo's subdomain gateway) + name='andref-ipfs-depot', + namespace='default', + image=images['andref-ipfs-depot'].fullyQualifiedImageReferencePinned, + port=8080, // HTTP listen port (matches the whale image's ExposedPorts) + ):: { + local this = self, + // The app builds upload links as /u/; it is reached over https via Traefik. + local appBaseUrl = 'https://' + publicHostname, + + // Bot token + guild id + the scoped kubo RPC token, supplied by the caller from sops. stringData + // lets Kubernetes base64-encode them; the container reads them via envFrom. + secret: { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: name + '-secrets', namespace: namespace }, + type: 'Opaque', + stringData: { + DISCORD_BOT_TOKEN: discordBotToken, + DISCORD_GUILD_ID: std.toString(discordGuildId), + KUBO_RPC_TOKEN: kuboRpcToken, + }, + }, + + deployment: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: name, namespace: namespace }, + spec: { + replicas: 1, + strategy: { type: 'Recreate' }, // single in-memory token store -- never run two pods at once + selector: { matchLabels: { app: name } }, + template: { + metadata: { labels: {} + this.deployment.spec.selector.matchLabels }, + spec: { + tolerations: [ + { key: 'ephemeral', operator: 'Exists', effect: 'NoSchedule' }, + ], + containers: [ + { + name: name, + image: image, + env: [ + { name: 'KUBO_RPC_BASE', value: kuboRpcBase }, + { name: 'GATEWAY_BASE_DOMAIN', value: gatewayBaseDomain }, + { name: 'APP_BASE_URL', value: appBaseUrl }, + { name: 'BIND_ADDR', value: '0.0.0.0:' + std.toString(port) }, + ], + envFrom: [{ secretRef: { name: this.secret.metadata.name } }], // discord + kubo tokens + ports: [{ name: 'http', containerPort: port }], + readinessProbe: { + httpGet: { path: '/healthz', port: 'http' }, + initialDelaySeconds: 5, + periodSeconds: 15, + }, + livenessProbe: { + httpGet: { path: '/healthz', port: 'http' }, + periodSeconds: 30, + }, + resources: { + requests: { memory: '32Mi', cpu: '10m' }, + limits: { memory: '256Mi', cpu: '1' }, + }, + }, + ], + }, + }, + }, + }, + + service: { + apiVersion: 'v1', + kind: 'Service', + metadata: { name: name, namespace: namespace }, + spec: { + selector: {} + this.deployment.spec.template.metadata.labels, + ports: [{ + name: 'http', + port: port, + targetPort: utils.assertEqualAndReturn(this.deployment.spec.template.spec.containers[0].ports[0].name, 'http'), + }], + type: 'ClusterIP', + }, + }, + + // Public TLS: cert-manager issues a cert for publicHostname into -tls (DNS-01 -- no inbound + // reachability needed), which Traefik serves. Mirrors lib/kubo.libsonnet's gateway certificate. + certificate: { + apiVersion: 'cert-manager.io/v1', + kind: 'Certificate', + metadata: { name: name + '-tls', namespace: namespace }, + spec: { + secretName: name + '-tls', + dnsNames: [publicHostname], + issuerRef: { name: issuerName, kind: 'ClusterIssuer' }, + }, + }, + + // PUBLIC internet ingress: Discord members open the upload link from anywhere, so this is a + // Traefik (not tailnet) ingress on the websecure entrypoint, terminating TLS with the cert above + // and proxying to the ClusterIP Service. The DNS record for publicHostname lives in eight/. + ingress: { + apiVersion: 'networking.k8s.io/v1', + kind: 'Ingress', + metadata: { + name: name, + namespace: namespace, + annotations: { 'traefik.ingress.kubernetes.io/router.entrypoints': 'websecure' }, + }, + spec: { + ingressClassName: 'traefik', + tls: [{ + hosts: [publicHostname], + secretName: utils.assertEqualAndReturn(this.certificate.spec.secretName, name + '-tls'), + }], + rules: [{ + host: publicHostname, + http: { + paths: [{ + path: '/', + pathType: 'Prefix', + backend: { + service: { + name: this.service.metadata.name, + port: { number: utils.assertEqualAndReturn(this.service.spec.ports[0].port, port) }, + }, + }, + }], + }, + }], + }, + }, + }, +} diff --git a/milky-way/lib/images.libsonnet b/milky-way/lib/images.libsonnet index eaa0c50..139a683 100644 --- a/milky-way/lib/images.libsonnet +++ b/milky-way/lib/images.libsonnet @@ -130,6 +130,13 @@ local images = { fullyQualifiedRepository: "docker.io/yuto7/autobrr", defaultDigest: { hash: std.trim(importstr "exports/whale/digests/autobrr.txt") }, }, + // andref-ipfs-depot: our Discord-gated IPFS uploader (lib/andref-ipfs-depot.libsonnet), a + // whale-built Rust binary. Digest from exports/whale/digests/andref-ipfs-depot.txt (written by + // `nix run ./flake-profiles/whale#andref-ipfs-depot-push`). + "andref-ipfs-depot": { + fullyQualifiedRepository: "docker.io/yuto7/andref-ipfs-depot", + defaultDigest: { hash: std.trim(importstr "exports/whale/digests/andref-ipfs-depot.txt") }, + }, // Kubo (go-ipfs), the reference IPFS implementation -- run here as a VPN-fronted pinned-mirror // node (lib/kubo.libsonnet). Multi-arch INDEX digest (k3s resolves the per-node arch; the index // includes linux/amd64 for methanol), same convention as the *arr/qbittorrent pins; tagHint is diff --git a/milky-way/lib/kubo.libsonnet b/milky-way/lib/kubo.libsonnet index 096ca17..0a1cdfe 100644 --- a/milky-way/lib/kubo.libsonnet +++ b/milky-way/lib/kubo.libsonnet @@ -30,9 +30,10 @@ local images = import 'milky-way/lib/images.libsonnet'; // Instead the wrapper lets init create the repo, then re-asserts our keys via `ipfs config` every // boot (declarative for the keys we manage; kubo owns identity/datastore/pinset on the PVC). // -// RPC API is locked down with API.Authorizations (admin RPC must NEVER be open): there are two bearer -// grants -- the test service (scoped to the minimum AllowedPaths needed to verify the node, see -// testAllowedPaths) and the WebUI (full /api/v0, see webuiAllowedPaths). Storage: the repo +// RPC API is locked down with API.Authorizations (admin RPC must NEVER be open): there are up to three +// bearer grants -- the test service (scoped to the minimum AllowedPaths needed to verify the node, see +// testAllowedPaths), the WebUI (full /api/v0, see webuiAllowedPaths), and an OPTIONAL depot uploader +// (scoped to /api/v0/add, enabled by passing depotRpcToken; the andref-ipfs-depot app). Storage: the repo // (identity/datastore/pinset) is on iSCSI (RWO) -- the datastore does file locking, unsafe over NFS // (same rationale as jellyfin's SQLite) -- so an RWO PVC means the old pod must release it before a new // one mounts, hence strategy: Recreate. @@ -97,6 +98,9 @@ local images = import 'milky-way/lib/images.libsonnet'; testAllowedPaths=self.defaultTestAllowedPaths, webuiAuthName='webui', // API.Authorizations entry name for the WebUI webuiAllowedPaths=['/api/v0'], // full RPC: everything the WebUI needs for normal operation + depotRpcToken=null, // optional -> a THIRD API.Authorizations bearer grant for the andref-ipfs-depot uploader (lib/andref-ipfs-depot.libsonnet). null = no depot grant. + depotAuthName='depot', // API.Authorizations entry name for the depot uploader + depotAllowedPaths=['/api/v0/add'], // least privilege: `add` with pin=true both stores AND pins, so nothing else is needed webuiProxyPort=8081, // nginx sidecar listen port (fronts /webui + RPC, injects the token) webuiCid='bafybeihxglpcfyarpm7apn7xpezbuoqgk3l5chyk7w4gvrjwk45rqohlmm', // bundled WebUI (ipfs-webui v4.12.0) that kubo v0.42.0's /webui redirects to; pinned so NoFetch can serve it. Update on kubo bumps. nginxImage=images.nginx.fullyQualifiedImageReferencePinned, @@ -120,6 +124,16 @@ local images = import 'milky-way/lib/images.libsonnet'; // is single-sourced from this jsonnet (no newlines, so it embeds cleanly into the shell --json arg). local allowedPathsJson = std.manifestJsonEx(testAllowedPaths, '', ''), local webuiAllowedPathsJson = std.manifestJsonEx(webuiAllowedPaths, '', ''), + local depotAllowedPathsJson = std.manifestJsonEx(depotAllowedPaths, '', ''), + + // Optional THIRD RPC grant (the andref-ipfs-depot uploader): a shell-JSON fragment appended to + // API.Authorizations, with the token referenced as the env var $KUBO_DEPOT_RPC_TOKEN (set from + // the Secret) so the literal stays out of this ConfigMap -- same treatment as test/webui. Empty + // (no extra entry) when depotRpcToken is null. + local depotAuthzEntry = + if depotRpcToken != null then + ',\\"$DEPOT_AUTH_NAME\\":{\\"AuthSecret\\":\\"bearer:$KUBO_DEPOT_RPC_TOKEN\\",\\"AllowedPaths\\":$DEPOT_ALLOWED_PATHS_JSON}' + else '', // The nginx WebUI-proxy sidecar config. It carries the bearer token, so it lives in a Secret (below), // not a ConfigMap. Proxies everything to kubo over loopback (NOT the VPN), and injects the token so the @@ -187,10 +201,12 @@ local images = import 'milky-way/lib/images.libsonnet'; # Bound the swarm so gluetun (which tracks every connection in its firewall/netns) doesn't OOM: # a pinned mirror that serves/announces pinned content needs no large peer set. ipfs config --json Swarm.ConnMgr '{"Type":"basic","LowWater":30,"HighWater":100,"GracePeriod":"20s"}' - # Two bearer grants in ONE call (the key is overwritten wholesale): the scoped test verifier and the - # full-/api/v0 WebUI (consumed by the nginx sidecar, which injects this token on the browser's behalf). + # Bearer grants in ONE call (the key is overwritten wholesale): the scoped test verifier, the + # full-/api/v0 WebUI (consumed by the nginx sidecar, which injects this token on the browser's + # behalf), and -- when a depot token is configured -- the scoped andref-ipfs-depot uploader + # (%(depotAuthzEntry)s expands to its entry, or to nothing). ipfs config --json API.Authorizations \ - "{\"$TEST_AUTH_NAME\":{\"AuthSecret\":\"bearer:$KUBO_TEST_RPC_TOKEN\",\"AllowedPaths\":$ALLOWED_PATHS_JSON},\"$WEBUI_AUTH_NAME\":{\"AuthSecret\":\"bearer:$KUBO_WEBUI_RPC_TOKEN\",\"AllowedPaths\":$WEBUI_ALLOWED_PATHS_JSON}}" + "{\"$TEST_AUTH_NAME\":{\"AuthSecret\":\"bearer:$KUBO_TEST_RPC_TOKEN\",\"AllowedPaths\":$ALLOWED_PATHS_JSON},\"$WEBUI_AUTH_NAME\":{\"AuthSecret\":\"bearer:$KUBO_WEBUI_RPC_TOKEN\",\"AllowedPaths\":$WEBUI_ALLOWED_PATHS_JSON}%(depotAuthzEntry)s}" # --- dynamic: wait for gluetun's NAT-PMP forwarded port, then listen on + announce :PORT --- # gluetun's up-command writes the port to /pf/port. Wait for a non-empty, non-zero value: gluetun @@ -237,7 +253,7 @@ local images = import 'milky-way/lib/images.libsonnet'; pin_webui & exec ipfs daemon - |||) % { ipfsPath: ipfsPath, apiPort: apiPort, gatewayPort: gatewayPort, controlPort: controlPort, gatewayPublicGatewayKey: gatewayPublicGatewayKey }, + |||) % { ipfsPath: ipfsPath, apiPort: apiPort, gatewayPort: gatewayPort, controlPort: controlPort, gatewayPublicGatewayKey: gatewayPublicGatewayKey, depotAuthzEntry: depotAuthzEntry }, // Liveness: restart kubo if gluetun's forwarded port changed (kubo can't re-announce without a // restart). Conservative -- only restart on a CONFIRMED change; if either value is unavailable @@ -289,7 +305,10 @@ local images = import 'milky-way/lib/images.libsonnet'; kind: 'Secret', metadata: { name: name + '-rpc-auth', namespace: namespace }, type: 'Opaque', - stringData: { KUBO_TEST_RPC_TOKEN: testRpcToken, KUBO_WEBUI_RPC_TOKEN: webuiRpcToken }, + stringData: { + KUBO_TEST_RPC_TOKEN: testRpcToken, + KUBO_WEBUI_RPC_TOKEN: webuiRpcToken, + } + (if depotRpcToken != null then { KUBO_DEPOT_RPC_TOKEN: depotRpcToken } else {}), }, // nginx WebUI-proxy config (carries the bearer token -> Secret, not ConfigMap), mounted over the @@ -333,7 +352,7 @@ local images = import 'milky-way/lib/images.libsonnet'; labels: {} + this.deployment.spec.selector.matchLabels, // The wrapper (ConfigMap mount) and token (Secret via envFrom) don't roll the Deployment // on their own; hashing them into the template makes a change to either roll the pod. - annotations: { 'checksum/config': std.md5(wrapperScript + testRpcToken + webuiRpcToken + nginxConf + webuiCid) }, + annotations: { 'checksum/config': std.md5(wrapperScript + testRpcToken + webuiRpcToken + nginxConf + webuiCid + (if depotRpcToken != null then depotRpcToken else '')) }, }, spec: { // kubo runs as uid `ipfs`; fsGroup makes the iSCSI repo volume group-writable so it can @@ -390,6 +409,10 @@ local images = import 'milky-way/lib/images.libsonnet'; { name: 'WEBUI_AUTH_NAME', value: webuiAuthName }, { name: 'WEBUI_ALLOWED_PATHS_JSON', value: webuiAllowedPathsJson }, { name: 'WEBUI_CID', value: webuiCid }, + // Non-secret depot grant inputs (the token itself comes via the Secret/envFrom). + // Unused by the wrapper when no depot token is configured (the entry is omitted). + { name: 'DEPOT_AUTH_NAME', value: depotAuthName }, + { name: 'DEPOT_ALLOWED_PATHS_JSON', value: depotAllowedPathsJson }, ], envFrom: [{ secretRef: { name: this.secret.metadata.name } }], // KUBO_TEST_RPC_TOKEN + KUBO_WEBUI_RPC_TOKEN ports: [ diff --git a/secrets/k8s-config/k8s-secret-values.jsonnet b/secrets/k8s-config/k8s-secret-values.jsonnet index 8d74993..b9a4610 100644 --- a/secrets/k8s-config/k8s-secret-values.jsonnet +++ b/secrets/k8s-config/k8s-secret-values.jsonnet @@ -1,5 +1,5 @@ { - "data": "ENC[AES256_GCM,data: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,iv:Lt63ABOvJxOs9wT3/nHNE9FeL2cOwwqIqswS5Eh4Cxk=,tag:xHGQ+KfpPvJBLt+Ok1/Umg==,type:str]", + "data": "ENC[AES256_GCM,data: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,iv:q+5oLSbvy1bZ4yoZleOLZXgPtJkJd+Pw7jpbPD8g81E=,tag:nv6vxp/9bdu+BiRJnWj95A==,type:str]", "sops": { "age": [ { @@ -11,8 +11,8 @@ "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZNjF5OUx2M1k2YlRiekda\nWHh0QWdQNGZrNFpiM1haZlBPeGRBZjJOOTJrCm9wOVE3cVZhYmNIVjgwWGtDVjRQ\neHNGWW81c0tLK0VYY0RnMk5PSjRzYlkKLS0tIGJQWEhaY0wwaVV0NUNXaXFraExy\na2NwZUhCbng1YWxpUGp6dU9OVXBTNVUKUtj4Ms9tlqFEXbT+cirIiFNZFD8oPbPx\nPu9zxmIl7BC2I1v/c9ijFft7XR+somx7wX5ISIIhBwTDq9Pr4fqovA==\n-----END AGE ENCRYPTED FILE-----\n" } ], - "lastmodified": "2026-06-25T07:24:47Z", - "mac": "ENC[AES256_GCM,data:2DZaRSt8JY8HINkQdbPT6kL6XNW+sbdGyuudBHfhKeCzuMBWF1T4xIDnAEvA6DOdzfIcjNfKzZiN3DZsF110kUnNR1g9fCv1E4AusIgzjBn6f3ISwS23ZFSIDpX6hmPQnuMShHAlw6JpO+Mtt0NCj4cFuS5cCMcVI9N4amkuHes=,iv:sRaBBx/Fqvyid5T1u7hP7wzm5r9nSbFKKrb72JRDHnc=,tag:dKR4xxC6saRYUQDid5+yjQ==,type:str]", + "lastmodified": "2026-06-26T06:35:08Z", + "mac": "ENC[AES256_GCM,data:JhQrjCkzu5eI+nfgezhsLJDy5sTSsk/ra46xs+u+KocsNAuyTz2od39LyecICG/DHj8HhXSF15e3omDhZ90IYnnNL7/0eHqVUWJZqJi6n1L4qxZjT1ONPos3g98ErAC+IO1fJldpkcgiXuXFkLgufT9Er4/iIMoNtyZqHOoM6Mw=,iv:CBMJ3FnvI8mkbJD+VQF2RvHAx+YEEdlXl4mC5p71Dpw=,tag:jYOMhIHxGnfaXNfm8RWIaw==,type:str]", "unencrypted_suffix": "_unencrypted", "version": "3.10.2" } diff --git a/venus/modules/nixos-darwin/sodium.nix b/venus/modules/nixos-darwin/sodium.nix index 742e85b..79fb5b6 100644 --- a/venus/modules/nixos-darwin/sodium.nix +++ b/venus/modules/nixos-darwin/sodium.nix @@ -99,6 +99,14 @@ in systems = [ "aarch64-linux" "x86_64-linux" ]; config = { boot.binfmt.emulatedSystems = [ "x86_64-linux" ]; + # The VM defaults to 1 vCPU / 3 GB, so a single x86_64 cargo build (e.g. whale's + # andref-ipfs-depot) grinds on one emulated core. Give it more so cargo parallelizes. + # Sized to leave the M1 (8 cores / 16 GB) headroom for macOS: ~4 cores + 6 GB are used + # ONLY while a build runs; the VM sits at ~0 when idle. Bump cautiously -- x86_64 steps run + # under TCG (pure emulation), so each vCPU can peg a host core during a build. + # mkForce: the nix-builder-vm profile already pins these (memorySize = 3072), so override. + virtualisation.cores = pkgs.lib.mkForce 4; + virtualisation.memorySize = pkgs.lib.mkForce (6 * 1024); # MiB (was 3072) }; }; }; diff --git a/whale/outputs.nix b/whale/outputs.nix index f1133df..bf6d090 100644 --- a/whale/outputs.nix +++ b/whale/outputs.nix @@ -19,6 +19,11 @@ patches = (old.patches or []) ++ [ ./patches/autobrr-rss-enclosure-type.patch ]; }); + # andref-ipfs-depot: our Rust binary (Discord-gated IPFS uploader), built by its own crane flake + # for x86_64-linux. The frontend assets are compiled into the binary (include_str!), so the image + # needs nothing but the binary + its runtime closure + TLS roots + an init. + andrefIpfsDepotBin = inputs.andref-ipfs-depot.packages.x86_64-linux.default; + # Creates an attrset with two system-keyed targets: the x86_64-linux image and a # per-host script to push it to the docker registry. # @param name: The name of the docker repository for the image. @@ -191,6 +196,30 @@ }; }; + # andref-ipfs-depot (Discord-gated IPFS uploader). Wraps the crane-built Rust binary above in a + # minimal layered image: dumb-init is PID 1 so k8s SIGTERM stops the pod promptly; cacert + + # SSL_CERT_FILE give the serenity bot's HTTPS calls to Discord a CA bundle. Listens on :8080 + # (matches lib/andref-ipfs-depot.libsonnet's containerPort + BIND_ADDR). See + # milky-way/lib/andref-ipfs-depot.libsonnet. + andref-ipfs-depot = image-nix-artifacts { + name = "andref-ipfs-depot"; + buildLayeredImageArg = { + tag = "latest"; + contents = [ + andrefIpfsDepotBin + imagePkgs.cacert + imagePkgs.dumb-init + ]; + config = { + Entrypoint = [ "dumb-init" "--" "${andrefIpfsDepotBin}/bin/andref-ipfs-depot" ]; + Env = [ + "SSL_CERT_FILE=${imagePkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + ]; + ExposedPorts = { "8080/tcp" = {}; }; + }; + }; + }; + # `nix develop` target for a long-lived `skopeo login`. Uses the same skopeo (and # nixpkgs) as the push-scripts, so the auth.json written here is always compatible. mkAuthShell = pkgs: pkgs.mkShell { @@ -211,12 +240,15 @@ in { grand-central-push = grand-central.push-script.x86_64-linux; autobrr-image = autobrr.image.x86_64-linux; autobrr-push = autobrr.push-script.x86_64-linux; + andref-ipfs-depot-image = andref-ipfs-depot.image.x86_64-linux; + andref-ipfs-depot-push = andref-ipfs-depot.push-script.x86_64-linux; }; aarch64-darwin = { whale-push-example = example-artifacts.push-script.aarch64-darwin; mopidy-push = mopidy.push-script.aarch64-darwin; grand-central-push = grand-central.push-script.aarch64-darwin; autobrr-push = autobrr.push-script.aarch64-darwin; + andref-ipfs-depot-push = andref-ipfs-depot.push-script.aarch64-darwin; }; }; -- 2.51.2