diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
new file mode 100644
index 0000000..ff2db6a
--- /dev/null
+++ b/.github/workflows/rust.yml
@@ -0,0 +1,36 @@
+---
+name: Rust
+"on":
+ push:
+ pull_request:
+permissions:
+ contents: read
+jobs:
+ build-test:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@1.96.0
+ with:
+ components: clippy, rustfmt
+ - name: Cache cargo registry and target dir
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: Rust -> target
+ - name: Build
+ run: |
+ cd ./Rust
+ cargo build --workspace --all-targets
+ - name: Test
+ run: |
+ cd ./Rust
+ cargo test --workspace
+ - name: Clippy
+ run: |
+ cd ./Rust
+ cargo clippy --workspace --all-targets -- -D warnings
+ - name: Format check
+ run: |
+ cd ./Rust
+ cargo fmt --all -- --check
diff --git a/.github/workflows/sonatype-jack.yml b/.github/workflows/sonatype-jack.yml
index 9bc19ed..ac6a966 100644
--- a/.github/workflows/sonatype-jack.yml
+++ b/.github/workflows/sonatype-jack.yml
@@ -10,11 +10,11 @@ jobs:
# runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set up Python 3.10
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
- python-version: 3.10
+ python-version: "3.10"
- name: Run jake from Sonatype
run: |-
cd ./Python
diff --git a/README.md b/README.md
index 7f0d545..3a835a3 100644
--- a/README.md
+++ b/README.md
@@ -11,9 +11,11 @@ The SDK contains these parts:
3. [PHP code example](PHP/index.php)
4. [Python code examples](Python/examples)
5. [Go package and code](Go/ixapi/README.md)
-6. [Maltego Transform](Maltego%20Transform/README.md)
+6. [Rust crate and CLI](Rust/README.md)
+7. [Maltego Transform](Maltego%20Transform/README.md)
Latest updates:
+* 30.06.2026 - New Rust SDK and command-line client
* 30.11.2024 - 0.6.2 installable from [Python Package Index](https://pypi.org/project/intelx/)
* 06.06.2024 - Python supports "Export Leaked Accounts" of identity.intelx.io
* 02.02.2024 - Python Package moved to [Python Repository](https://github.com/IntelligenceX/Python)
diff --git a/Rust/.env.sample b/Rust/.env.sample
new file mode 100644
index 0000000..c4559dc
--- /dev/null
+++ b/Rust/.env.sample
@@ -0,0 +1,2 @@
+INTELX_KEY="00000000-0000-0000-0000-000000000000"
+INTELX_BASE_URL="https://2.intelx.io"
diff --git a/Rust/.gitignore b/Rust/.gitignore
new file mode 100644
index 0000000..fedaa2b
--- /dev/null
+++ b/Rust/.gitignore
@@ -0,0 +1,2 @@
+/target
+.env
diff --git a/Rust/Cargo.lock b/Rust/Cargo.lock
new file mode 100644
index 0000000..3091ba8
--- /dev/null
+++ b/Rust/Cargo.lock
@@ -0,0 +1,2277 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "assert_cmd"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "libc",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "async-compression"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
+dependencies = [
+ "compression-codecs",
+ "compression-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[package]]
+name = "bstr"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde_core",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+
+[[package]]
+name = "cc"
+version = "1.2.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "colored"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
+dependencies = [
+ "lazy_static",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "comfy-table"
+version = "7.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47"
+dependencies = [
+ "crossterm",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "compression-codecs"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
+dependencies = [
+ "compression-core",
+ "flate2",
+ "memchr",
+]
+
+[[package]]
+name = "compression-core"
+version = "0.4.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossterm"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "document-features",
+ "parking_lot",
+ "rustix",
+ "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 = "deadpool"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
+dependencies = [
+ "deadpool-runtime",
+ "lazy_static",
+ "num_cpus",
+ "tokio",
+]
+
+[[package]]
+name = "deadpool-runtime"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "intelx"
+version = "0.1.0"
+dependencies = [
+ "bytes",
+ "dotenvy",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "uuid",
+ "wiremock",
+]
+
+[[package]]
+name = "intelx-cli"
+version = "0.1.0"
+dependencies = [
+ "assert_cmd",
+ "clap",
+ "colored",
+ "comfy-table",
+ "dotenvy",
+ "intelx",
+ "predicates",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "predicates"
+version = "3.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[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",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "termtree"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "async-compression",
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[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-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "wiremock"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
+dependencies = [
+ "assert-json-diff",
+ "base64",
+ "deadpool",
+ "futures",
+ "http",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "log",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Rust/Cargo.toml b/Rust/Cargo.toml
new file mode 100644
index 0000000..871764e
--- /dev/null
+++ b/Rust/Cargo.toml
@@ -0,0 +1,29 @@
+[workspace]
+resolver = "3"
+members = ["intelx", "intelx-cli"]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2024"
+rust-version = "1.96.0"
+license = "MIT"
+repository = "https://github.com/IntelligenceX/SDK"
+authors = ["Intelligence X "]
+
+[workspace.dependencies]
+reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "gzip", "rustls-tls"] }
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "fs", "io-util"] }
+tokio-util = { version = "0.7", features = ["io"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+serde_repr = "0.1"
+thiserror = "2"
+uuid = { version = "1", features = ["v4", "serde"] }
+url = "2"
+bytes = "1"
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+dotenvy = "0.15"
+futures-util = "0.3"
+
+intelx = { path = "intelx", version = "0.1.0" }
diff --git a/Rust/README.md b/Rust/README.md
new file mode 100644
index 0000000..2912152
--- /dev/null
+++ b/Rust/README.md
@@ -0,0 +1,276 @@
+# intelx Rust SDK
+
+## Introduction
+
+`intelx` is an async Rust SDK and command-line client for [intelx.io](https://intelx.io),
+the Intelligence X search engine and data archive. It is a Rust port of the
+[Python `intelx` SDK](../Python), covering intelligent search, phonebook search, file
+operations, and the Identity Service (leaked-account search and reverse-domain lookup).
+
+The workspace has two crates:
+
+* [`intelx/`](intelx) - the library crate.
+* [`intelx-cli/`](intelx-cli) - a `clap`-based command-line client built on the library,
+ producing the `intelx` binary.
+
+## Requirements
+
+* Rust **1.96.0** (pinned via [`rust-toolchain.toml`](rust-toolchain.toml); `rustup` will
+ install it automatically).
+
+## Installation
+
+### As a library
+
+From within this monorepo (before the crate is published to crates.io), add a path
+dependency:
+
+```toml
+[dependencies]
+intelx = { path = "../Rust/intelx" }
+```
+
+Once published, use the registry dependency instead:
+
+```bash
+cargo add intelx
+```
+
+### The CLI
+
+```bash
+cargo install --path Rust/intelx-cli
+```
+
+This installs a binary named `intelx`.
+
+## Setup
+
+You will need an API key from .
+
+### Environment variable
+
+Copy `.env.sample` to `.env` and set your values:
+
+```bash
+INTELX_KEY="00000000-0000-0000-0000-000000000000"
+INTELX_BASE_URL="https://2.intelx.io"
+```
+
+Examples and the CLI both load `.env` automatically via the [`dotenvy`](https://docs.rs/dotenvy)
+crate.
+
+### Via the CLI
+
+```bash
+export INTELX_KEY=00000000-0000-0000-0000-000000000000
+intelx search riseup.net
+```
+
+or pass it explicitly:
+
+```bash
+intelx --api-key "$INTELX_KEY" search riseup.net
+```
+
+## Usage as a CLI
+
+```bash
+# Quick search
+intelx search riseup.net
+
+# Search in specific buckets
+intelx search riseup.net --buckets "pastes,darknet.tor"
+
+# Search with 100 results
+intelx search riseup.net --limit 100
+
+# Download an item (requires --bucket)
+intelx download 29a97791-1138-40b3-8cf1-de1764e9d09c \
+ --bucket leaks.private.general --name test.txt
+
+# View the full contents of a search result instead of a short preview
+intelx search 3a4d5699-737c-4d22-8dbd-c5391ce805df --view
+
+# Export all matching files from a search
+intelx search email@email.com --export --export-format zip --limit 5 \
+ --buckets "pastes,leaks.private.general,leaks.logs,whois,usenet"
+
+# Extract emails from a phonebook search
+intelx search cia.gov --phonebook emails
+
+# Identity Portal: export leaked accounts
+intelx identity riseup.net export-accounts
+
+# Identity Portal: data leaks search
+intelx identity riseup.net data-leaks
+
+# Account capabilities
+intelx capabilities
+```
+
+Pass `--raw` to any command to print JSON instead of formatted output.
+
+## Usage as a library
+
+```rust
+let client = intelx::IntelXClient::new("00000000-0000-0000-0000-000000000000")?;
+let results = client.search(intelx::SearchParams::new("hackerone.com")).await?;
+```
+
+### Advanced search
+
+By default `maxresults` is `100`. The following parameters all have defaults but can be
+overridden via the [`SearchParams`](intelx/src/models/search.rs) builder:
+
+* `maxresults` = 100
+* `buckets` = `[]`
+* `timeout` = 5 (seconds)
+* `datefrom` / `dateto` = `""`
+* `sort` = `SortOrder::DateDesc`
+* `media` = 0
+* `terminate` = `[]`
+
+```rust
+let results = client
+ .search(intelx::SearchParams::new("hackerone.com").maxresults(200))
+ .await?;
+```
+
+#### Searching in specific buckets
+
+```rust
+let params = intelx::SearchParams::new("hackerone.com")
+ .maxresults(200)
+ .buckets(["darknet", "leaks.public", "leaks.private"]);
+let results = client.search(params).await?;
+```
+
+Your account must have access to every specified bucket, otherwise you will receive
+`401 Unauthorized`. The `leaks.private` bucket is only available on certain licenses.
+
+#### Filtering by date
+
+```rust
+let params = intelx::SearchParams::new("riseup.net")
+ .maxresults(200)
+ .datefrom("2014-01-01 00:00:00")
+ .dateto("2014-02-02 23:59:59");
+let results = client.search(params).await?;
+```
+
+#### Filtering by media type
+
+See the [Media Types](#media-types) table below for the available IDs.
+
+```rust
+let params = intelx::SearchParams::new("riseup.net")
+ .maxresults(200)
+ .media(1) // Paste document
+ .datefrom("2014-01-01 00:00:00")
+ .dateto("2014-02-02 23:59:59");
+let results = client.search(params).await?;
+```
+
+#### Statistics
+
+```rust
+let results = client
+ .search(intelx::SearchParams::new("riseup.net").maxresults(1000))
+ .await?;
+let stats = intelx::stats(&results);
+println!("{stats:?}");
+```
+
+### Viewing/reading files
+
+There is a fundamental difference between `file_view` and `file_read`: viewing is for
+quickly inspecting the contents of a file (assumed to be text); `file_read` is for direct
+data download, reliably returning binary contents (ZIP, PDF, etc) without encoding issues.
+
+#### Viewing
+
+```rust
+let results = client.search(intelx::SearchParams::new("riseup.net")).await?;
+let first = &results[0];
+let contents = client
+ .file_view(first.item_type, first.media, &first.storageid, &first.bucket, 0)
+ .await?;
+println!("{contents}");
+```
+
+#### Reading
+
+```rust
+let results = client.search(intelx::SearchParams::new("riseup.net")).await?;
+let first = &results[0];
+client
+ .file_read(&first.systemid.to_string(), intelx::FileReadType::Raw, &first.bucket,
+ std::path::Path::new("file.bin"))
+ .await?;
+```
+
+### Date handling
+
+Date fields (`Item::added`, `Item::date`) are kept as plain `String`s rather than
+`chrono::DateTime`, because the server's `YYYY-mm-dd HH:ii:ss` format is not RFC 3339.
+Parse them yourself with `chrono::NaiveDateTime::parse_from_str(&item.date, "%Y-%m-%d %H:%M:%S")`
+if you need typed dates.
+
+## Other notes
+
+### Media Types
+
+| ID | Media Type |
+|----|------------------------------------|
+| 0 | All |
+| 1 | Paste document |
+| 2 | Paste user |
+| 3 | Forum |
+| 4 | Forum board |
+| 5 | Forum thread |
+| 6 | Forum post |
+| 7 | Forum user |
+| 8 | Screenshot of website |
+| 9 | HTML copy of website |
+| 13 | Tweet |
+| 14 | URL |
+| 15 | PDF document |
+| 16 | Word document |
+| 17 | Excel document |
+| 18 | Powerpoint document |
+| 19 | Picture |
+| 20 | Audio file |
+| 21 | Video file |
+| 22 | Container file (ZIP/RAR/TAR, etc) |
+| 23 | HTML file |
+| 24 | Text file |
+
+### Format Types
+
+| ID | Format Type |
+|----|---------------------------------------|
+| 0 | textview of content |
+| 1 | hex view of content |
+| 2 | auto detect hex view or text view |
+| 3 | picture view |
+| 4 | not supported |
+| 5 | html inline view (sanitized) |
+| 6 | text view of pdf |
+| 7 | text view of html |
+| 8 | text view of word file |
+
+## Testing
+
+```bash
+cd Rust
+cargo test --workspace # unit + wiremock + CLI tests, no network/credentials
+cargo clippy --workspace --all-targets -- -D warnings
+cargo fmt --all -- --check
+INTELX_KEY=... cargo test -p intelx --test live_smoke -- --ignored # live API smoke test
+```
+
+# Contribute
+
+Please use the [issue tracker](https://github.com/IntelligenceX/SDK/issues) to report any
+bugs, security vulnerabilities, or feature requests.
\ No newline at end of file
diff --git a/Rust/changelog/0.1.0-CHANGELOG.md b/Rust/changelog/0.1.0-CHANGELOG.md
new file mode 100644
index 0000000..705173b
--- /dev/null
+++ b/Rust/changelog/0.1.0-CHANGELOG.md
@@ -0,0 +1,16 @@
+## [0.1.0]
+
+### Features
+
+- Initial Rust SDK: `intelx` library crate (async, `reqwest` + `tokio`) with full parity to
+ the Python SDK - intelligent search, phonebook search, file preview/view/read/treeview,
+ search export, and the Identity Service (`idsearch`, `export_accounts`, `reverse_domain`).
+- `intelx-cli` binary crate (`clap`-based) providing `search`, `identity`, `download`, and
+ `capabilities` subcommands.
+
+### Testing
+
+- Unit tests for request serialization, format resolution, and error mapping.
+- `wiremock`-backed HTTP integration tests (no live credentials required).
+- `assert_cmd`-backed CLI integration tests.
+- `#[ignore]`d live API smoke test, gated behind `INTELX_KEY`.
diff --git a/Rust/intelx-cli/Cargo.toml b/Rust/intelx-cli/Cargo.toml
new file mode 100644
index 0000000..530c176
--- /dev/null
+++ b/Rust/intelx-cli/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "intelx-cli"
+description = "Command-line client for the Intelligence X (intelx.io) API, built on the intelx crate"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+authors.workspace = true
+
+[[bin]]
+name = "intelx"
+path = "src/main.rs"
+
+[dependencies]
+intelx.workspace = true
+tokio.workspace = true
+clap = { version = "4", features = ["derive"] }
+dotenvy.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+colored = "2"
+comfy-table = "7"
+
+[dev-dependencies]
+assert_cmd = "2"
+predicates = "3"
diff --git a/Rust/intelx-cli/src/cli.rs b/Rust/intelx-cli/src/cli.rs
new file mode 100644
index 0000000..301be28
--- /dev/null
+++ b/Rust/intelx-cli/src/cli.rs
@@ -0,0 +1,158 @@
+use clap::{Args, Parser, Subcommand, ValueEnum};
+
+/// Command-line client for https://intelx.io, built on the `intelx` crate.
+#[derive(Debug, Parser)]
+#[command(name = "intelx", version, about, long_about = None)]
+pub struct Cli {
+ /// API key. Falls back to the `INTELX_KEY` environment variable.
+ #[arg(long, global = true)]
+ pub api_key: Option,
+
+ /// Base URL override. Falls back to `INTELX_BASE_URL`, then the per-command default.
+ #[arg(long, global = true)]
+ pub base_url: Option,
+
+ /// Print raw JSON instead of formatted output.
+ #[arg(long, global = true)]
+ pub raw: bool,
+
+ #[command(subcommand)]
+ pub command: Command,
+}
+
+#[derive(Debug, Subcommand)]
+pub enum Command {
+ /// Search the general Intelligence X index (intelligent search or phonebook search).
+ Search(SearchArgs),
+ /// Search the Identity Service for leaked accounts / reverse-domain activity.
+ Identity(IdentityArgs),
+ /// Download a single item by its system ID.
+ Download(DownloadArgs),
+ /// Show your account's API capabilities.
+ Capabilities,
+}
+
+#[derive(Debug, Args)]
+pub struct SearchArgs {
+ /// The search term (a "strong selector": email, domain, IP, hash, etc).
+ pub term: String,
+
+ /// Comma-separated list of buckets to search.
+ #[arg(long)]
+ pub buckets: Option,
+
+ /// Maximum number of results to return. Defaults to 10 for display, 100 internally.
+ #[arg(long)]
+ pub limit: Option,
+
+ /// Search timeout in seconds.
+ #[arg(long, default_value_t = 5)]
+ pub timeout: i32,
+
+ /// Starting date filter, `YYYY-mm-dd HH:ii:ss`.
+ #[arg(long)]
+ pub datefrom: Option,
+
+ /// Ending date filter, `YYYY-mm-dd HH:ii:ss`.
+ #[arg(long)]
+ pub dateto: Option,
+
+ /// Media type filter (0 = all).
+ #[arg(long, default_value_t = 0)]
+ pub media: i32,
+
+ /// Run a phonebook search instead, restricted to this selector type.
+ #[arg(long, value_enum)]
+ pub phonebook: Option,
+
+ /// With `--phonebook`, print only email selectors.
+ #[arg(long)]
+ pub emails: bool,
+
+ /// Show the full contents of each result instead of a short preview.
+ #[arg(long)]
+ pub view: bool,
+
+ /// Skip the text preview/view snippet entirely.
+ #[arg(long)]
+ pub nopreview: bool,
+
+ /// Print bucket-count statistics instead of individual results.
+ #[arg(long)]
+ pub stats: bool,
+
+ /// Export all matching files instead of printing results.
+ #[arg(long)]
+ pub export: bool,
+
+ /// Export format, used with `--export`.
+ #[arg(long, value_enum, default_value_t = ExportFormatArg::Zip)]
+ pub export_format: ExportFormatArg,
+
+ /// Directory to write exported/downloaded files to.
+ #[arg(long, default_value = ".")]
+ pub out_dir: std::path::PathBuf,
+}
+
+#[derive(Debug, Clone, Copy, ValueEnum)]
+pub enum PhonebookKind {
+ All,
+ Domains,
+ Emails,
+ Urls,
+}
+
+#[derive(Debug, Clone, Copy, ValueEnum)]
+pub enum ExportFormatArg {
+ Csv,
+ Zip,
+}
+
+#[derive(Debug, Args)]
+pub struct IdentityArgs {
+ /// The domain or email address to look up.
+ pub term: String,
+
+ /// Maximum number of results to return.
+ #[arg(long, default_value_t = 10)]
+ pub limit: i32,
+
+ /// Comma-separated list of buckets to search.
+ #[arg(long)]
+ pub buckets: Option,
+
+ /// Starting date filter, `YYYY-mm-dd HH:ii:ss`.
+ #[arg(long)]
+ pub datefrom: Option,
+
+ /// Ending date filter, `YYYY-mm-dd HH:ii:ss`.
+ #[arg(long)]
+ pub dateto: Option,
+
+ #[command(subcommand)]
+ pub kind: IdentityKind,
+}
+
+#[derive(Debug, Subcommand)]
+pub enum IdentityKind {
+ /// Search for leaked data (`/live/search/internal`).
+ DataLeaks,
+ /// Export leaked accounts to a TSV file (`/accounts/csv`).
+ ExportAccounts,
+ /// Reverse-domain lookup (`/reverse/domain`).
+ ReverseDomain,
+}
+
+#[derive(Debug, Args)]
+pub struct DownloadArgs {
+ /// System ID of the item to download.
+ pub id: String,
+
+ /// Bucket the item was found in.
+ #[arg(long)]
+ pub bucket: String,
+
+ /// Filename to save the item as. Defaults to `.bin`.
+ #[arg(long)]
+ pub name: Option,
+}
diff --git a/Rust/intelx-cli/src/commands/capabilities.rs b/Rust/intelx-cli/src/commands/capabilities.rs
new file mode 100644
index 0000000..fac5d43
--- /dev/null
+++ b/Rust/intelx-cli/src/commands/capabilities.rs
@@ -0,0 +1,10 @@
+use intelx::IntelXClient;
+
+use crate::output;
+
+pub async fn run(client: &IntelXClient) -> intelx::Result<()> {
+ output::info("Getting your API capabilities.\n");
+ let capabilities = client.get_capabilities().await?;
+ output::print_json(&capabilities);
+ Ok(())
+}
diff --git a/Rust/intelx-cli/src/commands/download.rs b/Rust/intelx-cli/src/commands/download.rs
new file mode 100644
index 0000000..c961f12
--- /dev/null
+++ b/Rust/intelx-cli/src/commands/download.rs
@@ -0,0 +1,23 @@
+use intelx::IntelXClient;
+
+use crate::cli::DownloadArgs;
+use crate::output;
+
+pub async fn run(client: &IntelXClient, args: DownloadArgs) -> intelx::Result<()> {
+ let filename = args.name.unwrap_or_else(|| format!("{}.bin", args.id));
+ let dest = std::path::Path::new(&filename);
+
+ match client
+ .file_read(&args.id, intelx::FileReadType::Raw, &args.bucket, dest)
+ .await
+ {
+ Ok(_) => {
+ output::info(&format!("Successfully downloaded the file '{filename}'."));
+ Ok(())
+ }
+ Err(err) => {
+ output::error(&format!("Failed to download item {}: {err}", args.id));
+ Err(err)
+ }
+ }
+}
diff --git a/Rust/intelx-cli/src/commands/identity.rs b/Rust/intelx-cli/src/commands/identity.rs
new file mode 100644
index 0000000..dc71f19
--- /dev/null
+++ b/Rust/intelx-cli/src/commands/identity.rs
@@ -0,0 +1,154 @@
+use intelx::IdentityClient;
+
+use crate::cli::{IdentityArgs, IdentityKind};
+use crate::output;
+
+fn parse_buckets(buckets: &Option) -> Vec {
+ buckets
+ .as_deref()
+ .map(|b| {
+ b.split(',')
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ .collect()
+ })
+ .unwrap_or_default()
+}
+
+fn write_tsv(filename: &str, headers: &[&str], rows: &[Vec]) -> std::io::Result<()> {
+ use std::io::Write;
+ let mut file = std::fs::File::create(filename)?;
+ writeln!(file, "{}", headers.join("\t"))?;
+ for row in rows {
+ writeln!(file, "{}", row.join("\t"))?;
+ }
+ Ok(())
+}
+
+pub async fn run(client: &IdentityClient, args: IdentityArgs, raw: bool) -> intelx::Result<()> {
+ let buckets = parse_buckets(&args.buckets);
+
+ match args.kind {
+ IdentityKind::DataLeaks => {
+ if !raw {
+ output::info(&format!("Starting data leaks search of \"{}\".", args.term));
+ }
+ let mut params = intelx::IdSearchParams::new(&args.term).maxresults(args.limit);
+ if let Some(bucket) = buckets.first() {
+ params = params.bucket(bucket.clone());
+ }
+ if let Some(datefrom) = &args.datefrom {
+ params = params.datefrom(datefrom.clone());
+ }
+ if let Some(dateto) = &args.dateto {
+ params = params.dateto(dateto.clone());
+ }
+ let records = client.idsearch(params).await?;
+
+ if raw {
+ output::print_json(&records);
+ return Ok(());
+ }
+
+ let rows: Vec> = records
+ .iter()
+ .filter_map(|r| r.item.as_ref())
+ .map(|item| vec![item.name.clone(), item.date.clone(), item.bucket.clone()])
+ .collect();
+ output::table(vec!["Name", "Date", "Bucket"], rows.clone());
+
+ let filename = format!("intelx-output-{}-data_leaks.tsv", args.term);
+ write_tsv(&filename, &["Name", "Date", "Bucket"], &rows)?;
+ output::info(&format!("Exported output to \"{filename}\"."));
+ }
+ IdentityKind::ExportAccounts => {
+ if !raw {
+ output::info(&format!("Starting account export of \"{}\".", args.term));
+ }
+ let mut params = intelx::ExportAccountsParams::new(&args.term).maxresults(args.limit);
+ if let Some(datefrom) = &args.datefrom {
+ params = params.datefrom(datefrom.clone());
+ }
+ if let Some(dateto) = &args.dateto {
+ params = params.dateto(dateto.clone());
+ }
+ let records = client.export_accounts(params).await?;
+
+ if raw {
+ output::print_json(&records);
+ return Ok(());
+ }
+
+ let rows: Vec> = records
+ .iter()
+ .map(|r| {
+ vec![
+ r.user.clone(),
+ r.password.clone(),
+ r.passwordtype.to_string(),
+ r.sourceshort.clone(),
+ ]
+ })
+ .collect();
+ output::table(
+ vec!["User", "Password", "Password Type", "Source Short"],
+ rows.clone(),
+ );
+
+ let filename = format!("intelx-output-{}-export_accounts.tsv", args.term);
+ write_tsv(
+ &filename,
+ &["User", "Password", "Password Type", "Source Short"],
+ &rows,
+ )?;
+ output::info(&format!("Exported output to \"{filename}\"."));
+ }
+ IdentityKind::ReverseDomain => {
+ if !raw {
+ output::info(&format!(
+ "Starting reverse domain export of \"{}\".",
+ args.term
+ ));
+ }
+ let mut params = intelx::ReverseDomainParams::new(&args.term).maxresults(args.limit);
+ if let Some(datefrom) = &args.datefrom {
+ params = params.datefrom(datefrom.clone());
+ }
+ if let Some(dateto) = &args.dateto {
+ params = params.dateto(dateto.clone());
+ }
+ let records = client.reverse_domain(params).await?;
+
+ if raw {
+ output::print_json(&records);
+ return Ok(());
+ }
+
+ let rows: Vec> = records
+ .iter()
+ .map(|r| {
+ vec![
+ r.user.clone(),
+ r.password.clone(),
+ r.url.clone(),
+ r.sourceshort.clone(),
+ ]
+ })
+ .collect();
+ output::table(
+ vec!["User", "Password", "URL", "Source Short"],
+ rows.clone(),
+ );
+
+ let filename = format!("intelx-output-{}-export_accounts.tsv", args.term);
+ write_tsv(
+ &filename,
+ &["User", "Password", "URL", "Source Short"],
+ &rows,
+ )?;
+ output::info(&format!("Exported output to \"{filename}\"."));
+ }
+ }
+
+ Ok(())
+}
diff --git a/Rust/intelx-cli/src/commands/mod.rs b/Rust/intelx-cli/src/commands/mod.rs
new file mode 100644
index 0000000..7ef4f73
--- /dev/null
+++ b/Rust/intelx-cli/src/commands/mod.rs
@@ -0,0 +1,4 @@
+pub mod capabilities;
+pub mod download;
+pub mod identity;
+pub mod search;
diff --git a/Rust/intelx-cli/src/commands/search.rs b/Rust/intelx-cli/src/commands/search.rs
new file mode 100644
index 0000000..31df071
--- /dev/null
+++ b/Rust/intelx-cli/src/commands/search.rs
@@ -0,0 +1,178 @@
+use intelx::IntelXClient;
+
+use crate::cli::{ExportFormatArg, PhonebookKind, SearchArgs};
+use crate::output;
+
+fn parse_buckets(buckets: &Option) -> Vec {
+ buckets
+ .as_deref()
+ .map(|b| {
+ b.split(',')
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ .collect()
+ })
+ .unwrap_or_default()
+}
+
+pub async fn run(client: &IntelXClient, args: SearchArgs, raw: bool) -> intelx::Result<()> {
+ if args.limit.is_none() && !args.stats && args.phonebook.is_none() && !raw {
+ output::warn("Limit argument not supplied, setting default to 10 results.");
+ }
+ let limit = args.limit.unwrap_or(10);
+ let buckets = parse_buckets(&args.buckets);
+
+ if let Some(phonebook) = args.phonebook {
+ return run_phonebook(client, &args, phonebook, buckets, raw).await;
+ }
+
+ if !raw {
+ output::info(&format!("Starting search of \"{}\".", args.term));
+ }
+
+ let mut params = intelx::SearchParams::new(&args.term)
+ .maxresults(if args.stats { 1000 } else { limit.max(100) })
+ .buckets(buckets)
+ .timeout(args.timeout)
+ .media(args.media);
+ if let Some(datefrom) = &args.datefrom {
+ params = params.datefrom(datefrom.clone());
+ }
+ if let Some(dateto) = &args.dateto {
+ params = params
+ .dateto(dateto.clone())
+ .sort(intelx::SortOrder::XScoreDesc);
+ }
+
+ if args.export {
+ let format = match args.export_format {
+ ExportFormatArg::Csv => intelx::ExportFormat::Csv,
+ ExportFormatArg::Zip => intelx::ExportFormat::Zip,
+ };
+ let path = client
+ .export_from_search(params, format, &args.out_dir)
+ .await?;
+ output::info(&format!(
+ "Exported search results to \"{}\".",
+ path.display()
+ ));
+ return Ok(());
+ }
+
+ let results = client.search(params).await?;
+
+ if raw {
+ output::print_json(&results);
+ return Ok(());
+ }
+
+ if args.stats {
+ let stats = intelx::stats(&results);
+ output::print_json(&stats);
+ return Ok(());
+ }
+
+ for result in results.iter().take(limit as usize) {
+ let name = if result.name.is_empty() {
+ "Untitled Document"
+ } else {
+ &result.name
+ };
+ println!(
+ "________________________________________________________________________________"
+ );
+ println!("> Name: {name}");
+ println!("> Date: {}", result.date);
+ println!("> Size: {} bytes", result.size);
+ println!("> Media: {}", result.mediah);
+ println!("> Bucket: {}", result.bucketh);
+ println!("> ID: {}", result.systemid);
+
+ if args.view {
+ let text = client
+ .file_view(
+ result.item_type,
+ result.media,
+ &result.storageid,
+ &result.bucket,
+ 0,
+ )
+ .await?;
+ if !text.is_empty() {
+ println!("\n{text}");
+ }
+ } else if !args.nopreview {
+ let preview_params = intelx::FilePreviewParams::new(
+ result.item_type,
+ result.media,
+ 0,
+ result.storageid.clone(),
+ )
+ .bucket(result.bucket.clone());
+ let text = client.file_preview(preview_params).await?;
+ if !text.is_empty() {
+ println!("\n{text}");
+ }
+ }
+ println!(
+ "________________________________________________________________________________"
+ );
+ }
+
+ Ok(())
+}
+
+async fn run_phonebook(
+ client: &IntelXClient,
+ args: &SearchArgs,
+ kind: PhonebookKind,
+ buckets: Vec,
+ raw: bool,
+) -> intelx::Result<()> {
+ if !raw {
+ output::info(&format!("Starting phonebook search of \"{}\".", args.term));
+ }
+
+ let target = match kind {
+ PhonebookKind::All => intelx::PhonebookTarget::All,
+ PhonebookKind::Domains => intelx::PhonebookTarget::Domains,
+ PhonebookKind::Emails => intelx::PhonebookTarget::EmailAddresses,
+ PhonebookKind::Urls => intelx::PhonebookTarget::Urls,
+ };
+
+ let mut params = intelx::PhonebookSearchParams::new(&args.term)
+ .maxresults(args.limit.unwrap_or(1000))
+ .buckets(buckets)
+ .target(target);
+ if let Some(datefrom) = &args.datefrom {
+ params.datefrom = datefrom.clone();
+ }
+ if let Some(dateto) = &args.dateto {
+ params.dateto = dateto.clone();
+ }
+
+ let pages = client.phonebook_search_all(params).await?;
+
+ if raw {
+ output::print_json(&pages);
+ return Ok(());
+ }
+
+ let selectors = intelx::flatten_selectors(&pages);
+ if args.emails {
+ for selector in &selectors {
+ if selector.selectortype == 1 {
+ println!("{}", selector.selectorvalue);
+ }
+ }
+ return Ok(());
+ }
+
+ let rows = selectors
+ .iter()
+ .map(|s| vec![s.selectortypeh.clone(), s.selectorvalue.clone()])
+ .collect();
+ output::table(vec!["Type", "Value"], rows);
+
+ Ok(())
+}
diff --git a/Rust/intelx-cli/src/main.rs b/Rust/intelx-cli/src/main.rs
new file mode 100644
index 0000000..c9fd4e5
--- /dev/null
+++ b/Rust/intelx-cli/src/main.rs
@@ -0,0 +1,89 @@
+mod cli;
+mod commands;
+mod output;
+
+use clap::Parser;
+use cli::{Cli, Command};
+use colored::Colorize;
+
+fn version() -> &'static str {
+ env!("CARGO_PKG_VERSION")
+}
+
+#[tokio::main]
+async fn main() -> std::process::ExitCode {
+ dotenvy::dotenv().ok();
+ tracing_subscriber::fmt()
+ .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
+ .with_writer(std::io::stderr)
+ .init();
+
+ let cli = Cli::parse();
+
+ if !cli.raw {
+ println!("{}", output::BANNER.bold());
+ println!("intelx v{}", version());
+ }
+
+ let api_key = cli
+ .api_key
+ .clone()
+ .or_else(|| std::env::var("INTELX_KEY").ok());
+ let Some(api_key) = api_key else {
+ output::error(
+ "No API key specified. Please use the \"--api-key\" parameter or set the environment variable \"INTELX_KEY\".",
+ );
+ return std::process::ExitCode::FAILURE;
+ };
+
+ let base_url = cli
+ .base_url
+ .clone()
+ .or_else(|| std::env::var("INTELX_BASE_URL").ok());
+
+ let result = run(api_key, base_url, cli).await;
+ match result {
+ Ok(()) => std::process::ExitCode::SUCCESS,
+ Err(err) => {
+ output::error(&err.to_string());
+ std::process::ExitCode::FAILURE
+ }
+ }
+}
+
+async fn run(api_key: String, base_url: Option, cli: Cli) -> intelx::Result<()> {
+ match cli.command {
+ Command::Search(args) => {
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Some(base_url) = base_url {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+ commands::search::run(&client, args, cli.raw).await
+ }
+ Command::Identity(args) => {
+ let mut builder = intelx::IdentityClient::builder().api_key(api_key);
+ if let Some(base_url) = base_url {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+ commands::identity::run(&client, args, cli.raw).await
+ }
+ Command::Download(args) => {
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Some(base_url) = base_url {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+ commands::download::run(&client, args).await
+ }
+ Command::Capabilities => {
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Some(base_url) = base_url {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+ commands::capabilities::run(&client).await
+ }
+ }
+}
diff --git a/Rust/intelx-cli/src/output.rs b/Rust/intelx-cli/src/output.rs
new file mode 100644
index 0000000..5f497b9
--- /dev/null
+++ b/Rust/intelx-cli/src/output.rs
@@ -0,0 +1,56 @@
+use colored::Colorize;
+use comfy_table::Table;
+
+pub const BANNER: &str = r#"
+ _____ _ ___ __
+ |_ _| | | | \ \ / /
+ | | _ __ | |_ ___| |\ V /
+ | || '_ \| __/ _ \ |/ \
+ _| || | | | || __/ / /^\ \
+ \___/_| |_|\__\___|_\/ \/
+
+ a command line client
+ for intelx.io
+"#;
+
+fn timestamp() -> String {
+ use std::time::{SystemTime, UNIX_EPOCH};
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default();
+ let secs_today = now.as_secs() % 86400;
+ format!(
+ "{:02}:{:02}:{:02}",
+ secs_today / 3600,
+ (secs_today % 3600) / 60,
+ secs_today % 60
+ )
+}
+
+pub fn info(message: &str) {
+ println!("{}", format!("[{}] {message}", timestamp()).green());
+}
+
+pub fn warn(message: &str) {
+ println!("{}", format!("[{}] {message}", timestamp()).yellow());
+}
+
+pub fn error(message: &str) {
+ eprintln!("{}", format!("[{}] {message}", timestamp()).red());
+}
+
+pub fn table(headers: Vec<&str>, rows: Vec>) {
+ let mut table = Table::new();
+ table.set_header(headers);
+ for row in rows {
+ table.add_row(row);
+ }
+ println!("{table}");
+}
+
+pub fn print_json(value: &T) {
+ match serde_json::to_string_pretty(value) {
+ Ok(json) => println!("{json}"),
+ Err(err) => error(&format!("failed to serialize output as JSON: {err}")),
+ }
+}
diff --git a/Rust/intelx-cli/tests/cli.rs b/Rust/intelx-cli/tests/cli.rs
new file mode 100644
index 0000000..20abed7
--- /dev/null
+++ b/Rust/intelx-cli/tests/cli.rs
@@ -0,0 +1,38 @@
+//! CLI-level integration tests: spawn the built binary and assert on its behavior. These never
+//! hit the network and require no credentials.
+
+use assert_cmd::Command;
+use predicates::str::contains;
+
+fn cmd() -> Command {
+ let mut cmd = Command::cargo_bin("intelx").unwrap();
+ cmd.env_remove("INTELX_KEY");
+ cmd.env_remove("INTELX_BASE_URL");
+ cmd
+}
+
+#[test]
+fn help_lists_all_subcommands() {
+ cmd()
+ .arg("--help")
+ .assert()
+ .success()
+ .stdout(contains("search"))
+ .stdout(contains("identity"))
+ .stdout(contains("download"))
+ .stdout(contains("capabilities"));
+}
+
+#[test]
+fn search_without_api_key_fails_with_clear_message() {
+ cmd()
+ .args(["search", "test.com"])
+ .assert()
+ .failure()
+ .stderr(contains("No API key specified"));
+}
+
+#[test]
+fn missing_subcommand_fails_with_usage() {
+ cmd().assert().failure().stderr(contains("Usage"));
+}
diff --git a/Rust/intelx/Cargo.toml b/Rust/intelx/Cargo.toml
new file mode 100644
index 0000000..1baf37c
--- /dev/null
+++ b/Rust/intelx/Cargo.toml
@@ -0,0 +1,62 @@
+[package]
+name = "intelx"
+description = "Async Rust SDK for the Intelligence X (intelx.io) search engine and data archive API"
+keywords = ["osint", "intelx", "intelligence-x", "search", "threat-intel"]
+categories = ["api-bindings"]
+readme = "../README.md"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+authors.workspace = true
+
+[dependencies]
+reqwest.workspace = true
+tokio.workspace = true
+tokio-util.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+serde_repr.workspace = true
+thiserror.workspace = true
+uuid.workspace = true
+url.workspace = true
+bytes.workspace = true
+tracing.workspace = true
+
+[dev-dependencies]
+tokio = { workspace = true, features = ["macros", "rt-multi-thread", "test-util"] }
+dotenvy.workspace = true
+wiremock = "0.6"
+tempfile = "3"
+tracing-subscriber.workspace = true
+
+[[example]]
+name = "search"
+
+[[example]]
+name = "advanced_search"
+
+[[example]]
+name = "authenticate_info"
+
+[[example]]
+name = "download"
+
+[[example]]
+name = "file_preview"
+
+[[example]]
+name = "file_view"
+
+[[example]]
+name = "idsearch"
+
+[[example]]
+name = "reverse_domain"
+
+[[example]]
+name = "search_with_results"
+
+[[example]]
+name = "stats"
diff --git a/Rust/intelx/examples/advanced_search.rs b/Rust/intelx/examples/advanced_search.rs
new file mode 100644
index 0000000..c7101fd
--- /dev/null
+++ b/Rust/intelx/examples/advanced_search.rs
@@ -0,0 +1,38 @@
+fn client_from_env() -> intelx::Result {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Ok(base_url) = std::env::var("INTELX_BASE_URL") {
+ builder = builder.base_url(base_url);
+ }
+ builder.build()
+}
+
+async fn count_in_buckets(
+ client: &intelx::IntelXClient,
+ target: &str,
+ buckets: &[&str],
+ label: &str,
+) -> intelx::Result<()> {
+ let params = intelx::SearchParams::new(target)
+ .buckets(buckets.iter().copied())
+ .maxresults(2000);
+ let results = client.search(params).await?;
+ println!(
+ "Found {} records for {target} in bucket '{label}'",
+ results.len()
+ );
+ Ok(())
+}
+
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ let client = client_from_env()?;
+ let target = "riseup.net";
+
+ count_in_buckets(&client, target, &["leaks.public", "leaks.private"], "leaks").await?;
+ count_in_buckets(&client, target, &["pastes"], "pastes").await?;
+ count_in_buckets(&client, target, &["darknet"], "darknet").await?;
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/authenticate_info.rs b/Rust/intelx/examples/authenticate_info.rs
new file mode 100644
index 0000000..16a4d5c
--- /dev/null
+++ b/Rust/intelx/examples/authenticate_info.rs
@@ -0,0 +1,15 @@
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Ok(base_url) = std::env::var("INTELX_BASE_URL") {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+
+ let capabilities: intelx::Capabilities = client.get_capabilities().await?;
+ println!("{capabilities:#?}");
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/download.rs b/Rust/intelx/examples/download.rs
new file mode 100644
index 0000000..71fd528
--- /dev/null
+++ b/Rust/intelx/examples/download.rs
@@ -0,0 +1,28 @@
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Ok(base_url) = std::env::var("INTELX_BASE_URL") {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+
+ let results = client
+ .search(intelx::SearchParams::new("riseup.net"))
+ .await?;
+ let first = results.first().expect("search returned no results");
+
+ let dest = std::path::Path::new("file1.bin");
+ client
+ .file_read(
+ &first.systemid.to_string(),
+ intelx::FileReadType::Raw,
+ &first.bucket,
+ dest,
+ )
+ .await?;
+ println!("Saved first search result to {}", dest.display());
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/file_preview.rs b/Rust/intelx/examples/file_preview.rs
new file mode 100644
index 0000000..d4857d5
--- /dev/null
+++ b/Rust/intelx/examples/file_preview.rs
@@ -0,0 +1,23 @@
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Ok(base_url) = std::env::var("INTELX_BASE_URL") {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+
+ let results = client
+ .search(intelx::SearchParams::new("riseup.net").maxresults(5))
+ .await?;
+ let first = results.first().expect("search returned no results");
+
+ let params = intelx::FilePreviewParams::new(1, first.media, 0, first.storageid.clone())
+ .bucket(first.bucket.clone())
+ .lines(20);
+ let preview = client.file_preview(params).await?;
+ println!("{preview}");
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/file_view.rs b/Rust/intelx/examples/file_view.rs
new file mode 100644
index 0000000..bdf12fa
--- /dev/null
+++ b/Rust/intelx/examples/file_view.rs
@@ -0,0 +1,22 @@
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Ok(base_url) = std::env::var("INTELX_BASE_URL") {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+
+ let results = client
+ .search(intelx::SearchParams::new("riseup.net").maxresults(5))
+ .await?;
+ let first = results.first().expect("search returned no results");
+
+ let view = client
+ .file_view(1, first.media, &first.storageid, &first.bucket, 0)
+ .await?;
+ println!("{view}");
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/idsearch.rs b/Rust/intelx/examples/idsearch.rs
new file mode 100644
index 0000000..c36555c
--- /dev/null
+++ b/Rust/intelx/examples/idsearch.rs
@@ -0,0 +1,29 @@
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+
+ let identity = intelx::IdentityClient::new(api_key.clone())?;
+ let general = intelx::IntelXClient::new(api_key)?;
+
+ let records = identity
+ .idsearch(intelx::IdSearchParams::new("john.doe@example.com"))
+ .await?;
+ let first_item = records
+ .first()
+ .and_then(|record| record.item.as_ref())
+ .expect("identity search returned no usable results");
+
+ let contents = general
+ .file_view(
+ first_item.item_type,
+ first_item.media,
+ &first_item.storageid,
+ &first_item.bucket,
+ 0,
+ )
+ .await?;
+ println!("{contents}");
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/reverse_domain.rs b/Rust/intelx/examples/reverse_domain.rs
new file mode 100644
index 0000000..0482d2f
--- /dev/null
+++ b/Rust/intelx/examples/reverse_domain.rs
@@ -0,0 +1,16 @@
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let identity = intelx::IdentityClient::new(api_key)?;
+
+ let params = intelx::ReverseDomainParams::new("riseup.net")
+ .maxresults(10)
+ .datefrom("2022-01-01 00:00:00")
+ .dateto("2022-06-01 00:00:00");
+ let results = identity.reverse_domain(params).await?;
+
+ println!("{results:#?}");
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/search.rs b/Rust/intelx/examples/search.rs
new file mode 100644
index 0000000..199b79a
--- /dev/null
+++ b/Rust/intelx/examples/search.rs
@@ -0,0 +1,23 @@
+fn client_from_env() -> intelx::Result {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Ok(base_url) = std::env::var("INTELX_BASE_URL") {
+ builder = builder.base_url(base_url);
+ }
+ builder.build()
+}
+
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ let client = client_from_env()?;
+
+ let results = client
+ .search(intelx::SearchParams::new("riseup.net"))
+ .await?;
+ for record in &results {
+ println!("Found media type {} in {}", record.media, record.bucket);
+ }
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/search_with_results.rs b/Rust/intelx/examples/search_with_results.rs
new file mode 100644
index 0000000..bc78860
--- /dev/null
+++ b/Rust/intelx/examples/search_with_results.rs
@@ -0,0 +1,41 @@
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Ok(base_url) = std::env::var("INTELX_BASE_URL") {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+
+ let params = intelx::SearchParams::new("riseup.net")
+ .maxresults(50)
+ .buckets(["leaks.public", "pastes"])
+ .timeout(5)
+ .datefrom("2021-01-01 00:00:00")
+ .dateto("2022-02-02 23:00:00")
+ .sort(intelx::SortOrder::DateDesc)
+ .media(0);
+
+ let search_id = client.intel_search(params).await?;
+ println!("Search ID: {search_id}");
+
+ let limit = 100;
+ loop {
+ let page = client.intel_search_result(search_id, limit).await?;
+ println!("Status: {:?}, records: {}", page.status, page.records.len());
+
+ for record in &page.records {
+ println!("{} {}", record.name, record.bucket);
+ }
+
+ if matches!(
+ page.status,
+ intelx::SearchStatus::NoMoreResults | intelx::SearchStatus::NotFound
+ ) {
+ break;
+ }
+ }
+
+ Ok(())
+}
diff --git a/Rust/intelx/examples/stats.rs b/Rust/intelx/examples/stats.rs
new file mode 100644
index 0000000..1a5c43c
--- /dev/null
+++ b/Rust/intelx/examples/stats.rs
@@ -0,0 +1,18 @@
+#[tokio::main]
+async fn main() -> intelx::Result<()> {
+ dotenvy::dotenv().ok();
+ let api_key = std::env::var("INTELX_KEY").expect("INTELX_KEY must be set");
+ let mut builder = intelx::IntelXClient::builder().api_key(api_key);
+ if let Ok(base_url) = std::env::var("INTELX_BASE_URL") {
+ builder = builder.base_url(base_url);
+ }
+ let client = builder.build()?;
+
+ let results = client
+ .search(intelx::SearchParams::new("riseup.net").maxresults(1000))
+ .await?;
+ let stats = intelx::stats(&results);
+ println!("{stats:#?}");
+
+ Ok(())
+}
diff --git a/Rust/intelx/src/client.rs b/Rust/intelx/src/client.rs
new file mode 100644
index 0000000..7a4f107
--- /dev/null
+++ b/Rust/intelx/src/client.rs
@@ -0,0 +1,264 @@
+//! The base HTTP client shared by every endpoint module.
+
+use std::time::Duration;
+
+use reqwest::header::{HeaderMap, HeaderValue};
+use serde::Serialize;
+use serde::de::DeserializeOwned;
+
+use crate::error::{IntelXError, Result, api_error_from_status};
+
+const DEFAULT_BASE_URL: &str = "https://2.intelx.io";
+const DEFAULT_USER_AGENT: &str = concat!("IX-Rust/", env!("CARGO_PKG_VERSION"));
+const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
+const DEFAULT_RATE_LIMIT: Duration = Duration::from_secs(1);
+
+/// An async client for the general Intelligence X API (default base URL `https://2.intelx.io`).
+///
+/// Construct one with [`IntelXClient::new`] for the common case, or [`IntelXClient::builder`]
+/// to configure a custom base URL, proxy, timeout, or rate limit.
+#[derive(Clone, Debug)]
+pub struct IntelXClient {
+ pub(crate) http: reqwest::Client,
+ pub(crate) base_url: url::Url,
+ pub(crate) api_key: String,
+ pub(crate) rate_limit: Duration,
+}
+
+/// Builder for [`IntelXClient`].
+pub struct IntelXClientBuilder {
+ api_key: Option,
+ base_url: String,
+ user_agent: String,
+ proxy: Option,
+ danger_accept_invalid_certs: bool,
+ timeout: Duration,
+ rate_limit: Duration,
+}
+
+impl Default for IntelXClientBuilder {
+ fn default() -> Self {
+ Self {
+ api_key: None,
+ base_url: DEFAULT_BASE_URL.to_string(),
+ user_agent: DEFAULT_USER_AGENT.to_string(),
+ proxy: None,
+ danger_accept_invalid_certs: false,
+ timeout: DEFAULT_TIMEOUT,
+ rate_limit: DEFAULT_RATE_LIMIT,
+ }
+ }
+}
+
+impl IntelXClientBuilder {
+ /// Sets the API key sent as the `X-Key` header on every request. Required.
+ pub fn api_key(mut self, api_key: impl Into) -> Self {
+ self.api_key = Some(api_key.into());
+ self
+ }
+
+ /// Overrides the base URL. Defaults to `https://2.intelx.io`.
+ pub fn base_url(mut self, base_url: impl Into) -> Self {
+ self.base_url = base_url.into();
+ self
+ }
+
+ /// Overrides the `User-Agent` header. Defaults to `IX-Rust/`.
+ pub fn user_agent(mut self, user_agent: impl Into) -> Self {
+ self.user_agent = user_agent.into();
+ self
+ }
+
+ /// Routes all requests through the given proxy URL.
+ pub fn proxy(mut self, proxy_url: impl Into) -> Self {
+ self.proxy = Some(proxy_url.into());
+ self
+ }
+
+ /// Disables TLS certificate verification. Mirrors the Python SDK's `verify=False`.
+ ///
+ /// This is dangerous: only use it against a known/trusted endpoint (e.g. for local
+ /// testing through an intercepting proxy).
+ pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
+ self.danger_accept_invalid_certs = accept;
+ self
+ }
+
+ /// Sets the per-request HTTP timeout. Defaults to 30 seconds.
+ pub fn timeout(mut self, timeout: Duration) -> Self {
+ self.timeout = timeout;
+ self
+ }
+
+ /// Sets the delay applied before each request to stay within the API's rate limit.
+ /// Defaults to 1 second, matching the Python SDK's `API_RATE_LIMIT`.
+ pub fn rate_limit(mut self, rate_limit: Duration) -> Self {
+ self.rate_limit = rate_limit;
+ self
+ }
+
+ /// Builds the [`IntelXClient`].
+ ///
+ /// Returns [`IntelXError::MissingApiKey`] if no API key was set, or
+ /// [`IntelXError::InvalidUrl`] if the configured base URL doesn't parse.
+ pub fn build(self) -> Result {
+ let api_key = self.api_key.ok_or(IntelXError::MissingApiKey)?;
+ let base_url = url::Url::parse(&self.base_url)?;
+
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ "X-Key",
+ HeaderValue::from_str(&api_key).map_err(|_| IntelXError::Api {
+ status: 0,
+ message: "invalid API key header value".into(),
+ })?,
+ );
+
+ let mut builder = reqwest::Client::builder()
+ .user_agent(self.user_agent)
+ .default_headers(headers)
+ .timeout(self.timeout)
+ .danger_accept_invalid_certs(self.danger_accept_invalid_certs);
+
+ if let Some(proxy) = self.proxy {
+ builder = builder.proxy(reqwest::Proxy::all(proxy)?);
+ }
+
+ let http = builder.build()?;
+
+ Ok(IntelXClient {
+ http,
+ base_url,
+ api_key,
+ rate_limit: self.rate_limit,
+ })
+ }
+}
+
+impl IntelXClient {
+ /// Creates a builder for configuring a client.
+ pub fn builder() -> IntelXClientBuilder {
+ IntelXClientBuilder::default()
+ }
+
+ /// Creates a client for `api_key` using all other defaults, equivalent to Python's
+ /// `intelx(api_key)`.
+ pub fn new(api_key: impl Into) -> Result {
+ Self::builder().api_key(api_key).build()
+ }
+
+ /// The configured base URL.
+ pub fn base_url(&self) -> &url::Url {
+ &self.base_url
+ }
+
+ /// The configured API key.
+ pub fn api_key(&self) -> &str {
+ &self.api_key
+ }
+
+ /// Returns the current user's API capabilities (allowed buckets, per-endpoint credits,
+ /// concurrent search limits). Mirrors the Python SDK's `GET_CAPABILITIES()`.
+ pub async fn get_capabilities(&self) -> Result {
+ self.rate_limit_sleep().await;
+ let no_params: [(&str, &str); 0] = [];
+ self.get("/authenticate/info", &no_params).await
+ }
+
+ fn resolve_url(&self, path: &str) -> Result {
+ if path.starts_with("http://") || path.starts_with("https://") {
+ Ok(url::Url::parse(path)?)
+ } else {
+ Ok(self.base_url.join(path.trim_start_matches('/'))?)
+ }
+ }
+
+ /// Sleeps for the configured rate-limit duration before issuing a request, mirroring the
+ /// Python SDK's `time.sleep(self.API_RATE_LIMIT)`.
+ pub(crate) async fn rate_limit_sleep(&self) {
+ tokio::time::sleep(self.rate_limit).await;
+ }
+
+ pub(crate) async fn get_response(
+ &self,
+ path: &str,
+ query: &(impl Serialize + ?Sized),
+ ) -> Result {
+ let url = self.resolve_url(path)?;
+ let response = self.http.get(url).query(query).send().await?;
+ Ok(response)
+ }
+
+ pub(crate) async fn get(
+ &self,
+ path: &str,
+ query: &(impl Serialize + ?Sized),
+ ) -> Result {
+ let response = self.get_response(path, query).await?;
+ Self::deserialize_or_error(response).await
+ }
+
+ pub(crate) async fn post_json(
+ &self,
+ path: &str,
+ body: &B,
+ ) -> Result {
+ let url = self.resolve_url(path)?;
+ let response = self.http.post(url).json(body).send().await?;
+ Self::deserialize_or_error(response).await
+ }
+
+ async fn deserialize_or_error(response: reqwest::Response) -> Result {
+ let status = response.status();
+ if !status.is_success() {
+ return Err(api_error_from_status(status));
+ }
+ let bytes = response.bytes().await?;
+ Ok(serde_json::from_slice(&bytes)?)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn build_fails_without_api_key() {
+ let err = IntelXClient::builder().build().unwrap_err();
+ assert!(matches!(err, IntelXError::MissingApiKey));
+ }
+
+ #[test]
+ fn builder_applies_python_compatible_defaults() {
+ let client = IntelXClient::new("test-key").unwrap();
+ assert_eq!(client.base_url().as_str(), "https://2.intelx.io/");
+ assert_eq!(client.api_key(), "test-key");
+ assert_eq!(client.rate_limit, DEFAULT_RATE_LIMIT);
+ }
+
+ #[test]
+ fn builder_overrides_are_applied() {
+ let client = IntelXClient::builder()
+ .api_key("k")
+ .base_url("https://example.test")
+ .rate_limit(Duration::from_millis(0))
+ .build()
+ .unwrap();
+ assert_eq!(client.base_url().as_str(), "https://example.test/");
+ assert_eq!(client.rate_limit, Duration::from_millis(0));
+ }
+
+ #[test]
+ fn resolve_url_joins_relative_paths_against_base() {
+ let client = IntelXClient::new("k").unwrap();
+ let url = client.resolve_url("/intelligent/search").unwrap();
+ assert_eq!(url.as_str(), "https://2.intelx.io/intelligent/search");
+ }
+
+ #[test]
+ fn resolve_url_leaves_absolute_urls_untouched() {
+ let client = IntelXClient::new("k").unwrap();
+ let url = client.resolve_url("https://other.example/x").unwrap();
+ assert_eq!(url.as_str(), "https://other.example/x");
+ }
+}
diff --git a/Rust/intelx/src/error.rs b/Rust/intelx/src/error.rs
new file mode 100644
index 0000000..ea352dd
--- /dev/null
+++ b/Rust/intelx/src/error.rs
@@ -0,0 +1,116 @@
+//! Error types returned by every fallible operation in this crate.
+
+use thiserror::Error;
+
+/// The crate-wide result alias: every public, fallible function in `intelx`
+/// returns `Result` rather than Python's mixed bool/int/status-code return values.
+pub type Result = std::result::Result;
+
+/// Errors that can occur while talking to the Intelligence X API.
+#[derive(Debug, Error)]
+pub enum IntelXError {
+ /// The underlying HTTP transport failed (connection, TLS, etc).
+ #[error("HTTP transport error: {0}")]
+ Http(#[from] reqwest::Error),
+
+ /// A response body could not be deserialized as the expected JSON shape.
+ #[error("JSON (de)serialization error: {0}")]
+ Json(#[from] serde_json::Error),
+
+ /// A local file system operation failed (writing a downloaded file, etc).
+ #[error("I/O error: {0}")]
+ Io(#[from] std::io::Error),
+
+ /// The configured base URL could not be parsed.
+ #[error("invalid base URL: {0}")]
+ InvalidUrl(#[from] url::ParseError),
+
+ /// The API responded with a non-success HTTP status code.
+ #[error("API error {status}: {message}")]
+ Api {
+ /// The HTTP status code returned by the server.
+ status: u16,
+ /// A human-readable description, see [`describe_status`].
+ message: String,
+ },
+
+ /// `/intelligent/search` (or `/phonebook/search`) rejected the search term
+ /// as not being a supported "strong selector" (status == 1 in the API).
+ #[error("invalid search term: the API rejected this term as not a supported selector")]
+ InvalidTerm,
+
+ /// The account has reached its maximum number of concurrent searches.
+ #[error("maximum concurrent searches reached")]
+ MaxConcurrentSearches,
+
+ /// The given search id is unknown to the API (it may have expired or already finished).
+ #[error("search id {0} not found")]
+ SearchNotFound(uuid::Uuid),
+
+ /// A file download response had no usable filename in its `Content-Disposition` header.
+ #[error("download response had no usable filename in the Content-Disposition header")]
+ MissingFilename,
+
+ /// `IntelXClientBuilder::build` was called without an API key.
+ #[error("an API key is required: call `.api_key(..)` on the builder before `.build()`")]
+ MissingApiKey,
+
+ /// A polling/search operation exceeded a caller-supplied deadline.
+ #[error("operation timed out")]
+ Timeout(#[from] tokio::time::error::Elapsed),
+}
+
+/// Maps an HTTP/API status code to a short human-readable description, mirroring the table
+/// used by the Python SDK's `intelx.get_error()`.
+pub fn describe_status(code: u16) -> &'static str {
+ match code {
+ 200 => "200 | Success",
+ 204 => "204 | No Content",
+ 400 => "400 | Bad Request",
+ 401 => "401 | Unauthorized",
+ 402 => "402 | Payment required",
+ 404 => "404 | Not Found",
+ 1 => "1 | Invalid term",
+ _ => "Unknown status code",
+ }
+}
+
+pub(crate) fn api_error_from_status(status: reqwest::StatusCode) -> IntelXError {
+ IntelXError::Api {
+ status: status.as_u16(),
+ message: describe_status(status.as_u16()).to_string(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn describe_status_matches_known_codes() {
+ assert_eq!(describe_status(200), "200 | Success");
+ assert_eq!(describe_status(204), "204 | No Content");
+ assert_eq!(describe_status(400), "400 | Bad Request");
+ assert_eq!(describe_status(401), "401 | Unauthorized");
+ assert_eq!(describe_status(402), "402 | Payment required");
+ assert_eq!(describe_status(404), "404 | Not Found");
+ assert_eq!(describe_status(1), "1 | Invalid term");
+ }
+
+ #[test]
+ fn describe_status_falls_back_for_unmapped_codes() {
+ assert_eq!(describe_status(418), "Unknown status code");
+ }
+
+ #[test]
+ fn api_error_from_status_carries_code_and_message() {
+ let err = api_error_from_status(reqwest::StatusCode::UNAUTHORIZED);
+ match err {
+ IntelXError::Api { status, message } => {
+ assert_eq!(status, 401);
+ assert_eq!(message, "401 | Unauthorized");
+ }
+ other => panic!("expected Api variant, got {other:?}"),
+ }
+ }
+}
diff --git a/Rust/intelx/src/file.rs b/Rust/intelx/src/file.rs
new file mode 100644
index 0000000..3ed53a2
--- /dev/null
+++ b/Rust/intelx/src/file.rs
@@ -0,0 +1,229 @@
+//! File operations: `/file/preview`, `/file/view`, `/file/read`.
+
+use std::path::Path;
+
+use crate::client::IntelXClient;
+use crate::error::{IntelXError, Result, api_error_from_status};
+
+/// `type` parameter for [`IntelXClient::file_read`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FileReadType {
+ /// No content disposition. Returns the raw binary file.
+ Raw = 0,
+ /// Content disposition; may fix line endings to CRLF for text files.
+ ContentDisposition = 1,
+}
+
+/// Parameters for [`IntelXClient::file_preview`], mirroring the Python SDK's `FILE_PREVIEW()`.
+#[derive(Debug, Clone)]
+pub struct FilePreviewParams {
+ /// Low-level content type (`Item::item_type`).
+ pub ctype: i32,
+ /// High-level media type (`Item::media`).
+ pub mediatype: i32,
+ /// Preview format: `0` = text, `1` = picture.
+ pub format: i32,
+ /// Storage ID of the item to preview.
+ pub sid: String,
+ /// Bucket the item was found in. Defaults to empty.
+ pub bucket: String,
+ /// `0` = don't escape HTML, `1` = default (escape).
+ pub escape: i32,
+ /// Maximum number of lines to return. Defaults to `8`.
+ pub lines: i32,
+}
+
+impl FilePreviewParams {
+ /// Creates preview parameters with the same defaults as the Python SDK's `FILE_PREVIEW()`.
+ pub fn new(ctype: i32, mediatype: i32, format: i32, sid: impl Into) -> Self {
+ Self {
+ ctype,
+ mediatype,
+ format,
+ sid: sid.into(),
+ bucket: String::new(),
+ escape: 0,
+ lines: 8,
+ }
+ }
+
+ /// Sets the bucket the item was found in.
+ pub fn bucket(mut self, bucket: impl Into) -> Self {
+ self.bucket = bucket.into();
+ self
+ }
+
+ /// Sets the maximum number of lines to return.
+ pub fn lines(mut self, lines: i32) -> Self {
+ self.lines = lines;
+ self
+ }
+}
+
+/// Picks the `/file/view` format code for a given content/media type, mirroring the Python
+/// SDK's `FILE_VIEW()` if/elif branch.
+///
+/// Pulled out as a pure function so the format-selection logic is unit-testable without an
+/// HTTP call.
+pub(crate) fn resolve_view_format(ctype: i32, mediatype: i32) -> i32 {
+ match mediatype {
+ 23 | 9 => 7, // HTML
+ 15 => 6, // PDF
+ 16 => 8, // Word
+ 18 => 10, // PowerPoint
+ 25 => 11, // Ebook
+ 17 => 9, // Excel
+ _ if ctype == 1 => 0, // Text
+ _ => 1, // Hex view fallback
+ }
+}
+
+impl IntelXClient {
+ /// Shows a preview of a file's contents based on its storage ID. Previews are capped at
+ /// 1000 characters server-side. Mirrors the Python SDK's `FILE_PREVIEW()`.
+ pub async fn file_preview(&self, params: FilePreviewParams) -> Result {
+ self.rate_limit_sleep().await;
+ let response = self
+ .get_response(
+ "/file/preview",
+ &[
+ ("c", params.ctype.to_string()),
+ ("m", params.mediatype.to_string()),
+ ("f", params.format.to_string()),
+ ("sid", params.sid),
+ ("b", params.bucket),
+ ("e", params.escape.to_string()),
+ ("l", params.lines.to_string()),
+ ],
+ )
+ .await?;
+ Ok(response.text().await?)
+ }
+
+ /// Shows a file's contents based on its storage ID, auto-selecting the view format from
+ /// the item's content/media type. Mirrors the Python SDK's `FILE_VIEW()`.
+ pub async fn file_view(
+ &self,
+ ctype: i32,
+ mediatype: i32,
+ sid: &str,
+ bucket: &str,
+ escape: i32,
+ ) -> Result {
+ self.rate_limit_sleep().await;
+ let format = resolve_view_format(ctype, mediatype);
+ let response = self
+ .get_response(
+ "/file/view",
+ &[
+ ("f", format.to_string()),
+ ("storageid", sid.to_string()),
+ ("bucket", bucket.to_string()),
+ ("escape", escape.to_string()),
+ ],
+ )
+ .await?;
+ Ok(response.text().await?)
+ }
+
+ /// Reads a file's raw contents and streams it to `dest`. Use this for direct data download.
+ /// Returns the number of bytes written. Mirrors the Python SDK's `FILE_READ()`.
+ pub async fn file_read(
+ &self,
+ system_id: &str,
+ kind: FileReadType,
+ bucket: &str,
+ dest: &Path,
+ ) -> Result {
+ self.rate_limit_sleep().await;
+ let response = self
+ .get_response(
+ "/file/read",
+ &[
+ ("type", (kind as i32).to_string()),
+ ("systemid", system_id.to_string()),
+ ("bucket", bucket.to_string()),
+ ],
+ )
+ .await?;
+
+ if !response.status().is_success() {
+ return Err(api_error_from_status(response.status()));
+ }
+
+ let bytes = response.bytes().await?;
+ tokio::fs::write(dest, &bytes).await?;
+ Ok(bytes.len() as u64)
+ }
+
+ /// Shows a treeview of an item that has multiple files/folders. Returns `None` if the
+ /// server reports it could not generate a tree (matching Python's `"Could not generate"`
+ /// substring check). Mirrors the Python SDK's `FILE_TREE_VIEW()`.
+ pub async fn file_tree_view(&self, sid: &str) -> Result