diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0bc676c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 + +updates: + # Rust / Cargo dependencies + - package-ecosystem: cargo + directory: / + schedule: + interval: daily + open-pull-requests-limit: 15 + groups: + rust-minor-patch: + applies-to: version-updates + update-types: + - minor + - patch + labels: + - dependencies + - rust + + # GitHub Actions — supply-chain risk for CI pipelines + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + labels: + - dependencies + - ci diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d4a3006 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: "1.93.1" + components: rustfmt, clippy + + - name: Check formatting + run: cargo fmt --check + + - name: Clippy (all features) + run: cargo clippy --all-features -- -D warnings + + - name: Clippy (desktop only, no WASM) + run: cargo clippy --no-default-features --features desktop -- -D warnings + + - name: Test + run: cargo test + + - name: Test (desktop feature) + run: cargo test --features desktop + + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 diff --git a/.github/workflows/upstream-extism-watch.yml b/.github/workflows/upstream-extism-watch.yml new file mode 100644 index 0000000..e4e13e4 --- /dev/null +++ b/.github/workflows/upstream-extism-watch.yml @@ -0,0 +1,39 @@ +name: Upstream Extism Watch + +on: + schedule: + - cron: "0 15 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-fixed-extism: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Explain watch condition + run: | + cat >> "$GITHUB_STEP_SUMMARY" <<'EOF' + This job tests whether a newer released Extism dependency clears the + temporary Wasmtime advisory ignores tracked in #2. If `cargo-deny` + fails with unused ignored advisories after the lockfile update, + update Extism, remove the stale ignores from `deny.toml`, and close #2. + EOF + + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: "1.93.1" + + - name: Try latest Extism-compatible lockfile + run: | + set -euxo pipefail + cargo update -p extism -p extism-convert -p extism-convert-macros -p extism-manifest + + - name: Check advisories after Extism update + uses: EmbarkStudios/cargo-deny-action@v2 + with: + rust-version: "1.93.1" + command: check advisories diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..614cea5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +.DS_Store +.idea/ +*.swp diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0603fba --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,27 @@ +# 1Password Rust SDK + +## Build Commands + +- `cargo fmt --check` — check formatting +- `cargo clippy -- -D warnings` — lint +- `cargo test` — run tests +- `cargo build` — build (default features) +- `cargo build --features desktop` — build with desktop app integration + +## Conventions + +- Edition 2024, Rust 1.93.1 +- `#![deny(unsafe_code)]` at crate root; `#[allow(unsafe_code)]` only on FFI modules +- `thiserror` for error types +- `serde` for all JSON serialization +- Inline `#[cfg(test)] mod tests` blocks +- Conventional commits: `type(scope): description` +- Run `cargo fmt --check && cargo clippy -- -D warnings && cargo test` before pushing +- Do NOT add `Co-Authored-By: Claude` lines to commit messages + +## Architecture + +Typed wrapper around an opaque WASM core binary. All API calls serialize params to JSON, +call `Core::invoke()`, and deserialize the response. Two core backends: +- `ExtismCore` (default) — embedded WASM via Extism +- `SharedLibCore` (feature: `desktop`) — native shared library from 1Password desktop app diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1c9db9d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3226 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[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 = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] + +[[package]] +name = "cbindgen" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799" +dependencies = [ + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "tempfile", + "toml", +] + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" + +[[package]] +name = "cranelift-control" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa" + +[[package]] +name = "cranelift-native" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "extism" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed8c5859bdab81d2eb4cd963eeacd8031d353b1ffb2fde43ee9179a0d6295120" +dependencies = [ + "anyhow", + "async-trait", + "cbindgen", + "extism-convert", + "extism-manifest", + "glob", + "libc", + "serde", + "serde_json", + "sha2", + "toml", + "tracing", + "tracing-subscriber", + "ureq", + "url", + "uuid", + "wasi-common", + "wasmtime", + "wiggle", +] + +[[package]] +name = "extism-convert" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1a8eac059a1730a21aa47f99a0c2075ba0ab88fd0c4e52e35027cf99cdf3e7" +dependencies = [ + "anyhow", + "base64", + "bytemuck", + "extism-convert-macros", + "prost", + "rmp-serde", + "serde", + "serde_json", +] + +[[package]] +name = "extism-convert-macros" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848f105dd6e1af2ea4bb4a76447658e8587167df3c4e4658c4258e5b14a5b051" +dependencies = [ + "manyhow", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "extism-manifest" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953a22ad322939ae4567ec73a34913a3a43dcbdfa648b8307d38fe56bb3a0acd" +dependencies = [ + "base64", + "serde", + "serde_json", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[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 = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[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-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-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[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-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + +[[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 = "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 = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "onepassword-sdk" +version = "0.4.1-beta.1" +dependencies = [ + "base64", + "chrono", + "extism", + "getrandom 0.2.17", + "libloading", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[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 = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pulley-interpreter" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9812652c1feb63cf39f8780cecac154a32b22b3665806c733cd4072547233a4" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56000349b6896e3d44286eb9c330891237f40b27fd43c1ccc84547d0b463cb40" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "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 = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[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 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "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 = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[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.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +dependencies = [ + "indexmap", + "toml_datetime 1.1.0+spec-1.1.0", + "toml_parser", + "winnow 1.0.1", +] + +[[package]] +name = "toml_parser" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +dependencies = [ + "winnow 1.0.1", +] + +[[package]] +name = "toml_writer" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-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 = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[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-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi-common" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49ffbbd04665d04028f66aee8f24ae7a1f46063f59a28fddfa52ca3091754a2" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "cap-fs-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "io-extras", + "io-lifetimes", + "log", + "rustix 1.1.4", + "system-interface", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af801b6f36459023eaec63fdbaedad2fd5a4ab7dc74ecc110a8b5d375c5775e4" +dependencies = [ + "anyhow", + "heck", + "im-rc", + "indexmap", + "log", + "petgraph", + "serde", + "serde_derive", + "serde_yaml", + "smallvec", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" +dependencies = [ + "leb128fmt", + "wasmparser 0.243.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasmparser" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.243.0", +] + +[[package]] +name = "wasmtime" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a83182bf04af87571b4c642300479501684f26bab5597f68f68cded5b098fd" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "hashbrown 0.15.5", + "indexmap", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb201c41aa23a3642365cfb2e4a183573d85127a3c9d528f56b9997c984541ab" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", + "wasmprinter", + "wasmtime-internal-component-util", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5b3069d1a67ba5969d0eb1ccd7e141367d4e713f4649aa90356c98e8f19bea" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix 1.1.4", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c924400db7b6ca996fef1b23beb0f41d5c809836b1ec60fc25b4057e2d25d9b" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.243.0", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3f65daf4bf3d74ca2fbbe20af0589c42e2b398a073486451425d94fd4afef4" + +[[package]] +name = "wasmtime-internal-cranelift" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633e889cdae76829738db0114ab3b02fce51ea4a1cd9675a67a65fce92e8b418" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-math", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb126adc5d0c72695cfb77260b357f1b81705a0f8fa30b3944e7c2219c17341" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e66ff7f90a8002187691ff6237ffd09f954a0ebb9de8b2ff7f5c62632134120" +dependencies = [ + "cc", + "object", + "rustix 1.1.4", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-math" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-internal-slab" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b63cbea1c0192c7feb7c0dfb35f47166988a3742f29f46b585ef57246c65764" + +[[package]] +name = "wasmtime-internal-unwinder" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c392c7e5fb891a7416e3c34cfbd148849271e8c58744fda875dde4bec4d6a" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f8b9796a3f0451a7b702508b303d654de640271ac80287176de222f187a237" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0063e61f1d0b2c20e9cfc58361a6513d074a23c80b417aac3033724f51648a0" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "587699ca7cae16b4a234ffcc834f37e75675933d533809919b52975f5609e2ef" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser 0.243.0", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "245.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cf1149285569120b8ce39db8b465e8a2b55c34cbb586bd977e43e2bc7300bf" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.245.1", +] + +[[package]] +name = "wat" +version = "1.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd48d1679b6858988cb96b154dda0ec5bbb09275b71db46057be37332d5477be" +dependencies = [ + "wast 245.0.1", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wiggle" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69a60bcbe1475c5dc9ec89210ade54823d44f742e283cba64f98f89697c4cec" +dependencies = [ + "anyhow", + "bitflags", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wiggle-macro", + "witx", +] + +[[package]] +name = "wiggle-generate" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f3dc0fd4dcfc7736434bb216179a2147835309abc09bf226736a40d484548f" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "syn", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea2aea744eded58ae092bf57110c27517dab7d5a300513ff13897325c5c5021" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wiggle-generate", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winch-codegen" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55de3ac5b8bd71e5f6c87a9e511dd3ceb194bdb58183c6a7bf21cd8c0e46fbc" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[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", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.243.0", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..982988b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "onepassword-sdk" +version = "0.4.1-beta.1" +edition = "2024" +publish = false +license = "MIT" +description = "1Password SDK for Rust" + +[features] +default = ["wasm"] +wasm = ["dep:extism", "dep:getrandom", "dep:chrono"] +desktop = ["dep:libloading", "dep:base64"] + +[dependencies] +base64 = { version = "0.22", optional = true } +chrono = { version = "0.4", default-features = false, features = ["clock"], optional = true } +extism = { version = "1", optional = true } +getrandom = { version = "0.2", optional = true } +libloading = { version = "0.8", optional = true } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" + +[dev-dependencies] diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..3fd9516 --- /dev/null +++ b/deny.toml @@ -0,0 +1,43 @@ +[graph] +all-features = true + +[advisories] +unmaintained = "all" +unused-ignored-advisory = "deny" +ignore = [ + { id = "RUSTSEC-2026-0085", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0086", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0087", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0088", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0089", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0091", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0092", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0093", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0094", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0095", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0096", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, + { id = "RUSTSEC-2026-0114", reason = "Upstream-blocked by extism 1.21.0's wasmtime ^41 dependency. The SDK runs vendor-provided 1Password core.wasm only; remove when extism/extism#901 is released." }, +] + +[bans] +multiple-versions = "warn" +wildcards = "deny" + +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "CDLA-Permissive-2.0", + "ISC", + "MPL-2.0", + "Unicode-3.0", + "Zlib", +] +confidence-threshold = 0.8 + +[sources] +unknown-registry = "deny" +unknown-git = "deny" diff --git a/docs/superpowers/plans/2026-03-30-onepassword-sdk-rust.md b/docs/superpowers/plans/2026-03-30-onepassword-sdk-rust.md new file mode 100644 index 0000000..b69f8c7 --- /dev/null +++ b/docs/superpowers/plans/2026-03-30-onepassword-sdk-rust.md @@ -0,0 +1,2688 @@ +# 1Password Rust SDK Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Port the 1Password Go SDK (v0.4.1-beta.1) to idiomatic Rust with full API parity. + +**Architecture:** Typed Rust wrapper around the same `core.wasm` binary used by Go/Python/JS SDKs. JSON-based RPC to the WASM core via Extism host SDK. Desktop app integration via `libloading` behind a `desktop` feature flag. + +**Tech Stack:** Rust 2024 edition, extism, serde/serde_json, thiserror, libloading (desktop feature), getrandom, chrono + +--- + +## File Structure + +| File | Responsibility | +|---|---| +| `Cargo.toml` | Crate manifest with `desktop` feature flag | +| `rust-toolchain.toml` | Pin Rust 1.93.1, rustfmt, clippy | +| `deny.toml` | License/advisory/source auditing | +| `.gitignore` | Ignore target/, IDE files | +| `CLAUDE.md` | Developer conventions | +| `src/lib.rs` | Crate root, `#![deny(unsafe_code)]`, public re-exports | +| `src/errors.rs` | `SdkError` enum via thiserror | +| `src/core.rs` | `Core` trait + `InnerClient` + `InvokeConfig` structs | +| `src/core_extism.rs` | `ExtismCore` — WASM runtime + host functions | +| `src/core_shared_lib.rs` | `SharedLibCore` — native FFI (desktop feature, cfg unix/windows) | +| `src/client.rs` | `Client` struct + `ClientBuilder` | +| `src/types.rs` | All domain types (structs, enums, serde) | +| `src/secrets.rs` | `SecretsApi` trait + `SecretsSource` impl | +| `src/items.rs` | `ItemsApi` trait + `ItemsSource` impl | +| `src/items_files.rs` | `ItemsFilesApi` trait + impl | +| `src/items_shares.rs` | `ItemsSharesApi` trait + impl | +| `src/vaults.rs` | `VaultsApi` trait + `VaultsSource` impl | +| `src/groups.rs` | `GroupsApi` trait + `GroupsSource` impl | +| `src/environments.rs` | `EnvironmentsApi` trait + impl | +| `wasm/core.wasm` | Embedded WASM binary (~9.1MB) | +| `examples/service_account.rs` | Service account usage example | +| `examples/desktop_app.rs` | Desktop app usage example | +| `.github/workflows/ci.yml` | CI: fmt, clippy, deny, test | + +--- + +### Task 1: Project Scaffolding + +**Files:** +- Create: `Cargo.toml` +- Create: `rust-toolchain.toml` +- Create: `deny.toml` +- Create: `.gitignore` +- Create: `CLAUDE.md` +- Create: `src/lib.rs` + +- [ ] **Step 1: Create `Cargo.toml`** + +```toml +[package] +name = "onepassword-sdk" +version = "0.4.1-beta.1" +edition = "2024" +publish = false +license = "MIT" +description = "1Password SDK for Rust" + +[features] +default = [] +desktop = ["dep:libloading"] + +[dependencies] +chrono = { version = "0.4", default-features = false, features = ["clock"] } +extism = "1" +getrandom = "0.2" +libloading = { version = "0.8", optional = true } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" + +[dev-dependencies] +``` + +- [ ] **Step 2: Create `rust-toolchain.toml`** + +```toml +[toolchain] +channel = "1.93.1" +components = ["rustfmt", "clippy"] +``` + +- [ ] **Step 3: Create `deny.toml`** + +```toml +[graph] +all-features = true + +[advisories] +unmaintained = "warn" + +[bans] +multiple-versions = "warn" +wildcards = "deny" + +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-3.0", + "Unicode-DFS-2016", + "OpenSSL", + "Zlib", +] +confidence-threshold = 0.8 + +[[licenses.exceptions]] +allow = ["MPL-2.0"] +crate = "webpki-roots" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +``` + +- [ ] **Step 4: Create `.gitignore`** + +``` +/target +.DS_Store +.idea/ +*.swp +``` + +- [ ] **Step 5: Create `CLAUDE.md`** + +```markdown +# 1Password Rust SDK + +## Build Commands + +- `cargo fmt --check` — check formatting +- `cargo clippy -- -D warnings` — lint +- `cargo test` — run tests +- `cargo build` — build (default features) +- `cargo build --features desktop` — build with desktop app integration + +## Conventions + +- Edition 2024, Rust 1.93.1 +- `#![deny(unsafe_code)]` at crate root; `#[allow(unsafe_code)]` only on FFI modules +- `thiserror` for error types +- `serde` for all JSON serialization +- Inline `#[cfg(test)] mod tests` blocks +- Conventional commits: `type(scope): description` +- Run `cargo fmt --check && cargo clippy -- -D warnings && cargo test` before pushing + +## Architecture + +Typed wrapper around an opaque WASM core binary. All API calls serialize params to JSON, +call `Core::invoke()`, and deserialize the response. Two core backends: +- `ExtismCore` (default) — embedded WASM via Extism +- `SharedLibCore` (feature: `desktop`) — native shared library from 1Password desktop app +``` + +- [ ] **Step 6: Create `src/lib.rs`** + +```rust +#![deny(unsafe_code)] + +mod client; +mod core; +mod core_extism; +mod errors; +mod types; + +mod secrets; +mod items; +mod items_files; +mod items_shares; +mod vaults; +mod groups; +mod environments; + +#[cfg(feature = "desktop")] +mod core_shared_lib; + +pub use client::{Client, ClientBuilder}; +pub use errors::SdkError; +pub use types::*; + +pub use secrets::SecretsApi; +pub use items::ItemsApi; +pub use items_files::ItemsFilesApi; +pub use items_shares::ItemsSharesApi; +pub use vaults::VaultsApi; +pub use groups::GroupsApi; +pub use environments::EnvironmentsApi; +``` + +- [ ] **Step 7: Verify it compiles (will fail — stubs needed, but Cargo.toml is valid)** + +Run: `cargo check 2>&1 | head -5` +Expected: Errors about missing modules (this is fine — we'll fill them in) + +- [ ] **Step 8: Commit** + +```bash +git add Cargo.toml rust-toolchain.toml deny.toml .gitignore CLAUDE.md src/lib.rs +git commit -m "feat: scaffold project with Cargo.toml, toolchain, and lib.rs" +``` + +--- + +### Task 2: Error Types + +**Files:** +- Create: `src/errors.rs` + +- [ ] **Step 1: Write test for error deserialization** + +Add to `src/errors.rs`: + +```rust +use serde::Deserialize; +use serde_json; + +#[derive(Debug, thiserror::Error)] +pub enum SdkError { + #[error("1Password SDK error: {message}")] + Core { name: String, message: String }, + + #[error("serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("WASM plugin error: {0}")] + Plugin(String), + + #[error("configuration error: {0}")] + Config(String), + + #[error("desktop session expired: {0}")] + DesktopSessionExpired(String), + + #[error("rate limit exceeded: {0}")] + RateLimitExceeded(String), + + #[error("shared library error: {0}")] + SharedLib(String), +} + +#[derive(Deserialize)] +struct CoreError { + name: String, + message: String, +} + +/// Deserialize a JSON error string from the WASM core into an SdkError. +/// Matches Go SDK's `unmarshalError` behavior. +pub(crate) fn unmarshal_error(err: &str) -> SdkError { + match serde_json::from_str::(err) { + Ok(core_err) => match core_err.name.as_str() { + "DesktopSessionExpired" => SdkError::DesktopSessionExpired(core_err.message), + "RateLimitExceeded" => SdkError::RateLimitExceeded(core_err.message), + _ => SdkError::Core { + name: core_err.name, + message: core_err.message, + }, + }, + Err(_) => SdkError::Core { + name: "Unknown".to_string(), + message: err.to_string(), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unmarshal_core_error() { + let err = unmarshal_error(r#"{"name":"SomeError","message":"something broke"}"#); + match err { + SdkError::Core { name, message } => { + assert_eq!(name, "SomeError"); + assert_eq!(message, "something broke"); + } + _ => panic!("expected Core error"), + } + } + + #[test] + fn unmarshal_session_expired() { + let err = unmarshal_error(r#"{"name":"DesktopSessionExpired","message":"session gone"}"#); + assert!(matches!(err, SdkError::DesktopSessionExpired(_))); + } + + #[test] + fn unmarshal_rate_limit() { + let err = unmarshal_error(r#"{"name":"RateLimitExceeded","message":"slow down"}"#); + assert!(matches!(err, SdkError::RateLimitExceeded(_))); + } + + #[test] + fn unmarshal_invalid_json() { + let err = unmarshal_error("not json at all"); + match err { + SdkError::Core { message, .. } => assert_eq!(message, "not json at all"), + _ => panic!("expected Core error for invalid JSON"), + } + } +} +``` + +- [ ] **Step 2: Verify tests pass** + +Run: `cargo test -p onepassword-sdk errors::tests 2>&1` +Expected: Will fail until other modules exist. We'll verify after Task 3. + +- [ ] **Step 3: Commit** + +```bash +git add src/errors.rs +git commit -m "feat: add SdkError type and unmarshal_error" +``` + +--- + +### Task 3: Core Trait and Internal Types + +**Files:** +- Create: `src/core.rs` + +- [ ] **Step 1: Write the Core trait and supporting types** + +```rust +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::errors::SdkError; + +pub(crate) const SDK_LANGUAGE: &str = "Rust"; +pub(crate) const DEFAULT_REQUEST_LIBRARY: &str = "reqwest"; +pub(crate) const MESSAGE_LIMIT: usize = 50 * 1024 * 1024; + +pub(crate) trait Core: Send + Sync { + fn init_client(&self, config: &[u8]) -> Result, SdkError>; + fn invoke(&self, invoke_config: &[u8]) -> Result, SdkError>; + fn release_client(&self, client_id: &[u8]); +} + +#[derive(Debug, Serialize)] +pub(crate) struct ClientConfig { + #[serde(rename = "serviceAccountToken")] + pub sa_token: String, + #[serde(rename = "programmingLanguage")] + pub language: String, + #[serde(rename = "sdkVersion")] + pub sdk_version: String, + #[serde(rename = "integrationName")] + pub integration_name: String, + #[serde(rename = "integrationVersion")] + pub integration_version: String, + #[serde(rename = "requestLibraryName")] + pub request_library_name: String, + #[serde(rename = "requestLibraryVersion")] + pub request_library_version: String, + #[serde(rename = "os")] + pub system_os: String, + #[serde(rename = "osVersion")] + pub system_os_version: String, + #[serde(rename = "architecture")] + pub system_arch: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub account_name: Option, +} + +impl ClientConfig { + pub fn new_default() -> Self { + Self { + sa_token: String::new(), + language: SDK_LANGUAGE.to_string(), + sdk_version: env!("CARGO_PKG_VERSION").to_string(), + integration_name: "Unknown".to_string(), + integration_version: "Unknown".to_string(), + request_library_name: DEFAULT_REQUEST_LIBRARY.to_string(), + request_library_version: "0.0.0".to_string(), + system_os: std::env::consts::OS.to_string(), + system_os_version: "0.0.0".to_string(), + system_arch: std::env::consts::ARCH.to_string(), + account_name: None, + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct InvokeConfig { + pub invocation: Invocation, +} + +#[derive(Debug, Serialize)] +pub(crate) struct Invocation { + #[serde(rename = "clientId", skip_serializing_if = "Option::is_none")] + pub client_id: Option, + pub parameters: Parameters, +} + +#[derive(Debug, Serialize)] +pub(crate) struct Parameters { + pub name: String, + pub parameters: HashMap, +} + +/// Wraps a Core implementation and handles JSON marshaling/unmarshaling. +pub(crate) struct CoreWrapper { + pub inner: Box, +} + +impl CoreWrapper { + pub fn init_client(&self, config: &ClientConfig) -> Result { + let config_bytes = serde_json::to_vec(config)?; + let res = self.inner.init_client(&config_bytes)?; + let id: u64 = serde_json::from_slice(&res)?; + Ok(id) + } + + pub fn invoke(&self, invoke_config: &InvokeConfig) -> Result { + let input = serde_json::to_vec(invoke_config)?; + if input.len() > MESSAGE_LIMIT { + return Err(SdkError::Config(format!( + "message size exceeds the limit of {} bytes", + MESSAGE_LIMIT + ))); + } + let res = self.inner.invoke(&input)?; + Ok(String::from_utf8_lossy(&res).into_owned()) + } + + pub fn release_client(&self, client_id: u64) { + if let Ok(id_bytes) = serde_json::to_vec(&client_id) { + self.inner.release_client(&id_bytes); + } + } +} + +/// The inner client state shared by all API implementations. +pub(crate) struct InnerClient { + pub id: u64, + pub config: ClientConfig, + pub core: CoreWrapper, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_config_has_correct_language() { + let config = ClientConfig::new_default(); + assert_eq!(config.language, "Rust"); + assert_eq!(config.system_os, std::env::consts::OS); + assert_eq!(config.system_arch, std::env::consts::ARCH); + } + + #[test] + fn invoke_config_serializes_correctly() { + let config = InvokeConfig { + invocation: Invocation { + client_id: Some(42), + parameters: Parameters { + name: "SecretsResolve".to_string(), + parameters: { + let mut m = HashMap::new(); + m.insert( + "secret_reference".to_string(), + serde_json::Value::String("op://vault/item/field".to_string()), + ); + m + }, + }, + }, + }; + let json = serde_json::to_string(&config).unwrap(); + assert!(json.contains("\"clientId\":42")); + assert!(json.contains("\"name\":\"SecretsResolve\"")); + } +} +``` + +- [ ] **Step 2: Verify tests pass** + +Run: `cargo test core::tests` +Expected: PASS (2 tests) + +- [ ] **Step 3: Commit** + +```bash +git add src/core.rs +git commit -m "feat: add Core trait, ClientConfig, InvokeConfig, CoreWrapper" +``` + +--- + +### Task 4: Domain Types + +**Files:** +- Create: `src/types.rs` + +This is the largest file — all domain types ported from Go's `types.go`. The Go version uses manual JSON marshal/unmarshal for tagged unions; Rust uses native `#[serde(tag, content)]` enums. + +- [ ] **Step 1: Write all simple struct types** + +Create `src/types.rs` with all struct types, string enums, and constants. The full content is: + +```rust +use serde::{Deserialize, Serialize}; + +// --- Error message type --- + +pub type ErrorMessage = String; + +// --- Address --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddressFieldDetails { + pub street: String, + pub city: String, + pub country: String, + pub zip: String, + pub state: String, +} + +// --- Documents & Files --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DocumentCreateParams { + pub name: String, + pub content: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileAttributes { + pub name: String, + pub id: String, + pub size: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileCreateParams { + pub name: String, + pub content: Vec, + pub section_id: String, + pub field_id: String, +} + +// --- Environments --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnvironmentVariable { + pub name: String, + pub value: String, + pub masked: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetVariablesResponse { + pub variables: Vec, +} + +// --- Password generation --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GeneratePasswordResponse { + pub password: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SeparatorType { + Digits, + DigitsAndSymbols, + Spaces, + Hyphens, + Underscores, + Periods, + Commas, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum WordListType { + FullWords, + Syllables, + ThreeLetters, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasswordRecipeMemorableInner { + pub separator_type: SeparatorType, + pub capitalize: bool, + pub word_list_type: WordListType, + pub word_count: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PasswordRecipePinInner { + pub length: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasswordRecipeRandomInner { + pub include_digits: bool, + pub include_symbols: bool, + pub length: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "parameters")] +pub enum PasswordRecipe { + Memorable(PasswordRecipeMemorableInner), + Pin(PasswordRecipePinInner), + Random(PasswordRecipeRandomInner), +} + +// --- SSH Keys --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SSHKeyAttributes { + pub public_key: String, + pub fingerprint: String, + pub key_type: String, +} + +// --- OTP --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OTPFieldDetails { + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, +} + +// --- Item field details (tagged enum) --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "content")] +pub enum ItemFieldDetails { + Otp(OTPFieldDetails), + SshKey(SSHKeyAttributes), + Address(AddressFieldDetails), +} + +// --- Item categories --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ItemCategory { + Login, + SecureNote, + CreditCard, + CryptoWallet, + Identity, + Password, + Document, + ApiCredentials, + BankAccount, + Database, + DriverLicense, + Email, + MedicalRecord, + Membership, + OutdoorLicense, + Passport, + Rewards, + Router, + Server, + SshKey, + SocialSecurityNumber, + SoftwareLicense, + Person, + Unsupported, +} + +// --- Item field types --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ItemFieldType { + Text, + Concealed, + CreditCardType, + CreditCardNumber, + Phone, + Url, + Totp, + Email, + Reference, + SshKey, + Menu, + MonthYear, + Address, + Date, + Unsupported, +} + +// --- Item state --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ItemState { + Active, + Archived, +} + +// --- Autofill --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AutofillBehavior { + AnywhereOnWebsite, + ExactDomain, + Never, +} + +// --- Website --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Website { + pub url: String, + pub label: String, + pub autofill_behavior: AutofillBehavior, +} + +// --- Item field --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemField { + pub id: String, + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub section_id: Option, + pub field_type: ItemFieldType, + pub value: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +// --- Item section --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemSection { + pub id: String, + pub title: String, +} + +// --- Item file --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemFile { + pub attributes: FileAttributes, + pub section_id: String, + pub field_id: String, +} + +// --- Item --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Item { + pub id: String, + pub title: String, + pub category: ItemCategory, + pub vault_id: String, + pub fields: Vec, + pub sections: Vec, + pub notes: String, + pub tags: Vec, + pub websites: Vec, + pub version: u32, + pub files: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub document: Option, + pub created_at: String, + pub updated_at: String, +} + +// --- Item create params --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemCreateParams { + pub category: ItemCategory, + pub vault_id: String, + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sections: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub notes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub websites: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub files: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub document: Option, +} + +// --- Item overview --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemOverview { + pub id: String, + pub title: String, + pub category: ItemCategory, + pub vault_id: String, + pub websites: Vec, + pub tags: Vec, + pub created_at: String, + pub updated_at: String, + pub state: ItemState, +} + +// --- Item list filter --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemListFilterByStateInner { + pub active: bool, + pub archived: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "content")] +pub enum ItemListFilter { + ByState(ItemListFilterByStateInner), +} + +// --- Item sharing --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ItemShareDuration { + OneHour, + OneDay, + SevenDays, + FourteenDays, + ThirtyDays, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AllowedType { + Authenticated, + Public, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AllowedRecipientType { + Email, + Domain, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemShareFiles { + pub allowed: bool, + pub max_size: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_types: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_recipient_types: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_expiry: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_expiry: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_views: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemShareAccountPolicy { + pub max_expiry: ItemShareDuration, + pub default_expiry: ItemShareDuration, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_views: Option, + pub allowed_types: Vec, + pub allowed_recipient_types: Vec, + pub files: ItemShareFiles, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidRecipientEmailInner { + pub email: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidRecipientDomainInner { + pub domain: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "parameters")] +pub enum ValidRecipient { + Email(ValidRecipientEmailInner), + Domain(ValidRecipientDomainInner), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemShareParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub recipients: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after: Option, + pub one_time_only: bool, +} + +// --- Batch response types --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +// --- Item update failure reasons --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "message")] +#[serde(rename_all = "camelCase")] +pub enum ItemUpdateFailureReason { + ItemValidationError(ErrorMessage), + ItemStatusPermissionError, + ItemStatusIncorrectItemVersion, + ItemStatusFileNotFound, + ItemStatusTooBig, + ItemNotFound, + Internal(ErrorMessage), +} + +// --- Items batch responses --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemsDeleteAllResponse { + pub individual_responses: + std::collections::HashMap>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ItemsGetAllError { + #[serde(rename = "itemNotFound")] + ItemNotFound, + #[serde(rename = "internal")] + Internal(ErrorMessage), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemsGetAllResponse { + pub individual_responses: Vec>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemsUpdateAllResponse { + pub individual_responses: Vec>, +} + +// --- Secrets --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolvedReference { + pub secret: String, + pub item_id: String, + pub vault_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "message")] +#[serde(rename_all = "camelCase")] +pub enum ResolveReferenceError { + Parsing(ErrorMessage), + FieldNotFound, + VaultNotFound, + TooManyVaults, + ItemNotFound, + TooManyItems, + TooManyMatchingFields, + NoMatchingSections, + IncompatibleTOTPQueryParameterField, + #[serde(rename = "unableToGenerateTotpCode")] + UnableToGenerateTOTPCode(ErrorMessage), + #[serde(rename = "sSHKeyMetadataNotFound")] + SSHKeyMetadataNotFound, + UnsupportedFileFormat, + #[serde(rename = "incompatibleSshKeyQueryParameterField")] + IncompatibleSSHKeyQueryParameterField, + UnableToParsePrivateKey, + #[serde(rename = "unableToFormatPrivateKeyToOpenSsh")] + UnableToFormatPrivateKeyToOpenSSH, + Other, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolveAllResponse { + pub individual_responses: + std::collections::HashMap>, +} + +// --- Groups --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum GroupType { + Owners, + Administrators, + Recovery, + ExternalAccountManagers, + TeamMembers, + UserDefined, + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum GroupState { + Active, + Deleted, + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum VaultAccessorType { + User, + Group, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultAccess { + pub vault_uuid: String, + pub accessor_type: VaultAccessorType, + pub accessor_uuid: String, + pub permissions: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Group { + pub id: String, + pub title: String, + pub description: String, + pub group_type: GroupType, + pub state: GroupState, + #[serde(skip_serializing_if = "Option::is_none")] + pub vault_access: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupAccess { + pub group_id: String, + pub permissions: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupGetParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub vault_permissions: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupVaultAccess { + pub vault_id: String, + pub group_id: String, + pub permissions: u32, +} + +// --- Vaults --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum VaultType { + Personal, + Everyone, + Transfer, + UserCreated, + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Vault { + pub id: String, + pub title: String, + pub description: String, + pub vault_type: VaultType, + pub active_item_count: u32, + pub content_version: u32, + pub attribute_version: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub access: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultCreateParams { + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_admins_access: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultGetParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub accessors: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultListParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub decrypt_details: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultOverview { + pub id: String, + pub title: String, + pub description: String, + pub vault_type: VaultType, + pub active_item_count: u32, + pub content_version: u32, + pub attribute_version: u32, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultUpdateParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +// --- Permissions constants --- + +pub mod permissions { + pub const READ_ITEMS: u32 = 32; + pub const REVEAL_ITEM_PASSWORD: u32 = 16; + pub const UPDATE_ITEMS: u32 = 64; + pub const CREATE_ITEMS: u32 = 128; + pub const ARCHIVE_ITEMS: u32 = 256; + pub const DELETE_ITEMS: u32 = 512; + pub const UPDATE_ITEM_HISTORY: u32 = 1024; + pub const SEND_ITEMS: u32 = 1_048_576; + pub const IMPORT_ITEMS: u32 = 2_097_152; + pub const EXPORT_ITEMS: u32 = 4_194_304; + pub const PRINT_ITEMS: u32 = 8_388_608; + pub const MANAGE_VAULT: u32 = 2; + pub const RECOVER_VAULT: u32 = 1; + pub const NO_ACCESS: u32 = 0; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn password_recipe_serializes_as_tagged_enum() { + let recipe = PasswordRecipe::Random(PasswordRecipeRandomInner { + include_digits: true, + include_symbols: false, + length: 20, + }); + let json = serde_json::to_string(&recipe).unwrap(); + assert!(json.contains(r#""type":"Random"#)); + assert!(json.contains(r#""parameters":#)); + } + + #[test] + fn item_field_details_roundtrip() { + let details = ItemFieldDetails::Otp(OTPFieldDetails { + code: Some("123456".to_string()), + error_message: None, + }); + let json = serde_json::to_string(&details).unwrap(); + let parsed: ItemFieldDetails = serde_json::from_str(&json).unwrap(); + match parsed { + ItemFieldDetails::Otp(otp) => assert_eq!(otp.code.unwrap(), "123456"), + _ => panic!("expected Otp variant"), + } + } + + #[test] + fn valid_recipient_roundtrip() { + let recipient = ValidRecipient::Email(ValidRecipientEmailInner { + email: "test@example.com".to_string(), + }); + let json = serde_json::to_string(&recipient).unwrap(); + assert!(json.contains(r#""type":"Email"#)); + let parsed: ValidRecipient = serde_json::from_str(&json).unwrap(); + match parsed { + ValidRecipient::Email(inner) => assert_eq!(inner.email, "test@example.com"), + _ => panic!("expected Email variant"), + } + } + + #[test] + fn item_list_filter_roundtrip() { + let filter = ItemListFilter::ByState(ItemListFilterByStateInner { + active: true, + archived: false, + }); + let json = serde_json::to_string(&filter).unwrap(); + let parsed: ItemListFilter = serde_json::from_str(&json).unwrap(); + match parsed { + ItemListFilter::ByState(inner) => { + assert!(inner.active); + assert!(!inner.archived); + } + } + } +} +``` + +- [ ] **Step 2: Run tests** + +Run: `cargo test types::tests` +Expected: PASS (4 tests) + +- [ ] **Step 3: Commit** + +```bash +git add src/types.rs +git commit -m "feat: add all domain types with serde support" +``` + +--- + +### Task 5: ExtismCore (WASM Runtime) + +**Files:** +- Create: `src/core_extism.rs` +- Copy: `wasm/core.wasm` from Go SDK + +- [ ] **Step 1: Copy the WASM binary** + +```bash +mkdir -p wasm +cp /tmp/onepassword-sdk-go/internal/wasm/core.wasm wasm/core.wasm +``` + +- [ ] **Step 2: Write `src/core_extism.rs`** + +```rust +use std::sync::Mutex; + +use extism::{host_fn, Manifest, Plugin, PluginBuilder, Wasm}; + +use crate::core::{Core, MESSAGE_LIMIT}; +use crate::errors::SdkError; + +static CORE_WASM: &[u8] = include_bytes!("../wasm/core.wasm"); + +const INVOKE_FUNC: &str = "invoke"; +const INIT_CLIENT_FUNC: &str = "init_client"; +const RELEASE_CLIENT_FUNC: &str = "release_client"; + +pub(crate) struct ExtismCore { + plugin: Mutex, +} + +impl ExtismCore { + pub fn new() -> Result { + let manifest = Manifest::new([Wasm::data(CORE_WASM)]) + .with_allowed_hosts(allowed_1p_hosts().into_iter()); + + let plugin = PluginBuilder::new(manifest) + .with_wasi(true) + .build() + .map_err(|e| SdkError::Plugin(format!("failed to initialize plugin: {e}")))?; + + Ok(Self { + plugin: Mutex::new(plugin), + }) + } +} + +impl Core for ExtismCore { + fn init_client(&self, config: &[u8]) -> Result, SdkError> { + let mut plugin = self.plugin.lock().unwrap(); + let res = plugin + .call::<&[u8], Vec>(INIT_CLIENT_FUNC, config) + .map_err(|e| SdkError::Plugin(e.to_string()))?; + Ok(res) + } + + fn invoke(&self, invoke_config: &[u8]) -> Result, SdkError> { + if invoke_config.len() > MESSAGE_LIMIT { + return Err(SdkError::Config(format!( + "message size exceeds the limit of {} bytes", + MESSAGE_LIMIT + ))); + } + let mut plugin = self.plugin.lock().unwrap(); + let res = plugin + .call::<&[u8], Vec>(INVOKE_FUNC, invoke_config) + .map_err(|e| SdkError::Plugin(e.to_string()))?; + Ok(res) + } + + fn release_client(&self, client_id: &[u8]) { + let mut plugin = self.plugin.lock().unwrap(); + let _ = plugin.call::<&[u8], Vec>(RELEASE_CLIENT_FUNC, client_id); + } +} + +fn allowed_1p_hosts() -> Vec { + [ + "*.1password.com", + "*.1password.ca", + "*.1password.eu", + "*.b5staging.com", + "*.b5dev.com", + "*.b5dev.ca", + "*.b5dev.eu", + "*.b5test.com", + "*.b5test.ca", + "*.b5test.eu", + "*.b5rev.com", + "*.b5local.com", + ] + .iter() + .map(|s| s.to_string()) + .collect() +} +``` + +**Note:** The Extism Rust SDK's API may differ from this sketch. The implementer must check the `extism` crate docs (use context7 or `cargo doc`) for the actual `PluginBuilder`/`Manifest`/`Plugin::call` API signatures. The Go SDK registers host functions for `random_fill_imported` (namespace `op-extism-core`), `unix_time_milliseconds_imported` (namespaces `op-now` and `zxcvbn`), and `utc_offset_seconds` (namespace `op-time`). These must be registered when building the plugin. The exact Rust API for host functions with namespaces needs to be looked up. If the Extism Rust SDK doesn't support namespaced host functions in the same way, this will need adaptation. + +- [ ] **Step 3: Verify it compiles** + +Run: `cargo check` +Expected: Should compile (or reveal Extism API differences to fix) + +- [ ] **Step 4: Commit** + +```bash +git add wasm/core.wasm src/core_extism.rs +git commit -m "feat: add ExtismCore WASM runtime with embedded core.wasm" +``` + +--- + +### Task 6: Client Builder + +**Files:** +- Create: `src/client.rs` + +- [ ] **Step 1: Write the Client and ClientBuilder** + +```rust +use std::collections::HashMap; +use std::sync::Arc; + +use crate::core::{ClientConfig, CoreWrapper, InnerClient, InvokeConfig, Invocation, Parameters}; +use crate::core_extism::ExtismCore; +use crate::errors::{unmarshal_error, SdkError}; +use crate::environments::{EnvironmentsApi, EnvironmentsSource}; +use crate::groups::{GroupsApi, GroupsSource}; +use crate::items::{ItemsApi, ItemsSource}; +use crate::secrets::{SecretsApi, SecretsSource}; +use crate::vaults::{VaultsApi, VaultsSource}; + +pub struct Client { + inner: Arc, +} + +impl Client { + pub fn builder() -> ClientBuilder { + ClientBuilder { + config: ClientConfig::new_default(), + } + } + + pub fn secrets(&self) -> impl SecretsApi + '_ { + SecretsSource::new(&self.inner) + } + + pub fn items(&self) -> impl ItemsApi + '_ { + ItemsSource::new(&self.inner) + } + + pub fn vaults(&self) -> impl VaultsApi + '_ { + VaultsSource::new(&self.inner) + } + + pub fn groups(&self) -> impl GroupsApi + '_ { + GroupsSource::new(&self.inner) + } + + pub fn environments(&self) -> impl EnvironmentsApi + '_ { + EnvironmentsSource::new(&self.inner) + } +} + +impl Drop for Client { + fn drop(&mut self) { + self.inner.core.release_client(self.inner.id); + } +} + +pub struct ClientBuilder { + config: ClientConfig, +} + +impl ClientBuilder { + pub fn service_account_token(mut self, token: &str) -> Self { + self.config.sa_token = token.to_string(); + self + } + + #[cfg(feature = "desktop")] + pub fn desktop_app_integration(mut self, account_name: &str) -> Self { + self.config.account_name = Some(account_name.to_string()); + self + } + + pub fn integration_info(mut self, name: &str, version: &str) -> Self { + self.config.integration_name = name.to_string(); + self.config.integration_version = version.to_string(); + self + } + + pub fn build(self) -> Result { + let has_sa_token = !self.config.sa_token.is_empty(); + let has_desktop = self.config.account_name.is_some(); + + if has_sa_token && has_desktop { + return Err(SdkError::Config( + "cannot use both SA token and desktop app authentication".to_string(), + )); + } + + let core_impl: Box = if has_desktop { + #[cfg(feature = "desktop")] + { + let account_name = self.config.account_name.as_ref().unwrap().clone(); + Box::new(crate::core_shared_lib::SharedLibCore::new(&account_name)?) + } + #[cfg(not(feature = "desktop"))] + { + return Err(SdkError::Config( + "desktop app integration requires the 'desktop' feature".to_string(), + )); + } + } else { + Box::new(ExtismCore::new()?) + }; + + let core = CoreWrapper { inner: core_impl }; + + let client_id = core + .init_client(&self.config) + .map_err(|e| SdkError::Config(format!("error initializing client: {e}")))?; + + let inner = Arc::new(InnerClient { + id: client_id, + config: self.config, + core, + }); + + Ok(Client { inner }) + } +} + +/// Invoke a method on the WASM core, with automatic retry on desktop session expiry. +pub(crate) fn client_invoke( + inner: &InnerClient, + method: &str, + params: HashMap, +) -> Result { + let invoke_config = InvokeConfig { + invocation: Invocation { + client_id: Some(inner.id), + parameters: Parameters { + name: method.to_string(), + parameters: params, + }, + }, + }; + + match inner.core.invoke(&invoke_config) { + Ok(response) => Ok(response), + Err(e) => Err(unmarshal_error(&e.to_string())), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builder_rejects_both_auth_methods() { + let mut builder = Client::builder() + .service_account_token("ops_test"); + builder.config.account_name = Some("myaccount".to_string()); + let result = builder.build(); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("cannot use both")); + } +} +``` + +- [ ] **Step 2: Run test** + +Run: `cargo test client::tests` +Expected: PASS + +- [ ] **Step 3: Commit** + +```bash +git add src/client.rs +git commit -m "feat: add Client struct and ClientBuilder with auth validation" +``` + +--- + +### Task 7: API Implementations — Secrets + +**Files:** +- Create: `src/secrets.rs` + +- [ ] **Step 1: Write SecretsApi trait and implementation** + +```rust +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::{GeneratePasswordResponse, PasswordRecipe, ResolveAllResponse}; + +pub trait SecretsApi { + fn resolve(&self, secret_reference: &str) -> Result; + fn resolve_all(&self, secret_references: &[String]) -> Result; +} + +pub(crate) struct SecretsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> SecretsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl SecretsApi for SecretsSource<'_> { + fn resolve(&self, secret_reference: &str) -> Result { + let mut params = HashMap::new(); + params.insert( + "secret_reference".to_string(), + serde_json::Value::String(secret_reference.to_string()), + ); + let result_string = client_invoke(self.inner, "SecretsResolve", params)?; + let result: String = serde_json::from_str(&result_string)?; + Ok(result) + } + + fn resolve_all(&self, secret_references: &[String]) -> Result { + let mut params = HashMap::new(); + params.insert( + "secret_references".to_string(), + serde_json::to_value(secret_references)?, + ); + let result_string = client_invoke(self.inner, "SecretsResolveAll", params)?; + let result: ResolveAllResponse = serde_json::from_str(&result_string)?; + Ok(result) + } +} + +/// Standalone secret utilities (no client required). +pub struct Secrets; + +impl Secrets { + pub fn validate_secret_reference(secret_reference: &str) -> Result<(), SdkError> { + let core = crate::core_extism::ExtismCore::new()?; + let core_wrapper = crate::core::CoreWrapper { + inner: Box::new(core), + }; + let invoke_config = crate::core::InvokeConfig { + invocation: crate::core::Invocation { + client_id: None, + parameters: crate::core::Parameters { + name: "ValidateSecretReference".to_string(), + parameters: { + let mut m = HashMap::new(); + m.insert( + "secret_reference".to_string(), + serde_json::Value::String(secret_reference.to_string()), + ); + m + }, + }, + }, + }; + core_wrapper.invoke(&invoke_config)?; + Ok(()) + } + + pub fn generate_password( + recipe: PasswordRecipe, + ) -> Result { + let core = crate::core_extism::ExtismCore::new()?; + let core_wrapper = crate::core::CoreWrapper { + inner: Box::new(core), + }; + let invoke_config = crate::core::InvokeConfig { + invocation: crate::core::Invocation { + client_id: None, + parameters: crate::core::Parameters { + name: "GeneratePassword".to_string(), + parameters: { + let mut m = HashMap::new(); + m.insert("recipe".to_string(), serde_json::to_value(&recipe)?); + m + }, + }, + }, + }; + let result_string = core_wrapper.invoke(&invoke_config)?; + let result: GeneratePasswordResponse = serde_json::from_str(&result_string)?; + Ok(result) + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/secrets.rs +git commit -m "feat: add SecretsApi with resolve, resolve_all, and standalone utils" +``` + +--- + +### Task 8: API Implementations — Items, ItemsFiles, ItemsShares + +**Files:** +- Create: `src/items.rs` +- Create: `src/items_files.rs` +- Create: `src/items_shares.rs` + +- [ ] **Step 1: Write `src/items.rs`** + +```rust +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::items_files::{ItemsFilesApi, ItemsFilesSource}; +use crate::items_shares::{ItemsSharesApi, ItemsSharesSource}; +use crate::types::*; + +pub trait ItemsApi { + fn create(&self, params: ItemCreateParams) -> Result; + fn create_all( + &self, + vault_id: &str, + params: &[ItemCreateParams], + ) -> Result; + fn get(&self, vault_id: &str, item_id: &str) -> Result; + fn get_all( + &self, + vault_id: &str, + item_ids: &[String], + ) -> Result; + fn put(&self, item: Item) -> Result; + fn delete(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError>; + fn delete_all( + &self, + vault_id: &str, + item_ids: &[String], + ) -> Result; + fn archive(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError>; + fn list( + &self, + vault_id: &str, + filters: &[ItemListFilter], + ) -> Result, SdkError>; + fn shares(&self) -> impl ItemsSharesApi + '_; + fn files(&self) -> impl ItemsFilesApi + '_; +} + +pub(crate) struct ItemsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> ItemsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl ItemsApi for ItemsSource<'_> { + fn create(&self, params: ItemCreateParams) -> Result { + let mut p = HashMap::new(); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result_string = client_invoke(self.inner, "ItemsCreate", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn create_all( + &self, + vault_id: &str, + params: &[ItemCreateParams], + ) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("params".to_string(), serde_json::to_value(params)?); + let result_string = client_invoke(self.inner, "ItemsCreateAll", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn get(&self, vault_id: &str, item_id: &str) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + let result_string = client_invoke(self.inner, "ItemsGet", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn get_all( + &self, + vault_id: &str, + item_ids: &[String], + ) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("item_ids".to_string(), serde_json::to_value(item_ids)?); + let result_string = client_invoke(self.inner, "ItemsGetAll", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn put(&self, item: Item) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + let result_string = client_invoke(self.inner, "ItemsPut", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn delete(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + client_invoke(self.inner, "ItemsDelete", p)?; + Ok(()) + } + + fn delete_all( + &self, + vault_id: &str, + item_ids: &[String], + ) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("item_ids".to_string(), serde_json::to_value(item_ids)?); + let result_string = client_invoke(self.inner, "ItemsDeleteAll", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn archive(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + client_invoke(self.inner, "ItemsArchive", p)?; + Ok(()) + } + + fn list( + &self, + vault_id: &str, + filters: &[ItemListFilter], + ) -> Result, SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("filters".to_string(), serde_json::to_value(filters)?); + let result_string = client_invoke(self.inner, "ItemsList", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn shares(&self) -> impl ItemsSharesApi + '_ { + ItemsSharesSource::new(self.inner) + } + + fn files(&self) -> impl ItemsFilesApi + '_ { + ItemsFilesSource::new(self.inner) + } +} +``` + +- [ ] **Step 2: Write `src/items_files.rs`** + +```rust +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::*; + +pub trait ItemsFilesApi { + fn attach(&self, item: Item, file_params: FileCreateParams) -> Result; + fn read( + &self, + vault_id: &str, + item_id: &str, + attr: FileAttributes, + ) -> Result, SdkError>; + fn delete(&self, item: Item, section_id: &str, field_id: &str) -> Result; + fn replace_document( + &self, + item: Item, + doc_params: DocumentCreateParams, + ) -> Result; +} + +pub(crate) struct ItemsFilesSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> ItemsFilesSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl ItemsFilesApi for ItemsFilesSource<'_> { + fn attach(&self, item: Item, file_params: FileCreateParams) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + p.insert("file_params".to_string(), serde_json::to_value(&file_params)?); + let result_string = client_invoke(self.inner, "ItemsFilesAttach", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn read( + &self, + vault_id: &str, + item_id: &str, + attr: FileAttributes, + ) -> Result, SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + p.insert("attr".to_string(), serde_json::to_value(&attr)?); + let result_string = client_invoke(self.inner, "ItemsFilesRead", p)?; + let result: Vec = serde_json::from_str(&result_string)?; + Ok(result) + } + + fn delete(&self, item: Item, section_id: &str, field_id: &str) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + p.insert( + "section_id".to_string(), + serde_json::Value::String(section_id.to_string()), + ); + p.insert( + "field_id".to_string(), + serde_json::Value::String(field_id.to_string()), + ); + let result_string = client_invoke(self.inner, "ItemsFilesDelete", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn replace_document( + &self, + item: Item, + doc_params: DocumentCreateParams, + ) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + p.insert("doc_params".to_string(), serde_json::to_value(&doc_params)?); + let result_string = client_invoke(self.inner, "ItemsFilesReplaceDocument", p)?; + Ok(serde_json::from_str(&result_string)?) + } +} +``` + +- [ ] **Step 3: Write `src/items_shares.rs`** + +```rust +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::*; + +pub trait ItemsSharesApi { + fn get_account_policy( + &self, + vault_id: &str, + item_id: &str, + ) -> Result; + fn validate_recipients( + &self, + policy: ItemShareAccountPolicy, + recipients: &[String], + ) -> Result, SdkError>; + fn create( + &self, + item: Item, + policy: ItemShareAccountPolicy, + params: ItemShareParams, + ) -> Result; +} + +pub(crate) struct ItemsSharesSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> ItemsSharesSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl ItemsSharesApi for ItemsSharesSource<'_> { + fn get_account_policy( + &self, + vault_id: &str, + item_id: &str, + ) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + let result_string = client_invoke(self.inner, "ItemsSharesGetAccountPolicy", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn validate_recipients( + &self, + policy: ItemShareAccountPolicy, + recipients: &[String], + ) -> Result, SdkError> { + let mut p = HashMap::new(); + p.insert("policy".to_string(), serde_json::to_value(&policy)?); + p.insert("recipients".to_string(), serde_json::to_value(recipients)?); + let result_string = + client_invoke(self.inner, "ItemsSharesValidateRecipients", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn create( + &self, + item: Item, + policy: ItemShareAccountPolicy, + params: ItemShareParams, + ) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + p.insert("policy".to_string(), serde_json::to_value(&policy)?); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result_string = client_invoke(self.inner, "ItemsSharesCreate", p)?; + Ok(serde_json::from_str(&result_string)?) + } +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/items.rs src/items_files.rs src/items_shares.rs +git commit -m "feat: add ItemsApi, ItemsFilesApi, and ItemsSharesApi" +``` + +--- + +### Task 9: API Implementations — Vaults, Groups, Environments + +**Files:** +- Create: `src/vaults.rs` +- Create: `src/groups.rs` +- Create: `src/environments.rs` + +- [ ] **Step 1: Write `src/vaults.rs`** + +```rust +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::*; + +pub trait VaultsApi { + fn create(&self, params: VaultCreateParams) -> Result; + fn list(&self, params: Option) -> Result, SdkError>; + fn get_overview(&self, vault_id: &str) -> Result; + fn get(&self, vault_id: &str, params: VaultGetParams) -> Result; + fn update(&self, vault_id: &str, params: VaultUpdateParams) -> Result; + fn delete(&self, vault_id: &str) -> Result<(), SdkError>; + fn grant_group_permissions( + &self, + vault_id: &str, + group_permissions: &[GroupAccess], + ) -> Result<(), SdkError>; + fn update_group_permissions( + &self, + group_permissions: &[GroupVaultAccess], + ) -> Result<(), SdkError>; + fn revoke_group_permissions( + &self, + vault_id: &str, + group_id: &str, + ) -> Result<(), SdkError>; +} + +pub(crate) struct VaultsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> VaultsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl VaultsApi for VaultsSource<'_> { + fn create(&self, params: VaultCreateParams) -> Result { + let mut p = HashMap::new(); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result_string = client_invoke(self.inner, "VaultsCreate", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn list(&self, params: Option) -> Result, SdkError> { + let mut p = HashMap::new(); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result_string = client_invoke(self.inner, "VaultsList", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn get_overview(&self, vault_id: &str) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + let result_string = client_invoke(self.inner, "VaultsGetOverview", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn get(&self, vault_id: &str, params: VaultGetParams) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("vault_params".to_string(), serde_json::to_value(¶ms)?); + let result_string = client_invoke(self.inner, "VaultsGet", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn update(&self, vault_id: &str, params: VaultUpdateParams) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result_string = client_invoke(self.inner, "VaultsUpdate", p)?; + Ok(serde_json::from_str(&result_string)?) + } + + fn delete(&self, vault_id: &str) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + client_invoke(self.inner, "VaultsDelete", p)?; + Ok(()) + } + + fn grant_group_permissions( + &self, + vault_id: &str, + group_permissions: &[GroupAccess], + ) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "group_permissions_list".to_string(), + serde_json::to_value(group_permissions)?, + ); + client_invoke(self.inner, "VaultsGrantGroupPermissions", p)?; + Ok(()) + } + + fn update_group_permissions( + &self, + group_permissions: &[GroupVaultAccess], + ) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "group_permissions_list".to_string(), + serde_json::to_value(group_permissions)?, + ); + client_invoke(self.inner, "VaultsUpdateGroupPermissions", p)?; + Ok(()) + } + + fn revoke_group_permissions( + &self, + vault_id: &str, + group_id: &str, + ) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "group_id".to_string(), + serde_json::Value::String(group_id.to_string()), + ); + client_invoke(self.inner, "VaultsRevokeGroupPermissions", p)?; + Ok(()) + } +} +``` + +- [ ] **Step 2: Write `src/groups.rs`** + +```rust +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::*; + +pub trait GroupsApi { + fn get(&self, group_id: &str, params: GroupGetParams) -> Result; +} + +pub(crate) struct GroupsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> GroupsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl GroupsApi for GroupsSource<'_> { + fn get(&self, group_id: &str, params: GroupGetParams) -> Result { + let mut p = HashMap::new(); + p.insert( + "group_id".to_string(), + serde_json::Value::String(group_id.to_string()), + ); + p.insert("group_params".to_string(), serde_json::to_value(¶ms)?); + let result_string = client_invoke(self.inner, "GroupsGet", p)?; + Ok(serde_json::from_str(&result_string)?) + } +} +``` + +- [ ] **Step 3: Write `src/environments.rs`** + +```rust +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::GetVariablesResponse; + +pub trait EnvironmentsApi { + fn get_variables(&self, environment_id: &str) -> Result; +} + +pub(crate) struct EnvironmentsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> EnvironmentsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl EnvironmentsApi for EnvironmentsSource<'_> { + fn get_variables(&self, environment_id: &str) -> Result { + let mut p = HashMap::new(); + p.insert( + "environment_id".to_string(), + serde_json::Value::String(environment_id.to_string()), + ); + let result_string = client_invoke(self.inner, "EnvironmentsGetVariables", p)?; + Ok(serde_json::from_str(&result_string)?) + } +} +``` + +- [ ] **Step 4: Verify full crate compiles** + +Run: `cargo check` +Expected: PASS (no desktop feature, so shared_lib module is excluded) + +- [ ] **Step 5: Run all tests** + +Run: `cargo test` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/vaults.rs src/groups.rs src/environments.rs +git commit -m "feat: add VaultsApi, GroupsApi, and EnvironmentsApi" +``` + +--- + +### Task 10: SharedLibCore (Desktop App Integration) + +**Files:** +- Create: `src/core_shared_lib.rs` + +- [ ] **Step 1: Write `src/core_shared_lib.rs`** + +This file uses `#[allow(unsafe_code)]` for FFI operations. It uses `libloading` for cross-platform dlopen/LoadLibrary. + +```rust +#![allow(unsafe_code)] + +use std::ffi::c_void; +use std::path::PathBuf; + +use libloading::{Library, Symbol}; +use serde::{Deserialize, Serialize}; + +use crate::core::Core; +use crate::errors::SdkError; + +#[derive(Serialize)] +struct Request<'a> { + kind: &'a str, + account_name: &'a str, + payload: &'a [u8], +} + +#[derive(Deserialize)] +struct Response { + success: bool, + payload: Vec, +} + +type SendMessageFn = unsafe extern "C" fn( + msg_ptr: *const u8, + msg_len: usize, + out_buf: *mut *mut u8, + out_len: *mut usize, + out_cap: *mut usize, +) -> i32; + +type FreeResponseFn = unsafe extern "C" fn(buf: *mut u8, len: usize, cap: usize); + +pub(crate) struct SharedLibCore { + account_name: String, + _library: Library, + send_message: SendMessageFn, + free_response: FreeResponseFn, +} + +// SAFETY: The shared library functions are thread-safe according to 1Password's documentation. +// The Go SDK also uses them across goroutines. +unsafe impl Send for SharedLibCore {} +unsafe impl Sync for SharedLibCore {} + +impl SharedLibCore { + pub fn new(account_name: &str) -> Result { + let lib_path = find_1password_lib_path()?; + + let library = unsafe { + Library::new(&lib_path) + .map_err(|e| SdkError::SharedLib(format!("failed to open library: {e}")))? + }; + + let send_message: SendMessageFn = unsafe { + let sym: Symbol = library + .get(b"op_sdk_ipc_send_message") + .map_err(|e| SdkError::SharedLib(format!("failed to load send_message: {e}")))?; + *sym + }; + + let free_response: FreeResponseFn = unsafe { + let sym: Symbol = library + .get(b"op_sdk_ipc_free_response") + .map_err(|e| SdkError::SharedLib(format!("failed to load free_message: {e}")))?; + *sym + }; + + Ok(Self { + account_name: account_name.to_string(), + _library: library, + send_message, + free_response, + }) + } + + fn call_shared_library(&self, kind: &str, payload: &[u8]) -> Result, SdkError> { + let request = Request { + kind, + account_name: &self.account_name, + payload, + }; + let input = serde_json::to_vec(&request)?; + + let mut out_buf: *mut u8 = std::ptr::null_mut(); + let mut out_len: usize = 0; + let mut out_cap: usize = 0; + + let ret_code = unsafe { + (self.send_message)( + input.as_ptr(), + input.len(), + &mut out_buf, + &mut out_len, + &mut out_cap, + ) + }; + + error_from_return_code(ret_code)?; + + let resp_bytes = unsafe { std::slice::from_raw_parts(out_buf, out_len).to_vec() }; + + unsafe { + (self.free_response)(out_buf, out_len, out_cap); + } + + let response: Response = serde_json::from_slice(&resp_bytes) + .map_err(|e| SdkError::SharedLib(format!("failed to parse response: {e}")))?; + + if response.success { + Ok(response.payload) + } else { + Err(SdkError::SharedLib( + String::from_utf8_lossy(&response.payload).into_owned(), + )) + } + } +} + +impl Core for SharedLibCore { + fn init_client(&self, config: &[u8]) -> Result, SdkError> { + self.call_shared_library("init_client", config) + } + + fn invoke(&self, invoke_config: &[u8]) -> Result, SdkError> { + self.call_shared_library("invoke", invoke_config) + } + + fn release_client(&self, client_id: &[u8]) { + let _ = self.call_shared_library("release_client", client_id); + } +} + +fn find_1password_lib_path() -> Result { + let home = dirs_next::home_dir() + .or_else(|| std::env::var("HOME").ok().map(PathBuf::from)) + .unwrap_or_default(); + + let locations: Vec = if cfg!(target_os = "macos") { + vec![ + PathBuf::from("/Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib"), + home.join("Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib"), + ] + } else if cfg!(target_os = "linux") { + vec![ + PathBuf::from("/usr/bin/1password/libop_sdk_ipc_client.so"), + PathBuf::from("/opt/1Password/libop_sdk_ipc_client.so"), + PathBuf::from("/snap/bin/1password/libop_sdk_ipc_client.so"), + ] + } else if cfg!(target_os = "windows") { + vec![ + home.join(r"AppData\Local\1Password\op_sdk_ipc_client.dll"), + PathBuf::from(r"C:\Program Files\1Password\app\8\op_sdk_ipc_client.dll"), + PathBuf::from(r"C:\Program Files (x86)\1Password\app\8\op_sdk_ipc_client.dll"), + home.join(r"AppData\Local\1Password\app\8\op_sdk_ipc_client.dll"), + ] + } else { + return Err(SdkError::SharedLib(format!( + "unsupported OS: {}", + std::env::consts::OS + ))); + }; + + for path in &locations { + if path.exists() { + return Ok(path.clone()); + } + } + + Err(SdkError::SharedLib( + "1Password desktop application not found".to_string(), + )) +} + +fn error_from_return_code(ret_code: i32) -> Result<(), SdkError> { + if ret_code == 0 { + return Ok(()); + } + + let msg = if cfg!(target_os = "macos") { + match ret_code { + -3 => "desktop app connection channel is closed. Make sure Settings > Developer > Integrate with other apps is enabled".to_string(), + -7 => "connection was unexpectedly dropped by the desktop app. Make sure the desktop app is running".to_string(), + _ => format!("an internal error occurred. Return code: {ret_code}"), + } + } else { + match ret_code { + -2 => "desktop app connection channel is closed. Make sure Settings > Developer > Integrate with other apps is enabled".to_string(), + -5 => "connection was unexpectedly dropped by the desktop app. Make sure the desktop app is running".to_string(), + _ => format!("an internal error occurred. Return code: {ret_code}"), + } + }; + + Err(SdkError::SharedLib(msg)) +} +``` + +**Note:** The `dirs_next` crate is used for `home_dir()`. Add it to `Cargo.toml`: + +```toml +[dependencies] +# ... existing deps ... +dirs-next = { version = "2", optional = true } + +[features] +desktop = ["dep:libloading", "dep:dirs-next"] +``` + +Alternatively, use `std::env::var("HOME")` directly and skip the extra dep. The implementer should decide. + +- [ ] **Step 2: Verify it compiles with the desktop feature** + +Run: `cargo check --features desktop` +Expected: PASS + +- [ ] **Step 3: Commit** + +```bash +git add src/core_shared_lib.rs +git commit -m "feat: add SharedLibCore for desktop app integration" +``` + +--- + +### Task 11: Examples + +**Files:** +- Create: `examples/service_account.rs` +- Create: `examples/desktop_app.rs` + +- [ ] **Step 1: Write `examples/service_account.rs`** + +```rust +use onepassword_sdk::{Client, SecretsApi}; + +fn main() { + let token = std::env::var("OP_SERVICE_ACCOUNT_TOKEN") + .expect("Set OP_SERVICE_ACCOUNT_TOKEN environment variable"); + + let client = Client::builder() + .service_account_token(&token) + .integration_info("My Rust App", "v1.0.0") + .build() + .expect("Failed to create client"); + + let secret = client + .secrets() + .resolve("op://vault/item/field") + .expect("Failed to resolve secret"); + + println!("Secret: {secret}"); +} +``` + +- [ ] **Step 2: Write `examples/desktop_app.rs`** + +```rust +#[cfg(feature = "desktop")] +fn main() { + use onepassword_sdk::{Client, SecretsApi}; + + let client = Client::builder() + .desktop_app_integration("my-account") + .integration_info("My Rust App", "v1.0.0") + .build() + .expect("Failed to create client"); + + let secret = client + .secrets() + .resolve("op://vault/item/field") + .expect("Failed to resolve secret"); + + println!("Secret: {secret}"); +} + +#[cfg(not(feature = "desktop"))] +fn main() { + eprintln!("This example requires the 'desktop' feature: cargo run --example desktop_app --features desktop"); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add examples/ +git commit -m "feat: add service_account and desktop_app examples" +``` + +--- + +### Task 12: CI Workflow + +**Files:** +- Create: `.github/workflows/ci.yml` + +- [ ] **Step 1: Write `.github/workflows/ci.yml`** + +```yaml +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: "1.93.1" + components: rustfmt, clippy + + - name: Check formatting + run: cargo fmt --check + + - name: Clippy + run: cargo clippy --all-features -- -D warnings + + - name: Test + run: cargo test + + - name: Test (desktop feature) + run: cargo test --features desktop + + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 +``` + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: add fmt, clippy, test, and cargo-deny checks" +``` + +--- + +### Task 13: Final Verification + +- [ ] **Step 1: Run fmt** + +Run: `cargo fmt --check` +Expected: PASS + +- [ ] **Step 2: Run clippy** + +Run: `cargo clippy -- -D warnings` +Expected: PASS (fix any warnings) + +- [ ] **Step 3: Run all tests** + +Run: `cargo test` +Expected: PASS + +- [ ] **Step 4: Run clippy with desktop feature** + +Run: `cargo clippy --features desktop -- -D warnings` +Expected: PASS + +- [ ] **Step 5: Verify the crate compiles with all features** + +Run: `cargo build --all-features` +Expected: PASS + +- [ ] **Step 6: Final commit (if any fixes were needed)** + +```bash +git add -A +git commit -m "fix: address clippy and formatting issues" +``` diff --git a/docs/superpowers/specs/2026-03-30-onepassword-sdk-rust-design.md b/docs/superpowers/specs/2026-03-30-onepassword-sdk-rust-design.md new file mode 100644 index 0000000..a6ba98b --- /dev/null +++ b/docs/superpowers/specs/2026-03-30-onepassword-sdk-rust-design.md @@ -0,0 +1,317 @@ +# 1Password Rust SDK — Design Spec + +## Overview + +Port of the [1Password Go SDK](https://github.com/1Password/onepassword-sdk-go/tree/v0.4.1-beta.1) to Rust. The SDK is a typed wrapper around an opaque `core.wasm` binary (and optionally a native shared library) that implements the 1Password protocol and cryptographic engine. The Rust SDK provides the same API surface as the Go SDK with idiomatic Rust patterns. + +**Source of truth:** Go SDK at tag `v0.4.1-beta.1`. + +**Conventions:** Follow patterns from the [cordon](https://github.com/codezero-llc/cordon) project — edition 2024, Rust 1.93.1, `thiserror`, `serde`, `cargo-deny`, inline tests, conventional commits. + +## Architecture + +``` +┌─────────────────────────────────────────────┐ +│ User Code │ +│ client.secrets().resolve("op://...") │ +├─────────────────────────────────────────────┤ +│ Public API Layer │ +│ SecretsApi, ItemsApi, VaultsApi, etc. │ +│ Typed Rust structs/enums ↔ JSON serde │ +├─────────────────────────────────────────────┤ +│ Client │ +│ Builder pattern, invoke() dispatcher │ +├─────────────────────────────────────────────┤ +│ Core Trait │ +│ init_client / invoke / release_client │ +├──────────────────┬──────────────────────────┤ +│ ExtismCore │ SharedLibCore │ +│ (WASM, default) │ (desktop feature, FFI) │ +│ include_bytes! │ libloading + dlopen │ +│ Mutex │ platform-specific │ +└──────────────────┴──────────────────────────┘ +``` + +All API methods serialize parameters to JSON, call `Core::invoke()`, and deserialize the response. This matches the Go SDK's `clientInvoke()` pattern exactly. + +## Crate Structure + +Single crate, not a workspace. Matches the Go SDK's single-package approach. + +``` +onepassword-sdk-rust/ + Cargo.toml + deny.toml + rust-toolchain.toml + .gitignore + .github/ + workflows/ + ci.yml + LICENSE + README.md + CLAUDE.md + src/ + lib.rs # Crate root, public re-exports, #![deny(unsafe_code)] + client.rs # Client struct, ClientBuilder + core.rs # Core trait definition + core_extism.rs # ExtismCore — WASM runtime (default) + core_shared_lib.rs # SharedLibCore — native FFI (desktop feature) + core_shared_lib_unix.rs # dlopen path for macOS/Linux + core_shared_lib_win.rs # LoadLibrary path for Windows + errors.rs # SdkError enum (thiserror) + types.rs # All domain types (serde structs/enums) + secrets.rs # SecretsApi trait + implementation + items.rs # ItemsApi trait + implementation + items_files.rs # ItemsFilesApi trait + implementation + items_shares.rs # ItemsSharesApi trait + implementation + vaults.rs # VaultsApi trait + implementation + groups.rs # GroupsApi trait + implementation + environments.rs # EnvironmentsApi trait + implementation + wasm/ + core.wasm # Embedded WASM binary (~9.1MB) + examples/ + service_account.rs + desktop_app.rs +``` + +## Core Abstraction + +```rust +pub(crate) trait Core: Send + Sync { + fn init_client(&self, config: &[u8]) -> Result, SdkError>; + fn invoke(&self, invoke_config: &[u8]) -> Result, SdkError>; + fn release_client(&self, client_id: &[u8]); +} +``` + +### ExtismCore (default) + +- Embeds `core.wasm` via `include_bytes!("../wasm/core.wasm")` +- Uses `extism` crate (Rust host SDK) to create and manage the plugin +- `Mutex` for thread-safe access (matches Go's `sync.Mutex` pattern) +- Implements host functions: `randomFill`, `getTime`, `getTimezoneOffset` +- 50MB message size limit (matches Go) + +### SharedLibCore (feature: `desktop`) + +- Loads `libop_sdk_ipc_client.{dylib,so,dll}` from the 1Password desktop app installation +- Uses `libloading` crate for cross-platform dynamic library loading +- `#[allow(unsafe_code)]` scoped to the FFI modules only +- Platform-specific discovery of the shared library path +- Separate modules for Unix (`dlopen`) and Windows (`LoadLibrary`) matching Go's `shared_lib_core_unix.go` / `shared_lib_core_windows.go` split + +## Client Builder + +```rust +use onepassword_sdk::{Client, ClientBuilder}; + +// Service account auth (default, WASM core) +let client = Client::builder() + .service_account_token("ops_...") + .integration_info("My App", "v1.0.0") + .build()?; + +// Desktop app auth (requires `desktop` feature) +let client = Client::builder() + .desktop_app_integration("My App") + .integration_info("My App", "v1.0.0") + .build()?; +``` + +Builder validates that exactly one auth method is set (service account XOR desktop app), matching Go's mutual exclusivity check. `integration_info` defaults to `("Unknown", "Unknown")` if not set. + +## Public API Surface + +### SecretsApi + +```rust +pub trait SecretsApi { + fn resolve(&self, secret_reference: &str) -> Result; + fn resolve_all(&self, secret_references: &[String]) -> Result; +} +``` + +Plus standalone functions (no client needed): +- `secrets::validate_secret_reference(reference: &str) -> Result<(), SdkError>` +- `secrets::generate_password(recipe: PasswordRecipe) -> Result` + +### ItemsApi + +```rust +pub trait ItemsApi { + fn create(&self, params: ItemCreateParams) -> Result; + fn create_all(&self, vault_id: &str, params: &[ItemCreateParams]) -> Result; + fn get(&self, vault_id: &str, item_id: &str) -> Result; + fn get_all(&self, vault_id: &str, item_ids: &[String]) -> Result; + fn put(&self, item: Item) -> Result; + fn delete(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError>; + fn delete_all(&self, vault_id: &str, item_ids: &[String]) -> Result; + fn archive(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError>; + fn list(&self, vault_id: &str, filters: &[ItemListFilter]) -> Result, SdkError>; + fn shares(&self) -> &dyn ItemsSharesApi; + fn files(&self) -> &dyn ItemsFilesApi; +} +``` + +### ItemsSharesApi + +```rust +pub trait ItemsSharesApi { + fn get_account_policy(&self, vault_id: &str, item_id: &str) -> Result; + fn validate_recipients(&self, policy: ItemShareAccountPolicy, recipients: &[String]) -> Result, SdkError>; + fn create(&self, item: Item, policy: ItemShareAccountPolicy, params: ItemShareParams) -> Result; +} +``` + +### ItemsFilesApi + +```rust +pub trait ItemsFilesApi { + fn attach(&self, item: Item, file_params: FileCreateParams) -> Result; + fn read(&self, vault_id: &str, item_id: &str, attr: FileAttributes) -> Result, SdkError>; + fn delete(&self, item: Item, section_id: &str, field_id: &str) -> Result; + fn replace_document(&self, item: Item, doc_params: DocumentCreateParams) -> Result; +} +``` + +### VaultsApi + +```rust +pub trait VaultsApi { + fn create(&self, params: VaultCreateParams) -> Result; + fn list(&self, params: Option) -> Result, SdkError>; + fn get_overview(&self, vault_id: &str) -> Result; + fn get(&self, vault_id: &str, params: VaultGetParams) -> Result; + fn update(&self, vault_id: &str, params: VaultUpdateParams) -> Result; + fn delete(&self, vault_id: &str) -> Result<(), SdkError>; + fn grant_group_permissions(&self, vault_id: &str, group_permissions: &[GroupAccess]) -> Result<(), SdkError>; + fn update_group_permissions(&self, group_permissions: &[GroupVaultAccess]) -> Result<(), SdkError>; + fn revoke_group_permissions(&self, vault_id: &str, group_id: &str) -> Result<(), SdkError>; +} +``` + +### GroupsApi + +```rust +pub trait GroupsApi { + fn get(&self, group_id: &str, params: GroupGetParams) -> Result; +} +``` + +### EnvironmentsApi + +```rust +pub trait EnvironmentsApi { + fn get_variables(&self, environment_id: &str) -> Result; +} +``` + +## Domain Types + +All types ported from the Go SDK's `types.go` (which is itself generated from Rust via `typeshare`). Key mapping decisions: + +| Go Pattern | Rust Pattern | +|---|---| +| Struct with exported fields | `pub struct` with `#[derive(Debug, Clone, Serialize, Deserialize)]` | +| Tagged union (`Type` + content field + manual JSON marshal) | `#[serde(tag = "type", content = "content")]` enum — native Rust, no manual impl | +| `[]string` | `Vec` | +| `[]byte` | `Vec` | +| `*string` (optional) | `Option` | +| Go constants (iota) | Rust enum variants | +| Bitmask permission constants | `pub const` values in a `permissions` module, or a bitflags type | + +### Key type categories + +- **Items:** `Item`, `ItemCreateParams`, `ItemOverview`, `ItemField`, `ItemSection`, `ItemCategory` (23 variants), `ItemFieldType` (15 variants), `ItemFieldDetails` (tagged enum: OTP/SSHKey/Address), `ItemState` +- **Files:** `FileAttributes`, `FileCreateParams`, `DocumentCreateParams`, `ItemFile` +- **Secrets:** `ResolveAllResponse`, `ResolvedReference`, `ResolveReferenceError` (17 variants), `PasswordRecipe` (tagged enum: Memorable/Pin/Random), `GeneratePasswordResponse` +- **Vaults:** `Vault`, `VaultOverview`, `VaultCreateParams`, `VaultUpdateParams`, `VaultGetParams`, `VaultListParams`, `VaultType` (5 variants) +- **Groups:** `Group`, `GroupGetParams`, `GroupType` (7 variants), `GroupState`, `GroupAccess`, `GroupVaultAccess`, `VaultAccess` +- **Sharing:** `ItemShareAccountPolicy`, `ItemShareParams`, `ItemShareDuration`, `ValidRecipient` (tagged enum), `AllowedType`, `AllowedRecipientType` +- **Environments:** `GetVariablesResponse`, `EnvironmentVariable` +- **Batch responses:** Generic `BatchResponse` for `ItemsUpdateAllResponse`, `ItemsGetAllResponse`, `ItemsDeleteAllResponse`, `ResolveAllResponse` + +## Error Handling + +```rust +#[derive(Debug, thiserror::Error)] +pub enum SdkError { + #[error("1Password SDK error: {message}")] + Core { name: String, message: String }, + + #[error("serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("WASM plugin error: {0}")] + Plugin(String), + + #[error("configuration error: {0}")] + Config(String), + + #[error("desktop session expired")] + DesktopSessionExpired, + + #[error("rate limit exceeded")] + RateLimitExceeded, + + #[error("shared library error: {0}")] + SharedLib(String), +} +``` + +Core errors arrive as JSON `{"name": "...", "message": "..."}` and are deserialized. `DesktopSessionExpired` triggers automatic client re-initialization and retry (matching Go behavior). + +Batch operations use per-item error types rather than failing the whole batch, matching Go's `Response[T, E]` pattern. + +## Resource Cleanup + +`Client` implements `Drop` to call `core.release_client()`. This is cleaner than Go's `runtime.SetFinalizer` approach — deterministic cleanup rather than GC-dependent. + +## Host Functions + +The WASM core imports three host functions that we must provide: + +| Function | Purpose | Implementation | +|---|---|---| +| `randomFill(ptr, len)` | Cryptographic random bytes | `getrandom` crate | +| `getTime()` | Current Unix timestamp (ms) | `std::time::SystemTime` | +| `getTimezoneOffset()` | UTC offset in minutes | `chrono::Local` or `time` crate | + +These are registered as Extism host functions when creating the plugin. + +## Feature Flags + +| Feature | Default | Description | +|---|---|---| +| (none/default) | yes | WASM-based core, service account auth | +| `desktop` | no | Desktop app integration via native shared library | + +## Dependencies + +| Crate | Purpose | +|---|---| +| `extism` | WASM plugin host runtime | +| `serde`, `serde_json` | JSON serialization for core RPC | +| `thiserror` | Error derive macros | +| `getrandom` | Cryptographic random for WASM host function | +| `chrono` or `time` | Timezone offset for WASM host function | +| `libloading` | Dynamic library loading (desktop feature only) | + +## Tooling & CI + +From cordon conventions: + +- `rust-toolchain.toml`: channel `1.93.1`, components `rustfmt`, `clippy` +- `deny.toml`: license allowlist, advisory db, ban wildcards, deny unknown sources +- CI workflow: `cargo fmt --check`, `cargo-deny check`, `cargo clippy -- -D warnings`, `cargo nextest run`, `cargo test --doc` +- `#![deny(unsafe_code)]` at crate root (not `forbid`, to allow scoped `#[allow]` on FFI modules) + +## Not At Parity + +These items cannot be achieved with the current approach and are flagged: + +1. **Code generation pipeline** — The Go SDK types are generated by `op-codegen`/`typeshare` from the private Rust core repo. We're hand-porting them. If the core WASM updates, types must be manually reconciled. This is acceptable for an exploratory port. + +2. **Automated core updates** — 1Password's `1PasswordSDKBot` pushes WASM updates to the Go/Python/JS SDKs automatically. Our repo won't receive these. Core updates require manually copying `core.wasm` from a reference SDK. + +3. **Extism host function ABI** — The exact ABI for host functions (memory layout, calling convention) needs to be validated against the actual WASM binary. The Go SDK's implementation is the reference, but Rust's Extism host SDK may have slightly different ergonomics. diff --git a/examples/desktop_app.rs b/examples/desktop_app.rs new file mode 100644 index 0000000..ad33e27 --- /dev/null +++ b/examples/desktop_app.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "desktop")] +fn main() { + use onepassword_sdk::{Client, SecretsApi}; + + let account = std::env::var("OP_ACCOUNT") + .expect("Set OP_ACCOUNT to your 1Password sign-in address (e.g. my.1password.com)"); + let secret_ref = std::env::var("OP_SECRET_REF") + .unwrap_or_else(|_| "op://MyVault/Example/username".to_string()); + + let client = Client::builder() + .desktop_app_integration(&account) + .integration_info("1Password Rust SDK Example", "v0.1.0") + .build() + .expect("Failed to create client"); + + let secret = client + .secrets() + .resolve(&secret_ref) + .expect("Failed to resolve secret"); + + println!("Secret length: {}", secret.len()); +} + +#[cfg(not(feature = "desktop"))] +fn main() { + eprintln!( + "This example requires the 'desktop' feature: cargo run --example desktop_app --features desktop" + ); +} diff --git a/examples/service_account.rs b/examples/service_account.rs new file mode 100644 index 0000000..78cf102 --- /dev/null +++ b/examples/service_account.rs @@ -0,0 +1,19 @@ +use onepassword_sdk::{Client, SecretsApi}; + +fn main() { + let token = std::env::var("OP_SERVICE_ACCOUNT_TOKEN") + .expect("Set OP_SERVICE_ACCOUNT_TOKEN environment variable"); + + let client = Client::builder() + .service_account_token(&token) + .integration_info("My Rust App", "v1.0.0") + .build() + .expect("Failed to create client"); + + let secret = client + .secrets() + .resolve("op://vault/item/field") + .expect("Failed to resolve secret"); + + println!("Secret length: {}", secret.len()); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..ee43e83 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.93.1" +components = ["rustfmt", "clippy"] diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..5f54841 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,271 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::AtomicU64; + +use crate::core::{ClientConfig, CoreWrapper, InnerClient, Invocation, InvokeConfig, Parameters}; +#[cfg(feature = "wasm")] +use crate::core_extism::ExtismCore; +use crate::environments::{EnvironmentsApi, EnvironmentsSource}; +use crate::errors::{SdkError, unmarshal_core_error}; +use crate::groups::{GroupsApi, GroupsSource}; +use crate::items::{ItemsApi, ItemsSource}; +use crate::secrets::{SecretsApi, SecretsSource}; +use crate::vaults::{VaultsApi, VaultsSource}; + +pub struct Client { + inner: Arc, +} + +impl std::fmt::Debug for Client { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Client") + .field("id", &self.inner.client_id()) + .finish() + } +} + +impl Client { + pub fn builder() -> ClientBuilder { + ClientBuilder { + config: ClientConfig::new_default(), + } + } + + pub fn secrets(&self) -> impl SecretsApi + '_ { + SecretsSource::new(&self.inner) + } + + pub fn items(&self) -> impl ItemsApi + '_ { + ItemsSource::new(&self.inner) + } + + pub fn vaults(&self) -> impl VaultsApi + '_ { + VaultsSource::new(&self.inner) + } + + pub fn groups(&self) -> impl GroupsApi + '_ { + GroupsSource::new(&self.inner) + } + + pub fn environments(&self) -> impl EnvironmentsApi + '_ { + EnvironmentsSource::new(&self.inner) + } +} + +impl Drop for Client { + fn drop(&mut self) { + self.inner.core.release_client(self.inner.client_id()); + } +} + +pub struct ClientBuilder { + config: ClientConfig, +} + +impl ClientBuilder { + pub fn service_account_token(mut self, token: &str) -> Self { + self.config.sa_token = token.to_string(); + self + } + + #[cfg(feature = "desktop")] + pub fn desktop_app_integration(mut self, account_name: &str) -> Self { + self.config.account_name = Some(account_name.to_string()); + self + } + + pub fn integration_info(mut self, name: &str, version: &str) -> Self { + self.config.integration_name = name.to_string(); + self.config.integration_version = version.to_string(); + self + } + + pub fn build(self) -> Result { + let has_sa_token = !self.config.sa_token.is_empty(); + let has_desktop = self.config.account_name.is_some(); + + if has_sa_token && has_desktop { + return Err(SdkError::Config( + "cannot use both SA token and desktop app authentication".to_string(), + )); + } + + if !has_sa_token && !has_desktop { + return Err(SdkError::Config( + "must set either service_account_token or desktop_app_integration".to_string(), + )); + } + + let core_impl: Box = if has_desktop { + #[cfg(feature = "desktop")] + { + let account_name = self.config.account_name.as_ref().unwrap().clone(); + Box::new(crate::core_shared_lib::SharedLibCore::new(&account_name)?) + } + #[cfg(not(feature = "desktop"))] + { + return Err(SdkError::Config( + "desktop app integration requires the 'desktop' feature".to_string(), + )); + } + } else { + #[cfg(feature = "wasm")] + { + Box::new(ExtismCore::new()?) + } + #[cfg(not(feature = "wasm"))] + { + return Err(SdkError::Config( + "WASM core requires the 'wasm' feature (enabled by default)".to_string(), + )); + } + }; + + let core = CoreWrapper { inner: core_impl }; + + let client_id = core + .init_client(&self.config) + .map_err(unmarshal_core_error)?; + + let inner = Arc::new(InnerClient { + id: AtomicU64::new(client_id), + config: self.config, + core, + retry_lock: std::sync::Mutex::new(()), + }); + + Ok(Client { inner }) + } +} + +/// Invoke a method on the WASM core. Used by all API implementations. +/// Automatically retries once on DesktopSessionExpired by re-initializing the client. +pub(crate) fn client_invoke( + inner: &InnerClient, + method: &str, + params: HashMap, +) -> Result { + let stale_id = inner.client_id(); + let invoke_config = InvokeConfig { + invocation: Invocation { + client_id: Some(stale_id), + parameters: Parameters { + name: method.to_string(), + parameters: params, + }, + }, + }; + + match inner.core.invoke(&invoke_config) { + Ok(response) => Ok(response), + Err(err) if matches!(err, SdkError::Plugin(_) | SdkError::SharedLib(_)) => { + let err = unmarshal_core_error(err); + if matches!(err, SdkError::DesktopSessionExpired(_)) { + retry_invoke( + inner, + method, + stale_id, + invoke_config.invocation.parameters.parameters, + ) + } else { + Err(err) + } + } + Err(err) => Err(err), + } +} + +fn retry_invoke( + inner: &InnerClient, + method: &str, + stale_id: u64, + params: HashMap, +) -> Result { + let _guard = inner + .retry_lock + .lock() + .map_err(|e| SdkError::Config(format!("retry lock poisoned: {e}")))?; + + let current_id = inner.client_id(); + let new_id = if current_id == stale_id { + let id = inner + .core + .init_client(&inner.config) + .map_err(unmarshal_core_error)?; + inner.core.release_client(stale_id); + inner.set_client_id(id); + id + } else { + current_id + }; + + let retry_config = InvokeConfig { + invocation: Invocation { + client_id: Some(new_id), + parameters: Parameters { + name: method.to_string(), + parameters: params, + }, + }, + }; + inner + .core + .invoke(&retry_config) + .map_err(unmarshal_core_error) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + #[test] + fn builder_rejects_both_auth_methods() { + let mut builder = Client::builder().service_account_token("ops_test"); + builder.config.account_name = Some("myaccount".to_string()); + let result = builder.build(); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("cannot use both")); + } + + struct RetryCore { + invoke_calls: AtomicUsize, + } + + impl crate::core::Core for RetryCore { + fn init_client(&self, _config: &[u8]) -> Result, SdkError> { + Ok(serde_json::to_vec(&99u64).unwrap()) + } + + fn invoke(&self, _invoke_config: &[u8]) -> Result, SdkError> { + if self.invoke_calls.fetch_add(1, Ordering::SeqCst) == 0 { + Err(SdkError::SharedLib( + r#"{"name":"DesktopSessionExpired","message":"expired"}"#.to_string(), + )) + } else { + Ok(br#""ok""#.to_vec()) + } + } + + fn release_client(&self, _client_id: &[u8]) {} + } + + #[test] + fn client_invoke_retries_shared_lib_session_expired() { + let inner = InnerClient { + id: AtomicU64::new(1), + config: ClientConfig::new_default(), + core: CoreWrapper { + inner: Box::new(RetryCore { + invoke_calls: AtomicUsize::new(0), + }), + }, + retry_lock: std::sync::Mutex::new(()), + }; + + let response = client_invoke(&inner, "SecretsResolve", HashMap::new()).unwrap(); + assert_eq!(response, r#""ok""#); + assert_eq!(inner.client_id(), 99); + } +} diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..96de648 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,242 @@ +use std::collections::HashMap; +use std::sync::Mutex; +use std::sync::atomic::{AtomicU64, Ordering}; + +use serde::Serialize; + +use crate::errors::{SdkError, invalid_utf8_error}; + +pub(crate) const SDK_LANGUAGE: &str = "Rust"; +pub(crate) const DEFAULT_REQUEST_LIBRARY: &str = "reqwest"; +const MESSAGE_LIMIT: usize = 50 * 1024 * 1024; + +pub(crate) trait Core: Send + Sync { + fn init_client(&self, config: &[u8]) -> Result, SdkError>; + fn invoke(&self, invoke_config: &[u8]) -> Result, SdkError>; + fn release_client(&self, client_id: &[u8]); +} + +#[derive(Serialize)] +pub(crate) struct ClientConfig { + #[serde( + rename = "serviceAccountToken", + skip_serializing_if = "String::is_empty" + )] + pub sa_token: String, + #[serde(rename = "programmingLanguage")] + pub language: String, + #[serde(rename = "sdkVersion")] + pub sdk_version: String, + #[serde(rename = "integrationName")] + pub integration_name: String, + #[serde(rename = "integrationVersion")] + pub integration_version: String, + #[serde(rename = "requestLibraryName")] + pub request_library_name: String, + #[serde(rename = "requestLibraryVersion")] + pub request_library_version: String, + #[serde(rename = "os")] + pub system_os: String, + #[serde(rename = "osVersion")] + pub system_os_version: String, + #[serde(rename = "architecture")] + pub system_arch: String, + #[serde(skip_serializing)] + pub account_name: Option, +} + +impl std::fmt::Debug for ClientConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClientConfig") + .field("sa_token", &"[REDACTED]") + .field("language", &self.language) + .field("sdk_version", &self.sdk_version) + .field("integration_name", &self.integration_name) + .field("integration_version", &self.integration_version) + .field("system_os", &self.system_os) + .field("system_arch", &self.system_arch) + .finish() + } +} + +impl ClientConfig { + pub fn new_default() -> Self { + Self { + sa_token: String::new(), + language: SDK_LANGUAGE.to_string(), + // Must match the Go SDK's version-build format (7-digit semver: Mmmppbb) + // for the WASM core we embedded (v0.4.1-beta.1 = 0040101) + sdk_version: "0040101".to_string(), + integration_name: "Unknown".to_string(), + integration_version: "Unknown".to_string(), + request_library_name: DEFAULT_REQUEST_LIBRARY.to_string(), + request_library_version: "0.0.0".to_string(), + system_os: std::env::consts::OS.to_string(), + system_os_version: "0.0.0".to_string(), + system_arch: std::env::consts::ARCH.to_string(), + account_name: None, + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct InvokeConfig { + pub invocation: Invocation, +} + +#[derive(Debug, Serialize)] +pub(crate) struct Invocation { + #[serde(rename = "clientId", skip_serializing_if = "Option::is_none")] + pub client_id: Option, + pub parameters: Parameters, +} + +#[derive(Debug, Serialize)] +pub(crate) struct Parameters { + pub name: String, + pub parameters: HashMap, +} + +/// Wraps a Core implementation and handles JSON marshaling/unmarshaling. +pub(crate) struct CoreWrapper { + pub inner: Box, +} + +impl CoreWrapper { + pub fn init_client(&self, config: &ClientConfig) -> Result { + let config_bytes = serde_json::to_vec(config)?; + let res = self.inner.init_client(&config_bytes)?; + let id: u64 = serde_json::from_slice(&res)?; + Ok(id) + } + + pub fn invoke(&self, invoke_config: &InvokeConfig) -> Result { + let input = serde_json::to_vec(invoke_config)?; + if input.len() > MESSAGE_LIMIT { + return Err(SdkError::Config(format!( + "message size exceeds the limit of {} bytes", + MESSAGE_LIMIT + ))); + } + let res = self.inner.invoke(&input)?; + String::from_utf8(res).map_err(invalid_utf8_error) + } + + pub fn release_client(&self, client_id: u64) { + if let Ok(id_bytes) = serde_json::to_vec(&client_id) { + self.inner.release_client(&id_bytes); + } + } +} + +/// The inner client state shared by all API implementations. +pub(crate) struct InnerClient { + pub id: AtomicU64, + pub config: ClientConfig, + pub core: CoreWrapper, + pub retry_lock: Mutex<()>, +} + +impl InnerClient { + pub fn client_id(&self) -> u64 { + self.id.load(Ordering::Acquire) + } + + pub fn set_client_id(&self, id: u64) { + self.id.store(id, Ordering::Release); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_config_has_correct_language() { + let config = ClientConfig::new_default(); + assert_eq!(config.language, "Rust"); + assert_eq!(config.system_os, std::env::consts::OS); + assert_eq!(config.system_arch, std::env::consts::ARCH); + } + + #[test] + fn invoke_config_serializes_correctly() { + let config = InvokeConfig { + invocation: Invocation { + client_id: Some(42), + parameters: Parameters { + name: "SecretsResolve".to_string(), + parameters: { + let mut m = HashMap::new(); + m.insert( + "secret_reference".to_string(), + serde_json::Value::String("op://vault/item/field".to_string()), + ); + m + }, + }, + }, + }; + let json = serde_json::to_string(&config).unwrap(); + assert!(json.contains("\"clientId\":42")); + assert!(json.contains("\"name\":\"SecretsResolve\"")); + } + + #[test] + fn service_account_config_serializes_token() { + let mut config = ClientConfig::new_default(); + config.sa_token = "ops_test".to_string(); + + let value = serde_json::to_value(&config).unwrap(); + assert_eq!(value["serviceAccountToken"], "ops_test"); + } + + #[test] + fn desktop_config_omits_auth_fields_from_core_config() { + let mut config = ClientConfig::new_default(); + config.account_name = Some("myaccount".to_string()); + + let value = serde_json::to_value(&config).unwrap(); + assert!(value.get("serviceAccountToken").is_none()); + assert!(value.get("account_name").is_none()); + } + + struct InvalidUtf8Core; + + impl Core for InvalidUtf8Core { + fn init_client(&self, _config: &[u8]) -> Result, SdkError> { + Ok(Vec::new()) + } + + fn invoke(&self, _invoke_config: &[u8]) -> Result, SdkError> { + Ok(vec![0xff]) + } + + fn release_client(&self, _client_id: &[u8]) {} + } + + #[test] + fn invoke_rejects_invalid_utf8_response() { + let wrapper = CoreWrapper { + inner: Box::new(InvalidUtf8Core), + }; + let config = InvokeConfig { + invocation: Invocation { + client_id: None, + parameters: Parameters { + name: "InvalidUtf8".to_string(), + parameters: HashMap::new(), + }, + }, + }; + + let err = wrapper.invoke(&config).unwrap_err(); + match err { + SdkError::Core { name, message } => { + assert_eq!(name, "InvalidUtf8"); + assert!(message.contains("not valid UTF-8")); + } + _ => panic!("expected InvalidUtf8 core error"), + } + } +} diff --git a/src/core_extism.rs b/src/core_extism.rs new file mode 100644 index 0000000..d1fa1f7 --- /dev/null +++ b/src/core_extism.rs @@ -0,0 +1,196 @@ +use std::sync::Mutex; + +use extism::{Manifest, Plugin, PluginBuilder, UserData, ValType, Wasm}; + +use crate::core::Core; +use crate::errors::SdkError; + +static CORE_WASM: &[u8] = include_bytes!("../wasm/core.wasm"); + +/// Allowed 1Password hosts that the WASM core may connect to. +const ALLOWED_HOSTS: &[&str] = &[ + "*.1password.com", + "*.1password.ca", + "*.1password.eu", + "*.b5staging.com", + "*.b5dev.com", + "*.b5dev.ca", + "*.b5dev.eu", + "*.b5test.com", + "*.b5test.ca", + "*.b5test.eu", + "*.b5rev.com", + "*.b5local.com", +]; + +/// ExtismCore implements the [`Core`] trait using an embedded WASM binary +/// loaded through the Extism runtime. +pub(crate) struct ExtismCore { + plugin: Mutex, +} + +impl ExtismCore { + /// Create a new `ExtismCore` by loading the embedded WASM binary and + /// registering required host functions. + pub fn new() -> Result { + let wasm = Wasm::data(CORE_WASM); + let manifest = + Manifest::new([wasm]).with_allowed_hosts(ALLOWED_HOSTS.iter().map(|h| h.to_string())); + + let no_data: UserData<()> = UserData::new(()); + + let plugin = PluginBuilder::new(manifest) + .with_wasi(true) + // random_fill_imported: fills a buffer with crypto-random bytes + // Takes I32 (length), returns I64 (pointer to allocated memory) + .with_function_in_namespace( + "op-extism-core", + "random_fill_imported", + [ValType::I32], + [ValType::I64], + no_data.clone(), + random_fill, + ) + // unix_time_milliseconds_imported in "op-now" namespace + .with_function_in_namespace( + "op-now", + "unix_time_milliseconds_imported", + [], + [ValType::I64], + no_data.clone(), + unix_time_milliseconds, + ) + // unix_time_milliseconds_imported in "zxcvbn" namespace + .with_function_in_namespace( + "zxcvbn", + "unix_time_milliseconds_imported", + [], + [ValType::I64], + no_data.clone(), + unix_time_milliseconds, + ) + // utc_offset_seconds in "op-time" namespace + .with_function_in_namespace( + "op-time", + "utc_offset_seconds", + [], + [ValType::I64], + no_data, + utc_offset_seconds, + ) + .build() + .map_err(|e| SdkError::Plugin(format!("failed to initialize WASM plugin: {e}")))?; + + Ok(Self { + plugin: Mutex::new(plugin), + }) + } +} + +impl Core for ExtismCore { + fn init_client(&self, config: &[u8]) -> Result, SdkError> { + let mut plugin = self + .plugin + .lock() + .map_err(|e| SdkError::Plugin(format!("mutex poisoned: {e}")))?; + let res = plugin + .call::<&[u8], &[u8]>("init_client", config) + .map_err(|e| SdkError::Plugin(e.to_string()))?; + Ok(res.to_vec()) + } + + fn invoke(&self, invoke_config: &[u8]) -> Result, SdkError> { + let mut plugin = self + .plugin + .lock() + .map_err(|e| SdkError::Plugin(format!("mutex poisoned: {e}")))?; + let res = plugin + .call::<&[u8], &[u8]>("invoke", invoke_config) + .map_err(|e| SdkError::Plugin(e.to_string()))?; + Ok(res.to_vec()) + } + + fn release_client(&self, client_id: &[u8]) { + if let Ok(mut plugin) = self.plugin.lock() { + let _ = plugin.call::<&[u8], &[u8]>("release_client", client_id); + } + } +} + +/// Host function: generate cryptographically random bytes. +/// +/// The WASM core calls this with a length (I32) and expects back a pointer (I64) +/// to the allocated memory block containing the random bytes. +fn random_fill( + plugin: &mut extism::CurrentPlugin, + inputs: &[extism::Val], + outputs: &mut [extism::Val], + _user_data: UserData<()>, +) -> Result<(), extism::Error> { + let length = random_fill_length(inputs)?; + let mut buf = vec![0u8; length]; + getrandom::getrandom(&mut buf) + .map_err(|e| extism::Error::msg(format!("getrandom failed: {e}")))?; + let handle = plugin.memory_new(&buf)?; + outputs[0] = plugin.memory_to_val(handle); + Ok(()) +} + +fn random_fill_length(inputs: &[extism::Val]) -> Result { + let length = inputs + .first() + .ok_or_else(|| extism::Error::msg("random_fill_imported missing length argument"))? + .unwrap_i32(); + if length < 0 { + return Err(extism::Error::msg(format!( + "random_fill_imported length must be non-negative, got {length}" + ))); + } + Ok(length as usize) +} + +/// Host function: return current Unix timestamp in milliseconds. +fn unix_time_milliseconds( + _plugin: &mut extism::CurrentPlugin, + _inputs: &[extism::Val], + outputs: &mut [extism::Val], + _user_data: UserData<()>, +) -> Result<(), extism::Error> { + let now = chrono::Utc::now().timestamp_millis(); + outputs[0] = extism::Val::I64(now); + Ok(()) +} + +/// Host function: return the local timezone's UTC offset in seconds. +fn utc_offset_seconds( + _plugin: &mut extism::CurrentPlugin, + _inputs: &[extism::Val], + outputs: &mut [extism::Val], + _user_data: UserData<()>, +) -> Result<(), extism::Error> { + let offset_secs = chrono::Local::now().offset().local_minus_utc(); + outputs[0] = extism::Val::I64(offset_secs as i64); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn random_fill_length_rejects_negative_lengths() { + let err = random_fill_length(&[extism::Val::I32(-1)]).unwrap_err(); + assert!(err.to_string().contains("non-negative")); + } + + #[test] + fn random_fill_length_rejects_empty_inputs() { + let err = random_fill_length(&[]).unwrap_err(); + assert!(err.to_string().contains("missing length argument")); + } + + #[test] + fn random_fill_length_accepts_non_negative_lengths() { + assert_eq!(random_fill_length(&[extism::Val::I32(32)]).unwrap(), 32); + } +} diff --git a/src/core_shared_lib.rs b/src/core_shared_lib.rs new file mode 100644 index 0000000..7e5c848 --- /dev/null +++ b/src/core_shared_lib.rs @@ -0,0 +1,287 @@ +use std::path::PathBuf; + +use base64::Engine; +use libloading::{Library, Symbol}; +use serde::{Deserialize, Serialize}; + +use crate::core::Core; +use crate::errors::SdkError; + +/// FFI signature for `op_sdk_ipc_send_message`. +/// +/// int32_t op_sdk_ipc_send_message( +/// const uint8_t* msg_ptr, size_t msg_len, +/// uint8_t** out_buf, size_t* out_len, size_t* out_cap +/// ); +type SendMessageFn = unsafe extern "C" fn( + msg_ptr: *const u8, + msg_len: usize, + out_buf: *mut *mut u8, + out_len: *mut usize, + out_cap: *mut usize, +) -> i32; + +/// FFI signature for `op_sdk_ipc_free_response`. +/// +/// void op_sdk_ipc_free_response(uint8_t* buf, size_t len, size_t cap); +type FreeResponseFn = unsafe extern "C" fn(buf: *mut u8, len: usize, cap: usize); + +/// JSON request envelope sent to the shared library. +/// The Go SDK sends payload as base64 (Go's json.Marshal of []byte). +/// The shared library expects this format. +#[derive(Serialize)] +struct Request { + kind: String, + account_name: String, + payload: String, +} + +/// JSON response envelope received from the shared library. +/// Go's json.Marshal encodes `[]byte` as a base64 string. +#[derive(Deserialize)] +struct Response { + success: bool, + #[serde(with = "base64_payload")] + payload: Vec, +} + +mod base64_payload { + use base64::Engine; + use serde::{Deserialize, Deserializer}; + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = String::deserialize(deserializer)?; + base64::engine::general_purpose::STANDARD + .decode(&s) + .map_err(serde::de::Error::custom) + } +} + +pub(crate) struct SharedLibCore { + account_name: String, + _library: Library, + send_message: SendMessageFn, + free_response: FreeResponseFn, +} + +// SAFETY: The shared library functions are thread-safe per 1Password's Go SDK usage +// (the Go SDK calls these across goroutines). +unsafe impl Send for SharedLibCore {} +unsafe impl Sync for SharedLibCore {} + +impl SharedLibCore { + pub fn new(account_name: &str) -> Result { + let lib_path = find_1password_lib_path()?; + + let library = unsafe { + Library::new(&lib_path) + .map_err(|e| SdkError::SharedLib(format!("failed to load library: {e}")))? + }; + + let send_message: SendMessageFn = unsafe { + let sym: Symbol = library + .get(b"op_sdk_ipc_send_message") + .map_err(|e| SdkError::SharedLib(format!("symbol not found: {e}")))?; + *sym + }; + + let free_response: FreeResponseFn = unsafe { + let sym: Symbol = library + .get(b"op_sdk_ipc_free_response") + .map_err(|e| SdkError::SharedLib(format!("symbol not found: {e}")))?; + *sym + }; + + Ok(Self { + account_name: account_name.to_string(), + _library: library, + send_message, + free_response, + }) + } + + fn call_shared_library(&self, kind: &str, payload: &[u8]) -> Result, SdkError> { + let request = Request { + kind: kind.to_string(), + account_name: self.account_name.clone(), + payload: base64::engine::general_purpose::STANDARD.encode(payload), + }; + let input = serde_json::to_vec(&request) + .map_err(|e| SdkError::SharedLib(format!("failed to serialize request: {e}")))?; + + let mut out_buf: *mut u8 = std::ptr::null_mut(); + let mut out_len: usize = 0; + let mut out_cap: usize = 0; + + let ret_code = unsafe { + (self.send_message)( + input.as_ptr(), + input.len(), + &mut out_buf, + &mut out_len, + &mut out_cap, + ) + }; + + if ret_code != 0 { + if !out_buf.is_null() { + unsafe { (self.free_response)(out_buf, out_len, out_cap) }; + } + error_from_return_code(ret_code)?; + } + + // Copy the response bytes before freeing the library-owned buffer. + let resp_bytes = if !out_buf.is_null() { + let bytes = if out_len > 0 { + unsafe { std::slice::from_raw_parts(out_buf, out_len).to_vec() } + } else { + Vec::new() + }; + unsafe { (self.free_response)(out_buf, out_len, out_cap) }; + bytes + } else { + Vec::new() + }; + + let response: Response = serde_json::from_slice(&resp_bytes) + .map_err(|e| SdkError::SharedLib(format!("failed to parse response: {e}")))?; + + if response.success { + Ok(response.payload) + } else { + let msg = String::from_utf8(response.payload).map_err(|e| { + SdkError::SharedLib(format!("error payload was not valid UTF-8: {e}")) + })?; + Err(SdkError::SharedLib(msg)) + } + } +} + +impl Core for SharedLibCore { + fn init_client(&self, config: &[u8]) -> Result, SdkError> { + self.call_shared_library("init_client", config) + } + + fn invoke(&self, invoke_config: &[u8]) -> Result, SdkError> { + self.call_shared_library("invoke", invoke_config) + } + + fn release_client(&self, client_id: &[u8]) { + let _ = self.call_shared_library("release_client", client_id); + } +} + +fn error_from_return_code(ret_code: i32) -> Result<(), SdkError> { + if ret_code == 0 { + return Ok(()); + } + + let msg = if cfg!(target_os = "macos") { + match ret_code { + -3 => "desktop app connection channel is closed. Make sure Settings > Developer > Integrate with other apps is enabled".to_string(), + -7 => "connection was unexpectedly dropped by the desktop app. Make sure the desktop app is running".to_string(), + _ => format!("an internal error occurred. Return code: {ret_code}"), + } + } else { + match ret_code { + -2 => "desktop app connection channel is closed. Make sure Settings > Developer > Integrate with other apps is enabled".to_string(), + -5 => "connection was unexpectedly dropped by the desktop app. Make sure the desktop app is running".to_string(), + _ => format!("an internal error occurred. Return code: {ret_code}"), + } + }; + + Err(SdkError::SharedLib(msg)) +} + +fn home_dir() -> PathBuf { + #[cfg(target_os = "windows")] + { + std::env::var("USERPROFILE") + .ok() + .map(PathBuf::from) + .unwrap_or_default() + } + #[cfg(not(target_os = "windows"))] + { + std::env::var("HOME") + .ok() + .map(PathBuf::from) + .unwrap_or_default() + } +} + +fn find_1password_lib_path() -> Result { + let home = home_dir(); + + let locations: Vec = if cfg!(target_os = "macos") { + vec![ + PathBuf::from( + "/Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib", + ), + home.join("Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib"), + ] + } else if cfg!(target_os = "linux") { + vec![ + PathBuf::from("/usr/bin/1password/libop_sdk_ipc_client.so"), + PathBuf::from("/opt/1Password/libop_sdk_ipc_client.so"), + PathBuf::from("/snap/bin/1password/libop_sdk_ipc_client.so"), + ] + } else if cfg!(target_os = "windows") { + vec![ + home.join(r"AppData\Local\1Password\op_sdk_ipc_client.dll"), + PathBuf::from(r"C:\Program Files\1Password\app\8\op_sdk_ipc_client.dll"), + PathBuf::from(r"C:\Program Files (x86)\1Password\app\8\op_sdk_ipc_client.dll"), + home.join(r"AppData\Local\1Password\app\8\op_sdk_ipc_client.dll"), + ] + } else { + return Err(SdkError::SharedLib(format!( + "unsupported OS: {}", + std::env::consts::OS + ))); + }; + + for path in &locations { + if path.exists() { + return Ok(path.clone()); + } + } + + Err(SdkError::SharedLib(format!( + "1Password desktop application not found. Searched: {}", + locations + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(", ") + ))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_payload_serializes_as_base64() { + let request = Request { + kind: "invoke".to_string(), + account_name: "account".to_string(), + payload: base64::engine::general_purpose::STANDARD.encode(b"payload"), + }; + + let value = serde_json::to_value(&request).unwrap(); + assert_eq!(value["payload"], "cGF5bG9hZA=="); + } + + #[test] + fn response_payload_deserializes_from_base64() { + let response_json = serde_json::json!({ + "success": true, + "payload": "cGF5bG9hZA==", + }) + .to_string(); + + let response: Response = serde_json::from_str(&response_json).unwrap(); + assert!(response.success); + assert_eq!(response.payload, b"payload"); + } +} diff --git a/src/environments.rs b/src/environments.rs new file mode 100644 index 0000000..24f83fd --- /dev/null +++ b/src/environments.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::GetVariablesResponse; + +pub trait EnvironmentsApi { + fn get_variables(&self, environment_id: &str) -> Result; +} + +pub(crate) struct EnvironmentsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> EnvironmentsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl EnvironmentsApi for EnvironmentsSource<'_> { + fn get_variables(&self, environment_id: &str) -> Result { + let mut p = HashMap::new(); + p.insert( + "environment_id".to_string(), + serde_json::Value::String(environment_id.to_string()), + ); + let result = client_invoke(self.inner, "EnvironmentsGetVariables", p)?; + Ok(serde_json::from_str(&result)?) + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..d76cfcb --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,124 @@ +use serde::Deserialize; + +#[derive(Debug, thiserror::Error)] +pub enum SdkError { + #[error("1Password SDK error: {message}")] + Core { name: String, message: String }, + + #[error("serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("WASM plugin error: {0}")] + Plugin(String), + + #[error("configuration error: {0}")] + Config(String), + + #[error("desktop session expired: {0}")] + DesktopSessionExpired(String), + + #[error("rate limit exceeded: {0}")] + RateLimitExceeded(String), + + #[error("shared library error: {0}")] + SharedLib(String), +} + +#[derive(Deserialize)] +struct CoreError { + name: String, + message: String, +} + +fn try_unmarshal_error(err: &str) -> Option { + match serde_json::from_str::(err) { + Ok(core_err) => match core_err.name.as_str() { + "DesktopSessionExpired" => Some(SdkError::DesktopSessionExpired(core_err.message)), + "RateLimitExceeded" => Some(SdkError::RateLimitExceeded(core_err.message)), + _ => Some(SdkError::Core { + name: core_err.name, + message: core_err.message, + }), + }, + Err(_) => None, + } +} + +/// Deserialize a JSON error string from the WASM core into an SdkError. +pub(crate) fn unmarshal_error(err: &str) -> SdkError { + try_unmarshal_error(err).unwrap_or_else(|| SdkError::Core { + name: "Unknown".to_string(), + message: err.to_string(), + }) +} + +/// Convert backend transport errors into typed SDK errors when they contain core JSON. +pub(crate) fn unmarshal_core_error(err: SdkError) -> SdkError { + match err { + SdkError::Plugin(msg) => unmarshal_error(&msg), + SdkError::SharedLib(msg) => try_unmarshal_error(&msg).unwrap_or(SdkError::SharedLib(msg)), + err => err, + } +} + +pub(crate) fn invalid_utf8_error(err: std::string::FromUtf8Error) -> SdkError { + SdkError::Core { + name: "InvalidUtf8".to_string(), + message: format!("core response was not valid UTF-8: {err}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unmarshal_core_json_error() { + let err = unmarshal_error(r#"{"name":"SomeError","message":"something broke"}"#); + match err { + SdkError::Core { name, message } => { + assert_eq!(name, "SomeError"); + assert_eq!(message, "something broke"); + } + _ => panic!("expected Core error"), + } + } + + #[test] + fn unmarshal_session_expired() { + let err = unmarshal_error(r#"{"name":"DesktopSessionExpired","message":"session gone"}"#); + assert!(matches!(err, SdkError::DesktopSessionExpired(_))); + } + + #[test] + fn unmarshal_rate_limit() { + let err = unmarshal_error(r#"{"name":"RateLimitExceeded","message":"slow down"}"#); + assert!(matches!(err, SdkError::RateLimitExceeded(_))); + } + + #[test] + fn unmarshal_invalid_json() { + let err = unmarshal_error("not json at all"); + match err { + SdkError::Core { message, .. } => assert_eq!(message, "not json at all"), + _ => panic!("expected Core error for invalid JSON"), + } + } + + #[test] + fn unmarshal_core_error_preserves_non_json_shared_lib_error() { + let err = unmarshal_core_error(SdkError::SharedLib("connection dropped".to_string())); + match err { + SdkError::SharedLib(message) => assert_eq!(message, "connection dropped"), + _ => panic!("expected SharedLib error"), + } + } + + #[test] + fn unmarshal_core_error_maps_json_shared_lib_error() { + let err = unmarshal_core_error(SdkError::SharedLib( + r#"{"name":"RateLimitExceeded","message":"slow down"}"#.to_string(), + )); + assert!(matches!(err, SdkError::RateLimitExceeded(_))); + } +} diff --git a/src/groups.rs b/src/groups.rs new file mode 100644 index 0000000..93fa842 --- /dev/null +++ b/src/groups.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::*; + +pub trait GroupsApi { + fn get(&self, group_id: &str, params: GroupGetParams) -> Result; +} + +pub(crate) struct GroupsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> GroupsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl GroupsApi for GroupsSource<'_> { + fn get(&self, group_id: &str, params: GroupGetParams) -> Result { + let mut p = HashMap::new(); + p.insert( + "group_id".to_string(), + serde_json::Value::String(group_id.to_string()), + ); + p.insert("group_params".to_string(), serde_json::to_value(¶ms)?); + let result = client_invoke(self.inner, "GroupsGet", p)?; + Ok(serde_json::from_str(&result)?) + } +} diff --git a/src/items.rs b/src/items.rs new file mode 100644 index 0000000..686b1a1 --- /dev/null +++ b/src/items.rs @@ -0,0 +1,171 @@ +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::items_files::{ItemsFilesApi, ItemsFilesSource}; +use crate::items_shares::{ItemsSharesApi, ItemsSharesSource}; +use crate::types::*; + +pub trait ItemsApi { + fn create(&self, params: ItemCreateParams) -> Result; + fn create_all( + &self, + vault_id: &str, + params: &[ItemCreateParams], + ) -> Result; + fn get(&self, vault_id: &str, item_id: &str) -> Result; + fn get_all(&self, vault_id: &str, item_ids: &[String]) + -> Result; + fn put(&self, item: Item) -> Result; + fn delete(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError>; + fn delete_all( + &self, + vault_id: &str, + item_ids: &[String], + ) -> Result; + fn archive(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError>; + fn list( + &self, + vault_id: &str, + filters: &[ItemListFilter], + ) -> Result, SdkError>; + fn shares(&self) -> impl ItemsSharesApi + '_; + fn files(&self) -> impl ItemsFilesApi + '_; +} + +pub(crate) struct ItemsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> ItemsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl ItemsApi for ItemsSource<'_> { + fn create(&self, params: ItemCreateParams) -> Result { + let mut p = HashMap::new(); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result = client_invoke(self.inner, "ItemsCreate", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn create_all( + &self, + vault_id: &str, + params: &[ItemCreateParams], + ) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("params".to_string(), serde_json::to_value(params)?); + let result = client_invoke(self.inner, "ItemsCreateAll", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn get(&self, vault_id: &str, item_id: &str) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + let result = client_invoke(self.inner, "ItemsGet", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn get_all( + &self, + vault_id: &str, + item_ids: &[String], + ) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("item_ids".to_string(), serde_json::to_value(item_ids)?); + let result = client_invoke(self.inner, "ItemsGetAll", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn put(&self, item: Item) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + let result = client_invoke(self.inner, "ItemsPut", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn delete(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + client_invoke(self.inner, "ItemsDelete", p)?; + Ok(()) + } + + fn delete_all( + &self, + vault_id: &str, + item_ids: &[String], + ) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("item_ids".to_string(), serde_json::to_value(item_ids)?); + let result = client_invoke(self.inner, "ItemsDeleteAll", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn archive(&self, vault_id: &str, item_id: &str) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + client_invoke(self.inner, "ItemsArchive", p)?; + Ok(()) + } + + fn list( + &self, + vault_id: &str, + filters: &[ItemListFilter], + ) -> Result, SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("filters".to_string(), serde_json::to_value(filters)?); + let result = client_invoke(self.inner, "ItemsList", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn shares(&self) -> impl ItemsSharesApi + '_ { + ItemsSharesSource::new(self.inner) + } + + fn files(&self) -> impl ItemsFilesApi + '_ { + ItemsFilesSource::new(self.inner) + } +} diff --git a/src/items_files.rs b/src/items_files.rs new file mode 100644 index 0000000..a9b78bf --- /dev/null +++ b/src/items_files.rs @@ -0,0 +1,92 @@ +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::*; + +pub trait ItemsFilesApi { + fn attach(&self, item: Item, file_params: FileCreateParams) -> Result; + fn read( + &self, + vault_id: &str, + item_id: &str, + attr: FileAttributes, + ) -> Result, SdkError>; + fn delete(&self, item: Item, section_id: &str, field_id: &str) -> Result; + fn replace_document( + &self, + item: Item, + doc_params: DocumentCreateParams, + ) -> Result; +} + +pub(crate) struct ItemsFilesSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> ItemsFilesSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl ItemsFilesApi for ItemsFilesSource<'_> { + fn attach(&self, item: Item, file_params: FileCreateParams) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + p.insert( + "file_params".to_string(), + serde_json::to_value(&file_params)?, + ); + let result = client_invoke(self.inner, "ItemsFilesAttach", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn read( + &self, + vault_id: &str, + item_id: &str, + attr: FileAttributes, + ) -> Result, SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + p.insert("attr".to_string(), serde_json::to_value(&attr)?); + let result = client_invoke(self.inner, "ItemsFilesRead", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn delete(&self, item: Item, section_id: &str, field_id: &str) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + p.insert( + "section_id".to_string(), + serde_json::Value::String(section_id.to_string()), + ); + p.insert( + "field_id".to_string(), + serde_json::Value::String(field_id.to_string()), + ); + let result = client_invoke(self.inner, "ItemsFilesDelete", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn replace_document( + &self, + item: Item, + doc_params: DocumentCreateParams, + ) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + p.insert("doc_params".to_string(), serde_json::to_value(&doc_params)?); + let result = client_invoke(self.inner, "ItemsFilesReplaceDocument", p)?; + Ok(serde_json::from_str(&result)?) + } +} diff --git a/src/items_shares.rs b/src/items_shares.rs new file mode 100644 index 0000000..40b6282 --- /dev/null +++ b/src/items_shares.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::*; + +pub trait ItemsSharesApi { + fn get_account_policy( + &self, + vault_id: &str, + item_id: &str, + ) -> Result; + fn validate_recipients( + &self, + policy: ItemShareAccountPolicy, + recipients: &[String], + ) -> Result, SdkError>; + fn create( + &self, + item: Item, + policy: ItemShareAccountPolicy, + params: ItemShareParams, + ) -> Result; +} + +pub(crate) struct ItemsSharesSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> ItemsSharesSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl ItemsSharesApi for ItemsSharesSource<'_> { + fn get_account_policy( + &self, + vault_id: &str, + item_id: &str, + ) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "item_id".to_string(), + serde_json::Value::String(item_id.to_string()), + ); + let result = client_invoke(self.inner, "ItemsSharesGetAccountPolicy", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn validate_recipients( + &self, + policy: ItemShareAccountPolicy, + recipients: &[String], + ) -> Result, SdkError> { + let mut p = HashMap::new(); + p.insert("policy".to_string(), serde_json::to_value(&policy)?); + p.insert("recipients".to_string(), serde_json::to_value(recipients)?); + let result = client_invoke(self.inner, "ItemsSharesValidateRecipients", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn create( + &self, + item: Item, + policy: ItemShareAccountPolicy, + params: ItemShareParams, + ) -> Result { + let mut p = HashMap::new(); + p.insert("item".to_string(), serde_json::to_value(&item)?); + p.insert("policy".to_string(), serde_json::to_value(&policy)?); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result = client_invoke(self.inner, "ItemsSharesCreate", p)?; + Ok(serde_json::from_str(&result)?) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6898b76 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,30 @@ +#![deny(unsafe_code)] + +mod client; +mod core; +#[cfg(feature = "wasm")] +mod core_extism; +mod environments; +mod errors; +mod groups; +mod items; +mod items_files; +mod items_shares; +mod secrets; +mod types; +mod vaults; + +#[cfg(feature = "desktop")] +#[allow(unsafe_code)] +mod core_shared_lib; + +pub use client::{Client, ClientBuilder}; +pub use environments::EnvironmentsApi; +pub use errors::SdkError; +pub use groups::GroupsApi; +pub use items::ItemsApi; +pub use items_files::ItemsFilesApi; +pub use items_shares::ItemsSharesApi; +pub use secrets::{Secrets, SecretsApi}; +pub use types::*; +pub use vaults::VaultsApi; diff --git a/src/secrets.rs b/src/secrets.rs new file mode 100644 index 0000000..cd34859 --- /dev/null +++ b/src/secrets.rs @@ -0,0 +1,116 @@ +use std::collections::HashMap; +#[cfg(feature = "wasm")] +use std::sync::LazyLock; + +use crate::client::client_invoke; +use crate::core::{CoreWrapper, InnerClient}; +use crate::errors::{SdkError, unmarshal_core_error}; +use crate::types::{GeneratePasswordResponse, PasswordRecipe, ResolveAllResponse}; + +#[cfg(feature = "wasm")] +static STANDALONE_CORE: LazyLock> = LazyLock::new(|| { + let core = crate::core_extism::ExtismCore::new().map_err(|e| e.to_string())?; + Ok(CoreWrapper { + inner: Box::new(core), + }) +}); + +#[cfg(feature = "wasm")] +fn standalone_core() -> Result<&'static CoreWrapper, SdkError> { + STANDALONE_CORE + .as_ref() + .map_err(|e| SdkError::Plugin(e.clone())) +} + +#[cfg(not(feature = "wasm"))] +fn standalone_core() -> Result<&'static CoreWrapper, SdkError> { + Err(SdkError::Config( + "standalone secrets require the 'wasm' feature (enabled by default)".to_string(), + )) +} + +pub trait SecretsApi { + fn resolve(&self, secret_reference: &str) -> Result; + fn resolve_all(&self, secret_references: &[String]) -> Result; +} + +pub(crate) struct SecretsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> SecretsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl SecretsApi for SecretsSource<'_> { + fn resolve(&self, secret_reference: &str) -> Result { + let mut params = HashMap::new(); + params.insert( + "secret_reference".to_string(), + serde_json::Value::String(secret_reference.to_string()), + ); + let result_string = client_invoke(self.inner, "SecretsResolve", params)?; + let result: String = serde_json::from_str(&result_string)?; + Ok(result) + } + + fn resolve_all(&self, secret_references: &[String]) -> Result { + let mut params = HashMap::new(); + params.insert( + "secret_references".to_string(), + serde_json::to_value(secret_references)?, + ); + let result_string = client_invoke(self.inner, "SecretsResolveAll", params)?; + let result: ResolveAllResponse = serde_json::from_str(&result_string)?; + Ok(result) + } +} + +/// Standalone secret utilities (no client required). +pub struct Secrets; + +impl Secrets { + pub fn validate_secret_reference(secret_reference: &str) -> Result<(), SdkError> { + let core = standalone_core()?; + let invoke_config = crate::core::InvokeConfig { + invocation: crate::core::Invocation { + client_id: None, + parameters: crate::core::Parameters { + name: "ValidateSecretReference".to_string(), + parameters: { + let mut m = HashMap::new(); + m.insert( + "secret_reference".to_string(), + serde_json::Value::String(secret_reference.to_string()), + ); + m + }, + }, + }, + }; + core.invoke(&invoke_config).map_err(unmarshal_core_error)?; + Ok(()) + } + + pub fn generate_password(recipe: PasswordRecipe) -> Result { + let core = standalone_core()?; + let invoke_config = crate::core::InvokeConfig { + invocation: crate::core::Invocation { + client_id: None, + parameters: crate::core::Parameters { + name: "GeneratePassword".to_string(), + parameters: { + let mut m = HashMap::new(); + m.insert("recipe".to_string(), serde_json::to_value(&recipe)?); + m + }, + }, + }, + }; + let result_string = core.invoke(&invoke_config).map_err(unmarshal_core_error)?; + let result: GeneratePasswordResponse = serde_json::from_str(&result_string)?; + Ok(result) + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..5520d66 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,749 @@ +use serde::{Deserialize, Serialize}; + +pub type ErrorMessage = String; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddressFieldDetails { + pub street: String, + pub city: String, + pub country: String, + pub zip: String, + pub state: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DocumentCreateParams { + pub name: String, + pub content: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileAttributes { + pub name: String, + pub id: String, + pub size: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileCreateParams { + pub name: String, + pub content: Vec, + pub section_id: String, + pub field_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnvironmentVariable { + pub name: String, + pub value: String, + pub masked: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetVariablesResponse { + pub variables: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct GeneratePasswordResponse { + pub password: String, +} + +impl std::fmt::Debug for GeneratePasswordResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GeneratePasswordResponse") + .field("password", &"[REDACTED]") + .finish() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SeparatorType { + Digits, + DigitsAndSymbols, + Spaces, + Hyphens, + Underscores, + Periods, + Commas, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum WordListType { + FullWords, + Syllables, + ThreeLetters, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasswordRecipeMemorableInner { + pub separator_type: SeparatorType, + pub capitalize: bool, + pub word_list_type: WordListType, + pub word_count: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PasswordRecipePinInner { + pub length: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasswordRecipeRandomInner { + pub include_digits: bool, + pub include_symbols: bool, + pub length: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "parameters")] +pub enum PasswordRecipe { + Memorable(PasswordRecipeMemorableInner), + Pin(PasswordRecipePinInner), + Random(PasswordRecipeRandomInner), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SSHKeyAttributes { + pub public_key: String, + pub fingerprint: String, + pub key_type: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OTPFieldDetails { + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "content")] +pub enum ItemFieldDetails { + Otp(OTPFieldDetails), + SshKey(SSHKeyAttributes), + Address(AddressFieldDetails), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ItemCategory { + Login, + SecureNote, + CreditCard, + CryptoWallet, + Identity, + Password, + Document, + ApiCredentials, + BankAccount, + Database, + DriverLicense, + Email, + MedicalRecord, + Membership, + OutdoorLicense, + Passport, + Rewards, + Router, + Server, + SshKey, + SocialSecurityNumber, + SoftwareLicense, + Person, + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ItemFieldType { + Text, + Concealed, + CreditCardType, + CreditCardNumber, + Phone, + Url, + Totp, + Email, + Reference, + SshKey, + Menu, + MonthYear, + Address, + Date, + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ItemState { + Active, + Archived, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AutofillBehavior { + AnywhereOnWebsite, + ExactDomain, + Never, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Website { + pub url: String, + pub label: String, + pub autofill_behavior: AutofillBehavior, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemField { + pub id: String, + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub section_id: Option, + pub field_type: ItemFieldType, + pub value: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +impl std::fmt::Debug for ItemField { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ItemField") + .field("id", &self.id) + .field("title", &self.title) + .field("section_id", &self.section_id) + .field("field_type", &self.field_type) + .field("value", &"[REDACTED]") + .field("details", &self.details) + .finish() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemSection { + pub id: String, + pub title: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemFile { + pub attributes: FileAttributes, + pub section_id: String, + pub field_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Item { + pub id: String, + pub title: String, + pub category: ItemCategory, + pub vault_id: String, + pub fields: Vec, + pub sections: Vec, + pub notes: String, + pub tags: Vec, + pub websites: Vec, + pub version: u32, + pub files: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub document: Option, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemCreateParams { + pub category: ItemCategory, + pub vault_id: String, + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sections: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub notes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub websites: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub files: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub document: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemOverview { + pub id: String, + pub title: String, + pub category: ItemCategory, + pub vault_id: String, + pub websites: Vec, + pub tags: Vec, + pub created_at: String, + pub updated_at: String, + pub state: ItemState, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemListFilterByStateInner { + pub active: bool, + pub archived: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "content")] +pub enum ItemListFilter { + ByState(ItemListFilterByStateInner), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ItemShareDuration { + OneHour, + OneDay, + SevenDays, + FourteenDays, + ThirtyDays, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AllowedType { + Authenticated, + Public, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AllowedRecipientType { + Email, + Domain, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemShareFiles { + pub allowed: bool, + pub max_size: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_types: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_recipient_types: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_expiry: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_expiry: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_views: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemShareAccountPolicy { + pub max_expiry: ItemShareDuration, + pub default_expiry: ItemShareDuration, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_views: Option, + pub allowed_types: Vec, + pub allowed_recipient_types: Vec, + pub files: ItemShareFiles, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidRecipientEmailInner { + pub email: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidRecipientDomainInner { + pub domain: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "parameters")] +pub enum ValidRecipient { + Email(ValidRecipientEmailInner), + Domain(ValidRecipientDomainInner), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemShareParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub recipients: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub expire_after: Option, + pub one_time_only: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "message")] +#[serde(rename_all = "camelCase")] +pub enum ItemUpdateFailureReason { + ItemValidationError(ErrorMessage), + ItemStatusPermissionError, + ItemStatusIncorrectItemVersion, + ItemStatusFileNotFound, + ItemStatusTooBig, + ItemNotFound, + Internal(ErrorMessage), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemsDeleteAllResponse { + pub individual_responses: + std::collections::HashMap>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ItemsGetAllError { + #[serde(rename = "itemNotFound")] + ItemNotFound, + #[serde(rename = "internal")] + Internal(ErrorMessage), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemsGetAllResponse { + pub individual_responses: Vec>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemsUpdateAllResponse { + pub individual_responses: Vec>, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolvedReference { + pub secret: String, + pub item_id: String, + pub vault_id: String, +} + +impl std::fmt::Debug for ResolvedReference { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedReference") + .field("secret", &"[REDACTED]") + .field("item_id", &self.item_id) + .field("vault_id", &self.vault_id) + .finish() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "message")] +#[serde(rename_all = "camelCase")] +pub enum ResolveReferenceError { + Parsing(ErrorMessage), + FieldNotFound, + VaultNotFound, + TooManyVaults, + ItemNotFound, + TooManyItems, + TooManyMatchingFields, + NoMatchingSections, + IncompatibleTOTPQueryParameterField, + #[serde(rename = "unableToGenerateTotpCode")] + UnableToGenerateTOTPCode(ErrorMessage), + #[serde(rename = "sSHKeyMetadataNotFound")] + SSHKeyMetadataNotFound, + UnsupportedFileFormat, + #[serde(rename = "incompatibleSshKeyQueryParameterField")] + IncompatibleSSHKeyQueryParameterField, + UnableToParsePrivateKey, + #[serde(rename = "unableToFormatPrivateKeyToOpenSsh")] + UnableToFormatPrivateKeyToOpenSSH, + Other, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolveAllResponse { + pub individual_responses: + std::collections::HashMap>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum GroupType { + Owners, + Administrators, + Recovery, + ExternalAccountManagers, + TeamMembers, + UserDefined, + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum GroupState { + Active, + Deleted, + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum VaultAccessorType { + User, + Group, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultAccess { + pub vault_uuid: String, + pub accessor_type: VaultAccessorType, + pub accessor_uuid: String, + pub permissions: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Group { + pub id: String, + pub title: String, + pub description: String, + pub group_type: GroupType, + pub state: GroupState, + #[serde(skip_serializing_if = "Option::is_none")] + pub vault_access: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupAccess { + pub group_id: String, + pub permissions: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupGetParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub vault_permissions: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupVaultAccess { + pub vault_id: String, + pub group_id: String, + pub permissions: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum VaultType { + Personal, + Everyone, + Transfer, + UserCreated, + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Vault { + pub id: String, + pub title: String, + pub description: String, + pub vault_type: VaultType, + pub active_item_count: u32, + pub content_version: u32, + pub attribute_version: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub access: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultCreateParams { + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_admins_access: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultGetParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub accessors: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultListParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub decrypt_details: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultOverview { + pub id: String, + pub title: String, + pub description: String, + pub vault_type: VaultType, + pub active_item_count: u32, + pub content_version: u32, + pub attribute_version: u32, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultUpdateParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +pub mod permissions { + pub const READ_ITEMS: u32 = 32; + pub const REVEAL_ITEM_PASSWORD: u32 = 16; + pub const UPDATE_ITEMS: u32 = 64; + pub const CREATE_ITEMS: u32 = 128; + pub const ARCHIVE_ITEMS: u32 = 256; + pub const DELETE_ITEMS: u32 = 512; + pub const UPDATE_ITEM_HISTORY: u32 = 1024; + pub const SEND_ITEMS: u32 = 1_048_576; + pub const IMPORT_ITEMS: u32 = 2_097_152; + pub const EXPORT_ITEMS: u32 = 4_194_304; + pub const PRINT_ITEMS: u32 = 8_388_608; + pub const MANAGE_VAULT: u32 = 2; + pub const RECOVER_VAULT: u32 = 1; + pub const NO_ACCESS: u32 = 0; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn password_recipe_serializes_as_tagged_enum() { + let recipe = PasswordRecipe::Random(PasswordRecipeRandomInner { + include_digits: true, + include_symbols: false, + length: 20, + }); + let json = serde_json::to_string(&recipe).unwrap(); + assert!(json.contains("\"type\":\"Random\"")); + assert!(json.contains("\"parameters\":")); + } + + #[test] + fn file_create_params_serializes_content_as_byte_array() { + let params = FileCreateParams { + name: "file.txt".to_string(), + content: b"hello".to_vec(), + section_id: "section".to_string(), + field_id: "field".to_string(), + }; + + let value = serde_json::to_value(¶ms).unwrap(); + assert_eq!( + value["content"], + serde_json::json!([104, 101, 108, 108, 111]) + ); + + let parsed: FileCreateParams = serde_json::from_value(value).unwrap(); + assert_eq!(parsed.content, b"hello"); + } + + #[test] + fn document_create_params_serializes_content_as_byte_array() { + let params = DocumentCreateParams { + name: "document.txt".to_string(), + content: b"document".to_vec(), + }; + + let value = serde_json::to_value(¶ms).unwrap(); + assert_eq!( + value["content"], + serde_json::json!([100, 111, 99, 117, 109, 101, 110, 116]) + ); + + let parsed: DocumentCreateParams = serde_json::from_value(value).unwrap(); + assert_eq!(parsed.content, b"document"); + } + + #[test] + fn file_read_response_deserializes_from_byte_array() { + let parsed: Vec = serde_json::from_str("[114,101,115,112,111,110,115,101]").unwrap(); + assert_eq!(parsed, b"response"); + } + + #[test] + fn item_field_details_roundtrip() { + let details = ItemFieldDetails::Otp(OTPFieldDetails { + code: Some("123456".to_string()), + error_message: None, + }); + let json = serde_json::to_string(&details).unwrap(); + let parsed: ItemFieldDetails = serde_json::from_str(&json).unwrap(); + match parsed { + ItemFieldDetails::Otp(otp) => assert_eq!(otp.code.unwrap(), "123456"), + _ => panic!("expected Otp variant"), + } + } + + #[test] + fn valid_recipient_roundtrip() { + let recipient = ValidRecipient::Email(ValidRecipientEmailInner { + email: "test@example.com".to_string(), + }); + let json = serde_json::to_string(&recipient).unwrap(); + assert!(json.contains("\"type\":\"Email\"")); + let parsed: ValidRecipient = serde_json::from_str(&json).unwrap(); + match parsed { + ValidRecipient::Email(inner) => assert_eq!(inner.email, "test@example.com"), + _ => panic!("expected Email variant"), + } + } + + #[test] + fn item_list_filter_roundtrip() { + let filter = ItemListFilter::ByState(ItemListFilterByStateInner { + active: true, + archived: false, + }); + let json = serde_json::to_string(&filter).unwrap(); + let parsed: ItemListFilter = serde_json::from_str(&json).unwrap(); + match parsed { + ItemListFilter::ByState(inner) => { + assert!(inner.active); + assert!(!inner.archived); + } + } + } +} diff --git a/src/vaults.rs b/src/vaults.rs new file mode 100644 index 0000000..121351e --- /dev/null +++ b/src/vaults.rs @@ -0,0 +1,138 @@ +use std::collections::HashMap; + +use crate::client::client_invoke; +use crate::core::InnerClient; +use crate::errors::SdkError; +use crate::types::*; + +pub trait VaultsApi { + fn create(&self, params: VaultCreateParams) -> Result; + fn list(&self, params: Option) -> Result, SdkError>; + fn get_overview(&self, vault_id: &str) -> Result; + fn get(&self, vault_id: &str, params: VaultGetParams) -> Result; + fn update(&self, vault_id: &str, params: VaultUpdateParams) -> Result; + fn delete(&self, vault_id: &str) -> Result<(), SdkError>; + fn grant_group_permissions( + &self, + vault_id: &str, + group_permissions: &[GroupAccess], + ) -> Result<(), SdkError>; + fn update_group_permissions( + &self, + group_permissions: &[GroupVaultAccess], + ) -> Result<(), SdkError>; + fn revoke_group_permissions(&self, vault_id: &str, group_id: &str) -> Result<(), SdkError>; +} + +pub(crate) struct VaultsSource<'a> { + inner: &'a InnerClient, +} + +impl<'a> VaultsSource<'a> { + pub fn new(inner: &'a InnerClient) -> Self { + Self { inner } + } +} + +impl VaultsApi for VaultsSource<'_> { + fn create(&self, params: VaultCreateParams) -> Result { + let mut p = HashMap::new(); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result = client_invoke(self.inner, "VaultsCreate", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn list(&self, params: Option) -> Result, SdkError> { + let mut p = HashMap::new(); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result = client_invoke(self.inner, "VaultsList", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn get_overview(&self, vault_id: &str) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + let result = client_invoke(self.inner, "VaultsGetOverview", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn get(&self, vault_id: &str, params: VaultGetParams) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("vault_params".to_string(), serde_json::to_value(¶ms)?); + let result = client_invoke(self.inner, "VaultsGet", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn update(&self, vault_id: &str, params: VaultUpdateParams) -> Result { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert("params".to_string(), serde_json::to_value(¶ms)?); + let result = client_invoke(self.inner, "VaultsUpdate", p)?; + Ok(serde_json::from_str(&result)?) + } + + fn delete(&self, vault_id: &str) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + client_invoke(self.inner, "VaultsDelete", p)?; + Ok(()) + } + + fn grant_group_permissions( + &self, + vault_id: &str, + group_permissions: &[GroupAccess], + ) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "group_permissions_list".to_string(), + serde_json::to_value(group_permissions)?, + ); + client_invoke(self.inner, "VaultsGrantGroupPermissions", p)?; + Ok(()) + } + + fn update_group_permissions( + &self, + group_permissions: &[GroupVaultAccess], + ) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "group_permissions_list".to_string(), + serde_json::to_value(group_permissions)?, + ); + client_invoke(self.inner, "VaultsUpdateGroupPermissions", p)?; + Ok(()) + } + + fn revoke_group_permissions(&self, vault_id: &str, group_id: &str) -> Result<(), SdkError> { + let mut p = HashMap::new(); + p.insert( + "vault_id".to_string(), + serde_json::Value::String(vault_id.to_string()), + ); + p.insert( + "group_id".to_string(), + serde_json::Value::String(group_id.to_string()), + ); + client_invoke(self.inner, "VaultsRevokeGroupPermissions", p)?; + Ok(()) + } +} diff --git a/wasm/core.wasm b/wasm/core.wasm new file mode 100644 index 0000000..f502ba4 Binary files /dev/null and b/wasm/core.wasm differ