From 10cf4c730117a1cd209cfb33a8c5018a28e5954d Mon Sep 17 00:00:00 2001 From: jonathanmagambo Date: Sun, 8 Mar 2026 16:26:15 -0400 Subject: [PATCH] feat(cli): overhaul TUI design and enhance CLI developer experience --- .github/workflows/release.yml | 88 ++ .gitignore | 3 + Cargo.lock | 1685 ++++++++++++++++++++++++++- Makefile | 18 + crates/bin/src/main.rs | 60 +- crates/cli/Cargo.toml | 5 + crates/cli/src/init.rs | 37 +- crates/cli/src/lib.rs | 1 + crates/cli/src/tui.rs | 1065 +++++++++++++++++ crates/server/Cargo.toml | 3 + crates/server/src/auth_api.rs | 96 ++ crates/server/src/lib.rs | 48 +- crates/server/tests/api_pipeline.rs | 6 +- crates/storage/src/engine.rs | 44 +- 14 files changed, 3041 insertions(+), 118 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 Makefile create mode 100644 crates/cli/src/tui.rs create mode 100644 crates/server/src/auth_api.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..921e627 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + name: Build backend release binaries + strategy: + matrix: + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install cross-compilation tools + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross + + - name: Install minimal nightly + uses: dtolnay/rust-toolchain@nightly + with: + targets: ${{ matrix.target }} + + - name: Build + run: cargo build --release --target ${{ matrix.target }} -p forge-bin + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Compress and package binaries + shell: bash + run: | + mkdir -p artifacts + if [ "${{ matrix.os }}" = "windows-latest" ]; then + cp target/${{ matrix.target }}/release/forgedb.exe artifacts/forgedb-${{ matrix.target }}.exe + else + cp target/${{ matrix.target }}/release/forgedb artifacts/forgedb-${{ matrix.target }} + chmod +x artifacts/forgedb-${{ matrix.target }} + tar -czvf artifacts/forgedb-${{ matrix.target }}.tar.gz -C artifacts forgedb-${{ matrix.target }} + rm artifacts/forgedb-${{ matrix.target }} + fi + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: forge-binaries-${{ matrix.target }} + path: artifacts/* + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: forge-binaries-* + merge-multiple: true + path: release-assets + + - name: Create Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: release-assets/* + draft: false + prerelease: false + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 76d8fd9..2954e66 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ # ForgeDB runtime data — never commit certs, keys, or encrypted DBs forgedb_data/ +forgedb.toml +*.redbx +*.log # OS noise .DS_Store diff --git a/Cargo.lock b/Cargo.lock index ff625f8..d71fdc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,12 @@ 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" @@ -135,6 +141,15 @@ dependencies = [ "term", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -237,21 +252,42 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -282,6 +318,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -294,6 +336,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.56" @@ -369,6 +420,12 @@ dependencies = [ "smol_str", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -434,7 +491,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -467,6 +524,59 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -488,6 +598,33 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -495,10 +632,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "ct-codecs" version = "1.1.6" @@ -520,8 +667,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -535,7 +692,20 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] @@ -544,11 +714,28 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + [[package]] name = "deranged" version = "0.5.8" @@ -559,6 +746,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "digest" version = "0.10.7" @@ -570,6 +779,26 @@ dependencies = [ "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.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dunce" version = "1.0.5" @@ -600,7 +829,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -630,6 +859,15 @@ dependencies = [ "log", ] +[[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 = "enum-ordinalize" version = "4.3.2" @@ -647,7 +885,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -666,6 +904,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set 0.5.3", + "regex", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -678,12 +935,35 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -702,6 +982,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "forge-auth" version = "0.2.0" @@ -744,10 +1030,15 @@ dependencies = [ name = "forge-cli" version = "0.2.0" dependencies = [ + "crossterm", "forge-auth", "forge-security", "forge-storage", "forge-types", + "ratatui", + "reqwest", + "serde_json", + "tokio", "toml", "tracing", ] @@ -794,6 +1085,7 @@ dependencies = [ name = "forge-server" version = "0.2.0" dependencies = [ + "aws-lc-rs", "axum", "bytes", "forge-auth", @@ -801,12 +1093,14 @@ dependencies = [ "forge-security", "forge-storage", "forge-types", + "hex", "hyper", "hyper-util", "json-patch", "pasetors", "redbx", "rmp-serde", + "serde", "serde_json", "tempfile", "tokio", @@ -1000,7 +1294,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1008,6 +1302,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heapless" @@ -1109,6 +1408,23 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", ] [[package]] @@ -1117,13 +1433,23 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -1150,6 +1476,87 @@ 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 = "id-arena" version = "2.3.0" @@ -1162,6 +1569,27 @@ 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" @@ -1186,14 +1614,52 @@ dependencies = [ ] [[package]] -name = "inout" -version = "0.1.4" +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "generic-array", ] +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling 0.23.0", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1215,6 +1681,28 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -1257,6 +1745,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + [[package]] name = "keccak" version = "0.1.6" @@ -1266,6 +1765,12 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lalrpop" version = "0.22.2" @@ -1273,7 +1778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.8.0", "ena", "itertools", "lalrpop-util", @@ -1316,6 +1821,15 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1340,6 +1854,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1375,7 +1901,7 @@ dependencies = [ "quote", "regex-automata", "regex-syntax", - "syn", + "syn 2.0.117", ] [[package]] @@ -1387,6 +1913,31 @@ dependencies = [ "logos-codegen", ] +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", +] + [[package]] name = "matchit" version = "0.8.4" @@ -1399,6 +1950,21 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "miette" version = "7.6.0" @@ -1419,7 +1985,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1428,6 +1994,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[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" @@ -1435,6 +2007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -1445,6 +2018,29 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[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 = "nonempty" version = "0.12.0" @@ -1469,6 +2065,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1478,6 +2085,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.37.3" @@ -1505,6 +2121,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "orion" version = "0.17.13" @@ -1581,16 +2212,102 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "petgraph" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ - "fixedbitset", + "fixedbitset 0.5.7", "indexmap 2.13.0", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "phf_shared" version = "0.11.3" @@ -1630,6 +2347,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "postcard" version = "1.1.3" @@ -1643,12 +2366,30 @@ dependencies = [ "serde", ] +[[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 = "precomputed-hash" version = "0.1.1" @@ -1673,7 +2414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1695,6 +2436,62 @@ dependencies = [ "cc", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 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 = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -1716,6 +2513,35 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "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", + "rand_core 0.9.5", +] + +[[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" @@ -1725,6 +2551,100 @@ 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 = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.11.0", + "compact_str", + "hashbrown 0.16.1", + "indoc", + "itertools", + "kasuari", + "lru", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.2", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width 0.2.2", +] + [[package]] name = "rcgen" version = "0.13.2" @@ -1759,7 +2679,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -1779,7 +2699,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1811,6 +2731,46 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -1844,6 +2804,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc-literal-escaper" version = "0.0.7" @@ -1865,7 +2831,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -1887,6 +2853,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -1902,9 +2880,37 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.9" @@ -1938,6 +2944,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.9.0" @@ -1968,6 +2983,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -2001,7 +3039,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2075,10 +3113,10 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2117,6 +3155,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2193,6 +3252,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.9" @@ -2211,12 +3276,44 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[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.117" @@ -2233,6 +3330,41 @@ 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.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "tempfile" @@ -2256,6 +3388,69 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.11.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset 0.4.2", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2282,7 +3477,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2293,7 +3488,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2313,7 +3508,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -2336,6 +3533,16 @@ dependencies = [ "time-core", ] +[[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" @@ -2376,7 +3583,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2465,11 +3672,14 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.11.0", "bytes", + "futures-util", "http", "http-body", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -2507,7 +3717,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2545,6 +3755,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typed-arena" version = "2.0.2" @@ -2557,6 +3773,12 @@ 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 = "unicode-ident" version = "1.0.24" @@ -2588,6 +3810,23 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.2.2", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -2628,6 +3867,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2640,6 +3897,7 @@ version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ + "atomic", "getrandom 0.4.2", "js-sys", "serde_core", @@ -2658,6 +3916,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -2668,6 +3935,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2705,6 +3981,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -2724,7 +4014,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2765,12 +4055,129 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap 2.13.0", "semver", ] +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +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-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2780,6 +4187,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -2801,7 +4214,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2812,7 +4225,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2821,6 +4234,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -2839,13 +4263,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2854,7 +4287,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2866,34 +4299,67 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2906,24 +4372,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2969,7 +4459,7 @@ dependencies = [ "heck", "indexmap 2.13.0", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2985,7 +4475,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2997,7 +4487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap 2.13.0", "log", "serde", @@ -3027,6 +4517,12 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "yasna" version = "0.5.2" @@ -3036,6 +4532,70 @@ dependencies = [ "time", ] +[[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.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" @@ -3053,7 +4613,40 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[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.117", ] [[package]] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c60af0f --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: install build test help + +HELP_WIDTH=12 + +help: + @echo "ForgeDB" + @echo " install Build and install binary to PATH" + @echo " build Compile all workspace crates" + @echo " test Run all unit and integration tests" + +install: + @cargo install --path crates/bin --force + +build: + @cargo build --workspace + +test: + @cargo test --workspace diff --git a/crates/bin/src/main.rs b/crates/bin/src/main.rs index b971e14..b7f5f15 100644 --- a/crates/bin/src/main.rs +++ b/crates/bin/src/main.rs @@ -36,6 +36,21 @@ enum Commands { /// Path to forgedb.toml config file. #[arg(long, default_value = "./forgedb.toml")] config: PathBuf, + + /// Instantly opens the TUI and connects to the running server. + #[arg(long, default_value = "true")] + with_tui: bool, + }, + + /// Start the Terminal Dashboard. + Tui { + /// Address to connect to + #[arg(long, default_value = "https://127.0.0.1:5826")] + url: String, + + /// PASETO token for authentication + #[arg(long)] + token: Option, }, } @@ -46,11 +61,12 @@ fn main() { let result = match cli.command { Commands::Init { data_dir, force } => cmd_init(data_dir, force), - Commands::Serve { config } => cmd_serve(config), + Commands::Serve { config, with_tui } => cmd_serve(config, with_tui), + Commands::Tui { url, token } => forge_cli::tui::run(url, token), }; if let Err(e) = result { - eprintln!("error: {e}"); + eprintln!("\x1b[1;31merror:\x1b[0m {e}"); std::process::exit(1); } } @@ -74,7 +90,7 @@ fn cmd_init(data_dir: PathBuf, force: bool) -> forge_types::Result<()> { }) } -fn cmd_serve(config_path: PathBuf) -> forge_types::Result<()> { +fn cmd_serve(config_path: PathBuf, with_tui: bool) -> forge_types::Result<()> { let toml_str = std::fs::read_to_string(&config_path).map_err(|e| { ForgeError::Config(format!( "failed to read config '{}': {e}", @@ -100,11 +116,19 @@ fn cmd_serve(config_path: PathBuf) -> forge_types::Result<()> { let engine = std::sync::Arc::new(engine); tracing::info!("database opened successfully"); - // We only need the public half for token verification. Keep the secret key - // safely out of memory unless we're actively issuing tokens. - let (_, public_key) = forge_auth::keys::load_keys(&config.data_dir)?; + // We only need the public half for token verification. + let (secret_key, public_key) = forge_auth::keys::load_keys(&config.data_dir)?; let public_key = std::sync::Arc::new(public_key); - tracing::info!("PASETO public key loaded for token verification"); + let secret_key = std::sync::Arc::new(secret_key); + tracing::info!("PASETO keys loaded successfully"); + + let admin_claims = forge_auth::TokenClaims::new("admin", 30 * 24 * 3600, Some("admin".into())); + let admin_token = forge_auth::issue_token(&admin_claims, &secret_key) + .map_err(|e| ForgeError::Auth(format!("failed to mint admin token: {e}")))?; + + println!( + "\n \x1b[1;36mDashboard Access Token (Valid 30 days):\x1b[0m \x1b[1;32m{admin_token}\x1b[0m\n" + ); // Load mandatory RLS Cedar policies. If missing, database refuses to start up. let policy_path = config.data_dir.join("policy.cedar"); @@ -126,7 +150,7 @@ fn cmd_serve(config_path: PathBuf) -> forge_types::Result<()> { let listener = forge_protocol::TlsListener::bind(config.bind_address, tls_config).await?; println!( - "ForgeDB v{} listening on {}", + "\x1b[1;32mForgeDB v{} listening on \x1b[1;36m{}\x1b[0m", env!("CARGO_PKG_VERSION"), config.bind_address ); @@ -142,11 +166,29 @@ fn cmd_serve(config_path: PathBuf) -> forge_types::Result<()> { engine: engine.clone(), writer, public_key: public_key.clone(), + secret_key: secret_key.clone(), policy_engine: policy_engine.clone(), cursor_signer, }; let app = forge_server::app(app_state); + if with_tui { + // Give the server a tiny bit to spin its sockets, then launch TUI in this thread. + let ip = config.bind_address.ip(); + let connect_url = if ip.is_unspecified() { + format!("https://127.0.0.1:{}", config.bind_address.port()) + } else { + format!("https://{}", config.bind_address) + }; + let token = admin_token.to_string(); + tokio::task::spawn_blocking(move || { + std::thread::sleep(std::time::Duration::from_millis(100)); + // ignore errors here, if TUI crashes just exit it + let _ = forge_cli::tui::run(connect_url, Some(token)); + std::process::exit(0); // If user exits TUI, kill process. + }); + } + loop { let (stream, _peer) = match listener.accept().await { Ok(res) => res, @@ -174,7 +216,7 @@ fn prompt_password(prompt: &str) -> forge_types::Result { return Ok(pw); } - eprint!("{prompt}"); + eprint!("\x1b[1;36m{prompt}\x1b[0m"); let mut input = String::new(); std::io::stdin() .read_line(&mut input) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 1fb59ea..52ac766 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -13,3 +13,8 @@ forge-storage = { workspace = true } forge-auth = { workspace = true } tracing = { workspace = true } toml = { workspace = true } +crossterm = "0.29.0" +ratatui = "0.30.0" +reqwest = { version = "0.13.2", features = ["json"] } +serde_json.workspace = true +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/cli/src/init.rs b/crates/cli/src/init.rs index 8b4e284..935c6b3 100644 --- a/crates/cli/src/init.rs +++ b/crates/cli/src/init.rs @@ -90,23 +90,38 @@ permit( tracing::info!(path = %config_path.display(), "wrote config file"); println!(); - println!(" ForgeDB initialized successfully!"); + println!(" \x1b[1;32mForgeDB initialized successfully!\x1b[0m"); println!(); - println!(" Data directory: {}", opts.data_dir.display()); - println!(" TLS certificate: {}", config.tls_cert_path.display()); - println!(" TLS private key: {}", config.tls_key_path.display()); println!( - " PASETO keys: {}", + " \x1b[1;36mData directory:\x1b[0m {}", + opts.data_dir.display() + ); + println!( + " \x1b[1;36mTLS certificate:\x1b[0m {}", + config.tls_cert_path.display() + ); + println!( + " \x1b[1;36mTLS private key:\x1b[0m {}", + config.tls_key_path.display() + ); + println!( + " \x1b[1;36mPASETO keys:\x1b[0m {}", opts.data_dir.join("paseto_*.key").display() ); - println!(" Database: {}", db_path.display()); - println!(" Policy: {}", policy_path.display()); - println!(" Config: {}", config_path.display()); + println!(" \x1b[1;36mDatabase:\x1b[0m {}", db_path.display()); + println!( + " \x1b[1;36mPolicy:\x1b[0m {}", + policy_path.display() + ); + println!( + " \x1b[1;36mConfig:\x1b[0m {}", + config_path.display() + ); println!(); - println!(" ⚠ The generated certificate is self-signed (dev only)."); - println!(" Replace it with a CA-signed cert for production."); + println!("\x1b[1;33m The generated certificate is self-signed (dev only).\x1b[0m"); + println!("\x1b[1;33m Replace it with a CA-signed cert for production.\x1b[0m"); println!(); - println!(" Next: run `forgedb serve` to start the server."); + println!(" \x1b[1mNext:\x1b[0m run \x1b[1;35m`forgedb serve`\x1b[0m to start the server."); println!(); Ok(()) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index f768053..99a8b71 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -3,5 +3,6 @@ //! The binary in `crates/bin` dispatches to functions here. pub mod init; +pub mod tui; pub use init::run_init; diff --git a/crates/cli/src/tui.rs b/crates/cli/src/tui.rs new file mode 100644 index 0000000..05895f4 --- /dev/null +++ b/crates/cli/src/tui.rs @@ -0,0 +1,1065 @@ +//! # ForgeDB TUI +//! +//! This module implements the interactive terminal interface for ForgeDB. +//! It's built on top of `ratatui` because, let's face it, sometimes you just +//! want to poke around your data without leaving the shell. It handles +//! everything from initial setup and login to document CRUD operations. +//! +//! We're using a fairly standard state-machine approach here with `AppScreen` +//! driving the layout. It's not the fanciest concurrent UI design, but it's +//! robust and easy to reason about when things go sideways. + +use std::io; + +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use ratatui::{ + Frame, Terminal, + backend::{Backend, CrosstermBackend}, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, +}; +use reqwest::Client; +use serde_json::Value; + +/// Current navigation focus in the dashboard +#[derive(PartialEq)] +enum Focus { + Collections, + Documents, +} + +/// The overall screen state of the app +#[derive(PartialEq)] +enum AppScreen { + Initializing, + Setup, + Login, + Dashboard, + Editor, + NewCollection, +} + +struct AuthForm { + token: String, + password: String, + confirm: String, + focused: usize, +} + +struct EditorState { + buffer: String, + cursor_pos: usize, + collection: String, + doc_id: Option, // None if new +} + +struct App { + url: String, + client: Client, + + screen: AppScreen, + status_msg: String, + + // Form data + auth_form: AuthForm, + bearer_token: Option, + + // Dashboard Data + focus: Focus, + collections: Vec, + collections_state: ListState, + + // Document List Data + docs: Vec, + docs_state: ListState, + + // Editor State + editor: Option, + + // New Collection Name + new_col_name: String, + + // New Collection Tracking + last_action_collection: Option, + is_deleting: bool, +} + +impl App { + fn new(url: String, token: Option) -> App { + let client = Client::builder() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + App { + url, + client, + screen: AppScreen::Initializing, + status_msg: "Press 'r' to initialize connection...".into(), + + auth_form: AuthForm { + token: token.unwrap_or_default(), + password: String::new(), + confirm: String::new(), + focused: 0, + }, + bearer_token: None, + + focus: Focus::Collections, + collections: vec![], + collections_state: ListState::default(), + docs: vec![], + docs_state: ListState::default(), + + editor: None, + new_col_name: String::new(), + last_action_collection: None, + is_deleting: false, + } + } + + async fn check_auth_status(&mut self) { + self.status_msg = format!("Checking server at {}...", self.url); + match self + .client + .get(format!("{}/_/auth/status", self.url)) + .send() + .await + { + Ok(resp) => { + if resp.status().is_success() { + let json: Value = resp.json().await.unwrap_or_default(); + let setup_req = json + .get("setup_required") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if setup_req { + self.screen = AppScreen::Setup; + self.status_msg = "Admin setup required. Please enter details.".to_string(); + if !self.auth_form.token.is_empty() { + self.auth_form.focused = 1; + } + } else { + self.screen = AppScreen::Login; + self.status_msg = "Please log in.".to_string(); + self.auth_form.focused = 1; + } + } else { + self.status_msg = format!("Server error: {}", resp.status()); + } + } + Err(e) => self.status_msg = format!("Connection failed: {}", e), + } + } + + async fn submit_setup(&mut self) { + if self.auth_form.password != self.auth_form.confirm { + self.status_msg = "Passwords do not match!".to_string(); + return; + } + let body = serde_json::json!({ "token": self.auth_form.token, "password": self.auth_form.password }); + match self + .client + .post(format!("{}/_/auth/setup", self.url)) + .json(&body) + .send() + .await + { + Ok(resp) => { + if resp.status().is_success() { + self.status_msg = "Setup successful! Proceed to Login...".to_string(); + self.screen = AppScreen::Login; + self.auth_form.password.clear(); + self.auth_form.confirm.clear(); + self.auth_form.focused = 1; + } else { + self.status_msg = format!("Setup failed: {}", resp.status()); + } + } + Err(e) => self.status_msg = format!("Request failed: {}", e), + } + } + + async fn submit_login(&mut self) { + let body = serde_json::json!({ "password": self.auth_form.password }); + match self + .client + .post(format!("{}/_/auth/login", self.url)) + .json(&body) + .send() + .await + { + Ok(resp) => { + if resp.status().is_success() { + let json: Value = resp.json().await.unwrap_or_default(); + if let Some(t) = json.get("token").and_then(|v| v.as_str()) { + self.bearer_token = Some(t.to_string()); + self.screen = AppScreen::Dashboard; + self.status_msg = "Logged in successfully!".to_string(); + self.fetch_schema().await; + } + } else { + self.status_msg = "Login failed: Unauthorized.".to_string(); + } + } + Err(e) => self.status_msg = format!("Request failed: {}", e), + } + } + + async fn fetch_schema(&mut self) { + let req = self.client.get(format!("{}/_/schema", self.url)); + let req = if let Some(t) = &self.bearer_token { + req.bearer_auth(t) + } else { + req + }; + match req.send().await { + Ok(resp) => { + if resp.status().is_success() { + let json: Value = resp.json().await.unwrap_or_default(); + self.collections.clear(); + if let Some(colls) = json.get("collections").and_then(|c| c.as_array()) { + for c in colls { + if let Some(name) = c.get("name").and_then(|n| n.as_str()) { + self.collections.push(name.to_string()); + } + } + } + if !self.collections.is_empty() { + if let Some(target) = &self.last_action_collection { + if let Some(idx) = self.collections.iter().position(|c| c == target) { + self.collections_state.select(Some(idx)); + } + self.last_action_collection = None; + } else if self.collections_state.selected().is_none() { + self.collections_state.select(Some(0)); + } + self.fetch_collection().await; + } + } + } + Err(_) => { + self.status_msg = "Failed to fetch schema.".into(); + } + } + } + + async fn fetch_collection(&mut self) { + if let Some(i) = self.collections_state.selected() { + let col = &self.collections[i]; + let req = self.client.get(format!("{}/v1/{}?limit=50", self.url, col)); + let req = if let Some(t) = &self.bearer_token { + req.bearer_auth(t) + } else { + req + }; + match req.send().await { + Ok(resp) => { + let json: Value = resp.json().await.unwrap_or_default(); + if let Some(data) = json.get("data").and_then(|d| d.as_array()) { + self.docs = data.clone(); + // Reset doc selection if we switched collections + if self.docs_state.selected().is_none() + || self.docs_state.selected().unwrap() >= self.docs.len() + { + self.docs_state.select(if !self.docs.is_empty() { + Some(0) + } else { + None + }); + } + self.status_msg = + format!("Loaded {} records from {}", self.docs.len(), col); + } + } + Err(_) => { + self.status_msg = "Failed to fetch collection data.".into(); + } + } + } + } + + async fn delete_document(&mut self) { + if let (Some(col_idx), Some(doc_idx)) = ( + self.collections_state.selected(), + self.docs_state.selected(), + ) { + let col = &self.collections[col_idx]; + let doc = &self.docs[doc_idx]; + if let Some(id) = doc.get("id").and_then(|v| v.as_str()) { + let req = self + .client + .delete(format!("{}/v1/{}/{}", self.url, col, id)); + let req = if let Some(t) = &self.bearer_token { + req.bearer_auth(t) + } else { + req + }; + if let Ok(resp) = req.send().await { + if resp.status().is_success() { + self.status_msg = "Document deleted.".into(); + self.fetch_collection().await; + } else { + self.status_msg = format!("Delete failed: {}", resp.status()); + } + } + } + } + self.is_deleting = false; + } + + async fn save_editor(&mut self) { + if let Some(editor) = &self.editor { + let payload: Value = match serde_json::from_str(&editor.buffer) { + Ok(v) => v, + Err(e) => { + self.status_msg = format!("Invalid JSON: {}", e); + return; + } + }; + + let res = if let Some(id) = &editor.doc_id { + // PATCH + let req = self + .client + .patch(format!("{}/v1/{}/{}", self.url, editor.collection, id)); + let req = if let Some(t) = &self.bearer_token { + req.bearer_auth(t) + } else { + req + }; + req.json(&payload).send().await + } else { + // POST + let req = self + .client + .post(format!("{}/v1/{}", self.url, editor.collection)); + let req = if let Some(t) = &self.bearer_token { + req.bearer_auth(t) + } else { + req + }; + req.json(&payload).send().await + }; + + match res { + Ok(resp) => { + if resp.status().is_success() { + self.status_msg = "Saved successfully.".into(); + self.last_action_collection = Some(editor.collection.clone()); + self.screen = AppScreen::Dashboard; + self.fetch_schema().await; + } else { + self.status_msg = format!("Save failed: {}", resp.status()); + } + } + Err(e) => self.status_msg = format!("Request failed: {}", e), + } + } + } + + fn handle_input(&mut self, code: KeyCode) { + match self.screen { + AppScreen::Setup => self.handle_setup_input(code), + AppScreen::Login => self.handle_login_input(code), + AppScreen::Editor => self.handle_editor_input(code), + AppScreen::NewCollection => self.handle_new_col_input(code), + _ => {} + } + } + + fn handle_setup_input(&mut self, code: KeyCode) { + match code { + KeyCode::Tab => self.auth_form.focused = (self.auth_form.focused + 1) % 3, + KeyCode::BackTab => self.auth_form.focused = (self.auth_form.focused + 2) % 3, + KeyCode::Backspace => match self.auth_form.focused { + 0 => { + self.auth_form.token.pop(); + } + 1 => { + self.auth_form.password.pop(); + } + 2 => { + self.auth_form.confirm.pop(); + } + _ => {} + }, + KeyCode::Char(c) => match self.auth_form.focused { + 0 => self.auth_form.token.push(c), + 1 => self.auth_form.password.push(c), + 2 => self.auth_form.confirm.push(c), + _ => {} + }, + _ => {} + } + } + + fn handle_login_input(&mut self, code: KeyCode) { + match code { + KeyCode::Backspace => { + self.auth_form.password.pop(); + } + KeyCode::Char(c) => { + self.auth_form.password.push(c); + } + _ => {} + } + } + + fn handle_editor_input(&mut self, code: KeyCode) { + if let Some(editor) = &mut self.editor { + match code { + KeyCode::Char(c) => { + editor.buffer.insert(editor.cursor_pos, c); + editor.cursor_pos += 1; + } + KeyCode::Backspace => { + if editor.cursor_pos > 0 { + editor.buffer.remove(editor.cursor_pos - 1); + editor.cursor_pos -= 1; + } + } + KeyCode::Left => { + if editor.cursor_pos > 0 { + editor.cursor_pos -= 1; + } + } + KeyCode::Right => { + if editor.cursor_pos < editor.buffer.len() { + editor.cursor_pos += 1; + } + } + KeyCode::Enter => { + editor.buffer.insert(editor.cursor_pos, '\n'); + editor.cursor_pos += 1; + } + _ => {} + } + } + } + + fn handle_new_col_input(&mut self, code: KeyCode) { + match code { + KeyCode::Char(c) => self.new_col_name.push(c), + KeyCode::Backspace => { + self.new_col_name.pop(); + } + _ => {} + } + } +} +/// Entry point for the ForgeDB TUI application. +/// +/// This function kicks off the terminal loop, handles raw mode switching, +/// and manages the overall lifecycle. If something blows up, we try to +/// restore the terminal state before exiting so the user isn't left with +/// a borked shell. +/// +/// # Examples +/// +/// ```rust +/// use forge_cli::tui; +/// +/// // Assuming the server is up at localhost:5826 +/// # fn main() -> Result<(), Box> { +/// tui::run("https://localhost:5826".to_string(), None)?; +/// # Ok(()) +/// # } +/// ``` +/// +/// # Errors +/// +/// Returns a `forge_types::Result` if terminal initialization or restoration fails. +pub fn run(url: String, token: Option) -> forge_types::Result<()> { + let mut terminal = setup_terminal()?; + let mut app = App::new(url, token); + let rt = tokio::runtime::Runtime::new().unwrap(); + + rt.block_on(async { + app.check_auth_status().await; + if let Err(e) = run_app(&mut terminal, &mut app).await { + eprintln!("TUI Error: {:?}", e); + } + }); + + restore_terminal(&mut terminal)?; + Ok(()) +} + +fn setup_terminal() -> forge_types::Result>> { + enable_raw_mode().unwrap(); + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + let backend = CrosstermBackend::new(stdout); + Terminal::new(backend).map_err(|e| forge_types::ForgeError::Config(e.to_string())) +} + +fn restore_terminal( + terminal: &mut Terminal>, +) -> forge_types::Result<()> { + disable_raw_mode().unwrap(); + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture, + ) + .unwrap(); + terminal.show_cursor().unwrap(); + Ok(()) +} + +async fn run_app(terminal: &mut Terminal, app: &mut App) -> io::Result<()> { + loop { + terminal + .draw(|f| ui(f, app)) + .map_err(|e| io::Error::other(e.to_string()))?; + + if let Ok(true) = crossterm::event::poll(std::time::Duration::from_millis(100)) + && let Event::Key(key) = event::read()? + { + if key.code == KeyCode::Esc { + if app.is_deleting { + app.is_deleting = false; + continue; + } + if app.screen == AppScreen::Dashboard { + return Ok(()); + } + app.screen = AppScreen::Dashboard; + continue; + } + if key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('c') { + return Ok(()); + } + + match app.screen { + AppScreen::Initializing => { + if key.code == KeyCode::Char('r') { + app.check_auth_status().await; + } + } + AppScreen::Setup => { + if key.code == KeyCode::Enter { + app.submit_setup().await; + } else { + app.handle_input(key.code); + } + } + AppScreen::Login => { + if key.code == KeyCode::Enter { + app.submit_login().await; + } else { + app.handle_input(key.code); + } + } + AppScreen::Dashboard => { + if app.is_deleting { + if key.code == KeyCode::Char('y') { + app.delete_document().await; + } else { + app.is_deleting = false; + } + continue; + } + match key.code { + KeyCode::Tab => { + app.focus = if app.focus == Focus::Collections { + Focus::Documents + } else { + Focus::Collections + }; + } + KeyCode::Down | KeyCode::Char('j') => { + if app.focus == Focus::Collections { + let len = app.collections.len(); + if len > 0 { + let i = match app.collections_state.selected() { + Some(i) => { + if i >= len - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + app.collections_state.select(Some(i)); + app.fetch_collection().await; + } + } else { + let len = app.docs.len(); + if len > 0 { + let i = match app.docs_state.selected() { + Some(i) => { + if i >= len - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + app.docs_state.select(Some(i)); + } + } + } + KeyCode::Up | KeyCode::Char('k') => { + if app.focus == Focus::Collections { + let len = app.collections.len(); + if len > 0 { + let i = match app.collections_state.selected() { + Some(i) => { + if i == 0 { + len - 1 + } else { + i - 1 + } + } + None => 0, + }; + app.collections_state.select(Some(i)); + app.fetch_collection().await; + } + } else { + let len = app.docs.len(); + if len > 0 { + let i = match app.docs_state.selected() { + Some(i) => { + if i == 0 { + len - 1 + } else { + i - 1 + } + } + None => 0, + }; + app.docs_state.select(Some(i)); + } + } + } + KeyCode::Char('n') => { + app.new_col_name.clear(); + app.screen = AppScreen::NewCollection; + } + KeyCode::Char('e') => { + // Gotta make sure we actually have a document and a collection selected. + // It's a bit defensive, sure, but I've seen too many TUIs blow up because + // they assumed state that wasn't there. Trust but verify. + if let (Some(i), Some(col_idx)) = + (app.docs_state.selected(), app.collections_state.selected()) + && let Some(doc) = app.docs.get(i) + { + let id = doc + .get("id") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let col = app.collections[col_idx].clone(); + let buf = serde_json::to_string_pretty(doc).unwrap_or_default(); + + app.editor = Some(EditorState { + buffer: buf, + cursor_pos: 0, + collection: col, + doc_id: id, + }); + app.screen = AppScreen::Editor; + } + } + KeyCode::Char('a') => { + if let Some(i) = app.collections_state.selected() { + let col = app.collections[i].clone(); + app.editor = Some(EditorState { + buffer: "{\n \"name\": \"new item\"\n}".into(), + cursor_pos: 15, + collection: col, + doc_id: None, + }); + app.screen = AppScreen::Editor; + } + } + KeyCode::Char('d') => { + if app.docs_state.selected().is_some() { + app.is_deleting = true; + } + } + KeyCode::Char('r') => { + app.fetch_schema().await; + } + _ => {} + } + } + AppScreen::Editor => { + if key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('s') { + app.save_editor().await; + } else { + app.handle_input(key.code); + } + } + AppScreen::NewCollection => { + if key.code == KeyCode::Enter && !app.new_col_name.is_empty() { + let col = app.new_col_name.clone(); + app.editor = Some(EditorState { + buffer: "{\n \"init\": true\n}".into(), + cursor_pos: 15, + collection: col, + doc_id: None, + }); + app.screen = AppScreen::Editor; + } else { + app.handle_input(key.code); + } + } + } + } + } +} + +fn ui(f: &mut Frame, app: &mut App) { + let size = f.area(); + f.render_widget( + Block::default().style(Style::default().bg(Color::White).fg(Color::Black)), + size, + ); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(1), + ]) + .split(size); + + draw_header(f, app, chunks[0]); + match app.screen { + AppScreen::Initializing => draw_initializing(f, app, chunks[1]), + AppScreen::Setup => draw_auth_form(f, app, chunks[1], true), + AppScreen::Login => draw_auth_form(f, app, chunks[1], false), + AppScreen::Dashboard => draw_dashboard(f, app, chunks[1]), + AppScreen::Editor => draw_editor(f, app, chunks[1]), + AppScreen::NewCollection => draw_new_col_dialog(f, app, chunks[1]), + } + draw_footer(f, app, chunks[2]); + + if app.is_deleting { + draw_delete_confirmation(f, chunks[1]); + } +} + +fn draw_header(f: &mut Frame, _app: &mut App, area: Rect) { + let text = Line::from(vec![ + Span::styled( + " Forge", + Style::default() + .fg(Color::Rgb(15, 23, 42)) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + "DB", + Style::default() + .fg(Color::Rgb(2, 132, 199)) + .add_modifier(Modifier::BOLD), + ), + Span::styled(" Terminal", Style::default().fg(Color::Rgb(100, 116, 139))), + ]); + f.render_widget( + Paragraph::new(text).block( + Block::default() + .borders(Borders::BOTTOM) + .border_style(Style::default().fg(Color::Rgb(226, 232, 240))), + ), + area, + ); +} + +fn draw_footer(f: &mut Frame, app: &mut App, area: Rect) { + let help = match app.screen { + AppScreen::Dashboard => { + " [N] New Col [A] Add [E] Edit [D] Del [R] Sync [TAB] Swap Focus [Esc] Quit " + } + AppScreen::Editor => " [Ctrl+S] Store [Esc] Back Type to edit JSON directly ", + AppScreen::NewCollection => " [Enter] Confirm [Esc] Cancel ", + _ => " [Esc] Exit ", + }; + let footer = Paragraph::new(Line::from(vec![ + Span::styled( + format!(" STATUS: {} ", app.status_msg), + Style::default() + .fg(Color::Rgb(15, 23, 42)) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + format!(" {}", help), + Style::default().fg(Color::Rgb(148, 163, 184)), + ), + ])); + f.render_widget(footer, area); +} + +fn draw_initializing(f: &mut Frame, app: &mut App, area: Rect) { + f.render_widget( + Paragraph::new(app.status_msg.clone()).alignment(Alignment::Center), + area, + ); +} + +fn draw_auth_form(f: &mut Frame, app: &mut App, area: Rect, is_setup: bool) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(12), Constraint::Min(0)]) + .split(center_rect(60, 40, area)); + + let block = Block::default() + .title(if is_setup { + " ADMIN SETUP " + } else { + " ADMIN LOGIN " + }) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Rgb(203, 213, 225))) + .style(Style::default().bg(Color::White).fg(Color::Black)); + + f.render_widget(Clear, chunks[0]); + f.render_widget(block.clone(), chunks[0]); + + let inputs = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(2), + Constraint::Length(2), + Constraint::Length(2), + Constraint::Min(0), + ]) + .split(block.inner(chunks[0])); + + let active = Style::default() + .fg(Color::Rgb(2, 132, 199)) + .add_modifier(Modifier::BOLD); + let inactive = Style::default().fg(Color::Rgb(148, 163, 184)); + + if is_setup { + f.render_widget( + Paragraph::new(format!("PASETO: {}", app.auth_form.token)).style( + if app.auth_form.focused == 0 { + active + } else { + inactive + }, + ), + inputs[0], + ); + } + f.render_widget( + Paragraph::new(format!( + "Password: {}", + "*".repeat(app.auth_form.password.len()) + )) + .style(if app.auth_form.focused == 1 { + active + } else { + inactive + }), + inputs[1], + ); + if is_setup { + f.render_widget( + Paragraph::new(format!( + "Confirm: {}", + "*".repeat(app.auth_form.confirm.len()) + )) + .style(if app.auth_form.focused == 2 { + active + } else { + inactive + }), + inputs[2], + ); + } +} + +fn draw_dashboard(f: &mut Frame, app: &mut App, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(15), // Tables + Constraint::Percentage(85), // Records + Preview + ]) + .split(area); + + let active_style = Style::default() + .fg(Color::Rgb(2, 132, 199)) + .add_modifier(Modifier::BOLD); + let inactive_style = Style::default().fg(Color::Rgb(226, 232, 240)); + + let items: Vec = app + .collections + .iter() + .map(|c| { + ListItem::new(format!(" {} ", c)).style(Style::default().fg(Color::Rgb(71, 85, 105))) + }) + .collect(); + + f.render_stateful_widget( + List::new(items) + .block( + Block::default() + .title(" TABLES ") + .borders(Borders::ALL) + .border_style(if app.focus == Focus::Collections { + active_style + } else { + inactive_style + }), + ) + .highlight_style( + Style::default() + .fg(Color::Rgb(2, 132, 199)) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(" "), + chunks[0], + &mut app.collections_state, + ); + + // Records & Preview split + let has_selection = app.docs_state.selected().is_some(); + let data_parts = Layout::default() + .direction(Direction::Horizontal) + .constraints(if has_selection { + [Constraint::Percentage(40), Constraint::Percentage(60)] + } else { + [Constraint::Percentage(100), Constraint::Min(0)] + }) + .split(chunks[1]); + + let doc_items: Vec = app + .docs + .iter() + .map(|d| { + let id = d.get("id").and_then(|v| v.as_str()).unwrap_or("?"); + ListItem::new(format!(" {} ", id)).style(Style::default().fg(Color::Rgb(30, 41, 59))) + }) + .collect(); + + f.render_stateful_widget( + List::new(doc_items) + .block( + Block::default() + .title(" RECORDS ") + .borders(Borders::ALL) + .border_style(if app.focus == Focus::Documents { + active_style + } else { + inactive_style + }), + ) + .highlight_style( + Style::default() + .fg(Color::Rgb(2, 132, 199)) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(" "), + data_parts[0], + &mut app.docs_state, + ); + + // Preview + if let Some(doc) = app.docs_state.selected().and_then(|i| app.docs.get(i)) { + f.render_widget( + Paragraph::new(serde_json::to_string_pretty(doc).unwrap_or_default()) + .block( + Block::default() + .title(" PREVIEW ") + .borders(Borders::ALL) + .border_style(inactive_style), + ) + .style(Style::default().fg(Color::Rgb(71, 85, 105))) + .wrap(Wrap { trim: false }), + data_parts[1], + ); + } +} + +fn draw_editor(f: &mut Frame, app: &mut App, area: Rect) { + if let Some(editor) = &app.editor { + let title = match &editor.doc_id { + Some(id) => format!(" UPDATING {} / {} ", editor.collection, id), + None => format!(" CREATING IN {} ", editor.collection), + }; + f.render_widget(Clear, area); + f.render_widget( + Paragraph::new(editor.buffer.as_str()) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Rgb(2, 132, 199))) + .style(Style::default().bg(Color::White).fg(Color::Black)), + ) + .style(Style::default().bg(Color::White).fg(Color::Black)), + area, + ); + } +} + +fn draw_new_col_dialog(f: &mut Frame, app: &mut App, area: Rect) { + let popup = center_rect(60, 20, area); + f.render_widget(Clear, popup); + f.render_widget( + Paragraph::new(format!("Name: {}", app.new_col_name)) + .block( + Block::default() + .title(" NEW COLLECTION ") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Rgb(2, 132, 199))) + .style(Style::default().bg(Color::White).fg(Color::Black)), + ) + .style(Style::default().bg(Color::White).fg(Color::Black)) + .alignment(Alignment::Center), + popup, + ); +} + +fn draw_delete_confirmation(f: &mut Frame, area: Rect) { + let popup = center_rect(40, 10, area); + f.render_widget(Clear, popup); + f.render_widget( + Paragraph::new("DELETE THIS RECORD?\n[Y] Yes [Esc] No") + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)) + .style(Style::default().bg(Color::White).fg(Color::Red)), + ) + .style(Style::default().fg(Color::Red).bg(Color::White)) + .alignment(Alignment::Center), + popup, + ); +} + +fn center_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 1da244b..790e77a 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -26,6 +26,9 @@ tokio-rustls = { workspace = true } tower-http = { workspace = true } json-patch = "4.1.0" redbx.workspace = true +serde.workspace = true +aws-lc-rs.workspace = true +hex.workspace = true [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/server/src/auth_api.rs b/crates/server/src/auth_api.rs new file mode 100644 index 0000000..c5301a4 --- /dev/null +++ b/crates/server/src/auth_api.rs @@ -0,0 +1,96 @@ +use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; +use serde::{Deserialize, Serialize}; + +use crate::AppState; + +#[derive(Serialize)] +pub struct AuthStatus { + pub setup_required: bool, +} + +pub async fn auth_status(State(state): State) -> impl IntoResponse { + let setup_required = state + .engine + .get("_system", "admin_auth") + .map(|o| o.is_none()) + .unwrap_or(true); + (StatusCode::OK, Json(AuthStatus { setup_required })) +} + +#[derive(Deserialize)] +pub struct SetupReq { + pub token: String, + pub password: String, +} + +pub async fn setup( + State(state): State, + Json(payload): Json, +) -> Result { + // verify token + forge_auth::validate_token(&payload.token, &state.public_key) + .map_err(|_| StatusCode::UNAUTHORIZED)?; + + let setup_required = state + .engine + .get("_system", "admin_auth") + .map(|o| o.is_none()) + .unwrap_or(true); + if !setup_required { + return Err(StatusCode::BAD_REQUEST); + } + + let hash = hex::encode(aws_lc_rs::digest::digest( + &aws_lc_rs::digest::SHA256, + payload.password.as_bytes(), + )); + let doc = serde_json::json!({ "hash": hash }); + let bytes = rmp_serde::to_vec_named(&doc).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + state + .engine + .insert("_system", "admin_auth", &bytes) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(StatusCode::OK) +} + +#[derive(Deserialize)] +pub struct LoginReq { + pub password: String, +} + +#[derive(Serialize)] +pub struct LoginRes { + pub token: String, +} + +pub async fn login( + State(state): State, + Json(payload): Json, +) -> Result { + let doc_bytes = state + .engine + .get("_system", "admin_auth") + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::UNAUTHORIZED)?; + + // Fallback to json decoding + let doc: serde_json::Value = forge_storage::document::deserialize_doc(&doc_bytes) + .unwrap_or_else(|_| serde_json::json!({})); + let stored_hash = doc.get("hash").and_then(|h| h.as_str()).unwrap_or(""); + let attempt_hash = hex::encode(aws_lc_rs::digest::digest( + &aws_lc_rs::digest::SHA256, + payload.password.as_bytes(), + )); + + if stored_hash != attempt_hash || stored_hash.is_empty() { + return Err(StatusCode::UNAUTHORIZED); + } + + let claims = forge_auth::TokenClaims::new("admin", 30 * 24 * 3600, Some("admin".into())); + let token = forge_auth::issue_token(&claims, &state.secret_key) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok((StatusCode::OK, Json(LoginRes { token }))) +} diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 807d0b4..97462ef 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -13,11 +13,12 @@ use forge_query::policy::PolicyEngine; use forge_security::CursorSigner; use forge_storage::WriteSender; -use pasetors::keys::AsymmetricPublicKey; +use pasetors::keys::{AsymmetricPublicKey, AsymmetricSecretKey}; use pasetors::version4::V4; use std::sync::Arc; pub mod audit; +pub mod auth_api; pub mod policy; use axum::{ @@ -67,18 +68,22 @@ pub struct AppState { pub engine: Arc, pub writer: WriteSender, pub public_key: Arc>, + pub secret_key: Arc>, pub policy_engine: Arc, pub cursor_signer: Arc, } /// Builds the master Axum router containing all ForgeDB v1 endpoints. pub fn app(state: AppState) -> Router { - // Unauthenticated routes — only the health check is truly public. - let public_routes = Router::new().route("/_/health", get(health)); - - // Authenticated API routes — full middleware pipeline - let api_routes = Router::new() - // Metadata moved here: requiring auth to prevent reconnaissance. + // Unauthenticated public routes + let public = Router::new() + .route("/_/health", get(health)) + .route("/_/auth/status", get(auth_api::auth_status)) + .route("/_/auth/setup", axum::routing::post(auth_api::setup)) + .route("/_/auth/login", axum::routing::post(auth_api::login)); + + // Authenticated API routes + let api = Router::new() .route("/_/schema", get(schema_info)) .route("/v1/{collection}", get(list_docs).post(insert_doc)) .route( @@ -94,27 +99,23 @@ pub fn app(state: AppState) -> Router { axum::routing::delete(drop_index), ) .route("/v1/_query", axum::routing::post(query_docs)) - // Everything inside /v1 requires a valid PASETO token. - // The middleware parses the Bearer header and rejects bad tokens fast. - .route_layer(axum::middleware::from_fn_with_state( + .layer(axum::middleware::from_fn_with_state( state.clone(), policy::require_policy, )) - // Audit intercepts right after Auth. It yields to Policy and then logs the outcome - // (Permit if 200 OK, Deny if Policy kicked it out). - .route_layer(axum::middleware::from_fn_with_state( + .layer(axum::middleware::from_fn_with_state( state.clone(), audit::audit_logger, )) - .route_layer(axum::middleware::from_fn_with_state( + .layer(axum::middleware::from_fn_with_state( state.public_key.clone(), forge_auth::middleware::require_auth, )); - public_routes - .merge(api_routes) + Router::new() + .merge(public) + .merge(api) .layer(TraceLayer::new_for_http()) - // Enforce a strict 5MB limit to prevent memory exhaustion attacks. .layer(axum::extract::DefaultBodyLimit::max(5 * 1024 * 1024)) .with_state(state) } @@ -229,25 +230,12 @@ async fn list_docs( if state.policy_engine.check_permit(&auth_ctx).is_ok() { valid_docs.push((id.clone(), bytes)); - // VULNERABILITY FIX: ONLY update last_scanned_id if the user is permitted - // to see this record, OR if it's the very last record of a full scan. - // However, to keep pagination strictly correct and not skip data, - // we update it here for permitted records. last_scanned_id = Some(id); if valid_docs.len() == limit { break; } } else { - // If denied, we still need to track that we scanned it so the NEXT - // cursor doesn't re-scan it, but we MUST NOT return this ID as a cursor - // in a way that implies it exists if the user can't see it? - // Actually, for keyset pagination, if we scanned it and denied it, - // the cursor should ideally still move past it. - // SECURITY TRADE-OFF: We use the ID as a cursor to avoid O(N) re-scans, - // but we recognize this leaks the existence of IDs. - // We minimize this by only returning the cursor if we have more data or - // met the limit. last_scanned_id = Some(id); } } diff --git a/crates/server/tests/api_pipeline.rs b/crates/server/tests/api_pipeline.rs index 2f0bc90..7a46cec 100644 --- a/crates/server/tests/api_pipeline.rs +++ b/crates/server/tests/api_pipeline.rs @@ -31,7 +31,8 @@ fn test_harness() -> (axum::Router, String, TempDir) { let writer = spawn_writer(engine.clone()); let kp = generate_keypair().expect("keypair gen"); - let public_key = Arc::new(kp.public); + let public_key = Arc::new(kp.public.clone()); + let secret_key = Arc::new(kp.secret.clone()); // Blanket permit — allows everything, which is what we want for happy-path tests. let policy = PolicyEngine::new("permit(principal, action, resource);").expect("policy parse"); @@ -41,6 +42,7 @@ fn test_harness() -> (axum::Router, String, TempDir) { engine: engine.clone(), writer, public_key, + secret_key, policy_engine, cursor_signer: std::sync::Arc::new(forge_security::CursorSigner::new(&[0u8; 32])), }; @@ -63,6 +65,7 @@ fn deny_harness() -> (axum::Router, String, TempDir) { let kp = generate_keypair().expect("keypair gen"); let public_key = Arc::new(kp.public); + let secret_key = Arc::new(kp.secret.clone()); // Empty policy set → deny by default. No permits, no access. let policy = PolicyEngine::new("").expect("empty policy"); @@ -72,6 +75,7 @@ fn deny_harness() -> (axum::Router, String, TempDir) { engine: engine.clone(), writer, public_key, + secret_key, policy_engine, cursor_signer: std::sync::Arc::new(forge_security::CursorSigner::new(&[0u8; 32])), }; diff --git a/crates/storage/src/engine.rs b/crates/storage/src/engine.rs index 6e54bf5..40c9b5d 100644 --- a/crates/storage/src/engine.rs +++ b/crates/storage/src/engine.rs @@ -118,11 +118,11 @@ impl StorageEngine { let registry_def = TableDefinition::<&str, &[u8]>::new(crate::index::INDEX_REGISTRY_TABLE); if let Ok(reg_table) = txn.open_table(registry_def) && let Ok(Some(bytes)) = reg_table.get(collection) - && let Ok(registry) = - crate::deserialize_doc::(bytes.value()) - { - return registry.fields; - } + && let Ok(registry) = + crate::deserialize_doc::(bytes.value()) + { + return registry.fields; + } Vec::new() } @@ -146,10 +146,11 @@ impl StorageEngine { let idx_def = TableDefinition::<&[u8], &[u8]>::new(&idx_table_name); if let Ok(mut idx_table) = txn.open_table(idx_def) { if let Some(old) = &old_doc - && let Ok(Some(old_val)) = crate::extract::extract_field_raw(old, field) { - let key = crate::index::format_index_key(old_val, id); - let _ = idx_table.remove(key.as_slice()); - } + && let Ok(Some(old_val)) = crate::extract::extract_field_raw(old, field) + { + let key = crate::index::format_index_key(old_val, id); + let _ = idx_table.remove(key.as_slice()); + } if let Ok(Some(new_val)) = crate::extract::extract_field_raw(doc, field) { let key = crate::index::format_index_key(new_val, id); let _ = idx_table.insert(key.as_slice(), &[] as &[u8]); @@ -208,10 +209,11 @@ impl StorageEngine { let idx_table_name = crate::index::index_table_name(collection, field); let idx_def = TableDefinition::<&[u8], &[u8]>::new(&idx_table_name); if let Ok(mut idx_table) = txn.open_table(idx_def) - && let Ok(Some(old_val)) = crate::extract::extract_field_raw(&old, field) { - let key = crate::index::format_index_key(old_val, id); - let _ = idx_table.remove(key.as_slice()); - } + && let Ok(Some(old_val)) = crate::extract::extract_field_raw(&old, field) + { + let key = crate::index::format_index_key(old_val, id); + let _ = idx_table.remove(key.as_slice()); + } } } } @@ -268,10 +270,10 @@ impl StorageEngine { if let Ok(mut idx_table) = txn.open_table(idx_def) { if let Some(old) = &old_doc && let Ok(Some(old_val)) = crate::extract::extract_field_raw(old, field) - { - let key = crate::index::format_index_key(old_val, id); - let _ = idx_table.remove(key.as_slice()); - } + { + let key = crate::index::format_index_key(old_val, id); + let _ = idx_table.remove(key.as_slice()); + } if let Ok(Some(new_val)) = crate::extract::extract_field_raw(payload, field) { let key = crate::index::format_index_key(new_val, id); @@ -325,10 +327,10 @@ impl StorageEngine { if let Ok(mut idx_table) = txn.open_table(idx_def) && let Ok(Some(old_val)) = crate::extract::extract_field_raw(&old, field) - { - let key = crate::index::format_index_key(old_val, id.as_str()); - let _ = idx_table.remove(key.as_slice()); - } + { + let key = crate::index::format_index_key(old_val, id.as_str()); + let _ = idx_table.remove(key.as_slice()); + } } } }