diff --git a/.gitignore b/.gitignore index c2a9c4b..0cf6978 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,10 @@ venv/** # Backup files *.bak *.bak.* + +# Rust build artifacts +/target + +# Frontend dependencies +frontend/node_modules +frontend/dist diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..92ab449 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3799 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.15.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml 1.1.2+spec-1.1.0", + "winnow 1.0.2", + "yaml-rust2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.7", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[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-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "pm-agent-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "pm-core", + "reqwest", + "rustls", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "pm-auth" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "pm-core", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "pm-ca" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "pm-core", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "pm-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "config", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "toml 0.8.23", + "tracing", + "tracing-subscriber", + "ulid", + "uuid", +] + +[[package]] +name = "pm-reports" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "pm-core", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "pm-web" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "pm-core", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "ulid", + "uuid", +] + +[[package]] +name = "pm-worker" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "futures", + "pm-core", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.7", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_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.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "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", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "sha1", + "thiserror", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.4", + "serde", + "web-time", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..176f9fc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,62 @@ +[workspace] +resolver = "2" +members = [ + "crates/pm-web", + "crates/pm-worker", + "crates/pm-core", + "crates/pm-agent-client", + "crates/pm-auth", + "crates/pm-ca", + "crates/pm-reports", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +authors = ["Echo "] +license = "MIT" + +[workspace.dependencies] +# Async runtime +tokio = { version = "1", features = ["full"] } + +# Web framework +axum = { version = "0.8", features = ["ws", "macros"] } +tower = { version = "0.5" } +tower-http = { version = "0.6", features = ["fs", "trace", "cors", "request-id"] } + +# Database +sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "macros", "migrate", "uuid", "chrono", "json"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1" } +toml = { version = "0.8" } + +# Error handling +thiserror = { version = "2" } +anyhow = { version = "1" } + +# Logging / Tracing +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +# UUID / ULID +uuid = { version = "1", features = ["v4", "serde"] } +ulid = { version = "1", features = ["serde"] } + +# Time +chrono = { version = "0.4", features = ["serde"] } + +# HTTP client +reqwest = { version = "0.12", features = ["rustls-tls", "json"] } + +# TLS +rustls = { version = "0.23" } + +# Config +config = { version = "0.15" } + +# Misc +bytes = { version = "1" } +futures = { version = "0.3" } diff --git a/SPEC.md b/SPEC.md index 966b5ef..2919a5b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -174,9 +174,7 @@ Management plane web application communicating with Linux Patch API agents on ea - Patch Manager host has network connectivity to all managed agents - Linux Patch API agent is installed and running on each managed host - Server administrators manually distribute mTLS and root certificates to managed clients -- PostgreSQL is available on the Patch Manager host -- Server administrators manually distribute mTLS and root certificates to managed clients -- PostgreSQL is available on the Patch Manager host +- PostgreSQL 16+ is available on the Patch Manager host - Hardware host provides full-disk encryption (no OS-level disk encryption managed by the application) ## Dependencies diff --git a/config/config.example.toml b/config/config.example.toml new file mode 100644 index 0000000..9952a3b --- /dev/null +++ b/config/config.example.toml @@ -0,0 +1,95 @@ +# Linux Patch Manager — Example Configuration +# Copy to /etc/patch-manager/config.toml and edit for your environment. +# +# Environment variable overrides follow the pattern: +# PATCH_MANAGER__SECTION__KEY=value +# e.g. PATCH_MANAGER__DATABASE__URL=postgres://... + +# ============================================================ +# Web Server +# ============================================================ +[server] +# Bind address for the HTTPS listener +host = "0.0.0.0" + +# HTTPS port (443 for production; 8443 for non-root dev) +port = 443 + +# Path to compiled React SPA static files +static_dir = "/usr/share/patch-manager/frontend" + +# ============================================================ +# Database +# ============================================================ +[database] +# PostgreSQL connection URL +url = "postgres://patch_manager:CHANGEME@localhost/patch_manager" + +# Connection pool sizing +max_connections = 20 +min_connections = 2 + +# Seconds to wait for a connection from the pool +acquire_timeout_secs = 30 + +# ============================================================ +# Background Worker +# ============================================================ +[worker] +# Agent health check interval (seconds). Default: 300 = 5 minutes +health_poll_interval_secs = 300 + +# Agent patch data poll interval (seconds). Default: 1800 = 30 minutes +patch_poll_interval_secs = 1800 + +# Maximum concurrent mTLS agent calls (Tokio Semaphore) +max_concurrent_agent_calls = 64 + +# Worker heartbeat write interval (seconds) +heartbeat_interval_secs = 30 + +# ============================================================ +# Logging +# ============================================================ +[logging] +# Log level: trace, debug, info, warn, error +# Override with RUST_LOG environment variable +level = "info" + +# Output format: "json" (production) or "pretty" (development) +format = "json" + +# ============================================================ +# Security +# ============================================================ +[security] +# IP whitelist: list of CIDRs or individual IPs allowed to connect. +# IMPORTANT: An empty list allows ALL IPs. Restrict this in production. +# Example: ["10.0.0.0/8", "192.168.1.50"] +ip_whitelist = [] + +# Ed25519 JWT signing key (private key, PEM format) +# Generate: openssl genpkey -algorithm ed25519 -out /etc/patch-manager/jwt/signing.pem +jwt_signing_key_path = "/etc/patch-manager/jwt/signing.pem" + +# Ed25519 JWT verification key (public key, PEM format) +# Generate: openssl pkey -in /etc/patch-manager/jwt/signing.pem -pubout -out /etc/patch-manager/jwt/verify.pem +jwt_verify_key_path = "/etc/patch-manager/jwt/verify.pem" + +# JWT access token TTL in seconds (default: 900 = 15 minutes) +jwt_access_ttl_secs = 900 + +# mTLS client certificate for agent communication +agent_client_cert_path = "/etc/patch-manager/certs/client.crt" +agent_client_key_path = "/etc/patch-manager/certs/client.key" + +# Internal CA certificate and private key +# Private key has 0600 permissions; protected by hardware-host FDE +ca_cert_path = "/etc/patch-manager/ca/ca.crt" +ca_key_path = "/etc/patch-manager/ca/ca.key" + +# Web UI TLS certificate (default: self-signed from internal CA) +# Set web_tls_strategy = 'operator_supplied' in system_config and +# point these paths to your certificate/key to use your own cert. +web_tls_cert_path = "/etc/patch-manager/tls/web.crt" +web_tls_key_path = "/etc/patch-manager/tls/web.key" diff --git a/crates/pm-agent-client/Cargo.toml b/crates/pm-agent-client/Cargo.toml new file mode 100644 index 0000000..87d3c90 --- /dev/null +++ b/crates/pm-agent-client/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pm-agent-client" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +pm-core = { path = "../pm-core" } +tokio = { workspace = true } +reqwest = { workspace = true } +rustls = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } diff --git a/crates/pm-agent-client/src/client.rs b/crates/pm-agent-client/src/client.rs new file mode 100644 index 0000000..b087e6a --- /dev/null +++ b/crates/pm-agent-client/src/client.rs @@ -0,0 +1,10 @@ +//! Agent HTTP client stub. +//! Full mTLS Rustls-based implementation arrives in M4. + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AgentClientError { + #[error("Not yet implemented")] + NotImplemented, +} diff --git a/crates/pm-agent-client/src/lib.rs b/crates/pm-agent-client/src/lib.rs new file mode 100644 index 0000000..fe6a869 --- /dev/null +++ b/crates/pm-agent-client/src/lib.rs @@ -0,0 +1,4 @@ +//! pm-agent-client — mTLS HTTP client for Linux Patch API agent communication. +//! +//! M1: Stub. Full implementation in M4. +pub mod client; diff --git a/crates/pm-auth/Cargo.toml b/crates/pm-auth/Cargo.toml new file mode 100644 index 0000000..186f013 --- /dev/null +++ b/crates/pm-auth/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pm-auth" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +pm-core = { path = "../pm-core" } +tokio = { workspace = true } +axum = { workspace = true } +sqlx = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } diff --git a/crates/pm-auth/src/jwt.rs b/crates/pm-auth/src/jwt.rs new file mode 100644 index 0000000..6bdd74a --- /dev/null +++ b/crates/pm-auth/src/jwt.rs @@ -0,0 +1 @@ +//! jwt — stub for M2. diff --git a/crates/pm-auth/src/lib.rs b/crates/pm-auth/src/lib.rs new file mode 100644 index 0000000..df5c611 --- /dev/null +++ b/crates/pm-auth/src/lib.rs @@ -0,0 +1,10 @@ +//! pm-auth — Authentication and authorization. +//! +//! Modules: password (Argon2id), jwt (EdDSA), refresh tokens, +//! mfa_totp, mfa_webauthn, rbac, session. +//! +//! M1: Stub. Full implementation in M2. +pub mod password; +pub mod jwt; +pub mod rbac; +pub mod session; diff --git a/crates/pm-auth/src/password.rs b/crates/pm-auth/src/password.rs new file mode 100644 index 0000000..274959f --- /dev/null +++ b/crates/pm-auth/src/password.rs @@ -0,0 +1 @@ +//! password — stub for M2. diff --git a/crates/pm-auth/src/rbac.rs b/crates/pm-auth/src/rbac.rs new file mode 100644 index 0000000..7ab6390 --- /dev/null +++ b/crates/pm-auth/src/rbac.rs @@ -0,0 +1 @@ +//! rbac — stub for M2. diff --git a/crates/pm-auth/src/session.rs b/crates/pm-auth/src/session.rs new file mode 100644 index 0000000..571a321 --- /dev/null +++ b/crates/pm-auth/src/session.rs @@ -0,0 +1 @@ +//! session — stub for M2. diff --git a/crates/pm-ca/Cargo.toml b/crates/pm-ca/Cargo.toml new file mode 100644 index 0000000..f4ed8ed --- /dev/null +++ b/crates/pm-ca/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pm-ca" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +pm-core = { path = "../pm-core" } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +chrono = { workspace = true } diff --git a/crates/pm-ca/src/ca.rs b/crates/pm-ca/src/ca.rs new file mode 100644 index 0000000..9dd0a79 --- /dev/null +++ b/crates/pm-ca/src/ca.rs @@ -0,0 +1 @@ +//! Internal CA stub for M8. diff --git a/crates/pm-ca/src/lib.rs b/crates/pm-ca/src/lib.rs new file mode 100644 index 0000000..2f36691 --- /dev/null +++ b/crates/pm-ca/src/lib.rs @@ -0,0 +1,7 @@ +//! pm-ca — Internal Certificate Authority. +//! +//! Issues and renews mTLS client certificates for agent communication. +//! Uses rcgen + rustls. CA key stored at /etc/patch-manager/ca/ca.key. +//! +//! M1: Stub. Full implementation in M8. +pub mod ca; diff --git a/crates/pm-core/Cargo.toml b/crates/pm-core/Cargo.toml new file mode 100644 index 0000000..1961bff --- /dev/null +++ b/crates/pm-core/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "pm-core" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +tokio = { workspace = true } +sqlx = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +toml = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +uuid = { workspace = true } +ulid = { workspace = true } +chrono = { workspace = true } +config = { workspace = true } +axum = { workspace = true } diff --git a/crates/pm-core/src/config.rs b/crates/pm-core/src/config.rs new file mode 100644 index 0000000..357d567 --- /dev/null +++ b/crates/pm-core/src/config.rs @@ -0,0 +1,137 @@ +use serde::{Deserialize, Serialize}; +use config::{Config, ConfigError, Environment, File}; + +/// Top-level application configuration. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AppConfig { + pub server: ServerConfig, + pub database: DatabaseConfig, + pub worker: WorkerConfig, + pub logging: LoggingConfig, + pub security: SecurityConfig, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ServerConfig { + /// Bind address for the web server + pub host: String, + /// HTTPS port + pub port: u16, + /// Path to static frontend assets + pub static_dir: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct DatabaseConfig { + /// Full PostgreSQL connection URL + pub url: String, + /// Maximum pool connections + pub max_connections: u32, + /// Minimum pool connections + pub min_connections: u32, + /// Connection acquire timeout in seconds + pub acquire_timeout_secs: u64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WorkerConfig { + /// Health poll interval in seconds (default: 300 = 5 min) + pub health_poll_interval_secs: u64, + /// Patch data poll interval in seconds (default: 1800 = 30 min) + pub patch_poll_interval_secs: u64, + /// Maximum concurrent agent calls + pub max_concurrent_agent_calls: usize, + /// Worker heartbeat interval in seconds + pub heartbeat_interval_secs: u64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct LoggingConfig { + /// Log level filter: trace, debug, info, warn, error + pub level: String, + /// Output format: json or pretty + pub format: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct SecurityConfig { + /// IP whitelist (CIDR or individual IPs); empty = allow all (not recommended) + pub ip_whitelist: Vec, + /// JWT signing key path (Ed25519 PEM) + pub jwt_signing_key_path: String, + /// JWT verification key path (Ed25519 public PEM) + pub jwt_verify_key_path: String, + /// JWT access token TTL in seconds (default: 900 = 15 min) + pub jwt_access_ttl_secs: u64, + /// Agent mTLS client cert path + pub agent_client_cert_path: String, + /// Agent mTLS client key path + pub agent_client_key_path: String, + /// Internal CA cert path + pub ca_cert_path: String, + /// Internal CA key path + pub ca_key_path: String, + /// Web UI TLS cert path + pub web_tls_cert_path: String, + /// Web UI TLS key path + pub web_tls_key_path: String, +} + +impl AppConfig { + /// Load configuration from a TOML file and environment variable overrides. + /// + /// Environment variables follow the pattern: `PATCH_MANAGER__SECTION__KEY` + /// e.g. `PATCH_MANAGER__DATABASE__URL=postgres://...` + pub fn load(config_path: &str) -> Result { + let cfg = Config::builder() + .add_source(File::with_name(config_path).required(false)) + .add_source( + Environment::with_prefix("PATCH_MANAGER") + .separator("__") + .try_parsing(true), + ) + .build()?; + + cfg.try_deserialize() + } +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + server: ServerConfig { + host: "0.0.0.0".to_string(), + port: 443, + static_dir: "/usr/share/patch-manager/frontend".to_string(), + }, + database: DatabaseConfig { + url: "postgres://patch_manager:changeme@localhost/patch_manager".to_string(), + max_connections: 20, + min_connections: 2, + acquire_timeout_secs: 30, + }, + worker: WorkerConfig { + health_poll_interval_secs: 300, + patch_poll_interval_secs: 1800, + max_concurrent_agent_calls: 64, + heartbeat_interval_secs: 30, + }, + logging: LoggingConfig { + level: "info".to_string(), + format: "json".to_string(), + }, + security: SecurityConfig { + ip_whitelist: vec![], + jwt_signing_key_path: "/etc/patch-manager/jwt/signing.pem".to_string(), + jwt_verify_key_path: "/etc/patch-manager/jwt/verify.pem".to_string(), + jwt_access_ttl_secs: 900, + agent_client_cert_path: "/etc/patch-manager/certs/client.crt".to_string(), + agent_client_key_path: "/etc/patch-manager/certs/client.key".to_string(), + ca_cert_path: "/etc/patch-manager/ca/ca.crt".to_string(), + ca_key_path: "/etc/patch-manager/ca/ca.key".to_string(), + web_tls_cert_path: "/etc/patch-manager/tls/web.crt".to_string(), + web_tls_key_path: "/etc/patch-manager/tls/web.key".to_string(), + }, + } + } +} diff --git a/crates/pm-core/src/db.rs b/crates/pm-core/src/db.rs new file mode 100644 index 0000000..453d403 --- /dev/null +++ b/crates/pm-core/src/db.rs @@ -0,0 +1,69 @@ +use sqlx::postgres::{PgPool, PgPoolOptions}; +use std::time::Duration; +use crate::config::DatabaseConfig; + +/// Initialize and return a PostgreSQL connection pool. +pub async fn init_pool(cfg: &DatabaseConfig) -> Result { + let pool = PgPoolOptions::new() + .max_connections(cfg.max_connections) + .min_connections(cfg.min_connections) + .acquire_timeout(Duration::from_secs(cfg.acquire_timeout_secs)) + .connect(&cfg.url) + .await?; + + tracing::info!( + max_connections = cfg.max_connections, + "PostgreSQL connection pool initialized" + ); + + Ok(pool) +} + +/// Run embedded SQLx migrations. +/// Uses a PostgreSQL advisory lock to ensure only one writer runs migrations. +pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> { + tracing::info!("Acquiring advisory lock for migrations"); + + // Advisory lock key — consistent hash of the application name + const LOCK_KEY: i64 = 0x7061_7463_686d_6772; // "patchmgr" bytes + + // Acquire advisory lock; blocks until granted + sqlx::query("SELECT pg_advisory_lock($1)") + .bind(LOCK_KEY) + .execute(pool) + .await + .map_err(|e| { + tracing::error!(error = %e, "Failed to acquire advisory lock"); + e + }) + .expect("Advisory lock must be acquired before running migrations"); + + tracing::info!("Running database migrations"); + let result = sqlx::migrate!("../../migrations").run(pool).await; + + // Always release the lock + sqlx::query("SELECT pg_advisory_unlock($1)") + .bind(LOCK_KEY) + .execute(pool) + .await + .ok(); + + match &result { + Ok(_) => tracing::info!("Database migrations completed successfully"), + Err(e) => tracing::error!(error = %e, "Database migrations failed"), + } + + result +} + +/// Check that the database schema is at the expected version. +/// Used by the worker to wait until migrations have been applied. +pub async fn check_schema_version(pool: &PgPool) -> Result { + let row: (i64,) = sqlx::query_as( + "SELECT COUNT(*) FROM _sqlx_migrations WHERE success = true", + ) + .fetch_one(pool) + .await?; + + Ok(row.0) +} diff --git a/crates/pm-core/src/error.rs b/crates/pm-core/src/error.rs new file mode 100644 index 0000000..e9cd4b0 --- /dev/null +++ b/crates/pm-core/src/error.rs @@ -0,0 +1,124 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Unified application error type. +#[derive(Debug, Error)] +pub enum AppError { + #[error("Not found: {0}")] + NotFound(String), + + #[error("Unauthorized: {0}")] + Unauthorized(String), + + #[error("Forbidden: {0}")] + Forbidden(String), + + #[error("Bad request: {0}")] + BadRequest(String), + + #[error("Conflict: {0}")] + Conflict(String), + + #[error("Unprocessable entity: {0}")] + UnprocessableEntity(String), + + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + + #[error("Internal error: {0}")] + Internal(#[from] anyhow::Error), + + #[error("Configuration error: {0}")] + Config(String), +} + +/// JSON error envelope returned to clients. +#[derive(Debug, Serialize, Deserialize)] +pub struct ErrorResponse { + pub error: ErrorDetail, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ErrorDetail { + /// Machine-readable error code (e.g. "not_found", "unauthorized") + pub code: String, + /// Human-readable message + pub message: String, + /// Request ID for correlation (set by middleware) + pub request_id: Option, + /// Optional structured details + pub details: Option, +} + +impl ErrorResponse { + pub fn new(code: impl Into, message: impl Into) -> Self { + Self { + error: ErrorDetail { + code: code.into(), + message: message.into(), + request_id: None, + details: None, + }, + } + } + + pub fn with_request_id(mut self, request_id: impl Into) -> Self { + self.error.request_id = Some(request_id.into()); + self + } + + pub fn with_details(mut self, details: serde_json::Value) -> Self { + self.error.details = Some(details); + self + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, code, message) = match &self { + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg.clone()), + AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, "unauthorized", msg.clone()), + AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, "forbidden", msg.clone()), + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg.clone()), + AppError::Conflict(msg) => (StatusCode::CONFLICT, "conflict", msg.clone()), + AppError::UnprocessableEntity(msg) => { + (StatusCode::UNPROCESSABLE_ENTITY, "unprocessable_entity", msg.clone()) + } + AppError::Database(e) => { + tracing::error!(error = %e, "Database error"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "internal_error", + "An internal error occurred".to_string(), + ) + } + AppError::Internal(e) => { + tracing::error!(error = %e, "Internal error"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "internal_error", + "An internal error occurred".to_string(), + ) + } + AppError::Config(msg) => { + tracing::error!(error = %msg, "Configuration error"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "config_error", + "Server configuration error".to_string(), + ) + } + }; + + let body = ErrorResponse::new(code, message); + (status, Json(body)).into_response() + } +} + +/// Convenience alias for handler return types. +pub type ApiResult = Result; diff --git a/crates/pm-core/src/lib.rs b/crates/pm-core/src/lib.rs new file mode 100644 index 0000000..109fe52 --- /dev/null +++ b/crates/pm-core/src/lib.rs @@ -0,0 +1,9 @@ +pub mod config; +pub mod db; +pub mod error; +pub mod logging; +pub mod request_id; + +// Re-export commonly used types +pub use error::{AppError, ErrorResponse}; +pub use config::AppConfig; diff --git a/crates/pm-core/src/logging.rs b/crates/pm-core/src/logging.rs new file mode 100644 index 0000000..c6a286e --- /dev/null +++ b/crates/pm-core/src/logging.rs @@ -0,0 +1,32 @@ +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use crate::config::LoggingConfig; + +/// Initialize the global tracing subscriber. +/// +/// Format is controlled by `cfg.format`: +/// - `"json"` — machine-readable JSON (production default) +/// - anything else — human-readable pretty output (development) +/// +/// Log level is controlled by `cfg.level` (e.g. `"info"`, `"debug"`). +/// The `RUST_LOG` environment variable overrides `cfg.level`. +pub fn init(cfg: &LoggingConfig) { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(&cfg.level)); + + match cfg.format.as_str() { + "json" => { + tracing_subscriber::registry() + .with(filter) + .with(fmt::layer().json().with_current_span(true)) + .init(); + } + _ => { + tracing_subscriber::registry() + .with(filter) + .with(fmt::layer().pretty()) + .init(); + } + } + + tracing::info!(format = %cfg.format, level = %cfg.level, "Logging initialized"); +} diff --git a/crates/pm-core/src/request_id.rs b/crates/pm-core/src/request_id.rs new file mode 100644 index 0000000..68cce01 --- /dev/null +++ b/crates/pm-core/src/request_id.rs @@ -0,0 +1,44 @@ +use axum::{ + extract::Request, + http::HeaderValue, + middleware::Next, + response::Response, +}; +use ulid::Ulid; + +/// HTTP header name for request correlation IDs. +pub const REQUEST_ID_HEADER: &str = "x-request-id"; + +/// Axum middleware that generates a ULID request ID and attaches it +/// to both the request extensions and the response header. +pub async fn request_id_middleware(mut req: Request, next: Next) -> Response { + // Use existing X-Request-Id if provided by upstream proxy, else generate + let id = req + .headers() + .get(REQUEST_ID_HEADER) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + .unwrap_or_else(|| Ulid::new().to_string()); + + // Insert as extension so handlers can access it + req.extensions_mut().insert(RequestId(id.clone())); + + let mut response = next.run(req).await; + + // Echo the ID back in the response + if let Ok(value) = HeaderValue::from_str(&id) { + response.headers_mut().insert(REQUEST_ID_HEADER, value); + } + + response +} + +/// Extractor type for retrieving the request ID inside handlers. +#[derive(Debug, Clone)] +pub struct RequestId(pub String); + +impl std::fmt::Display for RequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/pm-reports/Cargo.toml b/crates/pm-reports/Cargo.toml new file mode 100644 index 0000000..6b7cd5a --- /dev/null +++ b/crates/pm-reports/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pm-reports" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +pm-core = { path = "../pm-core" } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +chrono = { workspace = true } diff --git a/crates/pm-reports/src/csv.rs b/crates/pm-reports/src/csv.rs new file mode 100644 index 0000000..90eab04 --- /dev/null +++ b/crates/pm-reports/src/csv.rs @@ -0,0 +1 @@ +//! csv report generation stub for M9. diff --git a/crates/pm-reports/src/lib.rs b/crates/pm-reports/src/lib.rs new file mode 100644 index 0000000..70a79c2 --- /dev/null +++ b/crates/pm-reports/src/lib.rs @@ -0,0 +1,7 @@ +//! pm-reports — CSV and PDF report generation. +//! +//! Uses printpdf + plotters for in-process PDF with charts. +//! +//! M1: Stub. Full implementation in M9. +pub mod csv; +pub mod pdf; diff --git a/crates/pm-reports/src/pdf.rs b/crates/pm-reports/src/pdf.rs new file mode 100644 index 0000000..f4b1d1f --- /dev/null +++ b/crates/pm-reports/src/pdf.rs @@ -0,0 +1 @@ +//! pdf report generation stub for M9. diff --git a/crates/pm-web/Cargo.toml b/crates/pm-web/Cargo.toml new file mode 100644 index 0000000..a497da1 --- /dev/null +++ b/crates/pm-web/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pm-web" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[[bin]] +name = "pm-web" +path = "src/main.rs" + +[dependencies] +pm-core = { path = "../pm-core" } +tokio = { workspace = true } +axum = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +sqlx = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +uuid = { workspace = true } +ulid = { workspace = true } +chrono = { workspace = true } diff --git a/crates/pm-web/src/main.rs b/crates/pm-web/src/main.rs new file mode 100644 index 0000000..03c0e39 --- /dev/null +++ b/crates/pm-web/src/main.rs @@ -0,0 +1,137 @@ +//! pm-web — Linux Patch Manager web server. +//! +//! Serves the React SPA, exposes the REST API, and handles WebSocket relay. + +use axum::{ + extract::State, + http::StatusCode, + middleware, + response::Json, + routing::get, + Router, +}; +use pm_core::{ + config::AppConfig, + db, + logging, + request_id::request_id_middleware, +}; +use serde_json::{json, Value}; +use std::{net::SocketAddr, sync::Arc}; +use tower_http::{ + services::ServeDir, + trace::TraceLayer, +}; + +/// Shared application state threaded through Axum. +#[derive(Clone)] +pub struct AppState { + pub db: sqlx::PgPool, + pub config: Arc, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Load configuration + let config_path = std::env::var("PATCH_MANAGER_CONFIG") + .unwrap_or_else(|_| "/etc/patch-manager/config.toml".to_string()); + + let config = AppConfig::load(&config_path) + .unwrap_or_else(|_| { + eprintln!("Config file not found or invalid, using defaults"); + AppConfig::default() + }); + + // Initialize logging + logging::init(&config.logging); + + tracing::info!(version = env!("CARGO_PKG_VERSION"), "patch-manager-web starting"); + + // Initialize database pool + let pool = db::init_pool(&config.database).await?; + + // Run migrations (advisory lock guards single-writer) + db::run_migrations(&pool).await?; + + let state = AppState { + db: pool, + config: Arc::new(config.clone()), + }; + + // Build the application router + let app = build_router(state); + + // Bind address + let addr: SocketAddr = format!("{}:{}", config.server.host, config.server.port) + .parse() + .expect("Invalid bind address"); + + tracing::info!(%addr, "Listening"); + + // TODO M8: wrap with TLS (rustls). For M1 we bind plain HTTP for local dev. + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +/// Construct the full Axum router. +pub fn build_router(state: AppState) -> Router { + let static_dir = state.config.server.static_dir.clone(); + + Router::new() + // Health / status (unauthenticated) + .route("/status/health", get(health_handler)) + // API v1 routes (stub — expanded in later milestones) + .nest("/api/v1", api_v1_router()) + // Serve React SPA static files; fallback to index.html for client-side routing + .fallback_service( + ServeDir::new(&static_dir) + .append_index_html_on_directories(true), + ) + // Middleware stack + .layer(middleware::from_fn(request_id_middleware)) + .layer(TraceLayer::new_for_http()) + .with_state(state) +} + +/// API v1 sub-router — routes added per milestone. +fn api_v1_router() -> Router { + Router::new() + // M2+: auth routes will be nested here + // M3+: host/group/user routes + // M4+: fleet status, agent polling + // M5+: jobs + // M6+: maintenance windows + // M7+: websocket relay + // M8+: certificates + // M9+: reports + // M10+: settings +} + +/// GET /status/health — liveness probe. +/// +/// Returns 200 OK with a JSON payload including service name, version, +/// and basic database reachability. +async fn health_handler(State(state): State) -> Result, StatusCode> { + // Quick DB ping + let db_ok = sqlx::query("SELECT 1") + .execute(&state.db) + .await + .is_ok(); + + let status = if db_ok { "healthy" } else { "degraded" }; + + let body = json!({ + "service": "patch-manager-web", + "version": env!("CARGO_PKG_VERSION"), + "status": status, + "database": if db_ok { "ok" } else { "error" }, + }); + + if db_ok { + Ok(Json(body)) + } else { + Err(StatusCode::SERVICE_UNAVAILABLE) + } +} diff --git a/crates/pm-worker/Cargo.toml b/crates/pm-worker/Cargo.toml new file mode 100644 index 0000000..6022011 --- /dev/null +++ b/crates/pm-worker/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pm-worker" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[[bin]] +name = "pm-worker" +path = "src/main.rs" + +[dependencies] +pm-core = { path = "../pm-core" } +tokio = { workspace = true } +sqlx = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } +futures = { workspace = true } diff --git a/crates/pm-worker/src/main.rs b/crates/pm-worker/src/main.rs new file mode 100644 index 0000000..2ec990a --- /dev/null +++ b/crates/pm-worker/src/main.rs @@ -0,0 +1,128 @@ +//! pm-worker — Linux Patch Manager background worker. +//! +//! Handles scheduled polling, job execution, maintenance window scheduling, +//! retry logic, email notifications, and data pruning. + +use pm_core::{ + config::AppConfig, + db, + logging, +}; +use sqlx::PgPool; +use std::{sync::Arc, time::Duration}; +use tokio::time; + +/// Minimum number of applied migrations the worker requires before +/// accepting work. Prevents the worker from running against a schema +/// that hasn't been migrated yet. +const REQUIRED_MIGRATION_COUNT: i64 = 1; + +/// How long to wait between schema-version checks before giving up. +const SCHEMA_CHECK_TIMEOUT: Duration = Duration::from_secs(120); + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Load configuration + let config_path = std::env::var("PATCH_MANAGER_CONFIG") + .unwrap_or_else(|_| "/etc/patch-manager/config.toml".to_string()); + + let config = AppConfig::load(&config_path) + .unwrap_or_else(|_| { + eprintln!("Config file not found or invalid, using defaults"); + AppConfig::default() + }); + + // Initialize logging + logging::init(&config.logging); + + tracing::info!(version = env!("CARGO_PKG_VERSION"), "patch-manager-worker starting"); + + // Initialize database pool + let pool = db::init_pool(&config.database).await?; + + // Wait for schema to be at the expected version (web process runs migrations) + wait_for_schema(&pool).await?; + + let config = Arc::new(config); + + // Spawn worker tasks + let heartbeat_handle = tokio::spawn(run_heartbeat( + pool.clone(), + config.worker.heartbeat_interval_secs, + )); + + // TODO M4: spawn health_poller, patch_data_poller + // TODO M5: spawn job_executor + // TODO M6: spawn job_scheduler + + tracing::info!("Worker tasks started"); + + // Wait for all tasks (they run indefinitely) + let _ = tokio::join!(heartbeat_handle); + + Ok(()) +} + +/// Wait until the database schema has at least `REQUIRED_MIGRATION_COUNT` +/// successful migrations applied. Retries every 5 seconds up to +/// `SCHEMA_CHECK_TIMEOUT`. +async fn wait_for_schema(pool: &PgPool) -> anyhow::Result<()> { + let deadline = tokio::time::Instant::now() + SCHEMA_CHECK_TIMEOUT; + + loop { + match db::check_schema_version(pool).await { + Ok(count) if count >= REQUIRED_MIGRATION_COUNT => { + tracing::info!(migration_count = count, "Schema version check passed"); + return Ok(()); + } + Ok(count) => { + tracing::warn!( + migration_count = count, + required = REQUIRED_MIGRATION_COUNT, + "Schema not ready, waiting..." + ); + } + Err(e) => { + tracing::warn!(error = %e, "Schema version check failed, retrying..."); + } + } + + if tokio::time::Instant::now() >= deadline { + anyhow::bail!( + "Schema not ready after {}s — is the web process running migrations?", + SCHEMA_CHECK_TIMEOUT.as_secs() + ); + } + + time::sleep(Duration::from_secs(5)).await; + } +} + +/// Writes a heartbeat row to `worker_heartbeat` every `interval_secs`. +/// The web process can query this to confirm the worker is alive. +async fn run_heartbeat(pool: PgPool, interval_secs: u64) { + let interval = Duration::from_secs(interval_secs); + let mut ticker = time::interval(interval); + + loop { + ticker.tick().await; + + let result = sqlx::query( + r#" + INSERT INTO worker_heartbeat (id, last_seen, worker_version) + VALUES (1, NOW(), $1) + ON CONFLICT (id) DO UPDATE + SET last_seen = EXCLUDED.last_seen, + worker_version = EXCLUDED.worker_version + "#, + ) + .bind(env!("CARGO_PKG_VERSION")) + .execute(&pool) + .await; + + match result { + Ok(_) => tracing::debug!("Worker heartbeat written"), + Err(e) => tracing::error!(error = %e, "Worker heartbeat failed"), + } + } +} diff --git a/docs/runbooks/restore.md b/docs/runbooks/restore.md new file mode 100644 index 0000000..b4ede47 --- /dev/null +++ b/docs/runbooks/restore.md @@ -0,0 +1,76 @@ +# Linux Patch Manager — Backup & Restore Runbook + +## Overview + +This runbook covers backup and restoration of the Linux Patch Manager. +The application state lives in: +- PostgreSQL database (`patch_manager`) +- Internal CA private key (`/etc/patch-manager/ca/ca.key`) +- JWT signing key (`/etc/patch-manager/jwt/signing.pem`) +- Application config (`/etc/patch-manager/config.toml`) +- Operator-supplied TLS cert/key (if using `operator_supplied` strategy) + +## Backup + +### 1. Database +```bash +pg_dump -U patch_manager -Fc patch_manager > patch_manager_$(date +%Y%m%d_%H%M%S).dump +``` + +### 2. Configuration and Keys +```bash +tar -czf patch_manager_config_$(date +%Y%m%d_%H%M%S).tar.gz \ + /etc/patch-manager/ +``` +> **Security:** The archive contains private keys. Encrypt before storing: +> `gpg --symmetric patch_manager_config_*.tar.gz` + +### 3. Recommended Backup Schedule +- Database: daily pg_dump, retained 30 days +- Config/keys: on every change, retained indefinitely (encrypted) + +## Restore + +### Prerequisites +- Fresh Ubuntu 24.04 host +- Run `scripts/setup.sh` to create user, directories, and PostgreSQL + +### 1. Restore Configuration and Keys +```bash +tar -xzf patch_manager_config_.tar.gz -C / +chown -R patch-manager:patch-manager /etc/patch-manager/ +chmod 600 /etc/patch-manager/ca/ca.key +chmod 600 /etc/patch-manager/jwt/signing.pem +``` + +### 2. Restore Database +```bash +# Create empty database (if not already created by setup.sh) +sudo -u postgres createdb -O patch_manager patch_manager + +# Restore +pg_restore -U patch_manager -d patch_manager -Fc patch_manager_.dump +``` + +### 3. Install and Start Services +```bash +# Install binaries +cp pm-web pm-worker /usr/local/bin/ + +# Install frontend +scripts/build-frontend.sh + +# Start services +systemctl enable --now patch-manager-web patch-manager-worker +``` + +### 4. Verify +```bash +curl -k https://localhost/status/health +# Expected: {"status": "healthy", ...} +``` + +## Notes +- Migrations run automatically on web process startup. +- The CA private key is the most critical secret — losing it requires re-issuing all mTLS certificates. +- JWT signing key rotation is handled automatically every 90 days; no manual intervention needed. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2f8996e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + + Linux Patch Manager + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1ee43dd --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "patch-manager-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.0.0", + "@mui/material": "^7.0.0", + "axios": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.5.3", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@typescript-eslint/eslint-plugin": "^8.30.0", + "@typescript-eslint/parser": "^8.30.0", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.24.0", + "typescript": "^5.8.3", + "vite": "^6.3.3" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..301ded4 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,37 @@ +import { Routes, Route, Navigate } from 'react-router-dom' +import { CssBaseline, ThemeProvider } from '@mui/material' +import { lightTheme } from './theme/theme' + +// Placeholder pages — implemented in M2+ +const PlaceholderPage = ({ title }: { title: string }) => ( +
+

{title}

+

Coming soon in a future milestone.

+
+) + +function App() { + return ( + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) +} + +export default App diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..4e124f6 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,8 @@ +import axios from 'axios' + +// Base API client — JWT interceptors added in M2 +export const apiClient = axios.create({ + baseURL: '/api/v1', + headers: { 'Content-Type': 'application/json' }, + timeout: 30_000, +}) diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..34fc0d4 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,2 @@ +*, *::before, *::after { box-sizing: border-box; } +body { margin: 0; font-family: 'Roboto', sans-serif; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..38d3698 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + +) diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts new file mode 100644 index 0000000..52aa461 --- /dev/null +++ b/frontend/src/theme/theme.ts @@ -0,0 +1,23 @@ +import { createTheme } from '@mui/material/styles' + +export const lightTheme = createTheme({ + palette: { + mode: 'light', + primary: { main: '#1565C0' }, + secondary: { main: '#0288D1' }, + }, + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + }, +}) + +export const darkTheme = createTheme({ + palette: { + mode: 'dark', + primary: { main: '#42A5F5' }, + secondary: { main: '#26C6DA' }, + }, + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + }, +}) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..7678929 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,47 @@ +// Core TypeScript types — expanded per milestone + +export type UserRole = 'admin' | 'operator' +export type AuthProvider = 'local' | 'azure_sso' +export type HostHealthStatus = 'pending' | 'healthy' | 'degraded' | 'unreachable' +export type JobStatus = 'queued' | 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled' +export type JobKind = 'patch_apply' | 'patch_remove' | 'reboot' | 'rollback' + +export interface ApiError { + error: { + code: string + message: string + request_id?: string + details?: unknown + } +} + +export interface Host { + id: string + fqdn: string + ip_address: string + display_name: string + health_status: HostHealthStatus + os_family?: string + os_name?: string + agent_version?: string + registered_at: string +} + +export interface Group { + id: string + name: string + description: string + created_at: string +} + +export interface User { + id: string + username: string + display_name: string + email: string + role: UserRole + auth_provider: AuthProvider + mfa_enabled: boolean + is_active: boolean + last_login_at?: string +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..5413626 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..9734276 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': resolve(__dirname, './src'), + }, + }, + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + '/status': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + sourcemap: false, + rollupOptions: { + output: { + manualChunks: { + vendor: ['react', 'react-dom', 'react-router-dom'], + mui: ['@mui/material', '@mui/icons-material'], + }, + }, + }, + }, +}) diff --git a/migrations/001_initial_schema.sql b/migrations/001_initial_schema.sql new file mode 100644 index 0000000..e4b9b63 --- /dev/null +++ b/migrations/001_initial_schema.sql @@ -0,0 +1,385 @@ +-- Migration: 001_initial_schema +-- Description: Full initial schema for Linux Patch Manager +-- All tables, indexes, and constraints in a single initial migration. + +-- ============================================================ +-- Extensions +-- ============================================================ +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_bytes, crypt +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- fuzzy text search on host names + +-- ============================================================ +-- Enumerations +-- ============================================================ + +CREATE TYPE user_role AS ENUM ('admin', 'operator'); +CREATE TYPE auth_provider AS ENUM ('local', 'azure_sso'); +CREATE TYPE host_health_status AS ENUM ('pending', 'healthy', 'degraded', 'unreachable'); +CREATE TYPE job_status AS ENUM ('queued', 'pending', 'running', 'succeeded', 'failed', 'cancelled'); +CREATE TYPE job_kind AS ENUM ('patch_apply', 'patch_remove', 'reboot', 'rollback'); +CREATE TYPE window_recurrence AS ENUM ('once', 'daily', 'weekly', 'monthly'); +CREATE TYPE cert_status AS ENUM ('active', 'revoked', 'expired'); +CREATE TYPE audit_action AS ENUM ( + 'user_login', 'user_logout', 'user_login_failed', + 'user_created', 'user_deleted', 'user_updated', + 'host_registered', 'host_removed', + 'group_created', 'group_deleted', + 'group_membership_changed', + 'patch_job_created', 'patch_job_cancelled', 'patch_job_rollback', + 'maintenance_window_created', 'maintenance_window_updated', 'maintenance_window_deleted', + 'certificate_issued', 'certificate_renewed', 'certificate_revoked', 'certificate_downloaded', + 'config_changed', + 'discovery_scan_started' +); + +-- ============================================================ +-- Groups (defined before users/hosts for FK ordering) +-- ============================================================ + +CREATE TABLE groups ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE, + description TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_groups_name ON groups (name); + +-- ============================================================ +-- Users +-- ============================================================ + +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL DEFAULT '', + email TEXT NOT NULL UNIQUE, + role user_role NOT NULL DEFAULT 'operator', + auth_provider auth_provider NOT NULL DEFAULT 'local', + -- Local auth fields (NULL for SSO-only users) + password_hash TEXT, + -- MFA + totp_secret TEXT, -- NULL = TOTP not configured + webauthn_credential JSONB, -- NULL = WebAuthn not configured + mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE, + -- Azure SSO + azure_oid TEXT UNIQUE, -- Azure Object ID + -- Account state + is_active BOOLEAN NOT NULL DEFAULT TRUE, + force_password_reset BOOLEAN NOT NULL DEFAULT FALSE, + last_login_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_users_email ON users (email); +CREATE INDEX idx_users_azure_oid ON users (azure_oid) WHERE azure_oid IS NOT NULL; +CREATE INDEX idx_users_role ON users (role); + +-- ============================================================ +-- User <-> Group membership +-- ============================================================ + +CREATE TABLE user_groups ( + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, + assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id, group_id) +); + +CREATE INDEX idx_user_groups_group ON user_groups (group_id); + +-- ============================================================ +-- Refresh Tokens +-- ============================================================ + +CREATE TABLE refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + -- Stored as Argon2id hash of the opaque token bytes + token_hash TEXT NOT NULL UNIQUE, + issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + -- 1-hour sliding inactivity window; updated on each use + expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '1 hour', + revoked BOOLEAN NOT NULL DEFAULT FALSE, + revoked_at TIMESTAMPTZ, + user_agent TEXT, + ip_address INET +); + +CREATE INDEX idx_refresh_tokens_user ON refresh_tokens (user_id); +CREATE INDEX idx_refresh_tokens_expires ON refresh_tokens (expires_at) WHERE revoked = FALSE; + +-- ============================================================ +-- Hosts +-- ============================================================ + +CREATE TABLE hosts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + fqdn TEXT NOT NULL, + ip_address INET NOT NULL, + display_name TEXT NOT NULL DEFAULT '', + os_family TEXT, -- debian, rhel, alpine, arch + os_name TEXT, -- e.g. "Ubuntu 24.04" + arch TEXT, -- x86_64, aarch64, etc. + agent_version TEXT, + health_status host_health_status NOT NULL DEFAULT 'pending', + last_health_at TIMESTAMPTZ, + last_patch_at TIMESTAMPTZ, + -- Agent port (default 12443) + agent_port INTEGER NOT NULL DEFAULT 12443, + notes TEXT NOT NULL DEFAULT '', + registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT hosts_fqdn_ip_unique UNIQUE (fqdn, ip_address) +); + +CREATE INDEX idx_hosts_health_status ON hosts (health_status); +CREATE INDEX idx_hosts_fqdn ON hosts USING gin (fqdn gin_trgm_ops); +CREATE INDEX idx_hosts_ip ON hosts (ip_address); + +-- ============================================================ +-- Host <-> Group membership +-- ============================================================ + +CREATE TABLE host_groups ( + host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, + group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, + assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (host_id, group_id) +); + +CREATE INDEX idx_host_groups_group ON host_groups (group_id); + +-- ============================================================ +-- Host Health Data (cached results from 5-min polls) +-- ============================================================ + +CREATE TABLE host_health_data ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, + polled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + status host_health_status NOT NULL, + -- Raw JSON response from agent GET /api/v1/health + payload JSONB NOT NULL DEFAULT '{}' +); + +CREATE INDEX idx_host_health_host ON host_health_data (host_id, polled_at DESC); +-- Retained for 30 days (pruned by worker) + +-- ============================================================ +-- Host Patch Data (cached results from 30-min polls) +-- ============================================================ + +CREATE TABLE host_patch_data ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, + polled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + -- Raw JSON response from agent GET /api/v1/patches + available_patches JSONB NOT NULL DEFAULT '[]', + installed_packages JSONB NOT NULL DEFAULT '[]', + patch_count INTEGER NOT NULL DEFAULT 0, + cve_count INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX idx_host_patch_host ON host_patch_data (host_id, polled_at DESC); +-- Retained for 30 days (pruned by worker) + +-- ============================================================ +-- Maintenance Windows +-- ============================================================ + +CREATE TABLE maintenance_windows ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, + label TEXT NOT NULL DEFAULT '', + recurrence window_recurrence NOT NULL DEFAULT 'once', + -- Start time (UTC) + start_at TIMESTAMPTZ NOT NULL, + -- Duration in minutes + duration_minutes INTEGER NOT NULL DEFAULT 60, + -- For recurring windows: day-of-week (0=Sun) or day-of-month + recurrence_day INTEGER, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_mw_host ON maintenance_windows (host_id); +CREATE INDEX idx_mw_start ON maintenance_windows (start_at) WHERE enabled = TRUE; + +-- ============================================================ +-- Patch Jobs +-- ============================================================ + +CREATE TABLE patch_jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + kind job_kind NOT NULL DEFAULT 'patch_apply', + status job_status NOT NULL DEFAULT 'queued', + created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, + -- For rollback jobs: reference to original job + parent_job_id UUID REFERENCES patch_jobs(id) ON DELETE SET NULL, + -- Optional: restrict to a specific maintenance window + maintenance_window_id UUID REFERENCES maintenance_windows(id) ON DELETE SET NULL, + -- Immediate apply if TRUE; else queued for next window + immediate BOOLEAN NOT NULL DEFAULT FALSE, + -- Patch selection (list of package names / CVE IDs) + patch_selection JSONB NOT NULL DEFAULT '[]', + notes TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ +); + +CREATE INDEX idx_patch_jobs_status ON patch_jobs (status); +CREATE INDEX idx_patch_jobs_created ON patch_jobs (created_at DESC); +CREATE INDEX idx_patch_jobs_user ON patch_jobs (created_by_user_id); + +-- ============================================================ +-- Patch Job Hosts (per-host status within a batch job) +-- ============================================================ + +CREATE TABLE patch_job_hosts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + job_id UUID NOT NULL REFERENCES patch_jobs(id) ON DELETE CASCADE, + host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, + status job_status NOT NULL DEFAULT 'queued', + -- Agent-assigned async job ID + agent_job_id TEXT, + retry_count INTEGER NOT NULL DEFAULT 0, + -- Output / error from agent + output TEXT NOT NULL DEFAULT '', + error_message TEXT, + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + UNIQUE (job_id, host_id) +); + +CREATE INDEX idx_pjh_job ON patch_job_hosts (job_id); +CREATE INDEX idx_pjh_host ON patch_job_hosts (host_id); +CREATE INDEX idx_pjh_status ON patch_job_hosts (status); + +-- ============================================================ +-- Certificates +-- ============================================================ + +CREATE TABLE certificates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + -- NULL = root CA cert + host_id UUID REFERENCES hosts(id) ON DELETE CASCADE, + serial_number TEXT NOT NULL UNIQUE, + common_name TEXT NOT NULL, + status cert_status NOT NULL DEFAULT 'active', + issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ, + -- PEM-encoded certificate (public cert only, no key) + cert_pem TEXT NOT NULL +); + +CREATE INDEX idx_certs_host ON certificates (host_id); +CREATE INDEX idx_certs_status ON certificates (status); +CREATE INDEX idx_certs_expires ON certificates (expires_at); + +-- ============================================================ +-- Audit Log (tamper-evident, hash-chained) +-- ============================================================ + +CREATE TABLE audit_log ( + id BIGSERIAL PRIMARY KEY, + action audit_action NOT NULL, + actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, + actor_username TEXT, -- Snapshot at time of action + target_type TEXT, -- 'host', 'user', 'group', 'job', etc. + target_id TEXT, -- UUID or identifier of affected resource + details JSONB NOT NULL DEFAULT '{}', + ip_address INET, + request_id TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + -- Hash chain: SHA-256(prev_hash || row_data) + row_hash TEXT NOT NULL DEFAULT '' +); + +CREATE INDEX idx_audit_created ON audit_log (created_at DESC); +CREATE INDEX idx_audit_actor ON audit_log (actor_user_id); +CREATE INDEX idx_audit_action ON audit_log (action); +CREATE INDEX idx_audit_target ON audit_log (target_type, target_id); +-- Retained for 6 months (pruned by worker) + +-- ============================================================ +-- Azure SSO Configuration +-- ============================================================ + +CREATE TABLE azure_sso_config ( + id INTEGER PRIMARY KEY DEFAULT 1, -- singleton row + enabled BOOLEAN NOT NULL DEFAULT FALSE, + tenant_id TEXT NOT NULL DEFAULT '', + client_id TEXT NOT NULL DEFAULT '', + -- Encrypted at rest via hardware-host FDE; stored as-is in DB + client_secret TEXT NOT NULL DEFAULT '', + redirect_uri TEXT NOT NULL DEFAULT '', + scopes TEXT NOT NULL DEFAULT 'openid email profile', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT azure_sso_singleton CHECK (id = 1) +); + +-- ============================================================ +-- System Configuration (key/value runtime settings) +-- ============================================================ + +CREATE TABLE system_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Seed default system config values +INSERT INTO system_config (key, value, description) VALUES + ('health_poll_interval_secs', '300', 'Agent health check interval in seconds'), + ('patch_poll_interval_secs', '1800', 'Agent patch data poll interval in seconds'), + ('max_concurrent_agent_calls', '64', 'Maximum concurrent mTLS agent calls'), + ('data_retention_days', '30', 'Retention period for operational data (days)'), + ('audit_retention_days', '180', 'Retention period for audit log (days)'), + ('smtp_enabled', 'false', 'Enable email notifications'), + ('smtp_host', '', 'SMTP relay hostname'), + ('smtp_port', '587', 'SMTP relay port'), + ('smtp_username', '', 'SMTP auth username'), + ('smtp_password', '', 'SMTP auth password'), + ('smtp_from', '', 'From address for notifications'), + ('smtp_tls_mode', 'starttls', 'SMTP TLS mode: none, starttls, tls'), + ('web_tls_strategy', 'internal_ca', 'Web UI TLS cert strategy: internal_ca or operator_supplied'), + ('ip_whitelist', '[]', 'JSON array of allowed CIDR/IP strings; empty = allow all'); + +-- ============================================================ +-- Worker Heartbeat +-- ============================================================ + +CREATE TABLE worker_heartbeat ( + id INTEGER PRIMARY KEY DEFAULT 1, -- singleton row + last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(), + worker_version TEXT NOT NULL DEFAULT '', + CONSTRAINT worker_heartbeat_singleton CHECK (id = 1) +); + +-- ============================================================ +-- Discovery Results (transient; cleared before each scan) +-- ============================================================ + +CREATE TABLE discovery_results ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + scan_id UUID NOT NULL, + ip_address INET NOT NULL, + fqdn TEXT, + agent_version TEXT, + os_name TEXT, + agent_port INTEGER NOT NULL DEFAULT 12443, + discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + -- Whether the operator has registered this host + registered BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX idx_discovery_scan ON discovery_results (scan_id); +CREATE INDEX idx_discovery_ip ON discovery_results (ip_address); diff --git a/scripts/build-frontend.sh b/scripts/build-frontend.sh new file mode 100755 index 0000000..90a54ee --- /dev/null +++ b/scripts/build-frontend.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# ============================================================================= +# Linux Patch Manager — Frontend Build Script +# ============================================================================= +# Builds the React + TypeScript SPA and copies output to the system frontend dir. +# Run from the repository root. +# ============================================================================= + +set -euo pipefail + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +FRONTEND_DIR="${REPO_ROOT}/frontend" +DEST_DIR="${1:-/usr/share/patch-manager/frontend}" + +info "Building React SPA..." +cd "${FRONTEND_DIR}" + +# Install dependencies if node_modules not present +if [[ ! -d node_modules ]]; then + info "Installing npm dependencies..." + npm ci +fi + +# Build +info "Running vite build..." +npm run build + +# Deploy to destination +info "Copying build output to ${DEST_DIR}..." +mkdir -p "${DEST_DIR}" +rm -rf "${DEST_DIR:?}/" +cp -r dist/* "${DEST_DIR}/" + +info "Frontend build complete → ${DEST_DIR}" diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..38a2b3a --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# ============================================================================= +# Linux Patch Manager — Initial Host Setup Script +# ============================================================================= +# Run as root on the Ubuntu 24.04 Patch Manager host. +# This script: +# - Creates the service user/group +# - Creates required directories with correct permissions +# - Installs PostgreSQL if not present +# - Creates the database and user +# - Copies configuration and binaries +# - Installs systemd units +# - Generates initial Ed25519 JWT keys +# ============================================================================= + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } + +[[ $EUID -ne 0 ]] && error "This script must be run as root." + +SERVICE_USER="patch-manager" +SERVICE_GROUP="patch-manager" +CONFIG_DIR="/etc/patch-manager" +LOG_DIR="/var/log/patch-manager" +DATA_DIR="/opt/patch-manager" +FRONTEND_DIR="/usr/share/patch-manager/frontend" +BIN_DIR="/usr/local/bin" +DB_NAME="patch_manager" +DB_USER="patch_manager" +SYSTEMD_DIR="/etc/systemd/system" + +info "=== Linux Patch Manager Setup ===" + +# ----------------------------------------------------------------------- +# 1. Create service user +# ----------------------------------------------------------------------- +info "Creating service user '${SERVICE_USER}'..." +if ! id "${SERVICE_USER}" &>/dev/null; then + useradd --system --no-create-home --shell /usr/sbin/nologin \ + --comment "Linux Patch Manager service account" \ + "${SERVICE_USER}" + info "User '${SERVICE_USER}' created." +else + warn "User '${SERVICE_USER}' already exists, skipping." +fi + +# ----------------------------------------------------------------------- +# 2. Create required directories +# ----------------------------------------------------------------------- +info "Creating directories..." +mkdir -p \ + "${CONFIG_DIR}/ca" \ + "${CONFIG_DIR}/certs" \ + "${CONFIG_DIR}/jwt" \ + "${CONFIG_DIR}/tls" \ + "${LOG_DIR}" \ + "${DATA_DIR}" \ + "${FRONTEND_DIR}" + +chown -R "${SERVICE_USER}:${SERVICE_GROUP}" \ + "${CONFIG_DIR}" \ + "${LOG_DIR}" \ + "${DATA_DIR}" \ + "${FRONTEND_DIR}" + +chmod 750 "${CONFIG_DIR}/ca" "${CONFIG_DIR}/jwt" +info "Directories created." + +# ----------------------------------------------------------------------- +# 3. Install PostgreSQL 16 if not present +# ----------------------------------------------------------------------- +if ! command -v psql &>/dev/null; then + info "Installing PostgreSQL 16..." + apt-get update -qq + apt-get install -y postgresql-16 + systemctl enable --now postgresql +else + info "PostgreSQL already installed: $(psql --version)" +fi + +# ----------------------------------------------------------------------- +# 4. Create database and user +# ----------------------------------------------------------------------- +info "Creating database '${DB_NAME}' and user '${DB_USER}'..." +DB_PASSWORD=$(openssl rand -base64 32) + +sudo -u postgres psql -v ON_ERROR_STOP=1 < M2 (auth) + │ ├──> M3 (hosts/groups) + │ │ ├──> M4 (agent comm + dashboard) + │ │ │ ├──> M5 (patch deployment + jobs) + │ │ │ │ ├──> M6 (maintenance windows) + │ │ │ │ │ └──> M7 (websocket relay) + │ │ │ │ └──> M7 (websocket relay) + │ │ │ └──> M8 (CA + certs) + │ │ └──> M8 (CA + certs) + │ └──> M10 (settings) + ├──> M8 (CA + certs) [needed by M4 for mTLS] + └──> M9 (reports) + +M10 (settings) ──> M11 (email + audit hardening) +M11 ──> M12 (deployment + testing) +``` + +**Critical path:** M1 → M2 → M3 → M4 → M5 → M6 → M7 → M11 → M12 + +**Note:** M8 (CA) should be started early (after M1) since M4 (agent communication) requires mTLS client certs. + +--- + +## Estimated Effort + +| Milestone | Backend | Frontend | DB | Total | +|-----------|----------|----------|-----|-------| +| M1 | 3 days | 1 day | 1 day | 5 days | +| M2 | 4 days | 2 days | 0.5 day | 6.5 days | +| M3 | 3 days | 3 days | 0.5 day | 6.5 days | +| M4 | 3 days | 2 days | 0.5 day | 5.5 days | +| M5 | 4 days | 2 days | 0.5 day | 6.5 days | +| M6 | 2 days | 1.5 days | 0.5 day | 4 days | +| M7 | 2 days | 1.5 days | 0 | 3.5 days | +| M8 | 2 days | 1.5 days | 0 | 3.5 days | +| M9 | 3 days | 1.5 days | 0 | 4.5 days | +| M10 | 3 days | 2 days | 0.5 day | 5.5 days | +| M11 | 2 days | 1 day | 0.5 day | 3.5 days | +| M12 | 2 days | 0.5 days | 0.5 day | 3 days | +| **Total** | **33 days** | **19.5 days** | **5 days** | **~57.5 days** | + +With a single developer: ~12 weeks. With parallel backend/frontend: ~7-8 weeks. + +--- + +## Review Notes + +- [ ] Kelly to review and approve this plan before implementation begins +- [ ] Confirm milestone ordering and priorities +- [ ] Confirm whether M8 (CA) should be pulled forward to support M4 +- [ ] Confirm whether any milestones can be deferred to a later release