From 12992186a9d55b2c0f0585f7d7c686898834583a Mon Sep 17 00:00:00 2001 From: Maofeng Date: Mon, 27 Jul 2026 05:01:26 +0800 Subject: [PATCH] feat(console): add runtime management and WIT Registry - embed wasmeld-runtime behind an Axum management API - persist service metadata, metrics, and events with Toasty and libSQL - register immutable component artifacts with platform-owned limits - publish, list, inspect, and download binary WIT package versions - verify Runtime lifecycle, persistence, Registry immutability, and transitive fetch --- Cargo.lock | 2391 +++++++++++++++++++- Cargo.toml | 9 + crates/wasmeld-console/Cargo.toml | 27 + crates/wasmeld-console/README.md | 65 + crates/wasmeld-console/src/lib.rs | 1384 +++++++++++ crates/wasmeld-console/src/main.rs | 65 + crates/wasmeld-console/src/persistence.rs | 101 + crates/wasmeld-console/src/wit_registry.rs | 338 +++ crates/wasmeld-console/tests/api.rs | 618 +++++ 9 files changed, 4976 insertions(+), 22 deletions(-) create mode 100644 crates/wasmeld-console/Cargo.toml create mode 100644 crates/wasmeld-console/README.md create mode 100644 crates/wasmeld-console/src/lib.rs create mode 100644 crates/wasmeld-console/src/main.rs create mode 100644 crates/wasmeld-console/src/persistence.rs create mode 100644 crates/wasmeld-console/src/wit_registry.rs create mode 100644 crates/wasmeld-console/tests/api.rs diff --git a/Cargo.lock b/Cargo.lock index a0cb10e..b2b86c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,12 +17,72 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aegis" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58541132f980da31e9aa99f7bdee69bc84bf1e168b9b91ef2dbe8abb7b4ce5dd" +dependencies = [ + "cc", + "softaes", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + [[package]] name = "ambient-authority" version = "0.0.2" @@ -38,6 +98,22 @@ dependencies = [ "libc", ] +[[package]] +name = "antithesis_sdk" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08410fcac93669a476c006cd6c4512ac1e2b30fd117231a5d55d8a2c76599b82" +dependencies = [ + "libc", + "libloading", + "linkme", + "once_cell", + "rand 0.8.7", + "rustc_version_runtime", + "serde", + "serde_json", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -53,6 +129,41 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "aristo" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdeefa800110050103e2459a2f8dbdbd560ad046e30f1f87e24ce19c2cb8bde" +dependencies = [ + "aristo-macros", +] + +[[package]] +name = "aristo-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64a66d21a80182b35b1741997a6d2456911f54b7eb1918aa4e2382fc205268d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "assoc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8" + [[package]] name = "async-trait" version = "0.1.91" @@ -82,6 +193,12 @@ dependencies = [ "topological-sort", ] +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "aws-lc-rs" version = "1.17.3" @@ -105,18 +222,153 @@ dependencies = [ "pkg-config", ] +[[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 = "base64" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex 1.3.0", + "syn 2.0.119", + "which", +] + +[[package]] +name = "bit-set" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" +dependencies = [ + "borsh", + "serde", +] + [[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "bitpacking" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" +dependencies = [ + "crunchy", +] + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -126,15 +378,105 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "branches" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fba76cd916045514e3064707df9ed5282b94419444c2753a6c62ecaf458e56" +dependencies = [ + "rustc_version", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", ] +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.1" @@ -228,7 +570,22 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", +] + +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", ] [[package]] @@ -243,6 +600,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "cfg_block" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18758054972164c3264f7c8386f5fc6da6114cb46b619fd365d4e3b2dc3ae487" + [[package]] name = "chacha20" version = "0.10.1" @@ -254,11 +617,45 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clock-probe-component" version = "0.1.0" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.41.0", ] [[package]] @@ -289,6 +686,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -309,7 +715,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" name = "counter-component" version = "0.1.0" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.41.0", ] [[package]] @@ -387,7 +793,7 @@ dependencies = [ "log", "pulley-interpreter", "regalloc2", - "rustc-hash", + "rustc-hash 2.1.3", "serde", "smallvec", "target-lexicon", @@ -468,6 +874,15 @@ version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -477,12 +892,46 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -490,9 +939,88 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "datasketches" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c286de4e81ea2590afc24d754e0f83810c566f50a1388fa75ebd57928c0d9745" + +[[package]] +name = "deadpool" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" +dependencies = [ + "tokio", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -525,6 +1053,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "dunce" version = "1.0.5" @@ -535,7 +1069,7 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" name = "echo-component" version = "0.1.0" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.41.0", ] [[package]] @@ -565,12 +1099,43 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "env_filter", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -587,6 +1152,23 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "siphasher", +] + +[[package]] +name = "fastdivide" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" + [[package]] name = "fastrand" version = "2.5.0" @@ -597,7 +1179,7 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" name = "fault-component" version = "0.1.0" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.41.0", ] [[package]] @@ -627,12 +1209,24 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -653,12 +1247,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.33" @@ -747,6 +1357,36 @@ dependencies = [ "slab", ] +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "genawaiter-macro", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -770,6 +1410,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -779,11 +1431,21 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.32.3" @@ -795,21 +1457,43 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", "serde", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2 0.2.21", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2 0.2.21", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -817,6 +1501,33 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "1.4.2" @@ -856,6 +1567,12 @@ 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.11.0" @@ -869,6 +1586,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -897,7 +1615,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -938,6 +1656,31 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bbdf98e5e0aa827770acee171ebb568aaab975a36b56afdb5fd0e2f525750bb" +dependencies = [ + "icu_collator_data", + "icu_collections", + "icu_locale", + "icu_locale_core", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038ed8e5817f2059c2f3efb0945ba78d060d3d25e8f1a1bea5139f821a21a2f0" + [[package]] name = "icu_collections" version = "2.2.0" @@ -952,6 +1695,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locale" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.2.0" @@ -960,11 +1718,18 @@ checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + [[package]] name = "icu_normalizer" version = "2.2.0" @@ -976,6 +1741,9 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] @@ -1013,6 +1781,8 @@ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1026,6 +1796,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1047,6 +1823,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "index_vec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44faf5bb8861a9c72e20d3fb0fdbd59233e43056e2b80475ab0aacdc2e781355" + [[package]] name = "indexmap" version = "2.14.0" @@ -1059,6 +1841,33 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "intrusive-collections" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +dependencies = [ + "memoffset", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "io-extras" version = "0.18.4" @@ -1075,12 +1884,32 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" +[[package]] +name = "io-uring" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1166,24 +1995,81 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1202,18 +2088,55 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lz4_flex" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" + [[package]] name = "mach2" version = "0.4.3" @@ -1223,12 +2146,36 @@ dependencies = [ "libc", ] +[[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 = "maybe-owned" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" +[[package]] +name = "measure_time" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" +dependencies = [ + "log", +] + [[package]] name = "memchr" version = "2.8.3" @@ -1244,6 +2191,55 @@ dependencies = [ "rustix 1.1.4", ] +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -1260,6 +2256,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1281,6 +2283,92 @@ dependencies = [ "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 = "murmurhash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.37.3" @@ -1299,12 +2387,86 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oneshot" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ownedbytes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "pack1" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b7bb0ecf2e447b1f20ee94ee79ef6eed1e9d4b3c36ce1903b9dea3bf205523" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1323,6 +2485,42 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "pluralizer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "postcard" version = "1.1.3" @@ -1341,9 +2539,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ + "serde_core", + "writeable", "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" @@ -1363,6 +2569,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1372,6 +2587,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pulley-interpreter" version = "41.0.0" @@ -1406,7 +2644,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror", @@ -1426,9 +2664,9 @@ dependencies = [ "getrandom 0.4.3", "lru-slab", "rand 0.10.2", - "rand_pcg", + "rand_pcg 0.10.2", "ring", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", @@ -1461,12 +2699,24 @@ 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 = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.7" @@ -1474,10 +2724,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" @@ -1499,6 +2759,16 @@ dependencies = [ "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" @@ -1508,12 +2778,30 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand_pcg" version = "0.10.2" @@ -1523,27 +2811,94 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rapidhash" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regalloc2" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", "bumpalo", "hashbrown 0.15.5", "log", - "rustc-hash", + "rustc-hash 2.1.3", "smallvec", ] +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "reqwest" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1569,7 +2924,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower", - "tower-http", + "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", @@ -1591,6 +2946,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roaring" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" +dependencies = [ + "bytemuck", + "byteorder", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1606,6 +2987,16 @@ dependencies = [ "semver", ] +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1723,6 +3114,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -1741,6 +3138,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "3.7.0" @@ -1817,6 +3226,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -1826,6 +3246,24 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -1837,12 +3275,57 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "shuttle" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab17edba38d63047f46780cf7360acf7467fec2c048928689a5c1dd1c2b4e31" +dependencies = [ + "assoc", + "bitvec", + "cfg-if", + "generator", + "hex", + "owo-colors", + "rand 0.8.7", + "rand_core 0.6.4", + "rand_pcg 0.3.1", + "scoped-tls", + "smallvec", + "tracing", +] + +[[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.10" @@ -1865,6 +3348,30 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simsimd" +version = "6.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fb3bc3cdce07a7d7d4caa4c54f8aa967f6be41690482b54b24100a2253fa70" +dependencies = [ + "cc", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "sketches-ddsketch" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e40b6cf54d988dc1a2223531b969c9a9e30906ad90ef64890c27b4bfbb46ea" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.12" @@ -1890,6 +3397,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "softaes" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e14297decde697ddf377c25752aead0927d5cfc89c2684d2af96901a4ceeea" + [[package]] name = "spdx" version = "0.10.9" @@ -1899,11 +3412,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spin-component" version = "0.1.0" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.41.0", ] [[package]] @@ -1912,12 +3431,46 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "2.0.119" @@ -1976,6 +3529,160 @@ dependencies = [ "winx", ] +[[package]] +name = "tantivy" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edde6a10743fff00a4e1a8c9ef020bf5f3cbad301b7d2d39f2b07f123c4eac07" +dependencies = [ + "aho-corasick", + "arc-swap", + "base64 0.22.1", + "bitpacking", + "bon", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "datasketches", + "downcast-rs", + "fastdivide", + "fnv", + "fs4", + "htmlescape", + "itertools 0.14.0", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash 2.1.3", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time", + "typetag", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fed3d674429bcd2de5d0a6d1aa5495fed8afd9c5ecce993019caf7615f53fa4" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc" +dependencies = [ + "downcast-rs", + "fastdivide", + "itertools 0.14.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf10915aa75da3c3b0d58b58853d2e889efbaf32d4982a4c3715dde6bba23e5" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" +dependencies = [ + "byteorder", + "regex-syntax", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfadb8526b6da90704feb293b0701a6aae62ea14983143344be2dc5ce30f1d82" +dependencies = [ + "fnv", + "nom", + "ordered-float", + "serde", + "serde_json", +] + +[[package]] +name = "tantivy-sstable" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2cfc3ac5164cbadc28965ffb145a8f47582a60ae5897859ad8d4316596c606" +dependencies = [ + "futures-util", + "itertools 0.14.0", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-fst", + "zstd", +] + +[[package]] +name = "tantivy-stacker" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbb051742da9d53ca9e8fff43a9b10e319338b24e2c0e15d0372df19ffeb951" +dependencies = [ + "murmurhash32", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac258c2c6390673f2685813afeeafcb8c4e0ee7de8dd3fc46838dcc37263f98" +dependencies = [ + "serde", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.13.5" @@ -2024,6 +3731,45 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -2031,6 +3777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -2049,6 +3796,89 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toasty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2abf272caf351f58e03758745b6185576fcfe7a513b007683c2f8eec698eed2" +dependencies = [ + "async-trait", + "bit-set", + "by_address", + "deadpool", + "hashbrown 0.17.1", + "index_vec", + "indexmap", + "inventory", + "toasty-core", + "toasty-macros", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "toasty-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1650657e3784097859a1c31cf91b48e337f85684757e0c6ad44e887f0e009f" +dependencies = [ + "async-trait", + "bit-set", + "hashbrown 0.17.1", + "heck", + "indexmap", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "toasty-driver-turso" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cc0682b65eeab9b7d6e0bac3f309591df5b9b6df32f868a3a6ee5ca3636e24" +dependencies = [ + "async-trait", + "percent-encoding", + "serde", + "toasty-core", + "toasty-sql", + "tokio", + "tracing", + "turso", + "url", + "uuid", +] + +[[package]] +name = "toasty-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16966e54a422be54959b0ad12bdbdcffc101c77aeca7d49fefff33142c709a29" +dependencies = [ + "hashbrown 0.17.1", + "heck", + "pluralizer", + "proc-macro2", + "quote", + "syn 3.0.3", + "toasty-core", +] + +[[package]] +name = "toasty-sql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b19948205b682d1750a8cf331b0b7fa666baffe2a3a2c0a05d5e94103c0fcc08" +dependencies = [ + "serde", + "serde_json", + "toasty-core", +] + [[package]] name = "tokio" version = "1.53.1" @@ -2058,11 +3888,25 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -2073,6 +3917,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -2082,7 +3937,7 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.15", @@ -2097,6 +3952,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -2131,6 +4007,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2151,6 +4028,23 @@ dependencies = [ "url", ] +[[package]] +name = "tower-http" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "percent-encoding", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2169,11 +4063,25 @@ 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-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -2192,6 +4100,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]] @@ -2200,12 +4138,253 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "turso" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6c49aecd4abae3ffa88ab1e28b3de7a7497a9732be828e3a6f1855b6ea80fd" +dependencies = [ + "mimalloc", + "thiserror", + "tracing", + "tracing-subscriber", + "turso_core", + "turso_sdk_kit", + "turso_sync_sdk_kit", +] + +[[package]] +name = "turso_core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d19014707ed1ad4eadba5bf27394983b92697fe22b93e679e4ce9157cf8493" +dependencies = [ + "aegis", + "aes", + "aes-gcm", + "allocator-api2 0.4.0", + "antithesis_sdk", + "arc-swap", + "aristo", + "bigdecimal", + "bitflags", + "branches", + "bumpalo", + "bytemuck", + "cfg_aliases", + "cfg_block", + "chrono", + "crc32c", + "crossbeam-epoch", + "crossbeam-utils", + "either", + "fallible-iterator", + "fastbloom", + "hex", + "icu_collator", + "icu_locale", + "intrusive-collections", + "io-uring", + "libc", + "libloading", + "libm", + "loom", + "miette", + "num-bigint", + "num-traits", + "pack1", + "parking_lot", + "pastey", + "polling", + "rand 0.9.5", + "rapidhash", + "regex", + "regex-syntax", + "roaring", + "rustc-hash 2.1.3", + "rustix 1.1.4", + "ryu", + "serde_json", + "shuttle", + "simsimd", + "smallvec", + "strum", + "strum_macros", + "tantivy", + "tempfile", + "thiserror", + "tracing", + "tracing-subscriber", + "turso_ext", + "turso_macros", + "turso_parser", + "twox-hash", + "uncased", + "uuid", + "windows-sys 0.61.2", +] + +[[package]] +name = "turso_ext" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f075537ef0eef76bb25fb297cb5698e6582b42f4f4c082b88cd0dc6248d0ac53" +dependencies = [ + "chrono", + "getrandom 0.4.3", + "turso_macros", +] + +[[package]] +name = "turso_macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cf1ef8779d07a7bd8dc3d34a3e8cc6f59e929ef74aedf2bf8b2dfceda59d7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "turso_parser" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae794301cc7490b2fa0af5ef2f175aaba2b69e2abbc9bbaac2c91a68a04d075f" +dependencies = [ + "bitflags", + "memchr", + "miette", + "strum", + "strum_macros", + "thiserror", + "turso_macros", +] + +[[package]] +name = "turso_sdk_kit" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c4060696c394c0e38382f1757262c2e75bd7c65a139fddf8331d49f874cfe5d" +dependencies = [ + "bindgen", + "env_logger", + "parking_lot", + "tracing", + "tracing-appender", + "tracing-subscriber", + "turso_core", + "turso_ext", + "turso_sdk_kit_macros", +] + +[[package]] +name = "turso_sdk_kit_macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3478f8f4e06133fd218ba93039765c096a7126636bca9b0893f79bcfd41adaf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "turso_sync_engine" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341090cbeeb2866073b0a29c5279b138c8b8a67a85fbf636b26e9a11170988db" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc32c", + "genawaiter", + "http", + "libc", + "prost", + "roaring", + "serde", + "serde_json", + "thiserror", + "tracing", + "turso_core", + "turso_parser", + "uuid", +] + +[[package]] +name = "turso_sync_sdk_kit" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f51333f96fbef3c1c9bd7831b365d21e4078785d1c7f8dbd30dd338a30eb8b" +dependencies = [ + "bindgen", + "env_logger", + "genawaiter", + "parking_lot", + "tracing", + "tracing-appender", + "tracing-subscriber", + "turso_core", + "turso_sdk_kit", + "turso_sdk_kit_macros", + "turso_sync_engine", +] + +[[package]] +name = "twox-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" +dependencies = [ + "rand 0.10.2", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "typetag" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicase" version = "2.9.0" @@ -2218,12 +4397,28 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -2242,12 +4437,44 @@ dependencies = [ "serde", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "rand 0.10.2", + "serde_core", + "sha1_smol", + "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" @@ -2283,7 +4510,16 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" name = "wasi-clock-probe-component" version = "0.1.0" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.41.0", +] + +[[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 0.57.1", ] [[package]] @@ -2392,6 +4628,29 @@ dependencies = [ "wasmparser 0.243.0", ] +[[package]] +name = "wasmeld-console" +version = "0.1.0" +dependencies = [ + "axum", + "base64 0.23.0", + "semver", + "serde", + "serde_json", + "tempfile", + "thiserror", + "toasty", + "toasty-driver-turso", + "tokio", + "toml", + "tower", + "tower-http 0.7.0", + "tracing", + "tracing-subscriber", + "wasmeld-package", + "wasmeld-runtime", +] + [[package]] name = "wasmeld-package" version = "0.1.0" @@ -2562,7 +4821,7 @@ dependencies = [ "cranelift-frontend", "cranelift-native", "gimli", - "itertools", + "itertools 0.14.0", "log", "object", "pulley-interpreter", @@ -2754,6 +5013,34 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2763,6 +5050,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "winch-codegen" version = "41.0.0" @@ -2944,6 +5237,9 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] [[package]] name = "winx" @@ -2965,6 +5261,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.41.0" @@ -3092,12 +5394,27 @@ dependencies = [ "wasmparser 0.243.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yoke" version = "0.8.3" @@ -3177,6 +5494,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] @@ -3185,6 +5503,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -3235,3 +5554,31 @@ dependencies = [ "log", "simd-adler32", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 0c4b948..58ce0f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/wasmeld-package", "crates/wasmeld-runtime", + "crates/wasmeld-console", "components/echo", "components/counter", "components/fault", @@ -18,13 +19,21 @@ rust-version = "1.90" license = "Apache-2.0" [workspace.dependencies] +axum = { version = "0.8.9", features = ["multipart"] } +base64 = "0.23.0" reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "json", "multipart", "rustls"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" semver = "1.0.28" sha2 = "0.10.9" thiserror = "2.0.17" +toasty = "=0.9.0" +toasty-driver-turso = "=0.9.0" +tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } toml = "0.9.8" +tower-http = { version = "0.7.0", features = ["cors", "trace"] } +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] } wasmtime = { version = "=41.0.0", default-features = false, features = ["component-model", "cranelift", "runtime", "std"] } wasmtime-wasi = { version = "=41.0.0", default-features = false, features = ["p2"] } wit-bindgen = "=0.41.0" diff --git a/crates/wasmeld-console/Cargo.toml b/crates/wasmeld-console/Cargo.toml new file mode 100644 index 0000000..688c938 --- /dev/null +++ b/crates/wasmeld-console/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "wasmeld-console" +version = "0.1.0" +edition.workspace = true +rust-version = "1.95" +license.workspace = true + +[dependencies] +axum.workspace = true +base64.workspace = true +serde.workspace = true +serde_json.workspace = true +semver.workspace = true +thiserror.workspace = true +toasty.workspace = true +toasty-driver-turso.workspace = true +tokio.workspace = true +toml.workspace = true +tower-http.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +wasmeld-package = { path = "../wasmeld-package" } +wasmeld-runtime = { path = "../wasmeld-runtime" } + +[dev-dependencies] +tempfile = "3.23.0" +tower = { version = "0.5.2", features = ["util"] } diff --git a/crates/wasmeld-console/README.md b/crates/wasmeld-console/README.md new file mode 100644 index 0000000..a4088b8 --- /dev/null +++ b/crates/wasmeld-console/README.md @@ -0,0 +1,65 @@ +# Wasmeld Console + +`wasmeld-console` 是 `wasmeld-runtime` 的本地 HTTP 管理适配器。它负责: + +- 接收 `.wasmpkg`,校验并保存 `component.wasm` 与运行时 manifest +- 发布、查询和下载不可变的二进制 WIT Package +- 启动、停止和重启内嵌的 `wasmeld-runtime` +- 启动、停止和重启常驻 Actor +- 转发二进制调用 +- 暴露调用计数、状态和最近事件 + +Wasm 制品保存在本地文件系统。服务 manifest、调用计数和最近 256 条事件通过 +[Toasty](https://github.com/tokio-rs/toasty) 写入本地 libSQL 数据库,默认路径是 +`var/wasmeld/console.db`。 + +`wasmeld-console` 启动时会在同一进程内创建 `wasmeld-runtime`、加载 Wasmtime Engine, +并重新注册已保存的 Component。Runtime 停止后 Console HTTP 和数据库仍然在线; +再次启动 Runtime 会重新注册 Component,但 Actor 内存不做快照,服务运行状态统一 +恢复为 `stopped`。 + +## 启动 + +`wasmeld-console` 需要 Rust 1.95 或更高版本;Wasm 组件继续使用项目约定的 Rust 1.90 +工具链构建。 + +```bash +cargo +stable run -p wasmeld-console +``` + +默认监听 `127.0.0.1:8080`。 + +可用环境变量: + +- `WASMELD_ADDR` +- `WASMELD_ARTIFACT_DIR` +- `WASMELD_DATABASE_PATH` +- `WASMELD_WIT_REGISTRY_DIR` +- `WASMELD_ALLOWED_ORIGINS`,多个 Origin 使用逗号分隔 +- `RUST_LOG` + +## API + +| 方法 | 路径 | 用途 | +| --- | --- | --- | +| `GET` | `/healthz` | 健康检查 | +| `GET` | `/api/v1/runtime` | Runtime 状态 | +| `POST` | `/api/v1/runtime/start` | 启动 Runtime 并重新注册受管 Component | +| `POST` | `/api/v1/runtime/stop` | 停止 Runtime 并释放所有 Actor | +| `POST` | `/api/v1/runtime/restart` | 重建 Runtime,服务恢复为停止状态 | +| `GET` | `/api/v1/services` | 服务版本列表 | +| `POST` | `/api/v1/services` | multipart 上传单个 `package` 字段(`.wasmpkg`) | +| `POST` | `/api/v1/services/{id}/{revision}/start` | 启动 Actor | +| `POST` | `/api/v1/services/{id}/{revision}/stop` | 停止 Actor | +| `POST` | `/api/v1/services/{id}/{revision}/restart` | 重启 Actor | +| `POST` | `/api/v1/services/{id}/{revision}/invoke` | Base64 二进制调用 | +| `GET` | `/api/v1/events` | 最近 256 条持久化事件 | +| `GET` | `/api/v1/wit/packages` | WIT 包版本列表 | +| `POST` | `/api/v1/wit/packages` | multipart 发布单个二进制 WIT `package` 字段 | +| `GET` | `/api/v1/wit/packages/{namespace}/{name}/{version}` | WIT 包元数据 | +| `GET` | `/api/v1/wit/packages/{namespace}/{name}/{version}/content` | 下载 WIT 包 | + +组件包的构建与格式说明见 +[`docs/design/wasmeld-component-package.md`](../../docs/design/wasmeld-component-package.md)。 +WIT 依赖、Registry 和本地 replace 说明见 +[`docs/design/wit-package-management.md`](../../docs/design/wit-package-management.md)。 diff --git a/crates/wasmeld-console/src/lib.rs b/crates/wasmeld-console/src/lib.rs new file mode 100644 index 0000000..4d5e935 --- /dev/null +++ b/crates/wasmeld-console/src/lib.rs @@ -0,0 +1,1384 @@ +//! HTTP management adapter for [`wasmeld_runtime`]. +//! +//! The console persists component artifacts and manifests on the local +//! filesystem. Service metadata, invocation metrics, and events are persisted +//! to libSQL through Toasty. It also hosts an immutable filesystem-backed WIT +//! package Registry. Actor memory remains process-local and is never presented +//! as durable state. + +mod persistence; +mod wit_registry; + +use std::{ + collections::{BTreeMap, VecDeque}, + fs, io, + path::{Path, PathBuf}, + sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard}, + time::{Instant, SystemTime, UNIX_EPOCH}, +}; + +use axum::{ + Json, Router, + body::Body, + extract::{DefaultBodyLimit, Multipart, Path as AxumPath, State}, + http::{HeaderValue, Method, StatusCode, header}, + response::{IntoResponse, Response}, + routing::{get, post}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use thiserror::Error; +use tower_http::{ + cors::CorsLayer, + trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}, +}; +use tracing::Level; +use wasmeld_package::{ + ComponentPackage, PackageError, read_package, + wit_package::{WitPackageError, WitPackageMetadata}, +}; +use wasmeld_runtime::{ + ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceKey, ServiceManifest, +}; + +use persistence::{Persistence, StoredEvent, StoredService}; +use wit_registry::WitRegistry; +pub use wit_registry::WitRegistryError; + +const MAX_EVENTS: usize = 256; +const DEFAULT_MAX_ARTIFACT_BYTES: usize = 64 * 1024 * 1024; +const DEFAULT_MAX_WIT_PACKAGE_BYTES: usize = 4 * 1024 * 1024; + +/// Filesystem, persistence, upload, and Runtime policy used by [`Console`]. +#[derive(Clone, Debug)] +pub struct ConsoleConfig { + /// Directory for registered Component binaries and generated manifests. + pub artifact_dir: PathBuf, + /// Directory for immutable binary WIT Registry artifacts. + pub wit_registry_dir: PathBuf, + /// Local libSQL database file. + pub database_path: PathBuf, + /// Maximum accepted `.wasmpkg` and uncompressed Component size. + pub max_artifact_bytes: usize, + /// Maximum accepted binary WIT package size. + pub max_wit_package_bytes: usize, + /// Platform-owned limits injected into every registered service. + pub registration_limits: ResourceLimits, + /// Wasmtime Engine and epoch settings. + pub runtime: RuntimeConfig, +} + +impl Default for ConsoleConfig { + fn default() -> Self { + Self { + artifact_dir: PathBuf::from("var/wasmeld/components"), + wit_registry_dir: PathBuf::from("var/wasmeld/wit-packages"), + database_path: PathBuf::from("var/wasmeld/console.db"), + max_artifact_bytes: DEFAULT_MAX_ARTIFACT_BYTES, + max_wit_package_bytes: DEFAULT_MAX_WIT_PACKAGE_BYTES, + registration_limits: ResourceLimits::default(), + runtime: RuntimeConfig::default(), + } + } +} + +/// Management state that owns one optional Runtime and its durable control data. +/// +/// Runtime stop/start operations replace the in-process Wasmtime state while +/// registered service metadata, artifacts, metrics, and events remain managed +/// by the Console. +pub struct Console { + runtime: RwLock>, + runtime_config: RuntimeConfig, + artifact_dir: PathBuf, + wit_registry: WitRegistry, + max_artifact_bytes: usize, + registration_limits: ResourceLimits, + lifecycle: Mutex<()>, + state: Mutex, + persistence: Persistence, + persistence_sync: tokio::sync::Mutex<()>, +} + +struct ConsoleState { + runtime_started_at: Option, + next_event_id: u64, + services: BTreeMap, + events: VecDeque, +} + +#[derive(Clone)] +struct ServiceRecord { + manifest: ServiceManifest, + status: ServiceStatus, + updated_at_ms: u64, + calls: u64, + errors: u64, + last_latency_ms: Option, +} + +/// Lifecycle status exposed for one registered service version. +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ServiceStatus { + Running, + Stopped, + Faulted, +} + +/// API projection of a registered service and its current metrics. +#[derive(Clone, Debug, Serialize)] +pub struct ServiceView { + pub id: String, + pub revision: String, + pub artifact: String, + pub world: String, + pub status: ServiceStatus, + pub updated_at_ms: u64, + pub limits: ResourceLimits, + pub calls: u64, + pub errors: u64, + pub last_latency_ms: Option, +} + +impl ServiceRecord { + fn view(&self) -> ServiceView { + ServiceView { + id: self.manifest.id.clone(), + revision: self.manifest.revision.clone(), + artifact: self.manifest.component.clone(), + world: self.manifest.world.clone(), + status: self.status, + updated_at_ms: self.updated_at_ms, + limits: self.manifest.limits.clone(), + calls: self.calls, + errors: self.errors, + last_latency_ms: self.last_latency_ms, + } + } +} + +/// One persisted Console lifecycle or invocation event. +#[derive(Clone, Debug, Serialize)] +pub struct EventView { + pub id: u64, + pub timestamp_ms: u64, + pub kind: EventKind, + pub service_id: Option, + pub revision: Option, + pub message: String, +} + +/// Event category serialized by the management API. +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum EventKind { + Registered, + Started, + Stopped, + Invoked, + Failed, +} + +impl EventKind { + fn as_database_value(self) -> &'static str { + match self { + Self::Registered => "registered", + Self::Started => "started", + Self::Stopped => "stopped", + Self::Invoked => "invoked", + Self::Failed => "failed", + } + } + + fn from_database_value(value: &str) -> Result { + match value { + "registered" => Ok(Self::Registered), + "started" => Ok(Self::Started), + "stopped" => Ok(Self::Stopped), + "invoked" => Ok(Self::Invoked), + "failed" => Ok(Self::Failed), + _ => Err(ConsoleError::InvalidDatabaseData(format!( + "unsupported event kind {value:?}" + ))), + } + } +} + +impl TryFrom for EventView { + type Error = ConsoleError; + + fn try_from(event: StoredEvent) -> Result { + Ok(Self { + id: event.id, + timestamp_ms: event.timestamp_ms, + kind: EventKind::from_database_value(&event.kind)?, + service_id: event.service_id, + revision: event.revision, + message: event.message, + }) + } +} + +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct StartRequest { + init_config_base64: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct InvokeRequest { + input_base64: String, +} + +#[derive(Debug, Serialize)] +struct ServiceList { + services: Vec, +} + +#[derive(Debug, Serialize)] +struct EventList { + events: Vec, +} + +#[derive(Debug, Serialize)] +struct WitPackageList { + packages: Vec, +} + +#[derive(Debug, Serialize)] +struct RuntimeView { + status: RuntimeStatus, + uptime_ms: u64, + managed_services: usize, + registered_services: usize, + running_services: usize, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +enum RuntimeStatus { + Running, + Stopped, +} + +#[derive(Debug, Serialize)] +struct InvokeResponse { + output_base64: String, + output_bytes: usize, + latency_ms: u64, +} + +/// Errors raised by Console state, persistence, Registry, or Runtime operations. +#[derive(Debug, Error)] +pub enum ConsoleError { + #[error("{0}")] + InvalidRequest(String), + + #[error("service revision {0} is not managed by Wasmeld")] + ServiceNotFound(ServiceKey), + + #[error("Wasmeld Runtime is not running")] + RuntimeNotRunning, + + #[error("artifact exceeds the {limit}-byte limit")] + ArtifactTooLarge { limit: usize }, + + #[error(transparent)] + Package(#[from] PackageError), + + #[error(transparent)] + WitRegistry(#[from] WitRegistryError), + + #[error("failed to access {path}: {source}")] + Storage { + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error("failed to serialize manifest: {0}")] + ManifestSerialize(#[from] toml::ser::Error), + + #[error("database operation failed: {0}")] + Database(#[from] toasty::Error), + + #[error("database contains invalid console data: {0}")] + InvalidDatabaseData(String), + + #[error(transparent)] + Runtime(#[from] RuntimeError), + + #[error("blocking runtime task failed: {0}")] + Join(#[from] tokio::task::JoinError), + + #[error("console state lock was poisoned")] + LockPoisoned, +} + +impl Console { + /// Opens durable stores, restores service metadata, and starts a fresh Runtime. + /// + /// Persisted actors are deliberately restored as stopped services because + /// their Store and linear memory cannot be reconstructed from control data. + pub async fn new(config: ConsoleConfig) -> Result { + config.registration_limits.validate()?; + fs::create_dir_all(&config.artifact_dir).map_err(|source| ConsoleError::Storage { + path: config.artifact_dir.clone(), + source, + })?; + let artifact_dir = + fs::canonicalize(&config.artifact_dir).map_err(|source| ConsoleError::Storage { + path: config.artifact_dir.clone(), + source, + })?; + if let Some(parent) = config.database_path.parent() { + fs::create_dir_all(parent).map_err(|source| ConsoleError::Storage { + path: parent.to_path_buf(), + source, + })?; + } + let persistence = Persistence::open(&config.database_path).await?; + let wit_registry = + WitRegistry::open(&config.wit_registry_dir, config.max_wit_package_bytes)?; + let (stored_services, mut stored_events) = persistence.load().await?; + stored_events.sort_unstable_by_key(|event| std::cmp::Reverse(event.id)); + stored_events.truncate(MAX_EVENTS); + let next_event_id = stored_events + .first() + .map(|event| event.id.saturating_add(1)) + .unwrap_or(1); + let events = stored_events + .into_iter() + .map(EventView::try_from) + .collect::, _>>()?; + + let runtime = Runtime::new(config.runtime.clone())?; + let console = Self { + runtime: RwLock::new(Some(runtime)), + runtime_config: config.runtime, + artifact_dir, + wit_registry, + max_artifact_bytes: config.max_artifact_bytes, + registration_limits: config.registration_limits, + lifecycle: Mutex::new(()), + state: Mutex::new(ConsoleState { + runtime_started_at: Some(Instant::now()), + next_event_id, + services: BTreeMap::new(), + events, + }), + persistence, + persistence_sync: tokio::sync::Mutex::new(()), + }; + // The database is authoritative for metrics. Filesystem manifests are + // then used to recover artifacts that predate or missed a DB snapshot. + console.load_stored_services(stored_services)?; + console.load_manifest_services()?; + console.persist_snapshot().await?; + Ok(console) + } + + /// Returns the configured component upload limit. + pub fn max_artifact_bytes(&self) -> usize { + self.max_artifact_bytes + } + + /// Returns the configured binary WIT package upload limit. + pub fn max_wit_package_bytes(&self) -> usize { + self.wit_registry.max_package_bytes() + } + + /// Returns registered service versions in stable key order. + pub fn services(&self) -> Result, ConsoleError> { + let state = self.state()?; + Ok(state.services.values().map(ServiceRecord::view).collect()) + } + + /// Returns recent events from newest to oldest. + pub fn events(&self) -> Result, ConsoleError> { + let state = self.state()?; + Ok(state.events.iter().cloned().collect()) + } + + fn runtime_view(&self) -> Result { + let runtime = self.runtime()?; + let state = self.state()?; + match runtime.as_ref() { + Some(runtime) => { + let stats = runtime.stats()?; + Ok(RuntimeView { + status: RuntimeStatus::Running, + uptime_ms: state + .runtime_started_at + .map(|started_at| duration_ms(started_at.elapsed())) + .unwrap_or_default(), + managed_services: state.services.len(), + registered_services: stats.registered_services, + running_services: stats.running_actors, + }) + } + None => Ok(RuntimeView { + status: RuntimeStatus::Stopped, + uptime_ms: 0, + managed_services: state.services.len(), + registered_services: 0, + running_services: 0, + }), + } + } + + fn start_runtime(&self) -> Result { + let _lifecycle = self + .lifecycle + .lock() + .map_err(|_| ConsoleError::LockPoisoned)?; + let mut runtime = self + .runtime + .write() + .map_err(|_| ConsoleError::LockPoisoned)?; + + if runtime.is_some() { + drop(runtime); + return self.runtime_view(); + } + + match self.build_runtime() { + Ok(started) => { + *runtime = Some(started); + drop(runtime); + self.mark_runtime_started()?; + self.push_event( + EventKind::Started, + None, + "Wasmeld Runtime started".to_owned(), + )?; + self.runtime_view() + } + Err(error) => { + drop(runtime); + self.push_event( + EventKind::Failed, + None, + format!("Wasmeld Runtime failed to start: {error}"), + )?; + Err(error) + } + } + } + + fn stop_runtime(&self) -> Result { + let _lifecycle = self + .lifecycle + .lock() + .map_err(|_| ConsoleError::LockPoisoned)?; + let mut runtime = self + .runtime + .write() + .map_err(|_| ConsoleError::LockPoisoned)?; + let Some(running) = runtime.take() else { + drop(runtime); + return self.runtime_view(); + }; + let stopped = running.stop_all(); + drop(runtime); + self.mark_runtime_stopped()?; + + match stopped { + Ok(()) => { + self.push_event( + EventKind::Stopped, + None, + "Wasmeld Runtime stopped".to_owned(), + )?; + self.runtime_view() + } + Err(error) => { + self.push_event( + EventKind::Failed, + None, + format!("Wasmeld Runtime stopped with an actor shutdown error: {error}"), + )?; + Err(error.into()) + } + } + } + + fn restart_runtime(&self) -> Result { + let _lifecycle = self + .lifecycle + .lock() + .map_err(|_| ConsoleError::LockPoisoned)?; + let mut runtime = self + .runtime + .write() + .map_err(|_| ConsoleError::LockPoisoned)?; + + if let Some(running) = runtime.take() + && let Err(error) = running.stop_all() + { + drop(runtime); + self.mark_runtime_stopped()?; + self.push_event( + EventKind::Failed, + None, + format!("Wasmeld Runtime failed to restart while stopping actors: {error}"), + )?; + return Err(error.into()); + } + + match self.build_runtime() { + Ok(started) => { + *runtime = Some(started); + drop(runtime); + self.mark_runtime_started()?; + self.push_event( + EventKind::Started, + None, + "Wasmeld Runtime restarted".to_owned(), + )?; + self.runtime_view() + } + Err(error) => { + drop(runtime); + self.mark_runtime_stopped()?; + self.push_event( + EventKind::Failed, + None, + format!("Wasmeld Runtime failed to restart: {error}"), + )?; + Err(error) + } + } + } + + fn build_runtime(&self) -> Result { + let manifests = self + .state()? + .services + .values() + .map(|service| service.manifest.clone()) + .collect::>(); + let runtime = Runtime::new(self.runtime_config.clone())?; + for manifest in manifests { + runtime.register_from_file(manifest)?; + } + Ok(runtime) + } + + fn mark_runtime_started(&self) -> Result<(), ConsoleError> { + let mut state = self.state()?; + state.runtime_started_at = Some(Instant::now()); + for service in state.services.values_mut() { + service.status = ServiceStatus::Stopped; + } + Ok(()) + } + + fn mark_runtime_stopped(&self) -> Result<(), ConsoleError> { + let mut state = self.state()?; + let updated_at_ms = unix_time_ms(); + state.runtime_started_at = None; + for service in state.services.values_mut() { + if !matches!(service.status, ServiceStatus::Stopped) { + service.status = ServiceStatus::Stopped; + service.updated_at_ms = updated_at_ms; + } + } + Ok(()) + } + + fn register_package(&self, package_bytes: Vec) -> Result { + if package_bytes.len() > self.max_artifact_bytes { + return Err(ConsoleError::ArtifactTooLarge { + limit: self.max_artifact_bytes, + }); + } + let ComponentPackage { + manifest: package_manifest, + component: component_bytes, + } = read_package(&package_bytes, self.max_artifact_bytes)?; + + validate_path_segment("id", &package_manifest.id)?; + validate_path_segment("revision", &package_manifest.revision)?; + let key = ServiceKey::new( + package_manifest.id.clone(), + package_manifest.revision.clone(), + )?; + let _lifecycle = self + .lifecycle + .lock() + .map_err(|_| ConsoleError::LockPoisoned)?; + let runtime = self.runtime()?; + let runtime = runtime.as_ref().ok_or(ConsoleError::RuntimeNotRunning)?; + + if self.state()?.services.contains_key(&key) { + return Err(RuntimeError::ServiceAlreadyRegistered(key).into()); + } + + let basename = format!("{}@{}", package_manifest.id, package_manifest.revision); + let component_path = self.artifact_dir.join(format!("{basename}.wasm")); + let manifest_path = self.artifact_dir.join(format!("{basename}.toml")); + let manifest = ServiceManifest { + id: package_manifest.id, + revision: package_manifest.revision, + component: component_path.display().to_string(), + world: package_manifest.world, + limits: self.registration_limits.clone(), + }; + manifest.validate()?; + + write_atomic(&component_path, &component_bytes)?; + let manifest_toml = toml::to_string_pretty(&manifest)?; + if let Err(error) = write_atomic(&manifest_path, manifest_toml.as_bytes()) { + let _ = fs::remove_file(&component_path); + return Err(error); + } + + if let Err(error) = runtime.register(manifest.clone(), &component_bytes) { + let _ = fs::remove_file(&component_path); + let _ = fs::remove_file(&manifest_path); + return Err(error.into()); + } + + let view = self.insert_service(manifest, ServiceStatus::Stopped)?; + self.push_event( + EventKind::Registered, + Some(&key), + "component package registered".to_owned(), + )?; + Ok(view) + } + + fn start(&self, key: &ServiceKey, init_config: Vec) -> Result { + let runtime = self.runtime()?; + let runtime = runtime.as_ref().ok_or(ConsoleError::RuntimeNotRunning)?; + self.ensure_managed(key)?; + runtime.start(key, init_config)?; + let view = self.update_status(key, ServiceStatus::Running)?; + self.push_event(EventKind::Started, Some(key), "actor started".to_owned())?; + Ok(view) + } + + fn stop(&self, key: &ServiceKey) -> Result { + let runtime = self.runtime()?; + let runtime = runtime.as_ref().ok_or(ConsoleError::RuntimeNotRunning)?; + self.ensure_managed(key)?; + runtime.stop(key)?; + let view = self.update_status(key, ServiceStatus::Stopped)?; + self.push_event(EventKind::Stopped, Some(key), "actor stopped".to_owned())?; + Ok(view) + } + + fn restart(&self, key: &ServiceKey, init_config: Vec) -> Result { + let runtime = self.runtime()?; + let runtime = runtime.as_ref().ok_or(ConsoleError::RuntimeNotRunning)?; + self.ensure_managed(key)?; + match runtime.stop(key) { + Ok(()) | Err(RuntimeError::ActorUnavailable(_)) => {} + Err(error) => return Err(error.into()), + } + runtime.start(key, init_config)?; + let view = self.update_status(key, ServiceStatus::Running)?; + self.push_event(EventKind::Started, Some(key), "actor restarted".to_owned())?; + Ok(view) + } + + fn invoke(&self, key: &ServiceKey, input: Vec) -> Result { + let runtime = self.runtime()?; + let runtime = runtime.as_ref().ok_or(ConsoleError::RuntimeNotRunning)?; + self.ensure_managed(key)?; + let started_at = Instant::now(); + let result = runtime.invoke(key, input); + let latency_ms = duration_ms(started_at.elapsed()); + + let mut state = self.state()?; + let record = state + .services + .get_mut(key) + .ok_or_else(|| ConsoleError::ServiceNotFound(key.clone()))?; + record.calls = record.calls.saturating_add(1); + record.last_latency_ms = Some(latency_ms); + record.updated_at_ms = unix_time_ms(); + + match result { + Ok(output) => { + drop(state); + self.push_event( + EventKind::Invoked, + Some(key), + format!("invoke completed in {latency_ms} ms"), + )?; + Ok(InvokeResponse { + output_base64: BASE64.encode(&output), + output_bytes: output.len(), + latency_ms, + }) + } + Err(error) => { + record.errors = record.errors.saturating_add(1); + if matches!(error, RuntimeError::ActorFault { .. }) { + record.status = ServiceStatus::Faulted; + } + drop(state); + self.push_event(EventKind::Failed, Some(key), error.to_string())?; + Err(error.into()) + } + } + } + + fn load_stored_services( + &self, + stored_services: Vec, + ) -> Result<(), ConsoleError> { + let runtime = self.runtime()?; + let runtime = runtime.as_ref().ok_or(ConsoleError::RuntimeNotRunning)?; + for stored in stored_services { + let manifest = ServiceManifest::from_toml(&stored.manifest_toml)?; + validate_path_segment("id", &manifest.id)?; + validate_path_segment("revision", &manifest.revision)?; + let key = manifest.key()?; + if stored.service_key != key.to_string() { + return Err(ConsoleError::InvalidDatabaseData(format!( + "service key {:?} does not match manifest key {key}", + stored.service_key + ))); + } + runtime.register_from_file(manifest.clone())?; + self.state()?.services.insert( + key, + ServiceRecord { + manifest, + status: ServiceStatus::Stopped, + updated_at_ms: stored.updated_at_ms, + calls: stored.calls, + errors: stored.errors, + last_latency_ms: stored.last_latency_ms, + }, + ); + } + Ok(()) + } + + fn load_manifest_services(&self) -> Result<(), ConsoleError> { + let runtime = self.runtime()?; + let runtime = runtime.as_ref().ok_or(ConsoleError::RuntimeNotRunning)?; + let mut manifests = fs::read_dir(&self.artifact_dir) + .map_err(|source| ConsoleError::Storage { + path: self.artifact_dir.clone(), + source, + })? + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.extension() + .is_some_and(|extension| extension == "toml") + }) + .collect::>(); + manifests.sort(); + + for manifest_path in manifests { + let source = + fs::read_to_string(&manifest_path).map_err(|source| ConsoleError::Storage { + path: manifest_path.clone(), + source, + })?; + let manifest = ServiceManifest::from_toml(&source)?; + validate_path_segment("id", &manifest.id)?; + validate_path_segment("revision", &manifest.revision)?; + let key = manifest.key()?; + if self.state()?.services.contains_key(&key) { + continue; + } + runtime.register_from_file(manifest.clone())?; + self.insert_service(manifest, ServiceStatus::Stopped)?; + } + + Ok(()) + } + + async fn persist_snapshot(&self) -> Result<(), ConsoleError> { + // Serialize snapshots so a slower earlier write cannot overwrite a + // newer state captured by a concurrent HTTP operation. + let _sync = self.persistence_sync.lock().await; + let (services, events) = self.persistence_snapshot()?; + self.persistence.sync(services, events).await?; + Ok(()) + } + + fn persistence_snapshot(&self) -> Result<(Vec, Vec), ConsoleError> { + let state = self.state()?; + let services = state + .services + .iter() + .map(|(key, service)| { + Ok(StoredService { + service_key: key.to_string(), + manifest_toml: toml::to_string_pretty(&service.manifest)?, + updated_at_ms: service.updated_at_ms, + calls: service.calls, + errors: service.errors, + last_latency_ms: service.last_latency_ms, + }) + }) + .collect::, ConsoleError>>()?; + let events = state + .events + .iter() + .map(|event| StoredEvent { + id: event.id, + timestamp_ms: event.timestamp_ms, + kind: event.kind.as_database_value().to_owned(), + service_id: event.service_id.clone(), + revision: event.revision.clone(), + message: event.message.clone(), + }) + .collect(); + Ok((services, events)) + } + + fn insert_service( + &self, + manifest: ServiceManifest, + status: ServiceStatus, + ) -> Result { + let key = manifest.key()?; + let mut state = self.state()?; + let record = ServiceRecord { + manifest, + status, + updated_at_ms: unix_time_ms(), + calls: 0, + errors: 0, + last_latency_ms: None, + }; + let view = record.view(); + state.services.insert(key, record); + Ok(view) + } + + fn update_status( + &self, + key: &ServiceKey, + status: ServiceStatus, + ) -> Result { + let mut state = self.state()?; + let record = state + .services + .get_mut(key) + .ok_or_else(|| ConsoleError::ServiceNotFound(key.clone()))?; + record.status = status; + record.updated_at_ms = unix_time_ms(); + Ok(record.view()) + } + + fn ensure_managed(&self, key: &ServiceKey) -> Result<(), ConsoleError> { + if self.state()?.services.contains_key(key) { + Ok(()) + } else { + Err(ConsoleError::ServiceNotFound(key.clone())) + } + } + + fn push_event( + &self, + kind: EventKind, + key: Option<&ServiceKey>, + message: String, + ) -> Result<(), ConsoleError> { + let mut state = self.state()?; + let event = EventView { + id: state.next_event_id, + timestamp_ms: unix_time_ms(), + kind, + service_id: key.map(|key| key.id().to_owned()), + revision: key.map(|key| key.revision().to_owned()), + message, + }; + state.next_event_id = state.next_event_id.saturating_add(1); + state.events.push_front(event); + state.events.truncate(MAX_EVENTS); + Ok(()) + } + + fn state(&self) -> Result, ConsoleError> { + self.state.lock().map_err(|_| ConsoleError::LockPoisoned) + } + + fn runtime(&self) -> Result>, ConsoleError> { + self.runtime.read().map_err(|_| ConsoleError::LockPoisoned) + } +} + +/// Builds the Axum management API around a shared [`Console`]. +/// +/// The returned router exposes Runtime control, component registration, +/// invocation, events, and WIT Registry endpoints. +pub fn app(console: Arc, allowed_origins: Vec) -> Router { + let max_body_bytes = console + .max_artifact_bytes() + .max(console.max_wit_package_bytes()) + .saturating_add(1024 * 1024); + let mut cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_headers([header::CONTENT_TYPE]); + if !allowed_origins.is_empty() { + cors = cors.allow_origin(allowed_origins); + } + + Router::new() + .route("/healthz", get(health)) + .route("/api/v1/runtime", get(runtime_status)) + .route("/api/v1/runtime/start", post(start_runtime)) + .route("/api/v1/runtime/stop", post(stop_runtime)) + .route("/api/v1/runtime/restart", post(restart_runtime)) + .route("/api/v1/services", get(list_services).post(register)) + .route( + "/api/v1/wit/packages", + get(list_wit_packages).post(publish_wit_package), + ) + .route( + "/api/v1/wit/packages/{namespace}/{name}/{version}", + get(get_wit_package), + ) + .route( + "/api/v1/wit/packages/{namespace}/{name}/{version}/content", + get(download_wit_package), + ) + .route( + "/api/v1/services/{id}/{revision}/start", + post(start_service), + ) + .route("/api/v1/services/{id}/{revision}/stop", post(stop_service)) + .route( + "/api/v1/services/{id}/{revision}/restart", + post(restart_service), + ) + .route( + "/api/v1/services/{id}/{revision}/invoke", + post(invoke_service), + ) + .route("/api/v1/events", get(list_events)) + .layer(DefaultBodyLimit::max(max_body_bytes)) + .layer(cors) + .layer( + TraceLayer::new_for_http() + .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) + .on_response(DefaultOnResponse::new().level(Level::INFO)), + ) + .with_state(console) +} + +async fn health() -> Json { + Json(json!({ "status": "ok" })) +} + +async fn runtime_status( + State(console): State>, +) -> Result, ApiError> { + Ok(Json(console.runtime_view()?)) +} + +async fn start_runtime(State(console): State>) -> Result, ApiError> { + Ok(Json( + run_blocking(console, |console| console.start_runtime()).await?, + )) +} + +async fn stop_runtime(State(console): State>) -> Result, ApiError> { + Ok(Json( + run_blocking(console, |console| console.stop_runtime()).await?, + )) +} + +async fn restart_runtime( + State(console): State>, +) -> Result, ApiError> { + Ok(Json( + run_blocking(console, |console| console.restart_runtime()).await?, + )) +} + +async fn list_services(State(console): State>) -> Result, ApiError> { + Ok(Json(ServiceList { + services: console.services()?, + })) +} + +async fn list_events(State(console): State>) -> Result, ApiError> { + Ok(Json(EventList { + events: console.events()?, + })) +} + +async fn list_wit_packages( + State(console): State>, +) -> Result, ApiError> { + Ok(Json(WitPackageList { + packages: console.wit_registry.list()?, + })) +} + +async fn get_wit_package( + State(console): State>, + AxumPath((namespace, name, version)): AxumPath<(String, String, String)>, +) -> Result, ApiError> { + Ok(Json( + console + .wit_registry + .metadata(&format!("{namespace}:{name}"), &version)?, + )) +} + +async fn download_wit_package( + State(console): State>, + AxumPath((namespace, name, version)): AxumPath<(String, String, String)>, +) -> Result { + let (metadata, bytes) = console + .wit_registry + .download(&format!("{namespace}:{name}"), &version)?; + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/wasm") + .header(header::ETAG, format!("\"{}\"", metadata.sha256)) + .body(Body::from(bytes)) + .map_err(|error| { + ConsoleError::InvalidRequest(format!("failed to build package response: {error}")) + .into() + }) +} + +async fn publish_wit_package( + State(console): State>, + mut multipart: Multipart, +) -> Result<(StatusCode, Json), ApiError> { + let mut package = None; + while let Some(field) = multipart + .next_field() + .await + .map_err(|error| ConsoleError::InvalidRequest(format!("invalid multipart body: {error}")))? + { + match field.name() { + Some("package") => { + if package.is_some() { + return Err(ConsoleError::InvalidRequest( + "package field must occur exactly once".to_owned(), + ) + .into()); + } + package = Some( + field + .bytes() + .await + .map_err(|error| { + ConsoleError::InvalidRequest(format!( + "invalid WIT package field: {error}" + )) + })? + .to_vec(), + ); + } + Some(name) => { + return Err(ConsoleError::InvalidRequest(format!( + "unsupported multipart field {name}" + )) + .into()); + } + None => {} + } + } + let package = package + .ok_or_else(|| ConsoleError::InvalidRequest("package field is required".to_owned()))?; + if package.len() > console.max_wit_package_bytes() { + return Err(WitRegistryError::TooLarge { + limit: console.max_wit_package_bytes(), + } + .into()); + } + let metadata = run_blocking(console, move |console| { + Ok(console.wit_registry.publish(&package)?) + }) + .await?; + Ok((StatusCode::CREATED, Json(metadata))) +} + +async fn register( + State(console): State>, + mut multipart: Multipart, +) -> Result<(StatusCode, Json), ApiError> { + let mut package = None; + + while let Some(field) = multipart + .next_field() + .await + .map_err(|error| ConsoleError::InvalidRequest(format!("invalid multipart body: {error}")))? + { + match field.name() { + Some("package") => { + if package.is_some() { + return Err(ConsoleError::InvalidRequest( + "package field must occur exactly once".to_owned(), + ) + .into()); + } + package = Some( + field + .bytes() + .await + .map_err(|error| { + ConsoleError::InvalidRequest(format!("invalid package field: {error}")) + })? + .to_vec(), + ); + } + Some(name) => { + return Err(ConsoleError::InvalidRequest(format!( + "unsupported multipart field {name}" + )) + .into()); + } + None => {} + } + } + + let package = package + .ok_or_else(|| ConsoleError::InvalidRequest("package field is required".to_owned()))?; + let service = run_blocking(console, move |console| console.register_package(package)).await?; + Ok((StatusCode::CREATED, Json(service))) +} + +async fn start_service( + State(console): State>, + AxumPath((id, revision)): AxumPath<(String, String)>, + request: Option>, +) -> Result, ApiError> { + let key = api_key(id, revision)?; + let init_config = decode_optional_base64( + request + .as_ref() + .and_then(|Json(request)| request.init_config_base64.as_deref()), + )?; + Ok(Json( + run_blocking(console, move |console| console.start(&key, init_config)).await?, + )) +} + +async fn stop_service( + State(console): State>, + AxumPath((id, revision)): AxumPath<(String, String)>, +) -> Result, ApiError> { + let key = api_key(id, revision)?; + Ok(Json( + run_blocking(console, move |console| console.stop(&key)).await?, + )) +} + +async fn restart_service( + State(console): State>, + AxumPath((id, revision)): AxumPath<(String, String)>, + request: Option>, +) -> Result, ApiError> { + let key = api_key(id, revision)?; + let init_config = decode_optional_base64( + request + .as_ref() + .and_then(|Json(request)| request.init_config_base64.as_deref()), + )?; + Ok(Json( + run_blocking(console, move |console| console.restart(&key, init_config)).await?, + )) +} + +async fn invoke_service( + State(console): State>, + AxumPath((id, revision)): AxumPath<(String, String)>, + Json(request): Json, +) -> Result, ApiError> { + let key = api_key(id, revision)?; + let input = BASE64.decode(request.input_base64).map_err(|error| { + ConsoleError::InvalidRequest(format!("input_base64 is invalid: {error}")) + })?; + Ok(Json( + run_blocking(console, move |console| console.invoke(&key, input)).await?, + )) +} + +async fn run_blocking(console: Arc, operation: F) -> Result +where + T: Send + 'static, + F: FnOnce(Arc) -> Result + Send + 'static, +{ + // Wasmtime compilation and synchronous Actor calls must not occupy a Tokio + // async worker. Persist afterward even on failure because an operation may + // already have recorded metrics or an event before returning its error. + let operation_console = Arc::clone(&console); + let result = tokio::task::spawn_blocking(move || operation(operation_console)) + .await + .map_err(ConsoleError::from) + .and_then(|result| result); + let persisted = console.persist_snapshot().await; + + match result { + Ok(value) => { + persisted?; + Ok(value) + } + Err(error) => { + if let Err(persistence_error) = persisted { + tracing::error!( + error = %persistence_error, + "failed to persist console state after operation error" + ); + } + Err(error) + } + } +} + +fn api_key(id: String, revision: String) -> Result { + validate_path_segment("id", &id)?; + validate_path_segment("revision", &revision)?; + ServiceKey::new(id, revision).map_err(Into::into) +} + +fn validate_path_segment(name: &str, value: &str) -> Result<(), ConsoleError> { + let valid = !value.is_empty() + && value.len() <= 128 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')); + if valid { + Ok(()) + } else { + Err(ConsoleError::InvalidRequest(format!( + "{name} must contain only ASCII letters, digits, '.', '_' or '-' and be at most 128 bytes" + ))) + } +} + +fn decode_optional_base64(value: Option<&str>) -> Result, ConsoleError> { + match value { + Some(value) if !value.is_empty() => BASE64.decode(value).map_err(|error| { + ConsoleError::InvalidRequest(format!("init_config_base64 is invalid: {error}")) + }), + _ => Ok(Vec::new()), + } +} + +fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), ConsoleError> { + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("artifact"); + let temporary = path.with_file_name(format!(".{file_name}.tmp-{}", std::process::id())); + fs::write(&temporary, bytes).map_err(|source| ConsoleError::Storage { + path: temporary.clone(), + source, + })?; + if let Err(source) = fs::rename(&temporary, path) { + let _ = fs::remove_file(&temporary); + return Err(ConsoleError::Storage { + path: path.to_path_buf(), + source, + }); + } + Ok(()) +} + +fn unix_time_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(duration_ms) + .unwrap_or_default() +} + +fn duration_ms(duration: std::time::Duration) -> u64 { + u64::try_from(duration.as_millis()).unwrap_or(u64::MAX) +} + +struct ApiError(ConsoleError); + +impl From for ApiError { + fn from(error: ConsoleError) -> Self { + Self(error) + } +} + +impl From for ApiError { + fn from(error: WitRegistryError) -> Self { + Self(error.into()) + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let (status, code) = match &self.0 { + ConsoleError::InvalidRequest(_) => (StatusCode::BAD_REQUEST, "invalid_request"), + ConsoleError::ServiceNotFound(_) => (StatusCode::NOT_FOUND, "service_not_found"), + ConsoleError::RuntimeNotRunning => (StatusCode::CONFLICT, "runtime_not_running"), + ConsoleError::ArtifactTooLarge { .. } + | ConsoleError::Package(PackageError::ComponentTooLarge { .. }) + | ConsoleError::WitRegistry(WitRegistryError::TooLarge { .. }) + | ConsoleError::Runtime(RuntimeError::InputTooLarge { .. }) + | ConsoleError::Runtime(RuntimeError::OutputTooLarge { .. }) => { + (StatusCode::PAYLOAD_TOO_LARGE, "payload_too_large") + } + ConsoleError::Runtime(RuntimeError::ServiceNotRegistered(_)) => { + (StatusCode::NOT_FOUND, "service_not_registered") + } + ConsoleError::Runtime(RuntimeError::ServiceAlreadyRegistered(_)) + | ConsoleError::Runtime(RuntimeError::ActorUnavailable(_)) + | ConsoleError::Runtime(RuntimeError::ActorOverloaded(_)) => { + (StatusCode::CONFLICT, "runtime_conflict") + } + ConsoleError::WitRegistry(WitRegistryError::AlreadyExists(_)) => { + (StatusCode::CONFLICT, "wit_package_exists") + } + ConsoleError::WitRegistry(WitRegistryError::NotFound(_)) => { + (StatusCode::NOT_FOUND, "wit_package_not_found") + } + ConsoleError::Runtime(RuntimeError::DeadlineExceeded { .. }) => { + (StatusCode::REQUEST_TIMEOUT, "deadline_exceeded") + } + ConsoleError::Runtime( + RuntimeError::InvalidServiceKey + | RuntimeError::InvalidManifest(_) + | RuntimeError::ManifestParse(_) + | RuntimeError::ComponentCompilation(_) + | RuntimeError::UnsupportedImport(_) + | RuntimeError::ActorInitialization(_) + | RuntimeError::ComponentError { .. }, + ) => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_component"), + ConsoleError::Package(_) => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_package"), + ConsoleError::WitRegistry(WitRegistryError::InvalidPackage( + WitPackageError::Source { .. } + | WitPackageError::Encode(_) + | WitPackageError::Decode(_) + | WitPackageError::MissingVersion(_), + )) => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_wit_package"), + ConsoleError::Storage { .. } + | ConsoleError::ManifestSerialize(_) + | ConsoleError::Database(_) + | ConsoleError::InvalidDatabaseData(_) + | ConsoleError::Runtime(_) + | ConsoleError::WitRegistry(WitRegistryError::InvalidStorage(_)) + | ConsoleError::WitRegistry(WitRegistryError::Storage { .. }) + | ConsoleError::WitRegistry(WitRegistryError::LockPoisoned) + | ConsoleError::Join(_) + | ConsoleError::LockPoisoned => (StatusCode::INTERNAL_SERVER_ERROR, "internal_error"), + }; + + ( + status, + Json(json!({ + "error": { + "code": code, + "message": self.0.to_string(), + } + })), + ) + .into_response() + } +} diff --git a/crates/wasmeld-console/src/main.rs b/crates/wasmeld-console/src/main.rs new file mode 100644 index 0000000..5a8cbeb --- /dev/null +++ b/crates/wasmeld-console/src/main.rs @@ -0,0 +1,65 @@ +//! Standalone Wasmeld management server entry point. + +use std::{env, net::SocketAddr, path::PathBuf, sync::Arc}; + +use axum::http::HeaderValue; +use tracing::info; +use tracing_subscriber::EnvFilter; +use wasmeld_console::{Console, ConsoleConfig, app}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("wasmeld_console=info")), + ) + .init(); + + let address = env::var("WASMELD_ADDR") + .unwrap_or_else(|_| "127.0.0.1:8080".to_owned()) + .parse::()?; + let artifact_dir = env::var_os("WASMELD_ARTIFACT_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("var/wasmeld/components")); + let database_path = env::var_os("WASMELD_DATABASE_PATH") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("var/wasmeld/console.db")); + let wit_registry_dir = env::var_os("WASMELD_WIT_REGISTRY_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("var/wasmeld/wit-packages")); + let allowed_origins = allowed_origins()?; + let console = Arc::new( + Console::new(ConsoleConfig { + artifact_dir, + database_path, + wit_registry_dir, + ..ConsoleConfig::default() + }) + .await?, + ); + let application = app(console, allowed_origins); + let listener = tokio::net::TcpListener::bind(address).await?; + + info!(%address, "Wasmeld listening"); + axum::serve(listener, application) + .with_graceful_shutdown(shutdown_signal()) + .await?; + Ok(()) +} + +fn allowed_origins() -> Result, Box> { + let value = env::var("WASMELD_ALLOWED_ORIGINS") + .unwrap_or_else(|_| "http://localhost:3000,http://127.0.0.1:3000".to_owned()); + value + .split(',') + .filter(|origin| !origin.trim().is_empty()) + .map(|origin| origin.trim().parse::().map_err(Into::into)) + .collect() +} + +async fn shutdown_signal() { + if tokio::signal::ctrl_c().await.is_err() { + tracing::warn!("failed to install Ctrl+C handler"); + } +} diff --git a/crates/wasmeld-console/src/persistence.rs b/crates/wasmeld-console/src/persistence.rs new file mode 100644 index 0000000..cb716a8 --- /dev/null +++ b/crates/wasmeld-console/src/persistence.rs @@ -0,0 +1,101 @@ +//! Toasty models and transactional libSQL snapshots for Console control data. +//! +//! Component bytes and WIT packages remain filesystem artifacts. This module +//! persists only service manifests, metrics, and the bounded event history. + +use std::path::Path; + +use toasty::Db; +use toasty_driver_turso::Turso; +use tokio::sync::Mutex; + +#[derive(Clone, Debug, toasty::Model)] +pub(crate) struct StoredService { + #[key] + pub service_key: String, + pub manifest_toml: String, + pub updated_at_ms: u64, + pub calls: u64, + pub errors: u64, + pub last_latency_ms: Option, +} + +#[derive(Clone, Debug, toasty::Model)] +pub(crate) struct StoredEvent { + #[key] + pub id: u64, + pub timestamp_ms: u64, + pub kind: String, + pub service_id: Option, + pub revision: Option, + pub message: String, +} + +/// Serialized access to the Toasty database connection. +pub(crate) struct Persistence { + db: Mutex, +} + +impl Persistence { + /// Opens the database and installs the schema for a new file. + pub async fn open(path: &Path) -> toasty::Result { + let is_new_database = !path.exists(); + let db = Db::builder() + .models(toasty::models!(StoredService, StoredEvent)) + .build(Turso::file(path)) + .await?; + if is_new_database { + db.push_schema().await?; + } + Ok(Self { db: Mutex::new(db) }) + } + + /// Loads the complete service set and retained event history. + pub async fn load(&self) -> toasty::Result<(Vec, Vec)> { + let mut db = self.db.lock().await; + let services = StoredService::all().exec(&mut *db).await?; + let events = StoredEvent::all().exec(&mut *db).await?; + Ok((services, events)) + } + + /// Atomically upserts a Console snapshot and prunes expired events. + pub async fn sync( + &self, + services: Vec, + events: Vec, + ) -> toasty::Result<()> { + let mut db = self.db.lock().await; + let mut tx = db.transaction().await?; + + for service in services { + StoredService::upsert_by_service_key(service.service_key) + .manifest_toml(service.manifest_toml) + .updated_at_ms(service.updated_at_ms) + .calls(service.calls) + .errors(service.errors) + .last_latency_ms(service.last_latency_ms) + .exec(&mut tx) + .await?; + } + + if let Some(oldest_event) = events.last() { + StoredEvent::filter(StoredEvent::fields().id().lt(oldest_event.id)) + .delete() + .exec(&mut tx) + .await?; + } + + for event in events { + StoredEvent::upsert_by_id(event.id) + .timestamp_ms(event.timestamp_ms) + .kind(event.kind) + .service_id(event.service_id) + .revision(event.revision) + .message(event.message) + .exec(&mut tx) + .await?; + } + + tx.commit().await + } +} diff --git a/crates/wasmeld-console/src/wit_registry.rs b/crates/wasmeld-console/src/wit_registry.rs new file mode 100644 index 0000000..de2c4b6 --- /dev/null +++ b/crates/wasmeld-console/src/wit_registry.rs @@ -0,0 +1,338 @@ +//! Filesystem-backed immutable WIT package Registry. +//! +//! Artifacts are stored as +//! `////package.wasm`. The in-memory index is +//! rebuilt from artifact contents at startup; directory names and cached +//! metadata are never treated as the source of truth. + +use std::{ + collections::BTreeMap, + fs::{self, OpenOptions}, + io::{self, Write}, + path::{Path, PathBuf}, + sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; + +use thiserror::Error; +use wasmeld_package::wit_package::{WitPackageError, WitPackageMetadata, inspect_wit_package}; + +const ARTIFACT_FILE: &str = "package.wasm"; + +/// Process-local index over immutable binary WIT packages. +/// +/// The write lock covers both the duplicate check and file creation so +/// concurrent publishers cannot replace the same package version. +pub(crate) struct WitRegistry { + root: PathBuf, + max_package_bytes: usize, + packages: RwLock>, +} + +#[derive(Clone)] +struct StoredWitPackage { + metadata: WitPackageMetadata, + artifact: PathBuf, +} + +/// Errors produced by WIT Registry storage and integrity checks. +#[derive(Debug, Error)] +pub enum WitRegistryError { + #[error("WIT package exceeds the {limit}-byte limit")] + TooLarge { limit: usize }, + + #[error("WIT package {0} is already published and cannot be overwritten")] + AlreadyExists(String), + + #[error("WIT package {0} was not found")] + NotFound(String), + + #[error(transparent)] + InvalidPackage(#[from] WitPackageError), + + #[error("invalid WIT registry data: {0}")] + InvalidStorage(String), + + #[error("failed to access {path}: {source}")] + Storage { + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error("WIT registry lock was poisoned")] + LockPoisoned, +} + +impl WitRegistry { + /// Opens a Registry and validates every persisted package before serving it. + pub(crate) fn open( + root: impl AsRef, + max_package_bytes: usize, + ) -> Result { + let root = root.as_ref(); + fs::create_dir_all(root).map_err(|source| WitRegistryError::Storage { + path: root.to_path_buf(), + source, + })?; + let root = fs::canonicalize(root).map_err(|source| WitRegistryError::Storage { + path: root.to_path_buf(), + source, + })?; + let registry = Self { + root, + max_package_bytes, + packages: RwLock::new(BTreeMap::new()), + }; + registry.load()?; + Ok(registry) + } + + pub(crate) fn max_package_bytes(&self) -> usize { + self.max_package_bytes + } + + /// Publishes one immutable binary WIT package. + /// + /// Identity and dependency metadata are decoded from `bytes`; callers do + /// not provide trusted sidecar metadata. + pub(crate) fn publish(&self, bytes: &[u8]) -> Result { + if bytes.len() > self.max_package_bytes { + return Err(WitRegistryError::TooLarge { + limit: self.max_package_bytes, + }); + } + let metadata = inspect_wit_package(bytes)?; + let (namespace, name) = package_segments(&metadata.name)?; + let key = package_key(&metadata.name, &metadata.version); + let artifact = self + .root + .join(namespace) + .join(name) + .join(&metadata.version) + .join(ARTIFACT_FILE); + + let mut packages = self.packages_write()?; + if packages.contains_key(&key) || artifact.exists() { + return Err(WitRegistryError::AlreadyExists(key)); + } + let parent = artifact + .parent() + .expect("a package artifact always has a parent"); + fs::create_dir_all(parent).map_err(|source| WitRegistryError::Storage { + path: parent.to_path_buf(), + source, + })?; + // create_new is the filesystem-level immutability guard. It also + // protects against an artifact created outside the current process. + let write_result = (|| { + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&artifact) + .map_err(|source| WitRegistryError::Storage { + path: artifact.clone(), + source, + })?; + file.write_all(bytes) + .and_then(|()| file.sync_all()) + .map_err(|source| WitRegistryError::Storage { + path: artifact.clone(), + source, + }) + })(); + if let Err(error) = write_result { + let _ = fs::remove_file(&artifact); + return Err(error); + } + + packages.insert( + key, + StoredWitPackage { + metadata: metadata.clone(), + artifact, + }, + ); + Ok(metadata) + } + + /// Returns all package versions in deterministic key order. + pub(crate) fn list(&self) -> Result, WitRegistryError> { + Ok(self + .packages_read()? + .values() + .map(|package| package.metadata.clone()) + .collect()) + } + + /// Looks up metadata for one exact package version. + pub(crate) fn metadata( + &self, + name: &str, + version: &str, + ) -> Result { + let key = checked_key(name, version)?; + self.packages_read()? + .get(&key) + .map(|package| package.metadata.clone()) + .ok_or(WitRegistryError::NotFound(key)) + } + + /// Reads an exact package version and revalidates its content before serving it. + pub(crate) fn download( + &self, + name: &str, + version: &str, + ) -> Result<(WitPackageMetadata, Vec), WitRegistryError> { + let key = checked_key(name, version)?; + let package = self + .packages_read()? + .get(&key) + .cloned() + .ok_or_else(|| WitRegistryError::NotFound(key.clone()))?; + let bytes = fs::read(&package.artifact).map_err(|source| WitRegistryError::Storage { + path: package.artifact, + source, + })?; + // Detect manual file replacement or corruption after startup instead + // of returning bytes inconsistent with the indexed digest. + let inspected = inspect_wit_package(&bytes)?; + if inspected != package.metadata { + return Err(WitRegistryError::InvalidStorage(format!( + "stored artifact metadata changed for {key}" + ))); + } + Ok((package.metadata, bytes)) + } + + fn load(&self) -> Result<(), WitRegistryError> { + let mut loaded = BTreeMap::new(); + for namespace in directories(&self.root)? { + for name in directories(&namespace)? { + for version in directories(&name)? { + let artifact = version.join(ARTIFACT_FILE); + if !artifact.is_file() { + continue; + } + let bytes = + fs::read(&artifact).map_err(|source| WitRegistryError::Storage { + path: artifact.clone(), + source, + })?; + if bytes.len() > self.max_package_bytes { + return Err(WitRegistryError::TooLarge { + limit: self.max_package_bytes, + }); + } + // The artifact determines identity. Directory segments are + // checked afterward only as a storage-layout invariant. + let metadata = inspect_wit_package(&bytes)?; + let (expected_namespace, expected_name) = package_segments(&metadata.name)?; + let stored_namespace = namespace + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + let stored_name = name + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + let stored_version = version + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + if stored_namespace != expected_namespace + || stored_name != expected_name + || stored_version != metadata.version + { + return Err(WitRegistryError::InvalidStorage(format!( + "{} does not match package {}@{}", + artifact.display(), + metadata.name, + metadata.version + ))); + } + let key = package_key(&metadata.name, &metadata.version); + if loaded + .insert(key.clone(), StoredWitPackage { metadata, artifact }) + .is_some() + { + return Err(WitRegistryError::InvalidStorage(format!( + "duplicate package {key}" + ))); + } + } + } + } + *self.packages_write()? = loaded; + Ok(()) + } + + fn packages_read( + &self, + ) -> Result>, WitRegistryError> { + self.packages + .read() + .map_err(|_| WitRegistryError::LockPoisoned) + } + + fn packages_write( + &self, + ) -> Result>, WitRegistryError> { + self.packages + .write() + .map_err(|_| WitRegistryError::LockPoisoned) + } +} + +fn directories(path: &Path) -> Result, WitRegistryError> { + let mut directories = fs::read_dir(path) + .map_err(|source| WitRegistryError::Storage { + path: path.to_path_buf(), + source, + })? + .filter_map(|entry| entry.ok()) + .filter_map(|entry| { + entry + .file_type() + .ok() + .filter(|file_type| file_type.is_dir()) + .map(|_| entry.path()) + }) + .collect::>(); + directories.sort(); + Ok(directories) +} + +fn checked_key(name: &str, version: &str) -> Result { + package_segments(name)?; + semver::Version::parse(version) + .map_err(|error| WitRegistryError::InvalidStorage(error.to_string()))?; + Ok(package_key(name, version)) +} + +fn package_segments(name: &str) -> Result<(&str, &str), WitRegistryError> { + let Some((namespace, package)) = name.split_once(':') else { + return Err(WitRegistryError::InvalidStorage(format!( + "invalid WIT package name {name:?}" + ))); + }; + if package.contains(':') || !valid_segment(namespace) || !valid_segment(package) { + return Err(WitRegistryError::InvalidStorage(format!( + "invalid WIT package name {name:?}" + ))); + } + Ok((namespace, package)) +} + +fn valid_segment(value: &str) -> bool { + !value.is_empty() + && value.len() <= 128 + && !value.starts_with('-') + && !value.ends_with('-') + && value + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') +} + +fn package_key(name: &str, version: &str) -> String { + format!("{name}@{version}") +} diff --git a/crates/wasmeld-console/tests/api.rs b/crates/wasmeld-console/tests/api.rs new file mode 100644 index 0000000..51f5ee0 --- /dev/null +++ b/crates/wasmeld-console/tests/api.rs @@ -0,0 +1,618 @@ +use std::{ + fs, + io::Cursor, + path::{Path, PathBuf}, + process::Command, + sync::{Arc, OnceLock}, +}; + +use axum::{ + Router, + body::{Body, to_bytes}, + http::{Request, StatusCode, header}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +use serde_json::{Value, json}; +use tempfile::TempDir; +use tower::ServiceExt; +use wasmeld_console::{Console, ConsoleConfig, app}; +use wasmeld_package::{ + module::{ModuleLock, sync_dependencies}, + wit_package::build_wit_package, + write_package, +}; + +static COMPONENTS_BUILT: OnceLock<()> = OnceLock::new(); + +#[tokio::test] +async fn manages_a_resident_component_over_http() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let application = test_app(artifact_dir.path()).await; + let component = fs::read(echo_component()).expect("echo component should be readable"); + + let response = application + .clone() + .oneshot(package_request("echo", "0.1.0", &component)) + .await + .expect("register request should complete"); + let status = response.status(); + let registered = response_json(response).await; + assert_eq!(status, StatusCode::CREATED, "{registered}"); + assert_eq!(registered["id"], "echo"); + assert_eq!(registered["status"], "stopped"); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/services/echo/0.1.0/start")) + .await + .expect("start request should complete"); + assert_eq!(response.status(), StatusCode::OK); + + let response = application + .clone() + .oneshot(json_request( + "/api/v1/services/echo/0.1.0/invoke", + json!({ "input_base64": BASE64.encode(b"resident") }), + )) + .await + .expect("invoke request should complete"); + assert_eq!(response.status(), StatusCode::OK); + let invocation = response_json(response).await; + assert_eq!( + BASE64 + .decode(invocation["output_base64"].as_str().unwrap()) + .unwrap(), + b"resident" + ); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/services/echo/0.1.0/stop")) + .await + .expect("stop request should complete"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response_json(response).await["status"], "stopped"); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/services/echo/0.1.0/restart")) + .await + .expect("restart request should complete"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response_json(response).await["status"], "running"); + + let response = application + .oneshot(get_request("/api/v1/events")) + .await + .expect("events request should complete"); + let events = response_json(response).await; + assert!(events["events"].as_array().unwrap().len() >= 5); +} + +#[tokio::test] +async fn reloads_manifests_without_restoring_actor_memory() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let component = fs::read(echo_component()).expect("echo component should be readable"); + + { + let application = test_app(artifact_dir.path()).await; + let response = application + .clone() + .oneshot(package_request("persisted", "0.1.0", &component)) + .await + .expect("register request should complete"); + assert_eq!(response.status(), StatusCode::CREATED); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/services/persisted/0.1.0/start")) + .await + .expect("start request should complete"); + assert_eq!(response.status(), StatusCode::OK); + + let response = application + .oneshot(json_request( + "/api/v1/services/persisted/0.1.0/invoke", + json!({ "input_base64": BASE64.encode(b"persist metrics") }), + )) + .await + .expect("invoke request should complete"); + assert_eq!(response.status(), StatusCode::OK); + } + + let application = test_app(artifact_dir.path()).await; + let response = application + .clone() + .oneshot(get_request("/api/v1/services")) + .await + .expect("list request should complete"); + assert_eq!(response.status(), StatusCode::OK); + let body = response_json(response).await; + assert_eq!(body["services"][0]["id"], "persisted"); + assert_eq!(body["services"][0]["status"], "stopped"); + assert_eq!(body["services"][0]["calls"], 1); + + let response = application + .oneshot(get_request("/api/v1/events")) + .await + .expect("events request should complete"); + let body = response_json(response).await; + assert!(body["events"].as_array().unwrap().len() >= 3); +} + +#[tokio::test] +async fn manages_the_embedded_runtime_lifecycle() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let application = test_app(artifact_dir.path()).await; + let component = fs::read(echo_component()).expect("echo component should be readable"); + + let response = application + .clone() + .oneshot(get_request("/api/v1/runtime")) + .await + .expect("runtime status request should complete"); + let body = response_json(response).await; + assert_eq!(body["status"], "running"); + assert_eq!(body["managed_services"], 0); + + let response = application + .clone() + .oneshot(package_request("managed-runtime", "0.1.0", &component)) + .await + .expect("register request should complete"); + assert_eq!(response.status(), StatusCode::CREATED); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/runtime/stop")) + .await + .expect("runtime stop request should complete"); + let body = response_json(response).await; + assert_eq!(body["status"], "stopped"); + assert_eq!(body["managed_services"], 1); + assert_eq!(body["registered_services"], 0); + assert_eq!(body["running_services"], 0); + + let response = application + .clone() + .oneshot(json_request( + "/api/v1/services/managed-runtime/0.1.0/invoke", + json!({ "input_base64": BASE64.encode(b"stopped") }), + )) + .await + .expect("invoke request should complete"); + assert_eq!(response.status(), StatusCode::CONFLICT); + assert_eq!( + response_json(response).await["error"]["code"], + "runtime_not_running" + ); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/runtime/start")) + .await + .expect("runtime start request should complete"); + let body = response_json(response).await; + assert_eq!(body["status"], "running"); + assert_eq!(body["registered_services"], 1); + assert_eq!(body["running_services"], 0); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/services/managed-runtime/0.1.0/start")) + .await + .expect("actor start request should complete"); + assert_eq!(response.status(), StatusCode::OK); + + let response = application + .clone() + .oneshot(json_request( + "/api/v1/services/managed-runtime/0.1.0/invoke", + json!({ "input_base64": BASE64.encode(b"running again") }), + )) + .await + .expect("invoke request should complete"); + assert_eq!(response.status(), StatusCode::OK); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/runtime/restart")) + .await + .expect("runtime restart request should complete"); + let body = response_json(response).await; + assert_eq!(body["status"], "running"); + assert_eq!(body["registered_services"], 1); + assert_eq!(body["running_services"], 0); + + let response = application + .oneshot(get_request("/api/v1/services")) + .await + .expect("services request should complete"); + let body = response_json(response).await; + assert_eq!(body["services"][0]["status"], "stopped"); +} + +#[tokio::test] +async fn preserves_counter_memory_across_http_calls() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let application = test_app(artifact_dir.path()).await; + let component = fs::read(component_artifact("counter_component.wasm")) + .expect("counter component should be readable"); + + let response = application + .clone() + .oneshot(package_request("counter", "0.1.0", &component)) + .await + .expect("register request should complete"); + let status = response.status(); + let body = response_json(response).await; + assert_eq!(status, StatusCode::CREATED, "{body}"); + + let response = application + .clone() + .oneshot(empty_post("/api/v1/services/counter/0.1.0/start")) + .await + .expect("counter start should complete"); + assert_eq!(response.status(), StatusCode::OK); + + for expected in [1_u64, 2] { + let response = application + .clone() + .oneshot(json_request( + "/api/v1/services/counter/0.1.0/invoke", + json!({ "input_base64": "" }), + )) + .await + .expect("counter invocation should complete"); + assert_eq!(response.status(), StatusCode::OK); + let body = response_json(response).await; + let bytes = BASE64 + .decode(body["output_base64"].as_str().unwrap()) + .unwrap(); + assert_eq!( + u64::from_le_bytes(bytes.try_into().expect("counter returns eight bytes")), + expected + ); + } +} + +#[tokio::test] +async fn rejects_invalid_component_packages() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let application = test_app(artifact_dir.path()).await; + let response = application + .oneshot(package_bytes_request(b"not-a-package")) + .await + .expect("register request should complete"); + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); + assert_eq!( + response_json(response).await["error"]["code"], + "invalid_package" + ); +} + +#[tokio::test] +async fn publishes_and_downloads_immutable_wit_packages() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let package_source = TempDir::new().expect("temporary WIT source"); + fs::write( + package_source.path().join("package.wit"), + "package wasmeld:test-clock@1.2.3;\ninterface clock { now: func() -> u64; }\n", + ) + .unwrap(); + let package = build_wit_package(package_source.path()).unwrap(); + let application = test_app(artifact_dir.path()).await; + + let response = application + .clone() + .oneshot(wit_package_request(&package.bytes)) + .await + .expect("publish request should complete"); + let status = response.status(); + let body = response_json(response).await; + assert_eq!(status, StatusCode::CREATED, "{body}"); + assert_eq!(body["name"], "wasmeld:test-clock"); + assert_eq!(body["version"], "1.2.3"); + assert_eq!(body["sha256"], package.metadata.sha256); + + let response = application + .clone() + .oneshot(wit_package_request(&package.bytes)) + .await + .expect("duplicate publish request should complete"); + assert_eq!(response.status(), StatusCode::CONFLICT); + + let response = application + .clone() + .oneshot(get_request("/api/v1/wit/packages")) + .await + .unwrap(); + let list = response_json(response).await; + assert_eq!(list["packages"].as_array().unwrap().len(), 1); + + let response = application + .clone() + .oneshot(get_request("/api/v1/wit/packages/wasmeld/test-clock/1.2.3")) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response_json(response).await["name"], "wasmeld:test-clock"); + + let response = application + .oneshot(get_request( + "/api/v1/wit/packages/wasmeld/test-clock/1.2.3/content", + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + to_bytes(response.into_body(), usize::MAX).await.unwrap(), + package.bytes + ); + + let restarted = test_app(artifact_dir.path()).await; + let response = restarted + .oneshot(get_request("/api/v1/wit/packages")) + .await + .unwrap(); + assert_eq!( + response_json(response).await["packages"] + .as_array() + .unwrap() + .len(), + 1 + ); +} + +#[tokio::test] +async fn rejects_invalid_binary_wit_packages() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let application = test_app(artifact_dir.path()).await; + + let response = application + .oneshot(wit_package_request(b"not-a-wit-package")) + .await + .expect("publish request should complete"); + + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); + assert_eq!( + response_json(response).await["error"]["code"], + "invalid_wit_package" + ); +} + +#[tokio::test] +async fn client_fetches_transitive_wit_dependencies_from_the_registry() { + let artifact_dir = TempDir::new().expect("temporary artifact directory"); + let sources = TempDir::new().expect("temporary WIT sources"); + let clock = sources.path().join("clock"); + let timer = sources.path().join("timer"); + fs::create_dir(&clock).unwrap(); + fs::create_dir_all(timer.join("deps/clock")).unwrap(); + fs::write( + clock.join("package.wit"), + "package wasmeld:clock@1.0.0;\ninterface clock { now: func() -> u64; }\n", + ) + .unwrap(); + fs::copy( + clock.join("package.wit"), + timer.join("deps/clock/package.wit"), + ) + .unwrap(); + fs::write( + timer.join("package.wit"), + "package wasmeld:timer@1.0.0;\nworld timer-host {\n import wasmeld:clock/clock@1.0.0;\n}\n", + ) + .unwrap(); + let clock_package = build_wit_package(&clock).unwrap(); + let timer_package = build_wit_package(&timer).unwrap(); + assert_eq!(timer_package.metadata.dependencies.len(), 1); + + let application = test_app(artifact_dir.path()).await; + for package in [&clock_package.bytes, &timer_package.bytes] { + let response = application + .clone() + .oneshot(wit_package_request(package)) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + } + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve(listener, application).await.unwrap(); + }); + + let component = sources.path().join("component"); + fs::create_dir_all(component.join("wit")).unwrap(); + fs::write( + component.join("wit/world.wit"), + "package example:registry-client@0.1.0;\nworld client {\n include wasmeld:timer/timer-host@1.0.0;\n}\n", + ) + .unwrap(); + fs::write( + component.join("wasmeld.toml"), + format!( + "schema_version = 1\n\n[registry]\nurl = \"http://{address}\"\n\n[dependencies]\n\"wasmeld:timer\" = \"1.0.0\"\n" + ), + ) + .unwrap(); + let manifest = component.join("wasmeld.toml"); + let sync_manifest = manifest.clone(); + let report = tokio::task::spawn_blocking(move || sync_dependencies(sync_manifest, false)) + .await + .unwrap() + .unwrap(); + + assert_eq!(report.packages.len(), 2); + assert!( + component + .join("wit/deps/wasmeld-clock-1.0.0.wasm") + .is_file() + ); + assert!( + component + .join("wit/deps/wasmeld-timer-1.0.0.wasm") + .is_file() + ); + let lock = ModuleLock::read(component.join("wit.lock")).unwrap(); + assert_eq!(lock.packages.len(), 2); + assert!( + lock.packages + .iter() + .all(|package| package.source == format!("registry+http://{address}")) + ); + let locked_manifest = manifest.clone(); + tokio::task::spawn_blocking(move || sync_dependencies(locked_manifest, true)) + .await + .unwrap() + .unwrap(); + + server.abort(); +} + +async fn test_app(artifact_dir: &Path) -> Router { + let console = Console::new(ConsoleConfig { + artifact_dir: artifact_dir.to_path_buf(), + wit_registry_dir: artifact_dir.join("wit-packages"), + database_path: artifact_dir.join("console.db"), + ..ConsoleConfig::default() + }) + .await + .expect("console should start"); + app(Arc::new(console), Vec::new()) +} + +fn package_request(id: &str, revision: &str, component: &[u8]) -> Request { + let mut package = Cursor::new(Vec::new()); + let world = if id == "counter" { + "component:counter/counter-component@0.1.0" + } else { + "component:echo/echo-component@0.1.0" + }; + write_package(&mut package, id, revision, world, component) + .expect("test package should be valid"); + package_bytes_request(package.get_ref()) +} + +fn package_bytes_request(package: &[u8]) -> Request { + multipart_package_request( + "/api/v1/services", + "component.wasmpkg", + "application/vnd.wasm.component-package", + package, + ) +} + +fn wit_package_request(package: &[u8]) -> Request { + multipart_package_request( + "/api/v1/wit/packages", + "package.wasm", + "application/wasm", + package, + ) +} + +fn multipart_package_request( + uri: &str, + filename: &str, + content_type: &str, + package: &[u8], +) -> Request { + const BOUNDARY: &str = "wasmeld-console-test-boundary"; + let mut body = Vec::new(); + body.extend_from_slice(format!("--{BOUNDARY}\r\n").as_bytes()); + body.extend_from_slice( + format!("Content-Disposition: form-data; name=\"package\"; filename=\"{filename}\"\r\n") + .as_bytes(), + ); + body.extend_from_slice(format!("Content-Type: {content_type}\r\n\r\n").as_bytes()); + body.extend_from_slice(package); + body.extend_from_slice(b"\r\n"); + body.extend_from_slice(format!("--{BOUNDARY}--\r\n").as_bytes()); + + Request::builder() + .method("POST") + .uri(uri) + .header( + header::CONTENT_TYPE, + format!("multipart/form-data; boundary={BOUNDARY}"), + ) + .body(Body::from(body)) + .unwrap() +} + +fn json_request(uri: &str, value: Value) -> Request { + Request::builder() + .method("POST") + .uri(uri) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from(value.to_string())) + .unwrap() +} + +fn empty_post(uri: &str) -> Request { + Request::builder() + .method("POST") + .uri(uri) + .body(Body::empty()) + .unwrap() +} + +fn get_request(uri: &str) -> Request { + Request::builder().uri(uri).body(Body::empty()).unwrap() +} + +async fn response_json(response: axum::response::Response) -> Value { + let bytes = to_bytes(response.into_body(), usize::MAX) + .await + .expect("response body should be readable"); + serde_json::from_slice(&bytes).expect("response should be JSON") +} + +fn echo_component() -> PathBuf { + component_artifact("echo_component.wasm") +} + +fn component_artifact(name: &str) -> PathBuf { + COMPONENTS_BUILT.get_or_init(|| { + for manifest in [ + "components/echo/Cargo.toml", + "components/counter/Cargo.toml", + ] { + let status = Command::new("rustup") + .args([ + "run", + "1.90.0", + "cargo", + "build", + "--manifest-path", + manifest, + "--target", + "wasm32-wasip2", + "--release", + ]) + .current_dir(workspace_root()) + .status() + .expect("component build command should start"); + assert!(status.success(), "component build should succeed"); + } + }); + + workspace_root() + .join("target/wasm32-wasip2/release") + .join(name) +} + +fn workspace_root() -> &'static Path { + static ROOT: OnceLock = OnceLock::new(); + ROOT.get_or_init(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("console crate must live in the workspace") + .to_path_buf() + }) + .as_path() +}