From 6ae8b6e898f7899af044547025f142514c91133c Mon Sep 17 00:00:00 2001 From: Soma Nakamura Date: Fri, 13 Feb 2026 17:07:42 +0900 Subject: [PATCH] Initial commit --- .gitignore | 7 + Cargo.lock | 3980 +++++++++++++++++++++++++++++ Cargo.toml | 7 + README.md | 53 + backend/Cargo.toml | 30 + backend/migrations/001_init.sql | 155 ++ backend/src/api/audit.rs | 143 ++ backend/src/api/auth.rs | 446 ++++ backend/src/api/control_planes.rs | 293 +++ backend/src/api/mod.rs | 85 + backend/src/api/networks.rs | 631 +++++ backend/src/api/users.rs | 514 ++++ backend/src/app_state.rs | 11 + backend/src/audit_log.rs | 33 + backend/src/auth.rs | 122 + backend/src/config.rs | 93 + backend/src/control_plane.rs | 366 +++ backend/src/db.rs | 9 + backend/src/main.rs | 87 + backend/src/models.rs | 82 + backend/src/oidc.rs | 109 + backend/src/permissions.rs | 49 + backend/src/rbac.rs | 48 + config.example.toml | 25 + docker-compose.yml | 11 + frontend/.gitignore | 24 + frontend/README.md | 73 + frontend/eslint.config.js | 23 + frontend/index.html | 13 + frontend/package-lock.json | 3269 +++++++++++++++++++++++ frontend/package.json | 30 + frontend/public/vite.svg | 1 + frontend/src/App.css | 544 ++++ frontend/src/App.tsx | 1848 ++++++++++++++ frontend/src/api.ts | 421 +++ frontend/src/assets/react.svg | 1 + frontend/src/index.css | 60 + frontend/src/main.tsx | 10 + frontend/tsconfig.app.json | 28 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 7 + 42 files changed, 13774 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 backend/Cargo.toml create mode 100644 backend/migrations/001_init.sql create mode 100644 backend/src/api/audit.rs create mode 100644 backend/src/api/auth.rs create mode 100644 backend/src/api/control_planes.rs create mode 100644 backend/src/api/mod.rs create mode 100644 backend/src/api/networks.rs create mode 100644 backend/src/api/users.rs create mode 100644 backend/src/app_state.rs create mode 100644 backend/src/audit_log.rs create mode 100644 backend/src/auth.rs create mode 100644 backend/src/config.rs create mode 100644 backend/src/control_plane.rs create mode 100644 backend/src/db.rs create mode 100644 backend/src/main.rs create mode 100644 backend/src/models.rs create mode 100644 backend/src/oidc.rs create mode 100644 backend/src/permissions.rs create mode 100644 backend/src/rbac.rs create mode 100644 config.example.toml create mode 100644 docker-compose.yml create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api.ts create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be7d340 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +/backend/target +/frontend/node_modules +/frontend/dist +/config.toml +/.env +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b2707da --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3980 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[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 2.0.114", +] + +[[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 = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "fastrand", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "multer", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "config" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "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 = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-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-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.114", +] + +[[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 = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[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 2.0.114", +] + +[[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 = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "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 = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[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 = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[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 = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[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 1.4.0", + "http-body 1.0.1", + "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 = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.36", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots 1.0.5", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "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 = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lightscale-admin-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "axum", + "axum-extra", + "config", + "hex", + "http 1.4.0", + "openidconnect", + "rand 0.8.5", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "sqlx", + "thiserror 1.0.69", + "time", + "tokio", + "tower-http 0.5.2", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[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.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[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.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[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 = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.4.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-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.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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 = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom 0.2.17", + "http 0.2.12", + "rand 0.8.5", + "reqwest 0.11.27", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openidconnect" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47e80a9cfae4462dd29c41e987edd228971d6565553fbc14b8a11e666d91590" +dependencies = [ + "base64 0.13.1", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http 0.2.12", + "itertools", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_derive", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "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 = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[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 = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[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 0.23.36", + "socket2 0.6.1", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.36", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +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 = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +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 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.36", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.5", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.10.0", + "serde", + "serde_derive", +] + +[[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.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[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.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[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-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.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 2.0.114", +] + +[[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_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[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_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.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 = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.13.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.10.0", + "byteorder", + "bytes", + "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.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.10.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", + "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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[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 2.0.114", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.36", + "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-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", + "toml_datetime", + "toml_edit", +] + +[[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_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[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 1.0.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "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 2.10.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[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 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[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.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[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.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[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.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +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.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +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 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b1340a0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +members = ["backend"] +resolver = "2" + +[profile.release] +lto = true +codegen-units = 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c910e42 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# lightscale-admin + +A thin admin control plane for Lightscale. It stores operator metadata in CockroachDB and calls one or more Lightscale control plane APIs to manage networks, nodes, tokens, ACLs, key policies, and audit streams. The UI is a SPA (no SSR) and can be served by the backend or hosted separately. + +## Layout +- `backend/`: Rust (Axum) API server, `/admin/api` namespace. +- `frontend/`: Vite React SPA. + +## Quick start +1) Start CockroachDB (single node for local dev): + +```bash +cd /home/centra/dev/lightscale-admin + +docker compose up -d +``` + +2) Create a config: + +```bash +cp config.example.toml config.toml +``` + +3) Build the UI (optional if you run the Vite dev server): + +```bash +cd frontend +npm install +npm run build +``` + +4) Run the backend from the repo root: + +```bash +cargo run -p lightscale-admin-server +``` + +The admin UI will be served from `server.static_dir` if configured. Otherwise, run the Vite dev server and set `server.allowed_origins` to `http://localhost:5173`. + +## Configuration +Configuration loads from `config.toml` and `LS_ADMIN__` environment variables (nested keys separated by `__`). See `config.example.toml`. + +Key settings: +- `server.base_url`: used for OIDC redirect URLs. +- `auth.bootstrap_admin_email` / `auth.bootstrap_admin_password`: creates the first admin if the database is empty. +- `server.allowed_origins`: set when the UI is hosted separately (CORS + cookies). +- `server.static_dir`: serve the SPA from this folder (usually `../frontend/dist`). + +## Control planes +Create control planes in the UI and store their admin tokens. The admin API will call each control plane’s `/v1/*` endpoints to manage networks and nodes. + +## Multi-region notes +CockroachDB allows multi-region deployments. For production, run a multi-node cluster and point `database.url` at the load-balanced SQL endpoint. The admin API itself is stateless and can be deployed across regions. diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..42ea6ef --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "lightscale-admin-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +argon2 = "0.5" +axum = { version = "0.7", features = ["macros", "json"] } +axum-extra = { version = "0.9", features = ["cookie"] } +config = "0.14" +hex = "0.4" +openidconnect = { version = "3", default-features = false, features = ["reqwest", "rustls-tls"] } +rand = "0.8" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid", "time", "json", "migrate"] } +thiserror = "1" +time = { version = "0.3", features = ["serde", "macros"] } +tokio = { version = "1", features = ["full"] } +tower-http = { version = "0.5", features = ["cors", "fs", "trace"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2" +uuid = { version = "1", features = ["v4", "serde"] } + +[dev-dependencies] +http = "1" diff --git a/backend/migrations/001_init.sql b/backend/migrations/001_init.sql new file mode 100644 index 0000000..11427f2 --- /dev/null +++ b/backend/migrations/001_init.sql @@ -0,0 +1,155 @@ +CREATE TABLE users ( + id UUID PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + display_name TEXT, + password_hash TEXT, + disabled BOOL NOT NULL DEFAULT false, + super_admin BOOL NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE roles ( + id UUID PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE role_permissions ( + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + permission TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (role_id, permission) +); + +CREATE TABLE memberships ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + scope_type TEXT NOT NULL, + scope_id TEXT NOT NULL, + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + UNIQUE (user_id, scope_type, scope_id) +); + +CREATE TABLE sessions ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + last_seen_at TIMESTAMPTZ NOT NULL, + user_agent TEXT, + ip TEXT +); + +CREATE INDEX sessions_user_id_idx ON sessions (user_id); +CREATE INDEX sessions_expires_at_idx ON sessions (expires_at); + +CREATE TABLE control_planes ( + id UUID PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + base_url TEXT NOT NULL, + admin_token TEXT, + region TEXT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE networks ( + id UUID PRIMARY KEY, + control_plane_id UUID NOT NULL REFERENCES control_planes(id) ON DELETE CASCADE, + network_id TEXT NOT NULL, + name TEXT NOT NULL, + dns_domain TEXT, + overlay_v4 TEXT, + overlay_v6 TEXT, + requires_approval BOOL NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE (control_plane_id, network_id) +); + +CREATE INDEX networks_control_plane_idx ON networks (control_plane_id); + +CREATE TABLE audit_log ( + id UUID PRIMARY KEY, + actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, + action TEXT NOT NULL, + target_type TEXT, + target_id TEXT, + metadata JSONB, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX audit_log_created_at_idx ON audit_log (created_at); + +CREATE TABLE user_providers ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider TEXT NOT NULL, + subject TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + UNIQUE (provider, subject), + UNIQUE (user_id, provider) +); + +CREATE TABLE oidc_states ( + id UUID PRIMARY KEY, + provider_id TEXT NOT NULL, + state TEXT NOT NULL, + nonce TEXT NOT NULL, + verifier TEXT NOT NULL, + redirect TEXT, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + UNIQUE (provider_id, state) +); + +CREATE INDEX oidc_states_expires_at_idx ON oidc_states (expires_at); + +INSERT INTO roles (id, name, description, created_at) +VALUES + ('00000000-0000-0000-0000-000000000001', 'owner', 'Full access', now()), + ('00000000-0000-0000-0000-000000000002', 'admin', 'Network admin access', now()), + ('00000000-0000-0000-0000-000000000003', 'viewer', 'Read-only access', now()); + +INSERT INTO role_permissions (role_id, permission, created_at) +VALUES + ('00000000-0000-0000-0000-000000000001', 'control_planes:read', now()), + ('00000000-0000-0000-0000-000000000001', 'control_planes:write', now()), + ('00000000-0000-0000-0000-000000000001', 'networks:read', now()), + ('00000000-0000-0000-0000-000000000001', 'networks:write', now()), + ('00000000-0000-0000-0000-000000000001', 'nodes:read', now()), + ('00000000-0000-0000-0000-000000000001', 'nodes:write', now()), + ('00000000-0000-0000-0000-000000000001', 'tokens:write', now()), + ('00000000-0000-0000-0000-000000000001', 'acl:read', now()), + ('00000000-0000-0000-0000-000000000001', 'acl:write', now()), + ('00000000-0000-0000-0000-000000000001', 'key_policy:read', now()), + ('00000000-0000-0000-0000-000000000001', 'key_policy:write', now()), + ('00000000-0000-0000-0000-000000000001', 'audit:read', now()), + ('00000000-0000-0000-0000-000000000001', 'users:read', now()), + ('00000000-0000-0000-0000-000000000001', 'users:write', now()), + ('00000000-0000-0000-0000-000000000001', 'roles:read', now()), + ('00000000-0000-0000-0000-000000000001', 'roles:write', now()), + + ('00000000-0000-0000-0000-000000000002', 'control_planes:read', now()), + ('00000000-0000-0000-0000-000000000002', 'networks:read', now()), + ('00000000-0000-0000-0000-000000000002', 'networks:write', now()), + ('00000000-0000-0000-0000-000000000002', 'nodes:read', now()), + ('00000000-0000-0000-0000-000000000002', 'nodes:write', now()), + ('00000000-0000-0000-0000-000000000002', 'tokens:write', now()), + ('00000000-0000-0000-0000-000000000002', 'acl:read', now()), + ('00000000-0000-0000-0000-000000000002', 'acl:write', now()), + ('00000000-0000-0000-0000-000000000002', 'key_policy:read', now()), + ('00000000-0000-0000-0000-000000000002', 'key_policy:write', now()), + ('00000000-0000-0000-0000-000000000002', 'audit:read', now()), + ('00000000-0000-0000-0000-000000000002', 'users:read', now()), + ('00000000-0000-0000-0000-000000000002', 'roles:read', now()), + + ('00000000-0000-0000-0000-000000000003', 'control_planes:read', now()), + ('00000000-0000-0000-0000-000000000003', 'networks:read', now()), + ('00000000-0000-0000-0000-000000000003', 'nodes:read', now()), + ('00000000-0000-0000-0000-000000000003', 'audit:read', now()), + ('00000000-0000-0000-0000-000000000003', 'roles:read', now()); diff --git a/backend/src/api/audit.rs b/backend/src/api/audit.rs new file mode 100644 index 0000000..3567b39 --- /dev/null +++ b/backend/src/api/audit.rs @@ -0,0 +1,143 @@ +use crate::api::{ApiError, AuthUser}; +use crate::control_plane::AuditLogResponse as ControlPlaneAuditLog; +use crate::models::ControlPlane; +use crate::permissions::{ensure_permission_global, PERM_AUDIT_READ}; +use axum::extract::{Path, Query, State}; +use axum::routing::get; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use time::OffsetDateTime; +use uuid::Uuid; + +#[derive(Debug, Deserialize)] +struct AdminAuditQuery { + actor_user_id: Option, + action: Option, + limit: Option, +} + +#[derive(Debug, Deserialize)] +struct ControlPlaneAuditQuery { + network_id: Option, + node_id: Option, + limit: Option, +} + +#[derive(Debug, Serialize)] +struct AdminAuditEntry { + id: Uuid, + actor_user_id: Option, + actor_email: Option, + action: String, + target_type: Option, + target_id: Option, + metadata: Option, + created_at: OffsetDateTime, +} + +pub fn router() -> Router { + Router::new() + .route("/", get(list_admin_audit)) + .route("/control-planes/:id", get(control_plane_audit)) +} + +async fn list_admin_audit( + State(state): State, + AuthUser { user }: AuthUser, + Query(query): Query, +) -> Result>, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_AUDIT_READ).await?; + let limit = query.limit.unwrap_or(200).max(1).min(1000) as i64; + + let rows = sqlx::query_as::<_, AdminAuditRow>( + r#" + SELECT a.id, + a.actor_user_id, + u.email AS actor_email, + a.action, + a.target_type, + a.target_id, + a.metadata, + a.created_at + FROM audit_log a + LEFT JOIN users u ON u.id = a.actor_user_id + WHERE ($1::uuid IS NULL OR a.actor_user_id = $1) + AND ($2::text IS NULL OR a.action = $2) + ORDER BY a.created_at DESC + LIMIT $3 + "#, + ) + .bind(query.actor_user_id) + .bind(query.action) + .bind(limit) + .fetch_all(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + Ok(Json( + rows.into_iter() + .map(|row| AdminAuditEntry { + id: row.id, + actor_user_id: row.actor_user_id, + actor_email: row.actor_email, + action: row.action, + target_type: row.target_type, + target_id: row.target_id, + metadata: row.metadata, + created_at: row.created_at, + }) + .collect(), + )) +} + +async fn control_plane_audit( + State(state): State, + AuthUser { user }: AuthUser, + Path(control_plane_id): Path, + Query(query): Query, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_AUDIT_READ).await?; + let control_plane = fetch_control_plane(&state.pool, control_plane_id).await?; + let client = crate::control_plane::ControlPlaneClient::new( + control_plane.base_url, + control_plane.admin_token, + ); + let response = client + .audit_log(crate::control_plane::AuditLogQuery { + network_id: query.network_id, + node_id: query.node_id, + limit: query.limit, + }) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + Ok(Json(response)) +} + +#[derive(sqlx::FromRow)] +struct AdminAuditRow { + id: Uuid, + actor_user_id: Option, + actor_email: Option, + action: String, + target_type: Option, + target_id: Option, + metadata: Option, + created_at: OffsetDateTime, +} + +async fn fetch_control_plane(pool: &PgPool, id: Uuid) -> Result { + sqlx::query_as::<_, ControlPlane>( + r#" + SELECT id, name, base_url, admin_token, region, created_at, updated_at + FROM control_planes + WHERE id = $1 + "#, + ) + .bind(id) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::NotFound) +} diff --git a/backend/src/api/auth.rs b/backend/src/api/auth.rs new file mode 100644 index 0000000..f9e5a36 --- /dev/null +++ b/backend/src/api/auth.rs @@ -0,0 +1,446 @@ +use crate::api::{ApiError, AuthUser}; +use crate::app_state::AppState; +use crate::auth::{create_session, delete_session, hash_password, verify_password, SESSION_COOKIE}; +use crate::models::User; +use crate::oidc::{build_auth_request, exchange_code}; +use axum::extract::{Path, Query, State}; +use axum::response::Redirect; +use axum::routing::{get, post}; +use axum::Json; +use axum::Router; +use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; +use openidconnect::{Nonce, TokenResponse}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use sqlx::{FromRow, PgPool}; +use time::{Duration, OffsetDateTime}; +use uuid::Uuid; + +#[derive(Debug, Deserialize)] +struct LoginRequest { + email: String, + password: String, +} + +#[derive(Debug, Serialize)] +struct LoginResponse { + user: UserSummary, +} + +#[derive(Debug, Serialize)] +struct UserSummary { + id: Uuid, + email: String, + display_name: Option, + super_admin: bool, +} + +#[derive(Debug, Serialize)] +struct ProviderSummary { + id: String, + name: String, +} + +#[derive(Debug, Deserialize)] +struct OidcCallbackQuery { + code: String, + state: String, +} + +#[derive(Debug, Deserialize)] +struct OidcLoginQuery { + next: Option, +} + +#[derive(Debug, FromRow)] +struct OidcStateRow { + id: Uuid, + nonce: String, + verifier: String, + redirect: Option, + expires_at: OffsetDateTime, +} + +pub fn router() -> Router { + Router::new() + .route("/login", post(login)) + .route("/logout", post(logout)) + .route("/me", get(me)) + .route("/providers", get(list_providers)) + .route("/oidc/:provider/login", get(oidc_login)) + .route("/oidc/:provider/callback", get(oidc_callback)) +} + +async fn login( + State(state): State, + jar: CookieJar, + Json(req): Json, +) -> Result<(CookieJar, Json), ApiError> { + let user = sqlx::query_as::<_, User>( + "SELECT id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at FROM users WHERE email = $1", + ) + .bind(req.email.to_lowercase()) + .fetch_optional(&state.pool) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::Unauthorized)?; + + if user.disabled { + return Err(ApiError::Unauthorized); + } + + let hash = user.password_hash.clone().ok_or(ApiError::Unauthorized)?; + let ok = verify_password(&hash, &req.password).map_err(|_| ApiError::Internal)?; + if !ok { + return Err(ApiError::Unauthorized); + } + + let ttl = Duration::minutes(state.config.auth.session_ttl_minutes); + let token = create_session( + &state.pool, + user.id, + ttl, + None, + None, + ) + .await + .map_err(|_| ApiError::Internal)?; + + let cookie = build_cookie(&state, token); + let jar = jar.add(cookie); + + Ok(( + jar, + Json(LoginResponse { + user: UserSummary { + id: user.id, + email: user.email, + display_name: user.display_name, + super_admin: user.super_admin, + }, + }), + )) +} + +async fn logout( + State(state): State, + jar: CookieJar, +) -> Result<(CookieJar, Json), ApiError> { + let Some(cookie) = jar.get(SESSION_COOKIE) else { + return Ok((jar, Json(json!({ "ok": true })))); + }; + delete_session(&state.pool, cookie.value()) + .await + .map_err(|_| ApiError::Internal)?; + let jar = jar.remove(clear_cookie(&state)); + Ok((jar, Json(json!({ "ok": true })))) +} + +async fn me(AuthUser { user }: AuthUser) -> Result, ApiError> { + Ok(Json(UserSummary { + id: user.id, + email: user.email, + display_name: user.display_name, + super_admin: user.super_admin, + })) +} + +async fn list_providers(State(state): State) -> Result>, ApiError> { + let providers = state + .oidc + .values() + .map(|provider| ProviderSummary { + id: provider.id.clone(), + name: provider.name.clone(), + }) + .collect::>(); + Ok(Json(providers)) +} + +async fn oidc_login( + State(state): State, + Path(provider_id): Path, + Query(query): Query, +) -> Result { + let provider = state + .oidc + .get(&provider_id) + .ok_or(ApiError::NotFound)?; + let auth = build_auth_request(provider).map_err(|_| ApiError::Internal)?; + + let now = OffsetDateTime::now_utc(); + let expires_at = now + Duration::minutes(10); + + sqlx::query( + "INSERT INTO oidc_states (id, provider_id, state, nonce, verifier, redirect, expires_at, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + ) + .bind(Uuid::new_v4()) + .bind(&provider_id) + .bind(&auth.state) + .bind(&auth.nonce) + .bind(&auth.verifier) + .bind(query.next) + .bind(expires_at) + .bind(now) + .execute(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + Ok(Redirect::temporary(&auth.url)) +} + +async fn oidc_callback( + State(state): State, + Path(provider_id): Path, + Query(query): Query, + jar: CookieJar, +) -> Result<(CookieJar, Redirect), ApiError> { + let provider = state + .oidc + .get(&provider_id) + .ok_or(ApiError::NotFound)? + .clone(); + + let record = sqlx::query_as::<_, OidcStateRow>( + r#" + SELECT id, nonce, verifier, redirect, expires_at + FROM oidc_states + WHERE provider_id = $1 AND state = $2 + "#, + ) + .bind(&provider_id) + .bind(&query.state) + .fetch_optional(&state.pool) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::Unauthorized)?; + + if record.expires_at <= OffsetDateTime::now_utc() { + return Err(ApiError::Unauthorized); + } + + sqlx::query("DELETE FROM oidc_states WHERE id = $1") + .bind(record.id) + .execute(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + let token = exchange_code(&provider, query.code, record.verifier) + .await + .map_err(|_| ApiError::Unauthorized)?; + + let id_token = token.id_token().ok_or(ApiError::Unauthorized)?; + let claims = id_token + .claims(&provider.client.id_token_verifier(), &Nonce::new(record.nonce)) + .map_err(|_| ApiError::Unauthorized)?; + + let subject = claims.subject().as_str().to_string(); + let email = claims.email().map(|e| e.as_str().to_string()); + let display_name = claims.name().and_then(|n| n.get(None)).map(|n| n.to_string()); + + let user = ensure_oidc_user( + &state.pool, + &provider_id, + &subject, + email.clone(), + display_name.clone(), + &state.config, + ) + .await?; + + let ttl = Duration::minutes(state.config.auth.session_ttl_minutes); + let session_token = create_session(&state.pool, user.id, ttl, None, None) + .await + .map_err(|_| ApiError::Internal)?; + + let cookie = build_cookie(&state, session_token); + let jar = jar.add(cookie); + let redirect = record.redirect.unwrap_or_else(|| "/".to_string()); + Ok((jar, Redirect::temporary(&redirect))) +} + +async fn ensure_oidc_user( + pool: &PgPool, + provider_id: &str, + subject: &str, + email: Option, + display_name: Option, + config: &crate::config::Config, +) -> Result { + if let Some(user) = sqlx::query_as::<_, User>( + r#" + SELECT u.id, u.email, u.display_name, u.password_hash, u.disabled, u.super_admin, u.created_at, u.updated_at + FROM users u + JOIN user_providers p ON p.user_id = u.id + WHERE p.provider = $1 AND p.subject = $2 + "#, + ) + .bind(provider_id) + .bind(subject) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::Internal)? + { + return Ok(user); + } + + let mut tx = pool.begin().await.map_err(|_| ApiError::Internal)?; + + let maybe_user = if let Some(email) = &email { + sqlx::query_as::<_, User>( + "SELECT id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at FROM users WHERE email = $1", + ) + .bind(email.to_lowercase()) + .fetch_optional(&mut *tx) + .await + .map_err(|_| ApiError::Internal)? + } else { + None + }; + + let user = if let Some(user) = maybe_user { + user + } else { + if !config.auth.allow_user_signup && config.auth.bootstrap_admin_email.as_deref() != email.as_deref() { + return Err(ApiError::Forbidden); + } + let now = OffsetDateTime::now_utc(); + let user_id = Uuid::new_v4(); + let super_admin = email + .as_deref() + .map(|e| Some(e) == config.auth.bootstrap_admin_email.as_deref()) + .unwrap_or(false); + let row = sqlx::query_as::<_, User>( + r#" + INSERT INTO users (id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at) + VALUES ($1, $2, $3, NULL, false, $4, $5, $5) + RETURNING id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at + "#, + ) + .bind(user_id) + .bind(email.clone().unwrap_or_else(|| format!("{}@unknown", subject))) + .bind(display_name.clone()) + .bind(super_admin) + .bind(now) + .fetch_one(&mut *tx) + .await + .map_err(|_| ApiError::Internal)?; + + let role_id = sqlx::query_scalar::<_, Uuid>( + "SELECT id FROM roles WHERE name = 'viewer'", + ) + .fetch_one(&mut *tx) + .await + .map_err(|_| ApiError::Internal)?; + + sqlx::query( + "INSERT INTO memberships (id, user_id, scope_type, scope_id, role_id, created_at) VALUES ($1, $2, 'global', 'global', $3, $4)", + ) + .bind(Uuid::new_v4()) + .bind(row.id) + .bind(role_id) + .bind(now) + .execute(&mut *tx) + .await + .map_err(|_| ApiError::Internal)?; + + row + }; + + sqlx::query( + "INSERT INTO user_providers (id, user_id, provider, subject, created_at) VALUES ($1, $2, $3, $4, $5)", + ) + .bind(Uuid::new_v4()) + .bind(user.id) + .bind(provider_id) + .bind(subject) + .bind(OffsetDateTime::now_utc()) + .execute(&mut *tx) + .await + .map_err(|_| ApiError::Internal)?; + + tx.commit().await.map_err(|_| ApiError::Internal)?; + Ok(user) +} + +fn build_cookie(state: &AppState, token: String) -> Cookie<'static> { + let mut cookie = Cookie::build((SESSION_COOKIE, token)) + .http_only(true) + .path("/") + .same_site(SameSite::Lax) + .secure(state.config.auth.cookie_secure) + .build(); + if let Some(domain) = &state.config.auth.cookie_domain { + cookie.set_domain(domain.clone()); + } + cookie +} + +fn clear_cookie(state: &AppState) -> Cookie<'static> { + let mut cookie = Cookie::build((SESSION_COOKIE, "")) + .http_only(true) + .path("/") + .same_site(SameSite::Lax) + .secure(state.config.auth.cookie_secure) + .build(); + cookie.make_removal(); + if let Some(domain) = &state.config.auth.cookie_domain { + cookie.set_domain(domain.clone()); + } + cookie +} + +pub async fn ensure_bootstrap_admin(pool: &PgPool, config: &crate::config::Config) -> Result<(), ApiError> { + let Some(email) = config.auth.bootstrap_admin_email.as_ref() else { + return Ok(()); + }; + let Some(password) = config.auth.bootstrap_admin_password.as_ref() else { + return Ok(()); + }; + + let existing = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM users") + .fetch_one(pool) + .await + .map_err(|_| ApiError::Internal)?; + if existing > 0 { + return Ok(()); + } + + let now = OffsetDateTime::now_utc(); + let hash = hash_password(password).map_err(|_| ApiError::Internal)?; + let user = sqlx::query_as::<_, User>( + r#" + INSERT INTO users (id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at) + VALUES ($1, $2, $3, $4, false, true, $5, $5) + RETURNING id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at + "#, + ) + .bind(Uuid::new_v4()) + .bind(email.to_lowercase()) + .bind(Some("Bootstrap Admin".to_string())) + .bind(hash) + .bind(now) + .fetch_one(pool) + .await + .map_err(|_| ApiError::Internal)?; + + let role_id = sqlx::query_scalar::<_, Uuid>( + "SELECT id FROM roles WHERE name = 'owner'", + ) + .fetch_one(pool) + .await + .map_err(|_| ApiError::Internal)?; + + sqlx::query( + "INSERT INTO memberships (id, user_id, scope_type, scope_id, role_id, created_at) VALUES ($1, $2, 'global', 'global', $3, $4)", + ) + .bind(Uuid::new_v4()) + .bind(user.id) + .bind(role_id) + .bind(now) + .execute(pool) + .await + .map_err(|_| ApiError::Internal)?; + + Ok(()) +} diff --git a/backend/src/api/control_planes.rs b/backend/src/api/control_planes.rs new file mode 100644 index 0000000..9db9a4f --- /dev/null +++ b/backend/src/api/control_planes.rs @@ -0,0 +1,293 @@ +use crate::api::{ApiError, AuthUser}; +use crate::audit_log; +use crate::models::ControlPlane; +use crate::permissions::{ + ensure_permission_global, PERM_CONTROL_PLANES_READ, PERM_CONTROL_PLANES_WRITE, +}; +use axum::extract::{Path, State}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use time::OffsetDateTime; +use uuid::Uuid; + +#[derive(Debug, Serialize)] +struct ControlPlaneSummary { + id: Uuid, + name: String, + base_url: String, + region: Option, + has_admin_token: bool, + created_at: OffsetDateTime, + updated_at: OffsetDateTime, +} + +#[derive(Debug, Deserialize)] +struct CreateControlPlaneRequest { + name: String, + base_url: String, + admin_token: Option, + region: Option, +} + +#[derive(Debug, Deserialize)] +struct UpdateControlPlaneRequest { + name: Option, + base_url: Option, + admin_token: Option, + clear_admin_token: Option, + region: Option, +} + +#[derive(Debug, Serialize)] +struct VerifyResponse { + ok: bool, + status: Option, + body: Option, +} + +pub fn router() -> Router { + Router::new() + .route("/", get(list_control_planes).post(create_control_plane)) + .route( + "/:id", + get(get_control_plane) + .put(update_control_plane) + .delete(delete_control_plane), + ) + .route("/:id/verify", post(verify_control_plane)) +} + +async fn list_control_planes( + State(state): State, + AuthUser { user }: AuthUser, +) -> Result>, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_CONTROL_PLANES_READ).await?; + let rows = sqlx::query_as::<_, ControlPlane>( + r#" + SELECT id, name, base_url, admin_token, region, created_at, updated_at + FROM control_planes + ORDER BY name + "#, + ) + .fetch_all(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + let summary = rows + .into_iter() + .map(|row| ControlPlaneSummary { + id: row.id, + name: row.name, + base_url: row.base_url, + region: row.region, + has_admin_token: row.admin_token.is_some(), + created_at: row.created_at, + updated_at: row.updated_at, + }) + .collect(); + + Ok(Json(summary)) +} + +async fn create_control_plane( + State(state): State, + AuthUser { user }: AuthUser, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_CONTROL_PLANES_WRITE).await?; + + let now = OffsetDateTime::now_utc(); + let base_url = req.base_url.trim_end_matches('/').to_string(); + let record = sqlx::query_as::<_, ControlPlane>( + r#" + INSERT INTO control_planes (id, name, base_url, admin_token, region, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $6) + RETURNING id, name, base_url, admin_token, region, created_at, updated_at + "#, + ) + .bind(Uuid::new_v4()) + .bind(req.name) + .bind(base_url) + .bind(req.admin_token) + .bind(req.region) + .bind(now) + .fetch_one(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + audit_log::record( + &state.pool, + Some(user.id), + "control_plane.create", + Some("control_plane"), + Some(&record.id.to_string()), + None, + ) + .await?; + + Ok(Json(ControlPlaneSummary { + id: record.id, + name: record.name, + base_url: record.base_url, + region: record.region, + has_admin_token: record.admin_token.is_some(), + created_at: record.created_at, + updated_at: record.updated_at, + })) +} + +async fn get_control_plane( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_CONTROL_PLANES_READ).await?; + let record = fetch_control_plane(&state.pool, id).await?; + Ok(Json(ControlPlaneSummary { + id: record.id, + name: record.name, + base_url: record.base_url, + region: record.region, + has_admin_token: record.admin_token.is_some(), + created_at: record.created_at, + updated_at: record.updated_at, + })) +} + +async fn update_control_plane( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_CONTROL_PLANES_WRITE).await?; + let existing = fetch_control_plane(&state.pool, id).await?; + + let name = req.name.unwrap_or(existing.name); + let base_url = req + .base_url + .map(|value| value.trim_end_matches('/').to_string()) + .unwrap_or(existing.base_url); + let admin_token = if req.clear_admin_token.unwrap_or(false) { + None + } else { + req.admin_token.or(existing.admin_token) + }; + let region = req.region.or(existing.region); + let now = OffsetDateTime::now_utc(); + + let record = sqlx::query_as::<_, ControlPlane>( + r#" + UPDATE control_planes + SET name = $2, base_url = $3, admin_token = $4, region = $5, updated_at = $6 + WHERE id = $1 + RETURNING id, name, base_url, admin_token, region, created_at, updated_at + "#, + ) + .bind(id) + .bind(name) + .bind(base_url) + .bind(admin_token) + .bind(region) + .bind(now) + .fetch_one(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + audit_log::record( + &state.pool, + Some(user.id), + "control_plane.update", + Some("control_plane"), + Some(&record.id.to_string()), + None, + ) + .await?; + + Ok(Json(ControlPlaneSummary { + id: record.id, + name: record.name, + base_url: record.base_url, + region: record.region, + has_admin_token: record.admin_token.is_some(), + created_at: record.created_at, + updated_at: record.updated_at, + })) +} + +async fn delete_control_plane( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_CONTROL_PLANES_WRITE).await?; + let record = fetch_control_plane(&state.pool, id).await?; + sqlx::query("DELETE FROM control_planes WHERE id = $1") + .bind(id) + .execute(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + audit_log::record( + &state.pool, + Some(user.id), + "control_plane.delete", + Some("control_plane"), + Some(&record.id.to_string()), + None, + ) + .await?; + + Ok(Json(serde_json::json!({ "ok": true }))) +} + +async fn verify_control_plane( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_CONTROL_PLANES_READ).await?; + let record = fetch_control_plane(&state.pool, id).await?; + + let url = format!("{}/healthz", record.base_url.trim_end_matches('/')); + let client = reqwest::Client::new(); + let response = client.get(url).send().await; + + let mut out = VerifyResponse { + ok: false, + status: None, + body: None, + }; + + match response { + Ok(resp) => { + out.status = Some(resp.status().as_u16()); + out.ok = resp.status().is_success(); + if let Ok(text) = resp.text().await { + out.body = Some(text); + } + } + Err(_) => { + out.ok = false; + } + } + + Ok(Json(out)) +} + +async fn fetch_control_plane(pool: &PgPool, id: Uuid) -> Result { + sqlx::query_as::<_, ControlPlane>( + r#" + SELECT id, name, base_url, admin_token, region, created_at, updated_at + FROM control_planes + WHERE id = $1 + "#, + ) + .bind(id) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::NotFound) +} diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs new file mode 100644 index 0000000..40ef6c2 --- /dev/null +++ b/backend/src/api/mod.rs @@ -0,0 +1,85 @@ +pub mod auth; +pub mod control_planes; +pub mod networks; +pub mod users; +pub mod audit; + +use crate::app_state::AppState; +use crate::auth::{session_user, SESSION_COOKIE}; +use crate::models::User; +use axum::extract::FromRequestParts; +use axum::http::{request::Parts, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use axum::{routing::get, Router}; +use axum_extra::extract::cookie::{Cookie, CookieJar}; +use serde_json::json; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ApiError { + #[error("unauthorized")] + Unauthorized, + #[error("forbidden")] + Forbidden, + #[error("not found")] + NotFound, + #[error("bad request: {0}")] + BadRequest(String), + #[error("internal error")] + Internal, +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let status = match self { + ApiError::Unauthorized => StatusCode::UNAUTHORIZED, + ApiError::Forbidden => StatusCode::FORBIDDEN, + ApiError::NotFound => StatusCode::NOT_FOUND, + ApiError::BadRequest(_) => StatusCode::BAD_REQUEST, + ApiError::Internal => StatusCode::INTERNAL_SERVER_ERROR, + }; + let message = match &self { + ApiError::BadRequest(msg) => msg.clone(), + _ => self.to_string(), + }; + (status, Json(json!({ "error": message }))).into_response() + } +} + +#[derive(Clone, Debug)] +pub struct AuthUser { + pub user: User, +} + +#[axum::async_trait] +impl FromRequestParts for AuthUser { + type Rejection = ApiError; + + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result { + let jar = CookieJar::from_headers(&parts.headers); + let token = jar + .get(SESSION_COOKIE) + .map(Cookie::value) + .map(|v| v.to_string()) + .ok_or(ApiError::Unauthorized)?; + let user = session_user(&state.pool, &token) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::Unauthorized)?; + if user.disabled { + return Err(ApiError::Unauthorized); + } + Ok(Self { user }) + } +} + +pub fn router() -> Router { + Router::new() + .route("/healthz", get(|| async { "ok" })) + .nest("/auth", auth::router()) + .nest("/users", users::router()) + .nest("/control-planes", control_planes::router()) + .nest("/networks", networks::router()) + .nest("/audit", audit::router()) +} diff --git a/backend/src/api/networks.rs b/backend/src/api/networks.rs new file mode 100644 index 0000000..58ac844 --- /dev/null +++ b/backend/src/api/networks.rs @@ -0,0 +1,631 @@ +use crate::api::{ApiError, AuthUser}; +use crate::audit_log; +use crate::control_plane::{ + AdminNodesResponse, CreateNetworkRequest as CpCreateNetworkRequest, + CreateTokenRequest as CpCreateTokenRequest, CreateTokenResponse, EnrollmentToken, + KeyHistoryResponse, KeyPolicyResponse, KeyRotationRequest, KeyRotationResponse, + KeyRotationPolicy, NetworkInfo, +}; +use crate::models::{ControlPlane, Network}; +use crate::permissions::{ + ensure_permission_global, PERM_ACL_READ, PERM_ACL_WRITE, PERM_KEY_POLICY_READ, + PERM_KEY_POLICY_WRITE, PERM_NETWORKS_READ, PERM_NETWORKS_WRITE, PERM_NODES_READ, + PERM_NODES_WRITE, PERM_TOKENS_WRITE, PERM_AUDIT_READ, +}; +use axum::extract::{Path, Query, State}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sqlx::PgPool; +use time::OffsetDateTime; +use uuid::Uuid; + +#[derive(Debug, Serialize)] +struct NetworkSummary { + id: Uuid, + control_plane_id: Uuid, + control_plane_name: String, + network_id: String, + name: String, + dns_domain: Option, + overlay_v4: Option, + overlay_v6: Option, + requires_approval: bool, + created_at: OffsetDateTime, + updated_at: OffsetDateTime, +} + +#[derive(Debug, Deserialize)] +struct CreateNetworkRequest { + control_plane_id: Uuid, + name: String, + dns_domain: Option, + requires_approval: Option, + key_rotation_max_age_seconds: Option, + bootstrap_token_ttl_seconds: Option, + bootstrap_token_uses: Option, + bootstrap_token_tags: Option>, +} + +#[derive(Debug, Serialize)] +struct CreateNetworkResult { + network: NetworkSummary, + bootstrap_token: Option, +} + +#[derive(Debug, Deserialize)] +struct CreateTokenRequest { + ttl_seconds: u64, + uses: u32, + tags: Vec, +} + +#[derive(Debug, Deserialize)] +struct RevokeTokenRequest { + token_id: String, +} + +#[derive(Debug, Deserialize)] +struct RotateKeysRequest { + machine_public_key: Option, + wg_public_key: Option, +} + +#[derive(Debug, Deserialize)] +struct UpdateAclRequest { + policy: Value, +} + +#[derive(Debug, Deserialize)] +struct NetworkAuditQuery { + node_id: Option, + limit: Option, +} + +pub fn router() -> Router { + Router::new() + .route("/", get(list_networks).post(create_network)) + .route("/:id", get(get_network)) + .route("/:id/tokens", post(create_token)) + .route("/:id/tokens/revoke", post(revoke_token)) + .route("/:id/nodes", get(list_nodes)) + .route("/:id/nodes/:node_id/approve", post(approve_node)) + .route("/:id/nodes/:node_id/revoke", post(revoke_node)) + .route("/:id/nodes/:node_id/rotate-keys", post(rotate_keys)) + .route("/:id/nodes/:node_id/keys", get(node_keys)) + .route("/:id/acl", get(get_acl).put(update_acl)) + .route("/:id/key-policy", get(get_key_policy).put(update_key_policy)) + .route("/:id/audit", get(network_audit)) +} + +async fn list_networks( + State(state): State, + AuthUser { user }: AuthUser, +) -> Result>, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_NETWORKS_READ).await?; + let rows = sqlx::query_as::<_, NetworkRow>( + r#" + SELECT n.id, + n.control_plane_id, + n.network_id, + n.name, + n.dns_domain, + n.overlay_v4, + n.overlay_v6, + n.requires_approval, + n.created_at, + n.updated_at, + c.name AS control_plane_name + FROM networks n + JOIN control_planes c ON c.id = n.control_plane_id + ORDER BY n.created_at DESC + "#, + ) + .fetch_all(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + let out = rows + .into_iter() + .map(|row| NetworkSummary { + id: row.id, + control_plane_id: row.control_plane_id, + control_plane_name: row.control_plane_name, + network_id: row.network_id, + name: row.name, + dns_domain: row.dns_domain, + overlay_v4: row.overlay_v4, + overlay_v6: row.overlay_v6, + requires_approval: row.requires_approval, + created_at: row.created_at, + updated_at: row.updated_at, + }) + .collect(); + + Ok(Json(out)) +} + +async fn get_network( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_NETWORKS_READ).await?; + let row = fetch_network_summary(&state.pool, id).await?; + Ok(Json(NetworkSummary { + id: row.id, + control_plane_id: row.control_plane_id, + control_plane_name: row.control_plane_name, + network_id: row.network_id, + name: row.name, + dns_domain: row.dns_domain, + overlay_v4: row.overlay_v4, + overlay_v6: row.overlay_v6, + requires_approval: row.requires_approval, + created_at: row.created_at, + updated_at: row.updated_at, + })) +} + +async fn create_network( + State(state): State, + AuthUser { user }: AuthUser, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_NETWORKS_WRITE).await?; + let control_plane = fetch_control_plane(&state.pool, req.control_plane_id).await?; + let client = crate::control_plane::ControlPlaneClient::new( + control_plane.base_url.clone(), + control_plane.admin_token.clone(), + ); + + let response = client + .create_network(CpCreateNetworkRequest { + name: req.name, + dns_domain: req.dns_domain.clone(), + requires_approval: req.requires_approval, + key_rotation_max_age_seconds: req.key_rotation_max_age_seconds, + bootstrap_token_ttl_seconds: req.bootstrap_token_ttl_seconds, + bootstrap_token_uses: req.bootstrap_token_uses, + bootstrap_token_tags: req.bootstrap_token_tags.clone(), + }) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + let record = insert_network( + &state.pool, + &control_plane, + &response.network, + req.dns_domain, + ) + .await?; + + audit_log::record( + &state.pool, + Some(user.id), + "network.create", + Some("network"), + Some(&record.id.to_string()), + Some(serde_json::json!({ + "network_id": response.network.id, + "control_plane_id": control_plane.id, + })), + ) + .await?; + + let summary = fetch_network_summary(&state.pool, record.id).await?; + Ok(Json(CreateNetworkResult { + network: NetworkSummary { + id: summary.id, + control_plane_id: summary.control_plane_id, + control_plane_name: summary.control_plane_name, + network_id: summary.network_id, + name: summary.name, + dns_domain: summary.dns_domain, + overlay_v4: summary.overlay_v4, + overlay_v6: summary.overlay_v6, + requires_approval: summary.requires_approval, + created_at: summary.created_at, + updated_at: summary.updated_at, + }, + bootstrap_token: response.bootstrap_token, + })) +} + +async fn create_token( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_TOKENS_WRITE).await?; + let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .create_token( + &network.network_id, + CpCreateTokenRequest { + ttl_seconds: req.ttl_seconds, + uses: req.uses, + tags: req.tags, + }, + ) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + audit_log::record( + &state.pool, + Some(user.id), + "token.create", + Some("network"), + Some(&network.id.to_string()), + None, + ) + .await?; + + Ok(Json(response)) +} + +async fn revoke_token( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_TOKENS_WRITE).await?; + let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .revoke_token(&req.token_id) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + audit_log::record( + &state.pool, + Some(user.id), + "token.revoke", + Some("network"), + Some(&network.id.to_string()), + Some(serde_json::json!({ "token_id": req.token_id })), + ) + .await?; + + Ok(Json(response)) +} + +async fn list_nodes( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_NODES_READ).await?; + let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .list_nodes(&network.network_id) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + Ok(Json(response)) +} + +async fn approve_node( + State(state): State, + AuthUser { user }: AuthUser, + Path((id, node_id)): Path<(Uuid, String)>, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_NODES_WRITE).await?; + let (_network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .approve_node(&node_id) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + audit_log::record( + &state.pool, + Some(user.id), + "node.approve", + Some("node"), + Some(&node_id), + None, + ) + .await?; + + Ok(Json(response)) +} + +async fn revoke_node( + State(state): State, + AuthUser { user }: AuthUser, + Path((id, node_id)): Path<(Uuid, String)>, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_NODES_WRITE).await?; + let (_network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .revoke_node(&node_id) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + audit_log::record( + &state.pool, + Some(user.id), + "node.revoke", + Some("node"), + Some(&node_id), + None, + ) + .await?; + + Ok(Json(response)) +} + +async fn rotate_keys( + State(state): State, + AuthUser { user }: AuthUser, + Path((id, node_id)): Path<(Uuid, String)>, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_NODES_WRITE).await?; + let (_network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .rotate_keys( + &node_id, + KeyRotationRequest { + machine_public_key: req.machine_public_key, + wg_public_key: req.wg_public_key, + }, + ) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + audit_log::record( + &state.pool, + Some(user.id), + "node.rotate_keys", + Some("node"), + Some(&node_id), + None, + ) + .await?; + + Ok(Json(response)) +} + +async fn node_keys( + State(state): State, + AuthUser { user }: AuthUser, + Path((id, node_id)): Path<(Uuid, String)>, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_NODES_READ).await?; + let (_network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .node_keys(&node_id) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + Ok(Json(response)) +} + +async fn get_acl( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_ACL_READ).await?; + let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .get_acl(&network.network_id) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + Ok(Json(response)) +} + +async fn update_acl( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_ACL_WRITE).await?; + let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .update_acl(&network.network_id, req.policy) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + audit_log::record( + &state.pool, + Some(user.id), + "network.update_acl", + Some("network"), + Some(&network.id.to_string()), + None, + ) + .await?; + + Ok(Json(response)) +} + +async fn get_key_policy( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_KEY_POLICY_READ).await?; + let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .get_key_policy(&network.network_id) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + Ok(Json(response)) +} + +async fn update_key_policy( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_KEY_POLICY_WRITE).await?; + let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .update_key_policy(&network.network_id, req) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + + audit_log::record( + &state.pool, + Some(user.id), + "network.update_key_policy", + Some("network"), + Some(&network.id.to_string()), + None, + ) + .await?; + + Ok(Json(response)) +} + +async fn network_audit( + State(state): State, + AuthUser { user }: AuthUser, + Path(id): Path, + Query(query): Query, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_AUDIT_READ).await?; + let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; + let client = client_for(&control_plane); + let response = client + .audit_log(crate::control_plane::AuditLogQuery { + network_id: Some(network.network_id), + node_id: query.node_id, + limit: query.limit, + }) + .await + .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; + Ok(Json(response)) +} + +#[derive(sqlx::FromRow)] +struct NetworkRow { + id: Uuid, + control_plane_id: Uuid, + control_plane_name: String, + network_id: String, + name: String, + dns_domain: Option, + overlay_v4: Option, + overlay_v6: Option, + requires_approval: bool, + created_at: OffsetDateTime, + updated_at: OffsetDateTime, +} + +async fn fetch_network_summary(pool: &PgPool, id: Uuid) -> Result { + sqlx::query_as::<_, NetworkRow>( + r#" + SELECT n.id, + n.control_plane_id, + n.network_id, + n.name, + n.dns_domain, + n.overlay_v4, + n.overlay_v6, + n.requires_approval, + n.created_at, + n.updated_at, + c.name AS control_plane_name + FROM networks n + JOIN control_planes c ON c.id = n.control_plane_id + WHERE n.id = $1 + "#, + ) + .bind(id) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::NotFound) +} + +async fn fetch_network_with_cp( + pool: &PgPool, + network_id: Uuid, +) -> Result<(Network, ControlPlane), ApiError> { + let network = sqlx::query_as::<_, Network>( + r#" + SELECT id, control_plane_id, network_id, name, dns_domain, overlay_v4, overlay_v6, + requires_approval, created_at, updated_at + FROM networks + WHERE id = $1 + "#, + ) + .bind(network_id) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::NotFound)?; + + let control_plane = fetch_control_plane(pool, network.control_plane_id).await?; + Ok((network, control_plane)) +} + +async fn fetch_control_plane(pool: &PgPool, id: Uuid) -> Result { + sqlx::query_as::<_, ControlPlane>( + r#" + SELECT id, name, base_url, admin_token, region, created_at, updated_at + FROM control_planes + WHERE id = $1 + "#, + ) + .bind(id) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::NotFound) +} + +async fn insert_network( + pool: &PgPool, + control_plane: &ControlPlane, + network: &NetworkInfo, + dns_override: Option, +) -> Result { + let now = OffsetDateTime::now_utc(); + sqlx::query_as::<_, Network>( + r#" + INSERT INTO networks ( + id, control_plane_id, network_id, name, dns_domain, overlay_v4, overlay_v6, + requires_approval, created_at, updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9) + RETURNING id, control_plane_id, network_id, name, dns_domain, overlay_v4, overlay_v6, + requires_approval, created_at, updated_at + "#, + ) + .bind(Uuid::new_v4()) + .bind(control_plane.id) + .bind(&network.id) + .bind(&network.name) + .bind(dns_override.or_else(|| Some(network.dns_domain.clone()))) + .bind(&network.overlay_v4) + .bind(&network.overlay_v6) + .bind(network.requires_approval) + .bind(now) + .fetch_one(pool) + .await + .map_err(|_| ApiError::Internal) +} + +fn client_for(control_plane: &ControlPlane) -> crate::control_plane::ControlPlaneClient { + crate::control_plane::ControlPlaneClient::new( + control_plane.base_url.clone(), + control_plane.admin_token.clone(), + ) +} diff --git a/backend/src/api/users.rs b/backend/src/api/users.rs new file mode 100644 index 0000000..44688cc --- /dev/null +++ b/backend/src/api/users.rs @@ -0,0 +1,514 @@ +use crate::api::{ApiError, AuthUser}; +use crate::audit_log; +use crate::auth::hash_password; +use crate::models::{Membership, User}; +use crate::permissions::{ + ensure_permission_global, PERM_ROLES_READ, PERM_USERS_READ, PERM_USERS_WRITE, +}; +use axum::extract::{Path, State}; +use axum::routing::{delete, get, post}; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use time::OffsetDateTime; +use uuid::Uuid; + +#[derive(Debug, Serialize)] +struct UserSummary { + id: Uuid, + email: String, + display_name: Option, + disabled: bool, + super_admin: bool, + created_at: OffsetDateTime, + updated_at: OffsetDateTime, +} + +#[derive(Debug, Serialize)] +struct UserDetail { + user: UserSummary, + memberships: Vec, +} + +#[derive(Debug, Serialize)] +struct MembershipSummary { + id: Uuid, + role_id: Uuid, + role_name: String, + scope_type: String, + scope_id: String, + created_at: OffsetDateTime, +} + +#[derive(Debug, Serialize)] +struct RoleSummary { + id: Uuid, + name: String, + description: Option, + permissions: Vec, +} + +#[derive(Debug, Deserialize)] +struct CreateUserRequest { + email: String, + display_name: Option, + password: String, + role_id: Option, + super_admin: Option, +} + +#[derive(Debug, Deserialize)] +struct UpdateUserRequest { + display_name: Option, + disabled: Option, + super_admin: Option, +} + +#[derive(Debug, Deserialize)] +struct SetPasswordRequest { + password: String, +} + +#[derive(Debug, Deserialize)] +struct AddMembershipRequest { + role_id: Uuid, + scope_type: String, + scope_id: String, +} + +pub fn router() -> Router { + Router::new() + .route("/", get(list_users).post(create_user)) + .route("/roles", get(list_roles)) + .route("/:id", get(get_user).put(update_user)) + .route("/:id/password", post(set_password)) + .route("/:id/memberships", post(add_membership)) + .route("/:id/memberships/:membership_id", delete(delete_membership)) +} + +async fn list_users( + State(state): State, + AuthUser { user }: AuthUser, +) -> Result>, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_USERS_READ).await?; + let rows = sqlx::query_as::<_, User>( + r#" + SELECT id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at + FROM users + ORDER BY created_at DESC + "#, + ) + .fetch_all(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + Ok(Json( + rows.into_iter() + .map(|row| UserSummary { + id: row.id, + email: row.email, + display_name: row.display_name, + disabled: row.disabled, + super_admin: row.super_admin, + created_at: row.created_at, + updated_at: row.updated_at, + }) + .collect(), + )) +} + +async fn get_user( + State(state): State, + AuthUser { user }: AuthUser, + Path(user_id): Path, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_USERS_READ).await?; + let record = fetch_user(&state.pool, user_id).await?; + let memberships = fetch_memberships(&state.pool, user_id).await?; + + Ok(Json(UserDetail { + user: UserSummary { + id: record.id, + email: record.email, + display_name: record.display_name, + disabled: record.disabled, + super_admin: record.super_admin, + created_at: record.created_at, + updated_at: record.updated_at, + }, + memberships, + })) +} + +async fn create_user( + State(state): State, + AuthUser { user }: AuthUser, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_USERS_WRITE).await?; + + let super_admin = req.super_admin.unwrap_or(false); + if super_admin && !user.super_admin { + return Err(ApiError::Forbidden); + } + + let now = OffsetDateTime::now_utc(); + let email = req.email.to_lowercase(); + let hash = hash_password(&req.password).map_err(|_| ApiError::Internal)?; + + let new_user = sqlx::query_as::<_, User>( + r#" + INSERT INTO users (id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at) + VALUES ($1, $2, $3, $4, false, $5, $6, $6) + RETURNING id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at + "#, + ) + .bind(Uuid::new_v4()) + .bind(email) + .bind(req.display_name) + .bind(hash) + .bind(super_admin) + .bind(now) + .fetch_one(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + let role_id = match req.role_id { + Some(role) => role, + None => fetch_role_id(&state.pool, "viewer").await?, + }; + + let membership = insert_membership(&state.pool, new_user.id, role_id, "global", "global").await?; + + audit_log::record( + &state.pool, + Some(user.id), + "user.create", + Some("user"), + Some(&new_user.id.to_string()), + None, + ) + .await?; + + Ok(Json(UserDetail { + user: UserSummary { + id: new_user.id, + email: new_user.email, + display_name: new_user.display_name, + disabled: new_user.disabled, + super_admin: new_user.super_admin, + created_at: new_user.created_at, + updated_at: new_user.updated_at, + }, + memberships: vec![membership], + })) +} + +async fn update_user( + State(state): State, + AuthUser { user }: AuthUser, + Path(user_id): Path, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_USERS_WRITE).await?; + let existing = fetch_user(&state.pool, user_id).await?; + + if req.super_admin.is_some() && !user.super_admin { + return Err(ApiError::Forbidden); + } + + if req.disabled.unwrap_or(false) && user_id == user.id { + return Err(ApiError::BadRequest("cannot disable your own account".to_string())); + } + + if existing.super_admin && req.super_admin == Some(false) { + let remaining = sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM users WHERE super_admin = true AND id != $1", + ) + .bind(existing.id) + .fetch_one(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + if remaining == 0 { + return Err(ApiError::BadRequest("cannot remove last super admin".to_string())); + } + } + + let now = OffsetDateTime::now_utc(); + let updated = sqlx::query_as::<_, User>( + r#" + UPDATE users + SET display_name = $2, disabled = $3, super_admin = $4, updated_at = $5 + WHERE id = $1 + RETURNING id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at + "#, + ) + .bind(existing.id) + .bind(req.display_name.or(existing.display_name)) + .bind(req.disabled.unwrap_or(existing.disabled)) + .bind(req.super_admin.unwrap_or(existing.super_admin)) + .bind(now) + .fetch_one(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + audit_log::record( + &state.pool, + Some(user.id), + "user.update", + Some("user"), + Some(&updated.id.to_string()), + None, + ) + .await?; + + Ok(Json(UserSummary { + id: updated.id, + email: updated.email, + display_name: updated.display_name, + disabled: updated.disabled, + super_admin: updated.super_admin, + created_at: updated.created_at, + updated_at: updated.updated_at, + })) +} + +async fn set_password( + State(state): State, + AuthUser { user }: AuthUser, + Path(user_id): Path, + Json(req): Json, +) -> Result, ApiError> { + if user_id != user.id { + ensure_permission_global(&state.pool, &user, PERM_USERS_WRITE).await?; + } + + let hash = hash_password(&req.password).map_err(|_| ApiError::Internal)?; + let now = OffsetDateTime::now_utc(); + sqlx::query( + "UPDATE users SET password_hash = $2, updated_at = $3 WHERE id = $1", + ) + .bind(user_id) + .bind(hash) + .bind(now) + .execute(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + audit_log::record( + &state.pool, + Some(user.id), + "user.set_password", + Some("user"), + Some(&user_id.to_string()), + None, + ) + .await?; + + Ok(Json(serde_json::json!({ "ok": true }))) +} + +async fn add_membership( + State(state): State, + AuthUser { user }: AuthUser, + Path(user_id): Path, + Json(req): Json, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_USERS_WRITE).await?; + let membership = insert_membership( + &state.pool, + user_id, + req.role_id, + &req.scope_type, + &req.scope_id, + ) + .await?; + + audit_log::record( + &state.pool, + Some(user.id), + "membership.create", + Some("user"), + Some(&user_id.to_string()), + None, + ) + .await?; + + Ok(Json(membership)) +} + +async fn delete_membership( + State(state): State, + AuthUser { user }: AuthUser, + Path((_user_id, membership_id)): Path<(Uuid, Uuid)>, +) -> Result, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_USERS_WRITE).await?; + sqlx::query("DELETE FROM memberships WHERE id = $1") + .bind(membership_id) + .execute(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + audit_log::record( + &state.pool, + Some(user.id), + "membership.delete", + Some("membership"), + Some(&membership_id.to_string()), + None, + ) + .await?; + + Ok(Json(serde_json::json!({ "ok": true }))) +} + +async fn list_roles( + State(state): State, + AuthUser { user }: AuthUser, +) -> Result>, ApiError> { + ensure_permission_global(&state.pool, &user, PERM_ROLES_READ).await?; + + let rows = sqlx::query_as::<_, RolePermissionRow>( + r#" + SELECT r.id, r.name, r.description, rp.permission + FROM roles r + LEFT JOIN role_permissions rp ON rp.role_id = r.id + ORDER BY r.name, rp.permission + "#, + ) + .fetch_all(&state.pool) + .await + .map_err(|_| ApiError::Internal)?; + + let mut out: Vec = Vec::new(); + for row in rows { + if let Some(role) = out.iter_mut().find(|role| role.id == row.id) { + if let Some(permission) = row.permission { + role.permissions.push(permission); + } + continue; + } + let mut permissions = Vec::new(); + if let Some(permission) = row.permission.clone() { + permissions.push(permission); + } + out.push(RoleSummary { + id: row.id, + name: row.name, + description: row.description, + permissions, + }); + } + + Ok(Json(out)) +} + +#[derive(sqlx::FromRow)] +struct RolePermissionRow { + id: Uuid, + name: String, + description: Option, + permission: Option, +} + +async fn fetch_user(pool: &PgPool, user_id: Uuid) -> Result { + sqlx::query_as::<_, User>( + r#" + SELECT id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at + FROM users + WHERE id = $1 + "#, + ) + .bind(user_id) + .fetch_optional(pool) + .await + .map_err(|_| ApiError::Internal)? + .ok_or(ApiError::NotFound) +} + +async fn fetch_memberships(pool: &PgPool, user_id: Uuid) -> Result, ApiError> { + let rows = sqlx::query_as::<_, MembershipRow>( + r#" + SELECT m.id, m.role_id, r.name AS role_name, m.scope_type, m.scope_id, m.created_at + FROM memberships m + JOIN roles r ON r.id = m.role_id + WHERE m.user_id = $1 + ORDER BY m.created_at DESC + "#, + ) + .bind(user_id) + .fetch_all(pool) + .await + .map_err(|_| ApiError::Internal)?; + + Ok(rows + .into_iter() + .map(|row| MembershipSummary { + id: row.id, + role_id: row.role_id, + role_name: row.role_name, + scope_type: row.scope_type, + scope_id: row.scope_id, + created_at: row.created_at, + }) + .collect()) +} + +#[derive(sqlx::FromRow)] +struct MembershipRow { + id: Uuid, + role_id: Uuid, + role_name: String, + scope_type: String, + scope_id: String, + created_at: OffsetDateTime, +} + +async fn fetch_role_id(pool: &PgPool, role_name: &str) -> Result { + sqlx::query_scalar::<_, Uuid>("SELECT id FROM roles WHERE name = $1") + .bind(role_name) + .fetch_one(pool) + .await + .map_err(|_| ApiError::Internal) +} + +async fn insert_membership( + pool: &PgPool, + user_id: Uuid, + role_id: Uuid, + scope_type: &str, + scope_id: &str, +) -> Result { + let now = OffsetDateTime::now_utc(); + let membership = sqlx::query_as::<_, Membership>( + r#" + INSERT INTO memberships (id, user_id, scope_type, scope_id, role_id, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, user_id, scope_type, scope_id, role_id, created_at + "#, + ) + .bind(Uuid::new_v4()) + .bind(user_id) + .bind(scope_type) + .bind(scope_id) + .bind(role_id) + .bind(now) + .fetch_one(pool) + .await + .map_err(|_| ApiError::Internal)?; + + let role_name = sqlx::query_scalar::<_, String>("SELECT name FROM roles WHERE id = $1") + .bind(role_id) + .fetch_one(pool) + .await + .map_err(|_| ApiError::Internal)?; + + Ok(MembershipSummary { + id: membership.id, + role_id: membership.role_id, + role_name, + scope_type: membership.scope_type, + scope_id: membership.scope_id, + created_at: membership.created_at, + }) +} diff --git a/backend/src/app_state.rs b/backend/src/app_state.rs new file mode 100644 index 0000000..d846fdf --- /dev/null +++ b/backend/src/app_state.rs @@ -0,0 +1,11 @@ +use crate::config::Config; +use crate::oidc::OidcProvider; +use sqlx::PgPool; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct AppState { + pub pool: PgPool, + pub config: Config, + pub oidc: HashMap, +} diff --git a/backend/src/audit_log.rs b/backend/src/audit_log.rs new file mode 100644 index 0000000..915d6d6 --- /dev/null +++ b/backend/src/audit_log.rs @@ -0,0 +1,33 @@ +use crate::api::ApiError; +use serde_json::Value; +use sqlx::PgPool; +use time::OffsetDateTime; +use uuid::Uuid; + +pub async fn record( + pool: &PgPool, + actor_user_id: Option, + action: &str, + target_type: Option<&str>, + target_id: Option<&str>, + metadata: Option, +) -> Result<(), ApiError> { + let now = OffsetDateTime::now_utc(); + sqlx::query( + r#" + INSERT INTO audit_log (id, actor_user_id, action, target_type, target_id, metadata, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + "#, + ) + .bind(Uuid::new_v4()) + .bind(actor_user_id) + .bind(action) + .bind(target_type) + .bind(target_id) + .bind(metadata) + .bind(now) + .execute(pool) + .await + .map_err(|_| ApiError::Internal)?; + Ok(()) +} diff --git a/backend/src/auth.rs b/backend/src/auth.rs new file mode 100644 index 0000000..1145e3d --- /dev/null +++ b/backend/src/auth.rs @@ -0,0 +1,122 @@ +use crate::models::{Session, User}; +use anyhow::{anyhow, Result}; +use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use rand::RngCore; +use sha2::{Digest, Sha256}; +use sqlx::PgPool; +use time::{Duration, OffsetDateTime}; +use uuid::Uuid; + +pub const SESSION_COOKIE: &str = "ls_admin_session"; + +pub fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut rand::thread_rng()); + let argon = Argon2::default(); + Ok(argon + .hash_password(password.as_bytes(), &salt) + .map_err(|err| anyhow!("password hash failed: {}", err))? + .to_string()) +} + +pub fn verify_password(hash: &str, password: &str) -> Result { + let parsed = PasswordHash::new(hash) + .map_err(|err| anyhow!("invalid password hash: {}", err))?; + Ok(Argon2::default() + .verify_password(password.as_bytes(), &parsed) + .is_ok()) +} + +pub fn generate_token() -> String { + let mut random = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut random); + hex::encode(random) +} + +pub fn hash_token(token: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(token.as_bytes()); + hex::encode(hasher.finalize()) +} + +pub async fn create_session( + pool: &PgPool, + user_id: Uuid, + ttl: Duration, + user_agent: Option, + ip: Option, +) -> Result { + let token = generate_token(); + let token_hash = hash_token(&token); + let now = OffsetDateTime::now_utc(); + let expires_at = now + ttl; + sqlx::query_as::<_, Session>( + r#" + INSERT INTO sessions (id, user_id, token_hash, expires_at, created_at, last_seen_at, user_agent, ip) + VALUES ($1, $2, $3, $4, $5, $5, $6, $7) + RETURNING id, user_id, token_hash, expires_at, created_at, last_seen_at, user_agent, ip + "#, + ) + .bind(Uuid::new_v4()) + .bind(user_id) + .bind(token_hash) + .bind(expires_at) + .bind(now) + .bind(user_agent) + .bind(ip) + .fetch_one(pool) + .await?; + Ok(token) +} + +pub async fn delete_session(pool: &PgPool, token: &str) -> Result<()> { + let token_hash = hash_token(token); + sqlx::query("DELETE FROM sessions WHERE token_hash = $1") + .bind(token_hash) + .execute(pool) + .await?; + Ok(()) +} + +pub async fn session_user(pool: &PgPool, token: &str) -> Result> { + let token_hash = hash_token(token); + let now = OffsetDateTime::now_utc(); + let session = sqlx::query_as::<_, Session>( + r#" + SELECT id, user_id, token_hash, expires_at, created_at, last_seen_at, user_agent, ip + FROM sessions + WHERE token_hash = $1 + "#, + ) + .bind(&token_hash) + .fetch_optional(pool) + .await?; + + let Some(session) = session else { + return Ok(None); + }; + + if session.expires_at <= now { + return Ok(None); + } + + if now - Duration::minutes(5) > session.last_seen_at { + sqlx::query("UPDATE sessions SET last_seen_at = $1 WHERE id = $2") + .bind(now) + .bind(session.id) + .execute(pool) + .await?; + } + + let user = sqlx::query_as::<_, User>( + r#" + SELECT id, email, display_name, password_hash, disabled, super_admin, created_at, updated_at + FROM users + WHERE id = $1 + "#, + ) + .bind(session.user_id) + .fetch_optional(pool) + .await?; + + Ok(user) +} diff --git a/backend/src/config.rs b/backend/src/config.rs new file mode 100644 index 0000000..da29d4b --- /dev/null +++ b/backend/src/config.rs @@ -0,0 +1,93 @@ +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + pub server: ServerConfig, + pub database: DatabaseConfig, + pub auth: AuthConfig, + #[serde(default)] + pub oidc: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ServerConfig { + #[serde(default = "default_bind")] + pub bind: String, + #[serde(default = "default_base_url")] + pub base_url: String, + #[serde(default)] + pub allowed_origins: Vec, + #[serde(default)] + pub static_dir: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct DatabaseConfig { + pub url: String, + #[serde(default = "default_max_connections")] + pub max_connections: u32, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AuthConfig { + #[serde(default = "default_session_ttl_minutes")] + pub session_ttl_minutes: i64, + #[serde(default)] + pub cookie_domain: Option, + #[serde(default = "default_cookie_secure")] + pub cookie_secure: bool, + #[serde(default)] + pub bootstrap_admin_email: Option, + #[serde(default)] + pub bootstrap_admin_password: Option, + #[serde(default = "default_allow_user_signup")] + pub allow_user_signup: bool, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct OidcProviderConfig { + pub id: String, + pub name: String, + pub issuer_url: String, + pub client_id: String, + pub client_secret: String, + #[serde(default)] + pub scopes: Vec, + #[serde(default)] + pub extra_params: HashMap, +} + +impl Config { + pub fn load() -> Result { + let settings = config::Config::builder() + .add_source(config::File::with_name("config").required(false)) + .add_source(config::Environment::with_prefix("LS_ADMIN").separator("__")) + .build()?; + settings.try_deserialize() + } +} + +fn default_bind() -> String { + "0.0.0.0:8081".to_string() +} + +fn default_base_url() -> String { + "http://localhost:8081".to_string() +} + +fn default_max_connections() -> u32 { + 20 +} + +fn default_session_ttl_minutes() -> i64 { + 720 +} + +fn default_cookie_secure() -> bool { + false +} + +fn default_allow_user_signup() -> bool { + false +} diff --git a/backend/src/control_plane.rs b/backend/src/control_plane.rs new file mode 100644 index 0000000..092dc72 --- /dev/null +++ b/backend/src/control_plane.rs @@ -0,0 +1,366 @@ +use anyhow::{anyhow, Result}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Clone)] +pub struct ControlPlaneClient { + base_url: String, + admin_token: Option, + client: Client, +} + +impl ControlPlaneClient { + pub fn new(base_url: String, admin_token: Option) -> Self { + Self { + base_url: base_url.trim_end_matches('/').to_string(), + admin_token, + client: Client::new(), + } + } + + fn url(&self, path: &str) -> String { + format!("{}{}", self.base_url, path) + } + + fn auth_header(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + if let Some(token) = &self.admin_token { + req.header("X-Lightscale-Admin-Token", token) + } else { + req + } + } + + pub async fn create_network(&self, request: CreateNetworkRequest) -> Result { + let req = self.client.post(self.url("/v1/networks")).json(&request); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("create network failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn create_token(&self, network_id: &str, request: CreateTokenRequest) -> Result { + let req = self + .client + .post(self.url(&format!("/v1/networks/{}/tokens", network_id))) + .json(&request); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("create token failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn revoke_token(&self, token_id: &str) -> Result { + let req = self + .client + .post(self.url(&format!("/v1/tokens/{}/revoke", token_id))); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("revoke token failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn list_nodes(&self, network_id: &str) -> Result { + let req = self + .client + .get(self.url(&format!("/v1/admin/networks/{}/nodes", network_id))); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("list nodes failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn approve_node(&self, node_id: &str) -> Result { + let req = self + .client + .post(self.url(&format!("/v1/admin/nodes/{}/approve", node_id))); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("approve node failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn rotate_keys(&self, node_id: &str, request: KeyRotationRequest) -> Result { + let req = self + .client + .post(self.url(&format!("/v1/nodes/{}/rotate-keys", node_id))) + .json(&request); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("rotate keys failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn revoke_node(&self, node_id: &str) -> Result { + let req = self + .client + .post(self.url(&format!("/v1/nodes/{}/revoke", node_id))); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("revoke node failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn node_keys(&self, node_id: &str) -> Result { + let req = self + .client + .get(self.url(&format!("/v1/nodes/{}/keys", node_id))); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("node keys failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn get_acl(&self, network_id: &str) -> Result { + let req = self + .client + .get(self.url(&format!("/v1/networks/{}/acl", network_id))); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("get acl failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn update_acl(&self, network_id: &str, policy: Value) -> Result { + let req = self + .client + .put(self.url(&format!("/v1/networks/{}/acl", network_id))) + .json(&serde_json::json!({ "policy": policy })); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("update acl failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn get_key_policy(&self, network_id: &str) -> Result { + let req = self + .client + .get(self.url(&format!("/v1/networks/{}/key-policy", network_id))); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("get key policy failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn update_key_policy(&self, network_id: &str, policy: KeyRotationPolicy) -> Result { + let req = self + .client + .put(self.url(&format!("/v1/networks/{}/key-policy", network_id))) + .json(&policy); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("update key policy failed: {}", err))?; + Ok(resp.json().await?) + } + + pub async fn audit_log(&self, params: AuditLogQuery) -> Result { + let req = self.client.get(self.url("/v1/audit")).query(¶ms); + let resp = self + .auth_header(req) + .send() + .await? + .error_for_status() + .map_err(|err| anyhow!("audit log failed: {}", err))?; + Ok(resp.json().await?) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateNetworkRequest { + pub name: String, + pub dns_domain: Option, + pub requires_approval: Option, + pub key_rotation_max_age_seconds: Option, + pub bootstrap_token_ttl_seconds: Option, + pub bootstrap_token_uses: Option, + pub bootstrap_token_tags: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateNetworkResponse { + pub network: NetworkInfo, + pub bootstrap_token: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NetworkInfo { + pub id: String, + pub name: String, + pub overlay_v4: String, + pub overlay_v6: String, + pub dns_domain: String, + pub requires_approval: bool, + #[serde(default)] + pub key_rotation_max_age_seconds: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EnrollmentToken { + pub token: String, + pub expires_at: i64, + pub uses_left: u32, + pub tags: Vec, + pub revoked_at: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateTokenRequest { + pub ttl_seconds: u64, + pub uses: u32, + pub tags: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CreateTokenResponse { + pub token: EnrollmentToken, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AdminNodesResponse { + pub nodes: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NodeInfo { + pub id: String, + pub name: String, + pub dns_name: String, + pub ipv4: String, + pub ipv6: String, + pub wg_public_key: String, + pub machine_public_key: String, + pub endpoints: Vec, + pub tags: Vec, + pub routes: Vec, + pub last_seen: i64, + pub approved: bool, + #[serde(default)] + pub key_rotation_required: bool, + #[serde(default)] + pub revoked: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Route { + pub prefix: String, + pub kind: RouteKind, + pub enabled: bool, + #[serde(default)] + pub mapped_prefix: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RouteKind { + Subnet, + Exit, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ApproveNodeResponse { + pub node_id: String, + pub approved: bool, + pub approved_at: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyRotationRequest { + pub machine_public_key: Option, + pub wg_public_key: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyRotationResponse { + pub node_id: String, + pub machine_public_key: String, + pub wg_public_key: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RevokeNodeResponse { + pub node_id: String, + pub revoked_at: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyHistoryResponse { + pub node_id: String, + pub keys: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyRecord { + pub key_type: String, + pub public_key: String, + pub created_at: i64, + pub revoked_at: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyRotationPolicy { + pub max_age_seconds: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyPolicyResponse { + pub policy: KeyRotationPolicy, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuditLogResponse { + pub entries: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuditEntry { + pub id: String, + pub network_id: Option, + pub node_id: Option, + pub action: String, + pub timestamp: i64, + #[serde(default)] + pub detail: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuditLogQuery { + pub network_id: Option, + pub node_id: Option, + pub limit: Option, +} diff --git a/backend/src/db.rs b/backend/src/db.rs new file mode 100644 index 0000000..9ee7c2e --- /dev/null +++ b/backend/src/db.rs @@ -0,0 +1,9 @@ +use crate::config::DatabaseConfig; +use sqlx::{postgres::PgPoolOptions, PgPool}; + +pub async fn connect(config: &DatabaseConfig) -> Result { + PgPoolOptions::new() + .max_connections(config.max_connections) + .connect(&config.url) + .await +} diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..28c7ffb --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,87 @@ +mod api; +mod app_state; +mod audit_log; +mod auth; +mod config; +mod control_plane; +mod db; +mod models; +mod oidc; +mod permissions; +mod rbac; + +use crate::api::auth::ensure_bootstrap_admin; +use crate::app_state::AppState; +use crate::config::Config; +use axum::routing::get; +use axum::Router; +use std::net::SocketAddr; +use tower_http::cors::{AllowOrigin, CorsLayer, Any}; +use tower_http::services::{ServeDir, ServeFile}; +use tower_http::trace::TraceLayer; +use tracing_subscriber::EnvFilter; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + let config = Config::load()?; + let pool = db::connect(&config.database).await?; + sqlx::migrate!("./migrations").run(&pool).await?; + + ensure_bootstrap_admin(&pool, &config).await?; + + let oidc = oidc::load_providers(&config.oidc, &config.server.base_url).await?; + let state = AppState { + pool, + config: config.clone(), + oidc, + }; + + let mut app = Router::new() + .route("/healthz", get(|| async { "ok" })) + .nest("/admin/api", api::router()) + .layer(TraceLayer::new_for_http()); + + if let Some(static_dir) = &config.server.static_dir { + let index_path = format!("{}/index.html", static_dir.trim_end_matches('/')); + let service = ServeDir::new(static_dir).fallback(ServeFile::new(index_path)); + app = app.nest_service("/", service); + } + + if let Some(cors_layer) = build_cors(&config.server.allowed_origins) { + app = app.layer(cors_layer); + } + + let app = app.with_state(state); + + let addr: SocketAddr = config.server.bind.parse()?; + tracing::info!("lightscale-admin listening on {}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +fn build_cors(allowed_origins: &[String]) -> Option { + if allowed_origins.is_empty() { + return None; + } + + let origins = allowed_origins + .iter() + .map(|origin| origin.parse()) + .collect::, _>>() + .ok()?; + + Some( + CorsLayer::new() + .allow_origin(AllowOrigin::list(origins)) + .allow_methods(Any) + .allow_headers(Any) + .allow_credentials(true), + ) +} diff --git a/backend/src/models.rs b/backend/src/models.rs new file mode 100644 index 0000000..8f984e3 --- /dev/null +++ b/backend/src/models.rs @@ -0,0 +1,82 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use time::OffsetDateTime; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct User { + pub id: Uuid, + pub email: String, + pub display_name: Option, + pub password_hash: Option, + pub disabled: bool, + pub super_admin: bool, + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Role { + pub id: Uuid, + pub name: String, + pub description: Option, + pub created_at: OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Membership { + pub id: Uuid, + pub user_id: Uuid, + pub scope_type: String, + pub scope_id: String, + pub role_id: Uuid, + pub created_at: OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct ControlPlane { + pub id: Uuid, + pub name: String, + pub base_url: String, + pub admin_token: Option, + pub region: Option, + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Network { + pub id: Uuid, + pub control_plane_id: Uuid, + pub network_id: String, + pub name: String, + pub dns_domain: Option, + pub overlay_v4: Option, + pub overlay_v6: Option, + pub requires_approval: bool, + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Session { + pub id: Uuid, + pub user_id: Uuid, + pub token_hash: String, + pub expires_at: OffsetDateTime, + pub created_at: OffsetDateTime, + pub last_seen_at: OffsetDateTime, + pub user_agent: Option, + pub ip: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct AuditEntry { + pub id: Uuid, + pub actor_user_id: Option, + pub action: String, + pub target_type: Option, + pub target_id: Option, + pub metadata: Option, + pub created_at: OffsetDateTime, +} diff --git a/backend/src/oidc.rs b/backend/src/oidc.rs new file mode 100644 index 0000000..182e337 --- /dev/null +++ b/backend/src/oidc.rs @@ -0,0 +1,109 @@ +use crate::config::OidcProviderConfig; +use anyhow::{anyhow, Result}; +use openidconnect::core::{CoreClient, CoreProviderMetadata, CoreResponseType}; +use openidconnect::reqwest::async_http_client; +use openidconnect::{ + AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, + PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, +}; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct OidcProvider { + pub id: String, + pub name: String, + pub client: CoreClient, + pub scopes: Vec, + pub extra_params: HashMap, +} + +pub async fn load_providers( + providers: &[OidcProviderConfig], + base_url: &str, +) -> Result> { + let mut out = HashMap::new(); + for provider in providers { + let issuer = IssuerUrl::new(provider.issuer_url.clone())?; + let metadata = CoreProviderMetadata::discover_async(issuer, async_http_client).await?; + let redirect = format!( + "{}/admin/api/auth/oidc/{}/callback", + base_url.trim_end_matches('/'), + provider.id + ); + let client = CoreClient::from_provider_metadata( + metadata, + ClientId::new(provider.client_id.clone()), + Some(ClientSecret::new(provider.client_secret.clone())), + ) + .set_redirect_uri(RedirectUrl::new(redirect)?); + + out.insert( + provider.id.clone(), + OidcProvider { + id: provider.id.clone(), + name: provider.name.clone(), + client, + scopes: provider.scopes.clone(), + extra_params: provider.extra_params.clone(), + }, + ); + } + Ok(out) +} + +pub struct OidcAuthRequest { + pub url: String, + pub state: String, + pub nonce: String, + pub verifier: String, +} + +pub fn build_auth_request(provider: &OidcProvider) -> Result { + let (challenge, verifier) = PkceCodeChallenge::new_random_sha256(); + let mut request = provider + .client + .authorize_url( + AuthenticationFlow::::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .set_pkce_challenge(challenge); + + if provider.scopes.is_empty() { + request = request.add_scope(Scope::new("openid".to_string())); + request = request.add_scope(Scope::new("email".to_string())); + request = request.add_scope(Scope::new("profile".to_string())); + } else { + for scope in &provider.scopes { + request = request.add_scope(Scope::new(scope.clone())); + } + } + + for (key, value) in &provider.extra_params { + request = request.add_extra_param(key, value); + } + + let (url, csrf, nonce) = request.url(); + + Ok(OidcAuthRequest { + url: url.to_string(), + state: csrf.secret().to_string(), + nonce: nonce.secret().to_string(), + verifier: verifier.secret().to_string(), + }) +} + +pub async fn exchange_code( + provider: &OidcProvider, + code: String, + verifier: String, +) -> Result { + let token = provider + .client + .exchange_code(AuthorizationCode::new(code)) + .set_pkce_verifier(PkceCodeVerifier::new(verifier)) + .request_async(async_http_client) + .await + .map_err(|err| anyhow!("oidc token exchange failed: {}", err))?; + Ok(token) +} diff --git a/backend/src/permissions.rs b/backend/src/permissions.rs new file mode 100644 index 0000000..12772a3 --- /dev/null +++ b/backend/src/permissions.rs @@ -0,0 +1,49 @@ +use crate::api::ApiError; +use crate::models::User; +use crate::rbac::{self, SCOPE_GLOBAL}; +use sqlx::PgPool; + +pub const PERM_CONTROL_PLANES_READ: &str = "control_planes:read"; +pub const PERM_CONTROL_PLANES_WRITE: &str = "control_planes:write"; +pub const PERM_NETWORKS_READ: &str = "networks:read"; +pub const PERM_NETWORKS_WRITE: &str = "networks:write"; +pub const PERM_NODES_READ: &str = "nodes:read"; +pub const PERM_NODES_WRITE: &str = "nodes:write"; +pub const PERM_TOKENS_WRITE: &str = "tokens:write"; +pub const PERM_ACL_READ: &str = "acl:read"; +pub const PERM_ACL_WRITE: &str = "acl:write"; +pub const PERM_KEY_POLICY_READ: &str = "key_policy:read"; +pub const PERM_KEY_POLICY_WRITE: &str = "key_policy:write"; +pub const PERM_AUDIT_READ: &str = "audit:read"; +pub const PERM_USERS_READ: &str = "users:read"; +pub const PERM_USERS_WRITE: &str = "users:write"; +pub const PERM_ROLES_READ: &str = "roles:read"; +pub const PERM_ROLES_WRITE: &str = "roles:write"; + +pub async fn ensure_permission_global( + pool: &PgPool, + user: &User, + permission: &str, +) -> Result<(), ApiError> { + ensure_permission(pool, user, permission, SCOPE_GLOBAL, SCOPE_GLOBAL).await +} + +pub async fn ensure_permission( + pool: &PgPool, + user: &User, + permission: &str, + scope_type: &str, + scope_id: &str, +) -> Result<(), ApiError> { + if user.super_admin { + return Ok(()); + } + let allowed = rbac::user_has_permission(pool, user.id, permission, scope_type, scope_id) + .await + .map_err(|_| ApiError::Internal)?; + if allowed { + Ok(()) + } else { + Err(ApiError::Forbidden) + } +} diff --git a/backend/src/rbac.rs b/backend/src/rbac.rs new file mode 100644 index 0000000..f178735 --- /dev/null +++ b/backend/src/rbac.rs @@ -0,0 +1,48 @@ +use anyhow::Result; +use sqlx::PgPool; +use uuid::Uuid; + +pub const SCOPE_GLOBAL: &str = "global"; + +pub async fn user_has_permission( + pool: &PgPool, + user_id: Uuid, + permission: &str, + scope_type: &str, + scope_id: &str, +) -> Result { + let record = sqlx::query_scalar::<_, bool>( + "SELECT super_admin FROM users WHERE id = $1", + ) + .bind(user_id) + .fetch_one(pool) + .await?; + + if record { + return Ok(true); + } + + let permitted = sqlx::query_scalar::<_, bool>( + "\ + SELECT EXISTS (\n\ + SELECT 1\n\ + FROM memberships m\n\ + JOIN role_permissions rp ON rp.role_id = m.role_id\n\ + WHERE m.user_id = $1\n\ + AND rp.permission = $2\n\ + AND (\n\ + (m.scope_type = 'global' AND m.scope_id = 'global')\n\ + OR (m.scope_type = $3 AND m.scope_id = $4)\n\ + )\n\ + )\n\ + ", + ) + .bind(user_id) + .bind(permission) + .bind(scope_type) + .bind(scope_id) + .fetch_one(pool) + .await?; + + Ok(permitted) +} diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..61599de --- /dev/null +++ b/config.example.toml @@ -0,0 +1,25 @@ +[server] +bind = "0.0.0.0:8081" +base_url = "http://localhost:8081" +allowed_origins = ["http://localhost:5173"] +static_dir = "frontend/dist" + +[database] +url = "postgresql://root@localhost:26257/lightscale_admin?sslmode=disable" +max_connections = 20 + +[auth] +session_ttl_minutes = 720 +cookie_secure = false +# cookie_domain = "admin.lightscale.local" +bootstrap_admin_email = "admin@example.com" +bootstrap_admin_password = "change-me" +allow_user_signup = false + +[[oidc]] +id = "google" +name = "Google" +issuer_url = "https://accounts.google.com" +client_id = "" +client_secret = "" +scopes = ["openid", "email", "profile"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9edc5be --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + cockroach: + image: cockroachdb/cockroach:v23.1.11 + command: start-single-node --insecure --http-addr=0.0.0.0:8080 + ports: + - "26257:26257" + - "8080:8080" + volumes: + - cockroach-data:/cockroach/cockroach-data +volumes: + cockroach-data: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..072a57e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..c84e43a --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3269 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", + "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", + "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/type-utils": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.53.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", + "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", + "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.1", + "@typescript-eslint/types": "^8.53.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", + "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", + "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", + "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", + "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", + "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.1", + "@typescript-eslint/tsconfig-utils": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", + "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", + "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", + "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.277", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz", + "integrity": "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", + "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.53.1", + "@typescript-eslint/parser": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..eded7cd --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..d4ac191 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,544 @@ +:root { + --shadow: 0 24px 60px rgba(31, 27, 22, 0.14); + --shadow-soft: 0 12px 30px rgba(31, 27, 22, 0.08); +} + +#root { + min-height: 100vh; +} + +.screen { + min-height: 100vh; + padding: 48px; + display: flex; + align-items: center; + justify-content: center; +} + +.screen.center { + flex-direction: column; +} + +.loader { + font-size: 1.1rem; + letter-spacing: 0.02em; +} + +.login-grid { + width: min(1100px, 100%); + display: grid; + grid-template-columns: minmax(280px, 1fr) minmax(320px, 1.3fr); + gap: 32px; +} + +.login-panel { + background: var(--surface); + border-radius: 24px; + padding: 32px; + box-shadow: var(--shadow); + display: flex; + flex-direction: column; + gap: 24px; +} + +.login-hero { + background: linear-gradient(140deg, rgba(242, 92, 42, 0.15), rgba(43, 154, 143, 0.18)); + border-radius: 32px; + padding: 40px; + position: relative; + overflow: hidden; +} + +.login-hero::after { + content: ''; + position: absolute; + top: -40%; + right: -30%; + width: 320px; + height: 320px; + background: radial-gradient(circle, rgba(246, 196, 83, 0.5), transparent 70%); + opacity: 0.6; +} + +.hero-card { + position: relative; + z-index: 1; +} + +.hero-title { + font-family: var(--font-display); + font-size: 2.2rem; + margin-bottom: 12px; +} + +.hero-copy { + font-size: 1rem; + color: var(--ink-soft); + margin-bottom: 24px; +} + +.hero-points { + display: grid; + gap: 16px; +} + +.hero-points h4 { + margin-bottom: 6px; + font-size: 1rem; +} + +.brand { + display: flex; + align-items: center; + gap: 16px; +} + +.brand-mark { + display: grid; + place-items: center; + width: 48px; + height: 48px; + border-radius: 14px; + background: var(--accent); + color: #fff; + font-weight: 700; +} + +.brand-title { + font-family: var(--font-display); + font-size: 1.4rem; + margin: 0; +} + +.brand-sub { + color: var(--ink-soft); + margin: 4px 0 0; +} + +.app-shell { + display: grid; + grid-template-columns: 280px 1fr; + min-height: 100vh; +} + +.sidebar { + padding: 32px 24px; + background: linear-gradient(180deg, rgba(31, 27, 22, 0.95), rgba(31, 27, 22, 0.85)); + color: #f5efe6; + display: flex; + flex-direction: column; + gap: 32px; +} + +.brand-mini { + display: flex; + gap: 12px; + align-items: center; +} + +.brand-mini span { + width: 40px; + height: 40px; + border-radius: 12px; + background: rgba(242, 92, 42, 0.9); + display: grid; + place-items: center; + font-weight: 700; +} + +.nav { + display: grid; + gap: 12px; +} + +.nav button { + background: transparent; + border: 1px solid rgba(245, 239, 230, 0.1); + color: inherit; + padding: 12px 14px; + border-radius: 14px; + text-align: left; + cursor: pointer; + transition: all 0.2s ease; +} + +.nav button small { + display: block; + color: rgba(245, 239, 230, 0.6); + font-size: 0.75rem; +} + +.nav button.active, +.nav button:hover { + background: rgba(245, 239, 230, 0.12); + border-color: rgba(245, 239, 230, 0.3); +} + +.sidebar-footer { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 12px; + color: rgba(245, 239, 230, 0.8); +} + +.sidebar-footer button { + color: inherit; +} + +.main { + padding: 32px 40px 64px; +} + +.topbar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 24px; + margin-bottom: 24px; +} + +.topbar h1 { + font-family: var(--font-display); + margin-bottom: 8px; +} + +.selector-row { + display: flex; + align-items: flex-end; + gap: 16px; + flex-wrap: wrap; +} + +.selector-row label { + min-width: 180px; +} + +.banner { + padding: 12px 16px; + border-radius: 12px; + margin-bottom: 24px; + font-weight: 500; +} + +.banner.success { + background: rgba(43, 154, 143, 0.15); + color: #1d6f66; +} + +.banner.error { + background: rgba(242, 92, 42, 0.16); + color: #a93b16; +} + +.banner.warning { + background: rgba(246, 196, 83, 0.2); + color: #8a5e12; +} + +.banner.info { + background: rgba(31, 27, 22, 0.08); + color: var(--ink); +} + +.content { + display: grid; + gap: 24px; +} + +.grid { + display: grid; + gap: 24px; +} + +.grid.two { + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); +} + +.card { + background: var(--surface); + padding: 24px; + border-radius: 20px; + box-shadow: var(--shadow-soft); + display: flex; + flex-direction: column; + gap: 16px; + animation: riseIn 0.6s ease both; +} + +.card.highlight { + background: linear-gradient(140deg, rgba(43, 154, 143, 0.12), rgba(246, 196, 83, 0.25)); +} + +.stack { + display: grid; + gap: 12px; +} + +.inline { + display: grid; + gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); +} + +label span { + display: block; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--ink-soft); + margin-bottom: 6px; +} + +input, +select, +textarea { + width: 100%; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid var(--border); + background: #fff; + font-family: inherit; + font-size: 0.95rem; +} + +textarea { + min-height: 240px; + font-family: var(--font-mono); + font-size: 0.85rem; +} + +button { + border: none; + border-radius: 12px; + padding: 10px 16px; + background: var(--accent); + color: #fff; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +button:hover { + transform: translateY(-1px); + box-shadow: 0 10px 20px rgba(242, 92, 42, 0.2); +} + +button.ghost { + background: transparent; + color: var(--ink); + border: 1px solid var(--border); + box-shadow: none; +} + +button.ghost:hover { + background: rgba(31, 27, 22, 0.05); +} + +button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.button-row { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.list { + display: grid; + gap: 12px; +} + +.list-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 12px 14px; + border-radius: 14px; + background: rgba(31, 27, 22, 0.03); + animation: riseIn 0.5s ease both; +} + +.list-row.selectable { + border: 1px solid transparent; + cursor: pointer; +} + +.list-row.selectable.active, +.list-row.selectable:hover { + border-color: rgba(31, 27, 22, 0.2); + background: rgba(31, 27, 22, 0.05); +} + +.tag-row { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 6px; +} + +.tag { + padding: 4px 8px; + border-radius: 999px; + background: rgba(31, 27, 22, 0.08); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.tag.success { + background: rgba(43, 154, 143, 0.18); + color: #1c6b63; +} + +.tag.warning { + background: rgba(246, 196, 83, 0.2); + color: #8a5e12; +} + +.tag.danger { + background: rgba(242, 92, 42, 0.2); + color: #a93b16; +} + +.tag-row .tag { + margin-top: 6px; +} + +.mono { + font-family: var(--font-mono); + font-size: 0.8rem; +} + +.pill-row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.pill { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + padding: 8px 14px; + border: 1px solid var(--border); + background: #fff; + font-weight: 600; + cursor: pointer; +} + +.pill.active { + background: var(--ink); + color: #fff; + border-color: var(--ink); +} + +.notice { + padding: 8px 12px; + border-radius: 10px; + font-size: 0.85rem; +} + +.notice.error { + background: rgba(242, 92, 42, 0.12); + color: #a93b16; +} + +.callout { + padding: 16px; + border-radius: 14px; + border: 1px dashed var(--border); + background: rgba(43, 154, 143, 0.08); + display: grid; + gap: 8px; +} + +.callout code { + font-family: var(--font-mono); + font-size: 0.9rem; +} + +.section-title { + font-weight: 600; +} + +.meta { + display: flex; + gap: 12px; + flex-wrap: wrap; + color: var(--ink-soft); + font-size: 0.8rem; +} + +.divider { + height: 1px; + background: rgba(31, 27, 22, 0.1); +} + +.toggle { + display: flex; + gap: 12px; + align-items: center; +} + +.toggle input { + width: auto; +} + +.muted { + color: var(--ink-soft); +} + +@keyframes riseIn { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 960px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar { + flex-direction: row; + flex-wrap: wrap; + align-items: center; + } + + .nav { + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + } + + .main { + padding: 24px; + } + + .login-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .selector-row { + flex-direction: column; + align-items: stretch; + } + + .topbar { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..4b19b49 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,1848 @@ +import { useEffect, useMemo, useState, type FormEvent } from 'react' +import './App.css' +import { + addMembership, + approveNode, + createControlPlane, + createNetwork, + createToken, + createUser, + deleteMembership, + getAcl, + getKeyPolicy, + getMe, + getUser, + listAdminAudit, + listControlPlaneAudit, + listControlPlanes, + listNetworks, + listNodes, + listProviders, + listRoles, + listUsers, + login, + logout, + nodeKeys, + revokeNode, + revokeToken, + rotateNodeKeys, + setUserPassword, + updateAcl, + updateControlPlane, + updateKeyPolicy, + updateUser, + verifyControlPlane, + type AdminAuditEntry, + type AdminNodesResponse, + type AuditLogResponse, + type ControlPlaneSummary, + type CreateTokenResponse, + type EnrollmentToken, + type KeyHistoryResponse, + type KeyPolicyResponse, + type NetworkSummary, + type NodeInfo, + type ProviderSummary, + type RoleSummary, + type UserDetail, + type UserSummary, +} from './api' + +type SectionId = + | 'overview' + | 'control-planes' + | 'networks' + | 'nodes' + | 'tokens' + | 'acl' + | 'key-policy' + | 'users' + | 'audit' + +type BannerTone = 'info' | 'warning' | 'error' | 'success' + +type BannerMessage = { + tone: BannerTone + text: string +} + +const sections: { id: SectionId; label: string; hint: string }[] = [ + { id: 'overview', label: 'Overview', hint: 'Pulse & posture' }, + { id: 'control-planes', label: 'Control Planes', hint: 'Origins & routes' }, + { id: 'networks', label: 'Networks', hint: 'Provision & catalog' }, + { id: 'nodes', label: 'Nodes', hint: 'Approve & rotate' }, + { id: 'tokens', label: 'Tokens', hint: 'Invite securely' }, + { id: 'acl', label: 'ACL', hint: 'Policy canvas' }, + { id: 'key-policy', label: 'Key Policy', hint: 'Rotation cadence' }, + { id: 'users', label: 'Users', hint: 'Access graph' }, + { id: 'audit', label: 'Audit', hint: 'Trace every move' }, +] + +const DEFAULT_SCOPE = { scope_type: 'global', scope_id: 'global' } + +function App() { + const [authReady, setAuthReady] = useState(false) + const [currentUser, setCurrentUser] = useState(null) + + useEffect(() => { + getMe() + .then((user) => setCurrentUser(user)) + .catch(() => setCurrentUser(null)) + .finally(() => setAuthReady(true)) + }, []) + + if (!authReady) { + return ( +
+
Loading console...
+
+ ) + } + + if (!currentUser) { + return + } + + return ( + setCurrentUser(null)} + onUserUpdated={setCurrentUser} + /> + ) +} + +function LoginScreen({ onAuthed }: { onAuthed: (user: UserSummary) => void }) { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [providers, setProviders] = useState([]) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + useEffect(() => { + listProviders() + .then(setProviders) + .catch(() => setProviders([])) + }, []) + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault() + setLoading(true) + setError(null) + try { + const response = await login(email, password) + onAuthed(response.user) + } catch (err) { + setError((err as Error).message) + } finally { + setLoading(false) + } + } + + return ( +
+
+
+
+ LS +
+

Lightscale Admin

+

Orchestrate control planes with intent.

+
+
+ +
+ + + {error &&
{error}
} + +
+ + {providers.length > 0 && ( +
+

Single sign-on

+
+ {providers.map((provider) => ( + + {provider.name} + + ))} +
+
+ )} +
+
+
+

Zero single points of failure.

+

+ Build a multi-region control plane ledger backed by CockroachDB and manage it + in one place. +

+
+
+

Layered auth

+

Local accounts, OIDC, and scoped roles with audit trails.

+
+
+

Operational focus

+

Approve nodes, rotate keys, and author network policy rapidly.

+
+
+

Designed to scale

+

Aggregate multiple control planes without a monolith.

+
+
+
+
+
+
+ ) +} + +function AdminShell({ + user, + onLogout, + onUserUpdated, +}: { + user: UserSummary + onLogout: () => void + onUserUpdated: (user: UserSummary) => void +}) { + const [active, setActive] = useState('overview') + const [banner, setBanner] = useState(null) + + const [controlPlanes, setControlPlanes] = useState([]) + const [networks, setNetworks] = useState([]) + const [roles, setRoles] = useState([]) + const [users, setUsers] = useState([]) + const [selectedControlPlaneId, setSelectedControlPlaneId] = useState('') + const [selectedNetworkId, setSelectedNetworkId] = useState('') + const [selectedUser, setSelectedUser] = useState(null) + + const [nodesResponse, setNodesResponse] = useState(null) + const [keyHistory, setKeyHistory] = useState(null) + const [aclText, setAclText] = useState('') + const [keyPolicy, setKeyPolicy] = useState(null) + const [tokenResult, setTokenResult] = useState(null) + const [revokedToken, setRevokedToken] = useState(null) + const [bootstrapToken, setBootstrapToken] = useState(null) + + const [adminAudit, setAdminAudit] = useState([]) + const [controlPlaneAudit, setControlPlaneAudit] = useState(null) + + const selectedNetwork = useMemo( + () => networks.find((network) => network.id === selectedNetworkId) || null, + [networks, selectedNetworkId], + ) + + const selectedNodeCount = nodesResponse?.nodes.length ?? 0 + + useEffect(() => { + refreshControlPlanes() + refreshNetworks() + refreshRoles() + refreshUsers() + }, []) + + useEffect(() => { + if (controlPlanes.length && !selectedControlPlaneId) { + setSelectedControlPlaneId(controlPlanes[0].id) + } + }, [controlPlanes, selectedControlPlaneId]) + + useEffect(() => { + if (networks.length && !selectedNetworkId) { + setSelectedNetworkId(networks[0].id) + } + }, [networks, selectedNetworkId]) + + useEffect(() => { + if (!selectedNetworkId) return + listNodes(selectedNetworkId) + .then(setNodesResponse) + .catch((err) => handleError(err, 'error')) + getAcl(selectedNetworkId) + .then((policy) => setAclText(JSON.stringify(policy, null, 2))) + .catch(() => setAclText('')) + getKeyPolicy(selectedNetworkId) + .then(setKeyPolicy) + .catch(() => setKeyPolicy(null)) + }, [selectedNetworkId]) + + useEffect(() => { + if (active !== 'audit') return + listAdminAudit({ limit: 200 }) + .then(setAdminAudit) + .catch((err) => handleError(err, 'error')) + }, [active]) + + const refreshControlPlanes = async () => { + try { + const data = await listControlPlanes() + setControlPlanes(data) + } catch (err) { + handleError(err, 'error') + } + } + + const refreshNetworks = async () => { + try { + const data = await listNetworks() + setNetworks(data) + } catch (err) { + handleError(err, 'error') + } + } + + const refreshRoles = async () => { + try { + const data = await listRoles() + setRoles(data) + } catch (err) { + handleError(err, 'error') + } + } + + const refreshUsers = async () => { + try { + const data = await listUsers() + setUsers(data) + } catch (err) { + handleError(err, 'error') + } + } + + const handleError = (err: unknown, tone: BannerTone) => { + setBanner({ tone, text: (err as Error).message }) + setTimeout(() => setBanner(null), 4000) + } + + const handleLogout = async () => { + try { + await logout() + } finally { + onLogout() + } + } + + const handleSelectUser = async (userId: string) => { + try { + const detail = await getUser(userId) + setSelectedUser(detail) + } catch (err) { + handleError(err, 'error') + } + } + + const handleUpdateCurrentUser = async () => { + try { + const refreshed = await getMe() + onUserUpdated(refreshed) + } catch (err) { + handleError(err, 'warning') + } + } + + const handleNodeAction = async (action: () => Promise) => { + try { + await action() + if (selectedNetworkId) { + const updated = await listNodes(selectedNetworkId) + setNodesResponse(updated) + } + } catch (err) { + handleError(err, 'error') + } + } + + const handleAclSave = async () => { + if (!selectedNetworkId) return + try { + const parsed = JSON.parse(aclText) + await updateAcl(selectedNetworkId, parsed) + handleError(new Error('ACL updated.'), 'success') + } catch (err) { + handleError(err, 'error') + } + } + + const handleKeyPolicySave = async (maxAge: number | null) => { + if (!selectedNetworkId) return + try { + const updated = await updateKeyPolicy(selectedNetworkId, { + max_age_seconds: maxAge || null, + }) + setKeyPolicy(updated) + handleError(new Error('Key policy updated.'), 'success') + } catch (err) { + handleError(err, 'error') + } + } + + return ( +
+ + +
+
+
+

{sections.find((section) => section.id === active)?.label}

+

+ {selectedNetwork + ? `${selectedNetwork.name} · ${selectedNetwork.control_plane_name}` + : 'Select a network to unlock node actions.'} +

+
+
+ + + +
+
+ + {banner &&
{banner.text}
} + +
+ {active === 'overview' && ( +
+
+

Control plane mesh

+

{controlPlanes.length} control planes online.

+
+
+ Networks + {networks.length} +
+
+ Nodes (selected) + {selectedNodeCount} +
+
+
+
+

Active signals

+

Audit log for admin actions and control plane events.

+ +
+
+

Role coverage

+

+ {roles.length} roles configured. {users.length} users in + directory. +

+ +
+
+

Network posture

+

Keep ACLs tight and key rotation moving.

+ +
+
+ )} + + {active === 'control-planes' && ( + { + const created = await createControlPlane(payload) + setControlPlanes((prev) => [...prev, created]) + }} + onUpdate={async (id, payload) => { + const updated = await updateControlPlane(id, payload) + setControlPlanes((prev) => + prev.map((plane) => (plane.id === updated.id ? updated : plane)), + ) + }} + onVerify={async (id) => verifyControlPlane(id)} + onBanner={setBanner} + /> + )} + + {active === 'networks' && ( + { + const result = await createNetwork(payload) + setNetworks((prev) => [...prev, result.network]) + setBootstrapToken(result.bootstrap_token || null) + setSelectedNetworkId(result.network.id) + }} + onSelect={(id) => setSelectedNetworkId(id)} + selectedNetworkId={selectedNetworkId} + bootstrapToken={bootstrapToken} + /> + )} + + {active === 'nodes' && ( + + handleNodeAction(() => approveNode(selectedNetworkId, nodeId)) + } + onRevoke={(nodeId) => + handleNodeAction(() => revokeNode(selectedNetworkId, nodeId)) + } + onRotate={(nodeId) => + handleNodeAction(() => rotateNodeKeys(selectedNetworkId, nodeId, {})) + } + onViewKeys={async (nodeId) => { + if (!selectedNetworkId) return + try { + const history = await nodeKeys(selectedNetworkId, nodeId) + setKeyHistory(history) + } catch (err) { + handleError(err, 'error') + } + }} + /> + )} + + {active === 'tokens' && ( + { + if (!selectedNetworkId) return + const response = await createToken(selectedNetworkId, payload) + setTokenResult(response) + setRevokedToken(null) + }} + onRevoke={async (tokenId) => { + if (!selectedNetworkId) return + const response = await revokeToken(selectedNetworkId, tokenId) + setRevokedToken(response) + }} + /> + )} + + {active === 'acl' && ( + + )} + + {active === 'key-policy' && ( + + )} + + {active === 'users' && ( + { + const created = await createUser(payload) + setUsers((prev) => [created.user, ...prev]) + setSelectedUser(created) + }} + onUpdate={async (userId, payload) => { + const updated = await updateUser(userId, payload) + setUsers((prev) => + prev.map((record) => (record.id === updated.id ? updated : record)), + ) + if (selectedUser && selectedUser.user.id === updated.id) { + setSelectedUser({ ...selectedUser, user: updated }) + } + }} + onSetPassword={async (userId, password) => { + await setUserPassword(userId, password) + }} + onAddMembership={async (userId, payload) => { + const membership = await addMembership(userId, payload) + if (selectedUser && selectedUser.user.id === userId) { + setSelectedUser({ + ...selectedUser, + memberships: [membership, ...selectedUser.memberships], + }) + } + }} + onDeleteMembership={async (userId, membershipId) => { + await deleteMembership(userId, membershipId) + if (selectedUser && selectedUser.user.id === userId) { + setSelectedUser({ + ...selectedUser, + memberships: selectedUser.memberships.filter( + (membership) => membership.id !== membershipId, + ), + }) + } + }} + /> + )} + + {active === 'audit' && ( + { + const entries = await listAdminAudit({ limit: 200 }) + setAdminAudit(entries) + }} + onFetchControlPlane={async (id, params) => { + const response = await listControlPlaneAudit(id, params) + setControlPlaneAudit(response) + }} + /> + )} +
+
+
+ ) +} + +function ControlPlaneSection({ + controlPlanes, + onCreate, + onUpdate, + onVerify, + onBanner, +}: { + controlPlanes: ControlPlaneSummary[] + onCreate: (payload: { + name: string + base_url: string + admin_token?: string + region?: string + }) => Promise + onUpdate: ( + id: string, + payload: { + name?: string + base_url?: string + admin_token?: string + clear_admin_token?: boolean + region?: string + }, + ) => Promise + onVerify: (id: string) => Promise<{ ok: boolean; status?: number; body?: string }> + onBanner: (message: BannerMessage) => void +}) { + const [form, setForm] = useState({ + id: '', + name: '', + base_url: '', + admin_token: '', + region: '', + clear_admin_token: false, + }) + const [loading, setLoading] = useState(false) + + const resetForm = () => { + setForm({ + id: '', + name: '', + base_url: '', + admin_token: '', + region: '', + clear_admin_token: false, + }) + } + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault() + setLoading(true) + try { + if (form.id) { + await onUpdate(form.id, { + name: form.name, + base_url: form.base_url, + admin_token: form.admin_token || undefined, + clear_admin_token: form.clear_admin_token, + region: form.region || undefined, + }) + onBanner({ tone: 'success', text: 'Control plane updated.' }) + } else { + await onCreate({ + name: form.name, + base_url: form.base_url, + admin_token: form.admin_token || undefined, + region: form.region || undefined, + }) + onBanner({ tone: 'success', text: 'Control plane created.' }) + } + resetForm() + } catch (err) { + onBanner({ tone: 'error', text: (err as Error).message }) + } finally { + setLoading(false) + } + } + + return ( +
+
+

Create or update control plane

+
+ + + + {form.id && ( + + )} + +
+ + {form.id && ( + + )} +
+
+
+
+

Registered control planes

+
+ {controlPlanes.map((plane) => ( +
+
+ {plane.name} +

{plane.base_url}

+ {plane.region || 'unassigned'} + {plane.has_admin_token ? ( + token stored + ) : ( + no token + )} +
+
+ + +
+
+ ))} + {controlPlanes.length === 0 && ( +

No control planes yet.

+ )} +
+
+
+ ) +} + +function NetworksSection({ + controlPlanes, + networks, + onCreate, + onSelect, + selectedNetworkId, + bootstrapToken, +}: { + controlPlanes: ControlPlaneSummary[] + networks: NetworkSummary[] + onCreate: (payload: { + control_plane_id: string + name: string + dns_domain?: string + requires_approval?: boolean + key_rotation_max_age_seconds?: number + bootstrap_token_ttl_seconds?: number + bootstrap_token_uses?: number + bootstrap_token_tags?: string[] + }) => Promise + onSelect: (id: string) => void + selectedNetworkId: string + bootstrapToken: EnrollmentToken | null +}) { + const [form, setForm] = useState({ + control_plane_id: '', + name: '', + dns_domain: '', + requires_approval: false, + key_rotation_max_age_seconds: '', + bootstrap_token_ttl_seconds: '', + bootstrap_token_uses: '1', + bootstrap_token_tags: '', + }) + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault() + if (!form.control_plane_id) return + await onCreate({ + control_plane_id: form.control_plane_id, + name: form.name, + dns_domain: form.dns_domain || undefined, + requires_approval: form.requires_approval, + key_rotation_max_age_seconds: form.key_rotation_max_age_seconds + ? Number(form.key_rotation_max_age_seconds) + : undefined, + bootstrap_token_ttl_seconds: form.bootstrap_token_ttl_seconds + ? Number(form.bootstrap_token_ttl_seconds) + : undefined, + bootstrap_token_uses: form.bootstrap_token_uses + ? Number(form.bootstrap_token_uses) + : undefined, + bootstrap_token_tags: form.bootstrap_token_tags + ? form.bootstrap_token_tags.split(',').map((tag) => tag.trim()) + : undefined, + }) + setForm({ + control_plane_id: form.control_plane_id, + name: '', + dns_domain: '', + requires_approval: false, + key_rotation_max_age_seconds: '', + bootstrap_token_ttl_seconds: '', + bootstrap_token_uses: '1', + bootstrap_token_tags: '', + }) + } + + return ( +
+
+

Provision a network

+
+ + + + +
+ + +
+
+ + +
+ +
+ + {bootstrapToken && ( +
+

Bootstrap token

+ {bootstrapToken.token} +
+ Uses left: {bootstrapToken.uses_left} + Expires: {formatEpoch(bootstrapToken.expires_at)} +
+
+ )} +
+
+

Known networks

+
+ {networks.map((network, index) => ( + + ))} + {networks.length === 0 &&

No networks yet.

} +
+
+
+ ) +} + +function NodesSection({ + network, + nodes, + keyHistory, + onApprove, + onRevoke, + onRotate, + onViewKeys, +}: { + network: NetworkSummary | null + nodes: NodeInfo[] + keyHistory: KeyHistoryResponse | null + onApprove: (nodeId: string) => void + onRevoke: (nodeId: string) => void + onRotate: (nodeId: string) => void + onViewKeys: (nodeId: string) => void +}) { + if (!network) { + return
Select a network to view nodes.
+ } + + return ( +
+
+

Nodes in {network.name}

+
+ {nodes.map((node) => ( +
+
+ {node.name} +

{node.dns_name}

+
+ {node.approved ? ( + approved + ) : ( + pending + )} + {node.revoked && revoked} + {node.key_rotation_required && ( + rotate keys + )} +
+ Last seen: {formatEpoch(node.last_seen)} +
+
+ {!node.approved && !node.revoked && ( + + )} + {!node.revoked && ( + + )} + + +
+
+ ))} + {nodes.length === 0 &&

No nodes yet.

} +
+
+
+

Key history

+ {keyHistory ? ( +
+ {keyHistory.keys.map((key) => ( +
+
+ {key.key_type} +

{key.public_key}

+
+
+ {formatEpoch(key.created_at)} + {key.revoked_at && ( + + revoked {formatEpoch(key.revoked_at)} + + )} +
+
+ ))} +
+ ) : ( +

Select a node to view key history.

+ )} +
+
+ ) +} + +function TokensSection({ + network, + tokenResult, + revokedToken, + onCreate, + onRevoke, +}: { + network: NetworkSummary | null + tokenResult: CreateTokenResponse | null + revokedToken: EnrollmentToken | null + onCreate: (payload: { ttl_seconds: number; uses: number; tags: string[] }) => void + onRevoke: (tokenId: string) => void +}) { + const [form, setForm] = useState({ ttl_seconds: '3600', uses: '1', tags: '' }) + const [revokeId, setRevokeId] = useState('') + + if (!network) { + return
Select a network to manage tokens.
+ } + + return ( +
+
+

Create enrollment token

+
{ + event.preventDefault() + onCreate({ + ttl_seconds: Number(form.ttl_seconds), + uses: Number(form.uses), + tags: form.tags + ? form.tags.split(',').map((tag) => tag.trim()) + : [], + }) + }} + className="stack" + > + + + + +
+ {tokenResult && ( +
+

Generated token

+ {tokenResult.token.token} +
+ Uses left: {tokenResult.token.uses_left} + Expires: {formatEpoch(tokenResult.token.expires_at)} +
+
+ )} +
+
+

Revoke token

+
{ + event.preventDefault() + onRevoke(revokeId) + }} + className="stack" + > + + +
+ {revokedToken && ( +
+

Revoked token

+ {revokedToken.token} + revoked +
+ )} +
+
+ ) +} + +function AclSection({ + network, + aclText, + onChange, + onSave, +}: { + network: NetworkSummary | null + aclText: string + onChange: (value: string) => void + onSave: () => void +}) { + if (!network) { + return
Select a network to edit ACL.
+ } + + return ( +
+

ACL policy for {network.name}

+