diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ece8dc..b373731 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [0.10.4] - 2025-10-24
+
+### Changed
+- Vendored `telegram-webapp-sdk` locally and patched it to use the workspace `masterror` implementation,
+ replacing the outdated crates.io release that depended on `masterror 0.3.5`.
+- Marked the SDK's `masterror` dependency optional so disabling the `telegram-webapp-sdk` feature no longer
+ pulls the crate transitively.
+
+### Build
+- Added a `[patch.crates-io]` override for `telegram-webapp-sdk` to ensure the workspace uses the vendored
+ source and derives against the current `masterror` APIs.
+
### Documentation
- Described `#[provide]` telemetry providers and `#[app_error]` conversions with
end-to-end examples in the derive guide ([README](README.md#structured-telemetry-providers-and-apperror-mappings),
diff --git a/Cargo.lock b/Cargo.lock
index e637e0d..b40ebcc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1038,12 +1038,6 @@ dependencies = [
"foldhash",
]
-[[package]]
-name = "hashbrown"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
-
[[package]]
name = "hashlink"
version = "0.10.0"
@@ -1361,7 +1355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
- "hashbrown 0.16.0",
+ "hashbrown 0.15.5",
"serde",
"serde_core",
]
@@ -1515,19 +1509,7 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "masterror"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d38aeb53944762378aa5219b9929f2f3346a25fdd9b61266c24a487200c87fd"
-dependencies = [
- "http 1.3.1",
- "serde",
- "thiserror",
- "tracing",
-]
-
-[[package]]
-name = "masterror"
-version = "0.10.3"
+version = "0.10.4"
dependencies = [
"actix-web",
"axum",
@@ -2789,15 +2771,12 @@ checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790"
[[package]]
name = "telegram-webapp-sdk"
version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb72a0fd8e65f7c9279d164e9b00661e6685f74c0ba63e7f17b30662d5aed21b"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
"hex",
"hmac-sha256",
"js-sys",
- "masterror 0.3.5",
"once_cell",
"percent-encoding",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index 078f9a6..c9b3a8d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "masterror"
-version = "0.10.3"
+version = "0.10.4"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
@@ -52,6 +52,10 @@ openapi = ["dep:utoipa"]
masterror-derive = { version = "0.6.0", path = "masterror-derive" }
masterror-template = { version = "0.3.1", path = "masterror-template" }
+
+[patch.crates-io]
+telegram-webapp-sdk = { path = "telegram-webapp-sdk" }
+
[dependencies]
masterror-derive = { workspace = true }
masterror-template = { workspace = true }
diff --git a/README.md b/README.md
index 406f85f..6555662 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`.
~~~toml
[dependencies]
-masterror = { version = "0.10.3", default-features = false }
+masterror = { version = "0.10.4", default-features = false }
# or with features:
-# masterror = { version = "0.10.3", features = [
+# masterror = { version = "0.10.4", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
@@ -66,10 +66,10 @@ masterror = { version = "0.10.3", default-features = false }
~~~toml
[dependencies]
# lean core
-masterror = { version = "0.10.3", default-features = false }
+masterror = { version = "0.10.4", default-features = false }
# with Axum/Actix + JSON + integrations
-# masterror = { version = "0.10.3", features = [
+# masterror = { version = "0.10.4", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
@@ -623,13 +623,13 @@ assert_eq!(resp.status, 401);
Minimal core:
~~~toml
-masterror = { version = "0.10.3", default-features = false }
+masterror = { version = "0.10.4", default-features = false }
~~~
API (Axum + JSON + deps):
~~~toml
-masterror = { version = "0.10.3", features = [
+masterror = { version = "0.10.4", features = [
"axum", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
@@ -638,7 +638,7 @@ masterror = { version = "0.10.3", features = [
API (Actix + JSON + deps):
~~~toml
-masterror = { version = "0.10.3", features = [
+masterror = { version = "0.10.4", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
diff --git a/telegram-webapp-sdk/.cargo-ok b/telegram-webapp-sdk/.cargo-ok
new file mode 100644
index 0000000..5f8b795
--- /dev/null
+++ b/telegram-webapp-sdk/.cargo-ok
@@ -0,0 +1 @@
+{"v":1}
\ No newline at end of file
diff --git a/telegram-webapp-sdk/.cargo_vcs_info.json b/telegram-webapp-sdk/.cargo_vcs_info.json
new file mode 100644
index 0000000..237560f
--- /dev/null
+++ b/telegram-webapp-sdk/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "67fc43a229f57bb856aac4b2dfaaa095951107be"
+ },
+ "path_in_vcs": ""
+}
\ No newline at end of file
diff --git a/telegram-webapp-sdk/.github/FUNDING.yml b/telegram-webapp-sdk/.github/FUNDING.yml
new file mode 100644
index 0000000..a299cb7
--- /dev/null
+++ b/telegram-webapp-sdk/.github/FUNDING.yml
@@ -0,0 +1,15 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: raprojects # Replace with a single Open Collective username
+ko_fi: rozanov # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+polar: # Replace with a single Polar username
+buy_me_a_coffee: raprogramm # Replace with a single Buy Me a Coffee username
+thanks_dev: # Replace with a single thanks.dev username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] # TODO: make an link to usdt wallet
diff --git a/telegram-webapp-sdk/.github/workflows/ci.yml b/telegram-webapp-sdk/.github/workflows/ci.yml
new file mode 100644
index 0000000..1ae556e
--- /dev/null
+++ b/telegram-webapp-sdk/.github/workflows/ci.yml
@@ -0,0 +1,13 @@
+name: CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+
+jobs:
+ ci:
+ uses: ./.github/workflows/reusable-ci.yml
+ with:
+ all-features: true
+
diff --git a/telegram-webapp-sdk/.github/workflows/release.yml b/telegram-webapp-sdk/.github/workflows/release.yml
new file mode 100644
index 0000000..4b2db91
--- /dev/null
+++ b/telegram-webapp-sdk/.github/workflows/release.yml
@@ -0,0 +1,57 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v*"
+
+jobs:
+ checks:
+ uses: ./.github/workflows/reusable-ci.yml
+ with:
+ all-features: true
+
+ publish:
+ runs-on: ubuntu-latest
+ needs: checks
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Read MSRV from Cargo.toml
+ id: msrv
+ shell: bash
+ run: |
+ set -euo pipefail
+ if ! command -v jq >/dev/null 2>&1; then
+ sudo apt-get update -y && sudo apt-get install -y jq
+ fi
+ RV=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].rust_version // empty')
+ if [ -z "$RV" ]; then
+ echo "rust-version is not set in Cargo.toml"
+ exit 1
+ fi
+ if [[ "$RV" =~ ^[0-9]+\.[0-9]+$ ]]; then
+ RV="${RV}.0"
+ fi
+ echo "msrv=${RV}" >> "$GITHUB_OUTPUT"
+
+ - name: Ensure tag matches Cargo.toml version
+ shell: bash
+ run: |
+ set -euo pipefail
+ TAG="${GITHUB_REF#refs/tags/}"
+ FILE_VER=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].version')
+ if [ "v${FILE_VER}" != "${TAG}" ]; then
+ echo "Tag ${TAG} != Cargo.toml version v${FILE_VER}"
+ exit 1
+ fi
+
+ - name: Install Rust (${{ steps.msrv.outputs.msrv }})
+ uses: dtolnay/rust-toolchain@v1
+ with:
+ toolchain: ${{ steps.msrv.outputs.msrv }}
+
+ - name: Publish to crates.io
+ env:
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+ run: cargo +${{ steps.msrv.outputs.msrv }} publish --locked --token "$CARGO_REGISTRY_TOKEN"
diff --git a/telegram-webapp-sdk/.github/workflows/reusable-ci.yml b/telegram-webapp-sdk/.github/workflows/reusable-ci.yml
new file mode 100644
index 0000000..f8faeb2
--- /dev/null
+++ b/telegram-webapp-sdk/.github/workflows/reusable-ci.yml
@@ -0,0 +1,102 @@
+name: Reusable CI
+
+on:
+ workflow_call:
+ inputs:
+ all-features:
+ type: boolean
+ default: true
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+ env:
+ CARGO_LOCKED: "true" # don't mutate Cargo.lock during CI
+ CARGO_TERM_COLOR: always
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # Read MSRV (rust-version) from Cargo.toml
+ - name: Read MSRV from Cargo.toml
+ id: msrv
+ shell: bash
+ run: |
+ set -euo pipefail
+ if ! command -v jq >/dev/null 2>&1; then
+ sudo apt-get update -y && sudo apt-get install -y jq
+ fi
+ RV=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].rust_version // empty')
+ if [ -z "$RV" ]; then
+ echo "rust-version is not set in Cargo.toml"
+ exit 1
+ fi
+ [[ "$RV" =~ ^[0-9]+\.[0-9]+$ ]] && RV="${RV}.0"
+ echo "msrv=${RV}" >> "$GITHUB_OUTPUT"
+ echo "Using MSRV: $RV"
+
+ # Install MSRV for clippy/tests/package
+ - name: Install Rust (${{ steps.msrv.outputs.msrv }})
+ uses: dtolnay/rust-toolchain@v1
+ with:
+ toolchain: ${{ steps.msrv.outputs.msrv }}
+ components: clippy
+
+ # Pin nightly for rustfmt because unstable_features = true in .rustfmt.toml
+ - name: Install nightly rustfmt
+ uses: dtolnay/rust-toolchain@v1
+ with:
+ toolchain: nightly-2025-08-01
+ components: rustfmt
+
+ - name: Cache cargo
+ uses: Swatinem/rust-cache@v2
+ with:
+ save-if: ${{ github.ref == 'refs/heads/main' }}
+
+ # Ensure Cargo.lock is present when CARGO_LOCKED=1
+ - name: Verify lockfile is committed
+ shell: bash
+ run: |
+ set -euo pipefail
+ if [ ! -f Cargo.lock ]; then
+ echo "CARGO_LOCKED=1 but Cargo.lock is missing. Commit it or drop CARGO_LOCKED."
+ exit 1
+ fi
+
+ - name: Check formatting (nightly rustfmt)
+ run: cargo +nightly-2025-08-01 fmt --all -- --check
+
+ - name: Clippy (MSRV)
+ shell: bash
+ run: |
+ set -euo pipefail
+ if [ "${{ inputs.all-features }}" = "true" ]; then
+ cargo +${{ steps.msrv.outputs.msrv }} clippy --workspace --all-targets --all-features -- -D warnings
+ else
+ cargo +${{ steps.msrv.outputs.msrv }} clippy --workspace --all-targets -- -D warnings
+ fi
+
+ - name: Tests (MSRV)
+ shell: bash
+ run: |
+ set -euo pipefail
+ if [ "${{ inputs.all-features }}" = "true" ]; then
+ cargo +${{ steps.msrv.outputs.msrv }} test --workspace --all-features --no-fail-fast
+ else
+ cargo +${{ steps.msrv.outputs.msrv }} test --workspace --no-fail-fast
+ fi
+
+ - name: Ensure tree is clean before package
+ shell: bash
+ run: |
+ set -euo pipefail
+ if ! git diff --quiet; then
+ echo "Working tree is dirty:"
+ git status --porcelain
+ exit 1
+ fi
+
+ - name: Package (dry-run)
+ run: cargo +${{ steps.msrv.outputs.msrv }} package --locked
diff --git a/telegram-webapp-sdk/.gitignore b/telegram-webapp-sdk/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/telegram-webapp-sdk/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/telegram-webapp-sdk/.hooks/pre-commit b/telegram-webapp-sdk/.hooks/pre-commit
new file mode 100755
index 0000000..ded56d9
--- /dev/null
+++ b/telegram-webapp-sdk/.hooks/pre-commit
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+echo "🔧 Ensuring nightly rustfmt is available..."
+if ! rustup toolchain list | grep -q "^nightly"; then
+ rustup toolchain install nightly >/dev/null
+fi
+rustup component add rustfmt --toolchain nightly >/dev/null
+
+echo "🧹 Checking formatting with rustfmt..."
+cargo +nightly fmt --all -- --check
+
+echo "🔍 Running clippy (all features, all targets)..."
+cargo clippy --workspace --all-targets --all-features -- -D warnings
+
+echo "🧪 Running tests (all features)..."
+cargo test --workspace --all-features
+
+# Uncomment if you want to validate SQLx offline data
+# echo "📦 Validating SQLx prepare..."
+# cargo sqlx prepare --check --workspace
+
+echo "✅ All checks passed!"
+
diff --git a/telegram-webapp-sdk/.rustfmt.toml b/telegram-webapp-sdk/.rustfmt.toml
new file mode 100644
index 0000000..9f0782d
--- /dev/null
+++ b/telegram-webapp-sdk/.rustfmt.toml
@@ -0,0 +1,25 @@
+# Не добавлять запятые, если элемент один
+trailing_comma = "Never"
+
+# Скобки остаются на той же строке
+brace_style = "SameLineWhere"
+
+# Выравнивать поля структуры, если длина меньше указанного порога
+struct_field_align_threshold = 20
+
+# Форматировать комментарии внутри документации
+wrap_comments = true
+format_code_in_doc_comments = true
+
+# Не складывать литералы структур в одну строку
+struct_lit_single_line = false
+
+max_width = 99
+
+# Группировка импортов
+imports_granularity = "Crate" # Группировать импорты по крейтам
+group_imports = "StdExternalCrate" # Стандартные, внешние и локальные импорты в отдельных группах
+reorder_imports = true # Сортировать импорты внутри групп
+
+# Включить поддержку нестабильных функций (только для nightly)
+unstable_features = true
diff --git a/telegram-webapp-sdk/.scripts/release.sh b/telegram-webapp-sdk/.scripts/release.sh
new file mode 100755
index 0000000..7af14a2
--- /dev/null
+++ b/telegram-webapp-sdk/.scripts/release.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Определяем последнюю версию из CHANGELOG.md (первая строка, начинающаяся с "## [")
+TAG=$(grep -m1 -oP '^## \[\K[0-9]+\.[0-9]+\.[0-9]+' CHANGELOG.md)
+TAG="v$TAG"
+
+echo "📦 Preparing release for $TAG"
+
+# Проверяем, существует ли уже релиз
+if gh release view "$TAG" >/dev/null 2>&1; then
+ echo "⚠️ Release $TAG already exists on GitHub. Nothing to do."
+ exit 0
+fi
+
+# Вырезаем секцию для этого тега
+notes=$(awk "/^## \\[$(echo "$TAG" | sed 's/^v//')\\]/ {flag=1; next} /^## \\[/ && flag {exit} flag" CHANGELOG.md)
+
+if [ -z "$notes" ]; then
+ echo "❌ Could not extract changelog section for $TAG"
+ exit 1
+fi
+
+# Создаём релиз
+gh release create "$TAG" \
+ --title "$TAG" \
+ --notes "$notes"
+
+echo "✅ GitHub release $TAG created."
+
diff --git a/telegram-webapp-sdk/CHANGELOG.md b/telegram-webapp-sdk/CHANGELOG.md
new file mode 100644
index 0000000..ab7af4f
--- /dev/null
+++ b/telegram-webapp-sdk/CHANGELOG.md
@@ -0,0 +1,26 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [0.2.0] - 2025-09-12
+### Changed
+- Integrated macros into the main crate; `telegram-webapp-sdk-macros` crate removed.
+- Replaced attribute macros with declarative macros `telegram_app!`, `telegram_page!`, and `telegram_router!`.
+
+## [0.1.1] - 2025-09-12
+### Added
+- Implemented `CloudStorage.setItems`.
+
+## [0.1.0] - 2025-09-12
+### Added
+- Initial release with core WebApp utilities, Yew and Leptos integrations,
+ mock environment, and basic Bot API type definitions.
+- User API wrappers: `request_contact`, `request_phone_number`, and `open_contact`.
+- Accelerometer, gyroscope, and device orientation sensor APIs with start/stop,
+ value reading and event callbacks.
+- Home screen utilities: `add_to_home_screen` and `check_home_screen_status`.
diff --git a/telegram-webapp-sdk/Cargo.lock b/telegram-webapp-sdk/Cargo.lock
new file mode 100644
index 0000000..5e5ce58
--- /dev/null
+++ b/telegram-webapp-sdk/Cargo.lock
@@ -0,0 +1,3079 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "any_spawner"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d"
+dependencies = [
+ "futures",
+ "thiserror 2.0.16",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
+
+[[package]]
+name = "anymap2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
+
+[[package]]
+name = "async-lock"
+version = "3.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-once-cell"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "attribute-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0053e96dd3bec5b4879c23a138d6ef26f2cb936c9cdc96274ac2b9ed44b5bb54"
+dependencies = [
+ "attribute-derive-macro",
+ "derive-where",
+ "manyhow",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "attribute-derive-macro"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "463b53ad0fd5b460af4b1915fe045ff4d946d025fb6c4dc3337752eaa980f71b"
+dependencies = [
+ "collection_literals",
+ "interpolator",
+ "manyhow",
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "quote-use",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "base16"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
+
+[[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 = "boolinator"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "camino"
+version = "1.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5"
+
+[[package]]
+name = "cc"
+version = "1.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+[[package]]
+name = "codee"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd8bbfdadf2f8999c6e404697bc08016dce4a3d77dec465b36c9a0652fdb3327"
+dependencies = [
+ "serde",
+ "serde_json",
+ "thiserror 2.0.16",
+]
+
+[[package]]
+name = "collection_literals"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b3f65b8fb8e88ba339f7d23a390fe1b0896217da05e2a66c584c9b29a91df8"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "config"
+version = "0.15.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0faa974509d38b33ff89282db9c3295707ccf031727c0de9772038ec526852ba"
+dependencies = [
+ "convert_case 0.6.0",
+ "pathdiff",
+ "serde",
+ "toml 0.9.5",
+ "winnow 0.7.13",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "const-str"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "451d0640545a0553814b4c646eb549343561618838e9b42495f466131fe3ad49"
+
+[[package]]
+name = "const_format"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd"
+dependencies = [
+ "const_format_proc_macros",
+]
+
+[[package]]
+name = "const_format_proc_macros"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "const_str_slice_concat"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "dashmap"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "derive-where"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[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 = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "drain_filter_polyfill"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "either_of"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "216d23e0ec69759a17f05e1c553f3a6870e5ec73420fbb07807a6f34d5d1d5a4"
+dependencies = [
+ "paste",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472"
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+ "num_cpus",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "wasi 0.14.5+wasi-0.2.4",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "gloo"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d"
+dependencies = [
+ "gloo-console 0.2.3",
+ "gloo-dialogs 0.1.1",
+ "gloo-events 0.1.2",
+ "gloo-file 0.2.3",
+ "gloo-history 0.1.5",
+ "gloo-net 0.3.1",
+ "gloo-render 0.1.1",
+ "gloo-storage 0.2.2",
+ "gloo-timers 0.2.6",
+ "gloo-utils 0.1.7",
+ "gloo-worker 0.2.1",
+]
+
+[[package]]
+name = "gloo"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249"
+dependencies = [
+ "gloo-console 0.3.0",
+ "gloo-dialogs 0.2.0",
+ "gloo-events 0.2.0",
+ "gloo-file 0.3.0",
+ "gloo-history 0.2.2",
+ "gloo-net 0.4.0",
+ "gloo-render 0.2.0",
+ "gloo-storage 0.3.0",
+ "gloo-timers 0.3.0",
+ "gloo-utils 0.2.0",
+ "gloo-worker 0.4.0",
+]
+
+[[package]]
+name = "gloo-console"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
+dependencies = [
+ "gloo-utils 0.1.7",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-console"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
+dependencies = [
+ "gloo-utils 0.2.0",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-dialogs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-dialogs"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-events"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-events"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-file"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
+dependencies = [
+ "gloo-events 0.1.2",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-file"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
+dependencies = [
+ "gloo-events 0.2.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-history"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f"
+dependencies = [
+ "gloo-events 0.1.2",
+ "gloo-utils 0.1.7",
+ "serde",
+ "serde-wasm-bindgen 0.5.0",
+ "serde_urlencoded",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-history"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
+dependencies = [
+ "getrandom 0.2.16",
+ "gloo-events 0.2.0",
+ "gloo-utils 0.2.0",
+ "serde",
+ "serde-wasm-bindgen 0.6.5",
+ "serde_urlencoded",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils 0.1.7",
+ "http 0.2.12",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils 0.2.0",
+ "http 0.2.12",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils 0.2.0",
+ "http 1.3.1",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-render"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-render"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-storage"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
+dependencies = [
+ "gloo-utils 0.1.7",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-storage"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
+dependencies = [
+ "gloo-utils 0.2.0",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-worker"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a"
+dependencies = [
+ "anymap2",
+ "bincode",
+ "gloo-console 0.2.3",
+ "gloo-utils 0.1.7",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-worker"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400"
+dependencies = [
+ "bincode",
+ "futures",
+ "gloo-utils 0.2.0",
+ "gloo-worker-macros",
+ "js-sys",
+ "pinned",
+ "serde",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-worker-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "guardian"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac-sha256"
+version = "1.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425"
+
+[[package]]
+name = "html-escape"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+dependencies = [
+ "utf8-width",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "hydration_context"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283"
+dependencies = [
+ "futures",
+ "once_cell",
+ "or_poisoned",
+ "pin-project-lite",
+ "serde",
+ "throw_error",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "implicit-clone"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84"
+dependencies = [
+ "implicit-clone-derive",
+ "indexmap",
+]
+
+[[package]]
+name = "implicit-clone-derive"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21"
+dependencies = [
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "interpolator"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+
+[[package]]
+name = "inventory"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[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.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "leptos"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7a8710b4908a0e7b693113b906e4cf1bc87123b685404d090cdcd3e220bcab4"
+dependencies = [
+ "any_spawner",
+ "cfg-if",
+ "either_of",
+ "futures",
+ "getrandom 0.3.3",
+ "hydration_context",
+ "leptos_config",
+ "leptos_dom",
+ "leptos_hot_reload",
+ "leptos_macro",
+ "leptos_server",
+ "oco_ref",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn",
+ "slotmap",
+ "tachys",
+ "thiserror 2.0.16",
+ "throw_error",
+ "typed-builder",
+ "typed-builder-macro",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm_split_helpers",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_config"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240b4cb96284256a44872563cf029f24d6fe14bc341dcf0f4164e077cb5a1471"
+dependencies = [
+ "config",
+ "regex",
+ "serde",
+ "thiserror 2.0.16",
+ "typed-builder",
+]
+
+[[package]]
+name = "leptos_dom"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e920c8b2fd202b25786b0c72a00c745a6962fa923e600df6f3ec352d844be91"
+dependencies = [
+ "js-sys",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "tachys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_hot_reload"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d61ec3e1ff8aaee8c5151688550c0363f85bc37845450764c31ff7584a33f38"
+dependencies = [
+ "anyhow",
+ "camino",
+ "indexmap",
+ "parking_lot",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "serde",
+ "syn 2.0.106",
+ "walkdir",
+]
+
+[[package]]
+name = "leptos_macro"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a84c7e53895c786f1128e91c36a708435e301f487338d19f2f6b5b67bb39ece2"
+dependencies = [
+ "attribute-derive",
+ "cfg-if",
+ "convert_case 0.8.0",
+ "html-escape",
+ "itertools",
+ "leptos_hot_reload",
+ "prettyplease",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "rustc_version",
+ "server_fn_macro",
+ "syn 2.0.106",
+ "uuid",
+]
+
+[[package]]
+name = "leptos_server"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38acbf32649a4b127c8d4ccaed8fb388e19a746430a0ea8f8160e51e28c36e2d"
+dependencies = [
+ "any_spawner",
+ "base64",
+ "codee",
+ "futures",
+ "hydration_context",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "server_fn",
+ "tachys",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.175"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+[[package]]
+name = "linear-map"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[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 2.0.106",
+]
+
+[[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 = "masterror"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e7c3a243a6f697e05d0b971c22d0ac029b9080c20b2bbc5f4a3f43ea6024a60"
+dependencies = [
+ "http 1.3.1",
+ "serde",
+ "thiserror 2.0.16",
+ "tracing",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "minicov"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b"
+dependencies = [
+ "cc",
+ "walkdir",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "next_tuple"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "oco_ref"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d"
+dependencies = [
+ "serde",
+ "thiserror 2.0.16",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "or_poisoned"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pinned"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b"
+dependencies = [
+ "futures",
+ "rustversion",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[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.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "prokio"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488"
+dependencies = [
+ "futures",
+ "gloo 0.8.1",
+ "num_cpus",
+ "once_cell",
+ "pin-project",
+ "pinned",
+ "tokio",
+ "tokio-stream",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "quote-use"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e"
+dependencies = [
+ "quote",
+ "quote-use-macros",
+]
+
+[[package]]
+name = "quote-use-macros"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "reactive_graph"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27e4f808d01701256dc220e398d518684781bcd1b3b1a6c1c107fd41374f0624"
+dependencies = [
+ "any_spawner",
+ "async-lock",
+ "futures",
+ "guardian",
+ "hydration_context",
+ "indexmap",
+ "or_poisoned",
+ "pin-project-lite",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "serde",
+ "slotmap",
+ "thiserror 2.0.16",
+ "web-sys",
+]
+
+[[package]]
+name = "reactive_stores"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79983e88dfd1a2925e29a4853ab9161b234ea78dd0d44ed33a706c9cd5e35762"
+dependencies = [
+ "dashmap",
+ "guardian",
+ "itertools",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "reactive_stores_macro",
+ "rustc-hash",
+ "send_wrapper",
+]
+
+[[package]]
+name = "reactive_stores_macro"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fa40919eb2975100283b2a70e68eafce1e8bcf81f0622ff168e4c2b3f8d46bb"
+dependencies = [
+ "convert_case 0.8.0",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
+
+[[package]]
+name = "rstml"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56"
+dependencies = [
+ "derive-where",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.106",
+ "syn_derive",
+ "thiserror 2.0.16",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.143"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_qs"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352"
+dependencies = [
+ "percent-encoding",
+ "serde",
+ "thiserror 2.0.16",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "server_fn"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4efa7bb741386fb31a68269c81b1469c917d9adb1f4102a2d2684f11e3235389"
+dependencies = [
+ "base64",
+ "bytes",
+ "const-str",
+ "const_format",
+ "dashmap",
+ "futures",
+ "gloo-net 0.6.0",
+ "http 1.3.1",
+ "js-sys",
+ "pin-project-lite",
+ "rustc_version",
+ "rustversion",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn_macro_default",
+ "thiserror 2.0.16",
+ "throw_error",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d1a916793571234d1c4622153d42495d26605ed7b9d5d38a2699666cfef46b3"
+dependencies = [
+ "const_format",
+ "convert_case 0.8.0",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.106",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro_default"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00"
+dependencies = [
+ "server_fn_macro",
+ "syn 2.0.106",
+]
+
+[[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 = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "slotmap"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn_derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219"
+dependencies = [
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "tachys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dacbb26ffb2bbe6743702ee27c3e994c0caae86c92137278de9a9d92d383765c"
+dependencies = [
+ "any_spawner",
+ "async-trait",
+ "const_str_slice_concat",
+ "drain_filter_polyfill",
+ "either_of",
+ "erased",
+ "futures",
+ "html-escape",
+ "indexmap",
+ "itertools",
+ "js-sys",
+ "linear-map",
+ "next_tuple",
+ "oco_ref",
+ "or_poisoned",
+ "parking_lot",
+ "paste",
+ "reactive_graph",
+ "reactive_stores",
+ "rustc-hash",
+ "rustc_version",
+ "send_wrapper",
+ "slotmap",
+ "throw_error",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "telegram-webapp-sdk"
+version = "0.2.0"
+dependencies = [
+ "base64",
+ "ed25519-dalek",
+ "hex",
+ "hmac-sha256",
+ "inventory",
+ "js-sys",
+ "leptos",
+ "masterror",
+ "once_cell",
+ "percent-encoding",
+ "serde",
+ "serde-wasm-bindgen 0.6.5",
+ "serde_json",
+ "serde_urlencoded",
+ "thiserror 2.0.16",
+ "toml 0.8.23",
+ "urlencoding",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-bindgen-test",
+ "web-sys",
+ "yew",
+]
+
+[[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.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
+dependencies = [
+ "thiserror-impl 2.0.16",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "throw_error"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41e42a6afdde94f3e656fae18f837cb9bbe500a5ac5de325b09f3ec05b9c28e3"
+dependencies = [
+ "pin-project-lite",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.47.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+dependencies = [
+ "backtrace",
+ "io-uring",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
+ "toml_edit 0.22.27",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
+dependencies = [
+ "serde",
+ "serde_spanned 1.0.0",
+ "toml_datetime 0.7.0",
+ "toml_parser",
+ "winnow 0.7.13",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap",
+ "toml_datetime 0.6.11",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
+ "toml_write",
+ "winnow 0.7.13",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
+dependencies = [
+ "winnow 0.7.13",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typed-builder"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fef81aec2ca29576f9f6ae8755108640d0a86dd3161b2e8bca6cfa554e98f77d"
+dependencies = [
+ "typed-builder-macro",
+]
+
+[[package]]
+name = "typed-builder-macro"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ecb9ecf7799210407c14a8cfdfe0173365780968dc57973ed082211958e0b18"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "url"
+version = "2.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "utf8-width"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
+
+[[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.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[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"
+version = "0.14.5+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4"
+dependencies = [
+ "wasip2",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.0+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-bindgen-test"
+version = "0.3.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad"
+dependencies = [
+ "js-sys",
+ "minicov",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-bindgen-test-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-test-macro"
+version = "0.3.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm_split_helpers"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c50e0e45d0d871605a21fc4ee93a5380b7bdc41b5eda22e42f0777a4ce79b65c"
+dependencies = [
+ "async-once-cell",
+ "or_poisoned",
+ "wasm_split_macros",
+]
+
+[[package]]
+name = "wasm_split_macros"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f8a7f0bf54b0129a337aadfe8b716d843689f69c75b2a6413a0cff2e0d00982"
+dependencies = [
+ "base16",
+ "digest",
+ "quote",
+ "sha2",
+ "syn 2.0.106",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[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.0",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
+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.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "yew"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac"
+dependencies = [
+ "console_error_panic_hook",
+ "futures",
+ "gloo 0.10.0",
+ "implicit-clone",
+ "indexmap",
+ "js-sys",
+ "prokio",
+ "rustversion",
+ "serde",
+ "slab",
+ "thiserror 1.0.69",
+ "tokio",
+ "tracing",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "yew-macro",
+]
+
+[[package]]
+name = "yew-macro"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2"
+dependencies = [
+ "boolinator",
+ "once_cell",
+ "prettyplease",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
diff --git a/telegram-webapp-sdk/Cargo.toml b/telegram-webapp-sdk/Cargo.toml
new file mode 100644
index 0000000..ce76043
--- /dev/null
+++ b/telegram-webapp-sdk/Cargo.toml
@@ -0,0 +1,149 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2024"
+rust-version = "1.89"
+name = "telegram-webapp-sdk"
+version = "0.2.0"
+build = false
+autolib = false
+autobins = false
+autoexamples = false
+autotests = false
+autobenches = false
+description = "Telegram WebApp SDK for Rust"
+documentation = "https://docs.rs/telegram-webapp-sdk"
+readme = "README.md"
+keywords = [
+ "telegram",
+ "webapp",
+ "wasm",
+]
+categories = [
+ "web-programming",
+ "wasm",
+]
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/RAprogramm/telegram-webapp-sdk"
+
+[features]
+default = []
+leptos = ["dep:leptos"]
+macros = ["dep:inventory"]
+mock = ["dep:urlencoding"]
+yew = ["dep:yew"]
+
+[lib]
+name = "telegram_webapp_sdk"
+crate-type = [
+ "cdylib",
+ "rlib",
+]
+path = "src/lib.rs"
+
+[[test]]
+name = "closing_confirmation"
+path = "tests/closing_confirmation.rs"
+
+[[test]]
+name = "validate_init_data"
+path = "tests/validate_init_data.rs"
+
+[dependencies.base64]
+version = "0.22"
+
+[dependencies.ed25519-dalek]
+version = "2"
+
+[dependencies.hex]
+version = "0.4"
+
+[dependencies.hmac-sha256]
+version = "1"
+
+[dependencies.inventory]
+version = "0.3"
+optional = true
+
+[dependencies.js-sys]
+version = "0.3"
+
+[dependencies.leptos]
+version = "0.8"
+features = ["csr"]
+optional = true
+default-features = false
+
+[dependencies.masterror]
+path = ".."
+version = "0.10.4"
+optional = true
+
+[dependencies.once_cell]
+version = "1.21"
+
+[dependencies.percent-encoding]
+version = "2"
+
+[dependencies.serde]
+version = "1"
+features = ["derive"]
+
+[dependencies.serde-wasm-bindgen]
+version = "0.6"
+
+[dependencies.serde_json]
+version = "1"
+
+[dependencies.serde_urlencoded]
+version = "0.7"
+
+[dependencies.thiserror]
+version = "2"
+
+[dependencies.toml]
+version = "0.8"
+
+[dependencies.urlencoding]
+version = "2"
+optional = true
+
+[dependencies.wasm-bindgen]
+version = "0.2"
+
+[dependencies.wasm-bindgen-futures]
+version = "0.4"
+
+[dependencies.web-sys]
+version = "0.3"
+features = [
+ "Event",
+ "Window",
+ "Document",
+ "Element",
+ "HtmlElement",
+ "console",
+ "Location",
+ "CssStyleDeclaration",
+]
+
+[dependencies.yew]
+version = "0.21"
+features = ["csr"]
+optional = true
+default-features = false
+
+[dev-dependencies.wasm-bindgen-futures]
+version = "0.4"
+
+[dev-dependencies.wasm-bindgen-test]
+version = "0.3"
diff --git a/telegram-webapp-sdk/Cargo.toml.orig b/telegram-webapp-sdk/Cargo.toml.orig
new file mode 100644
index 0000000..18a79be
--- /dev/null
+++ b/telegram-webapp-sdk/Cargo.toml.orig
@@ -0,0 +1,75 @@
+[package]
+name = "telegram-webapp-sdk"
+version = "0.2.0"
+rust-version = "1.89"
+edition = "2024"
+description = "Telegram WebApp SDK for Rust"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/RAprogramm/telegram-webapp-sdk"
+readme = "README.md"
+documentation = "https://docs.rs/telegram-webapp-sdk"
+keywords = ["telegram", "webapp", "wasm"]
+categories = ["web-programming", "wasm"]
+
+[workspace.package]
+rust-version = "1.89"
+
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+wasm-bindgen = "0.2"
+wasm-bindgen-futures = "0.4"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+serde-wasm-bindgen = "0.6"
+serde_urlencoded = "0.7"
+once_cell = "1.21"
+js-sys = "0.3"
+web-sys = { version = "0.3", features = [
+ "Event",
+ "Window",
+ "Document",
+ "Element",
+ "HtmlElement",
+ "console",
+ "Location",
+ "CssStyleDeclaration",
+] }
+hmac-sha256 = "1"
+hex = "0.4"
+percent-encoding = "2"
+base64 = "0.22"
+ed25519-dalek = "2"
+thiserror = "2"
+masterror = { path = "..", version = "0.10.4", optional = true }
+urlencoding = { version = "2", optional = true }
+inventory = { version = "0.3", optional = true }
+toml = "0.8"
+
+[dependencies.yew]
+version = "0.21"
+optional = true
+default-features = false
+features = ["csr"]
+
+[dependencies.leptos]
+version = "0.8"
+optional = true
+default-features = false
+features = ["csr"]
+
+[features]
+default = []
+macros = ["dep:inventory"]
+yew = ["dep:yew"]
+leptos = ["dep:leptos"]
+mock = ["dep:urlencoding"]
+
+[workspace]
+members = ["demo"]
+
+[dev-dependencies]
+wasm-bindgen-test = "0.3"
+wasm-bindgen-futures = "0.4"
diff --git a/telegram-webapp-sdk/LICENSE-APACHE b/telegram-webapp-sdk/LICENSE-APACHE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/telegram-webapp-sdk/LICENSE-APACHE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/telegram-webapp-sdk/LICENSE-MIT b/telegram-webapp-sdk/LICENSE-MIT
new file mode 100644
index 0000000..a6632f7
--- /dev/null
+++ b/telegram-webapp-sdk/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 RAprogramm contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/telegram-webapp-sdk/Makefile.toml b/telegram-webapp-sdk/Makefile.toml
new file mode 100644
index 0000000..5bcb647
--- /dev/null
+++ b/telegram-webapp-sdk/Makefile.toml
@@ -0,0 +1,327 @@
+# Makefile.toml for cargo-make
+# Usage:
+# cargo make # pretty help (default)
+# cargo make ci # format, clippy, tests, package
+# cargo make tag # create git tag from Cargo.toml version (no push)
+# TAG=v0.3.0 cargo make release
+# CARGO_REGISTRY_TOKEN=... TAG=v0.3.0 cargo make publish
+
+[env]
+ALL_FEATURES = "true" # toggle --all-features for clippy/test
+
+# ------- Default task (no root 'default_task' key) -------
+
+[tasks.default]
+category = "Meta"
+description = "Default task -> help"
+run_task = "help"
+
+# ------- Core checks -------
+
+[tasks.format]
+category = "Format"
+clear = true
+description = "Format code using rustfmt on nightly (optional)"
+script_runner = "bash"
+script = ['cargo +nightly fmt --']
+
+[tasks.clippy]
+category = "Lint"
+clear = true
+script_runner = "bash"
+script = ['''
+ set -euo pipefail
+ if [ "${ALL_FEATURES}" = "true" ]; then
+ cargo clippy --workspace --all-targets --all-features -- -D warnings
+ else
+ cargo clippy --workspace --all-targets -- -D warnings
+ fi
+''']
+description = "Run clippy for all targets; fail on warnings"
+
+[tasks.test]
+category = "Test"
+clear = true
+script_runner = "bash"
+script = ['''
+ set -euo pipefail
+ if [ "${ALL_FEATURES}" = "true" ]; then
+ cargo test --workspace --all-features --no-fail-fast
+ else
+ cargo test --workspace --no-fail-fast
+ fi
+''']
+description = "Run tests (optionally with all features)"
+
+[tasks.package]
+category = "Package"
+clear = true
+command = "cargo"
+args = ["package", "--locked"]
+description = "Verify that the crate can be packaged cleanly"
+
+[tasks.ci]
+category = "Meta"
+dependencies = ["format", "clippy", "test", "package"]
+description = "Run format, clippy, tests, and packaging checks"
+
+# ------- Release gatekeeping -------
+
+[tasks.check_tag_env]
+clear = true
+script_runner = "bash"
+script = ['''
+ set -euo pipefail
+ # Ensure TAG is provided, e.g., TAG=v1.2.3 or refs/tags/v1.2.3
+ if [ -z "${TAG:-}" ]; then
+ echo "TAG env var is required, e.g. TAG=v1.2.3"
+ exit 1
+ fi
+''']
+description = "Ensure TAG env var is provided (e.g. TAG=v1.2.3)"
+
+[tasks.ensure_tag_matches_version]
+clear = true
+script_runner = "bash"
+script = [
+ '''
+ set -euo pipefail
+ # Normalize TAG from refs/tags/vX.Y.Z -> vX.Y.Z
+ TAG="${TAG#refs/tags/}"
+
+ # Prefer jq; fallback to sed. Select the root package of the workspace.
+ if command -v jq >/dev/null 2>&1; then
+ FILE_VER="$(cargo metadata --no-deps --format-version=1 \
+ | jq -r '.workspace_root as $root
+ | .packages[]
+ | select(.manifest_path == ($root + "/Cargo.toml"))
+ | .version')"
+ else
+ META="$(cargo metadata --no-deps --format-version=1)"
+ # Best-effort sed fallback: first "version" occurrence
+ FILE_VER="$(printf "%s" "$META" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
+ fi
+
+ if [ -z "${FILE_VER}" ]; then
+ echo "Unable to parse version from cargo metadata."
+ exit 1
+ fi
+
+ if [ "${TAG}" != "v${FILE_VER}" ]; then
+ echo "Tag ${TAG} != Cargo.toml version v${FILE_VER}"
+ exit 1
+ fi
+
+ echo "Tag ${TAG} matches Cargo.toml version v${FILE_VER}"
+''',
+]
+dependencies = ["check_tag_env"]
+description = "Ensure git tag matches Cargo.toml version (workspace root package)"
+
+[tasks.ensure_git_tag_exists]
+clear = true
+script_runner = "bash"
+script = ['''
+ set -euo pipefail
+ # Verify tag exists in the local repo
+ T="${TAG#refs/tags/}"
+ if ! git rev-parse -q --verify "refs/tags/${T}" >/dev/null; then
+ echo "Git tag '${T}' not found locally. Create and push it first:"
+ echo " cargo make tag && git push origin ${T}"
+ exit 1
+ fi
+''']
+dependencies = ["check_tag_env"]
+description = "Ensure the git tag exists locally"
+
+[tasks.check_token]
+clear = true
+script_runner = "bash"
+script = ['''
+ set -euo pipefail
+ if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
+ echo "CARGO_REGISTRY_TOKEN is required to publish."
+ exit 1
+ fi
+''']
+description = "Ensure crates.io token is present in env"
+
+# ------- Publish & Release -------
+
+[tasks.publish]
+category = "Release"
+clear = true
+script_runner = "bash"
+script = ['''
+ set -euo pipefail
+ cargo publish --locked --token "${CARGO_REGISTRY_TOKEN}"
+''']
+dependencies = [
+ "check_token",
+ "ensure_tag_matches_version",
+ "ensure_git_tag_exists",
+ "ci",
+]
+description = "Publish crate to crates.io after checks and tag/version verification"
+
+[tasks.tag]
+category = "Release"
+clear = true
+script_runner = "bash"
+script = [
+ '''
+ set -euo pipefail
+
+ # Prefer jq; fallback to sed. Select the root package of the workspace.
+ if command -v jq >/dev/null 2>&1; then
+ VER="$(cargo metadata --no-deps --format-version=1 \
+ | jq -r '.workspace_root as $root
+ | .packages[]
+ | select(.manifest_path == ($root + "/Cargo.toml"))
+ | .version')"
+ NAME="$(cargo metadata --no-deps --format-version=1 \
+ | jq -r '.workspace_root as $root
+ | .packages[]
+ | select(.manifest_path == ($root + "/Cargo.toml"))
+ | .name')"
+ else
+ META="$(cargo metadata --no-deps --format-version=1)"
+ # Fallback: first name/version occurrences
+ VER="$(printf "%s" "$META" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
+ NAME="$(printf "%s" "$META" | sed -n 's/.*\"name\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
+ fi
+
+ if [ -z "${VER}" ]; then
+ echo "Unable to extract version from Cargo metadata."
+ exit 1
+ fi
+
+ TAG="v${VER}"
+
+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
+ echo "Tag ${TAG} already exists."
+ exit 1
+ fi
+
+ # Lightweight tag with message for traceability
+ git tag -a "${TAG}" -m "${NAME:-crate} ${TAG}"
+ echo "Created tag ${TAG}. Push it with:"
+ echo " git push origin ${TAG}"
+''',
+]
+description = "Create annotated git tag vX.Y.Z from Cargo.toml version (does not push)"
+
+[tasks.release]
+category = "Release"
+clear = true
+dependencies = ["publish"]
+description = "Run checks and publish the crate (requires TAG and token)"
+
+# ------- Hooks -------
+
+[tasks.install-hooks]
+clear = true
+workspace = false
+description = "Install git pre-commit hook from .hooks/"
+script_runner = "bash"
+script = [
+ 'set -euo pipefail',
+ 'if [ ! -f .hooks/pre-commit ]; then echo "❌ .hooks/pre-commit not found!"; exit 1; fi',
+ 'echo "🔗 Linking .hooks/pre-commit to .git/hooks/pre-commit..."',
+ 'mkdir -p .git/hooks',
+ 'ln -sf ../../.hooks/pre-commit .git/hooks/pre-commit',
+ 'chmod +x .hooks/pre-commit',
+ 'echo "✅ pre-commit hook installed."',
+]
+
+# ------- Pretty Help -------
+
+[tasks.help]
+clear = true
+category = "Meta"
+description = "Pretty, colored help with examples"
+script_runner = "bash"
+script = [
+ '''
+ set -euo pipefail
+
+ if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
+ BOLD="\033[1m"; DIM="\033[2m"; RESET="\033[0m"
+ BLUE="\033[34m"; CYAN="\033[36m"; GREEN="\033[32m"; YELLOW="\033[33m"; MAGENTA="\033[35m"
+ else
+ BOLD=""; DIM=""; RESET=""; BLUE=""; CYAN=""; GREEN=""; YELLOW=""; MAGENTA=""
+ fi
+
+ hr() { printf "%s\n" "──────────────────────────────────────────────────────────────────────────────"; }
+ row() { printf " %b%-28s%b %b%s%b\n" "$GREEN" "$1" "$RESET" "$DIM" "$2" "$RESET"; }
+
+ # Prefer jq; fallback to sed. Root package only.
+ if command -v jq >/dev/null 2>&1; then
+ META="$(cargo metadata --no-deps --format-version=1)"
+ VER="$(printf "%s" "$META" | jq -r '.workspace_root as $root
+ | .packages[]
+ | select(.manifest_path == ($root + "/Cargo.toml"))
+ | .version')"
+ MSRV="$(printf "%s" "$META" | jq -r '.workspace_root as $root
+ | .packages[]
+ | select(.manifest_path == ($root + "/Cargo.toml"))
+ | .rust_version // "unknown"')"
+ NAME="$(printf "%s" "$META" | jq -r '.workspace_root as $root
+ | .packages[]
+ | select(.manifest_path == ($root + "/Cargo.toml"))
+ | .name')"
+ else
+ META="$(cargo metadata --no-deps --format-version=1)"
+ VER="$(printf "%s" "$META" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
+ MSRV="$(printf "%s" "$META" | sed -n 's/.*\"rust_version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"; [ -z "$MSRV" ] && MSRV="unknown"
+ NAME="$(printf "%s" "$META" | sed -n 's/.*\"name\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
+ fi
+
+ BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')"
+ LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || echo 'none')"
+
+ printf "%b%s%b %b(%s, MSRV %s, branch %s, last tag %s)%b\n" \
+ "$BOLD$BLUE" "${NAME:-crate} development tasks" "$RESET" \
+ "$DIM" "version ${VER:-unknown}" "${MSRV:-unknown}" "${BRANCH}" "${LAST_TAG}" "$RESET"
+ hr
+
+ printf "%b%s%b\n" "$BOLD$MAGENTA" "Common commands" "$RESET"
+ row "cargo make ci" "Run format, clippy, tests, and packaging"
+ row "cargo make tag" "Create git tag v${VER} from Cargo.toml (no push)"
+ row "TAG=v${VER} cargo make release" "Run checks and publish to crates.io"
+ row "ALL_FEATURES=false cargo make ci" "Run CI without --all-features"
+ echo
+
+ printf "%b%s%b\n" "$BOLD$MAGENTA" "Formatting" "$RESET"
+ row "cargo make format" "Format with nightly rustfmt (unstable features)"
+ echo
+
+ printf "%b%s%b\n" "$BOLD$MAGENTA" "Lint & Tests" "$RESET"
+ row "cargo make clippy" "Run clippy --workspace --all-targets -D warnings"
+ row "cargo make test" "Run tests --workspace"
+ echo
+
+ printf "%b%s%b\n" "$BOLD$MAGENTA" "Packaging & Release" "$RESET"
+ row "cargo make package" "Dry-run packaging (--locked)"
+ row "cargo make publish" "Publish to crates.io (requires TAG and token)"
+ echo
+
+ printf "%b%s%b\n" "$BOLD$DIM" "Environment variables:" "$RESET"
+ printf " %b%-22s%b %s\n" "$CYAN" "ALL_FEATURES=true|false" "$RESET" "Enable/disable --all-features for clippy/test"
+ printf " %b%-22s%b %s\n" "$CYAN" "TAG=vX.Y.Z" "$RESET" "Git tag matching Cargo.toml version"
+ printf " %b%-22s%b %s\n" "$CYAN" "CARGO_REGISTRY_TOKEN" "$RESET" "crates.io token for publish"
+ echo
+
+ printf "%b%s%b\n" "$BOLD$DIM" "Tips:" "$RESET"
+ printf " • Keep %bCargo.lock%b committed\n" "$BOLD" "$RESET"
+ printf " • Tag must exactly match version: %bv%s%b\n" "$BOLD" "${VER}" "$RESET"
+ echo
+''',
+]
+
+# Alias
+[tasks.h]
+clear = true
+run_task = "help"
+category = "Meta"
+description = "Alias: help"
diff --git a/telegram-webapp-sdk/README.md b/telegram-webapp-sdk/README.md
new file mode 100644
index 0000000..920a08b
--- /dev/null
+++ b/telegram-webapp-sdk/README.md
@@ -0,0 +1,462 @@
+
+
+# Telegram WebApp SDK
+
+[](https://crates.io/crates/telegram-webapp-sdk)
+[](https://docs.rs/telegram-webapp-sdk)
+[](https://crates.io/crates/telegram-webapp-sdk)
+
+
+[](https://github.com/RAprogramm/telegram-webapp-sdk/actions/workflows/ci.yml)
+
+`telegram-webapp-sdk` provides a type-safe and ergonomic wrapper around the [Telegram Web Apps](https://core.telegram.org/bots/webapps) JavaScript API.
+
+## Features
+
+- Comprehensive coverage of Telegram Web App JavaScript APIs.
+- Framework integrations for **Yew** and **Leptos**.
+- Optional macros for automatic initialization and routing.
+
+## Macros
+
+The macros are available with the `macros` feature. Enable it in your `Cargo.toml`:
+
+```toml
+telegram-webapp-sdk = { version = "0.2", features = ["macros"] }
+```
+
+Reduce boilerplate in Telegram Mini Apps using the provided macros:
+
+```rust,ignore
+telegram_page!("/", fn index() {
+ // render page
+});
+
+telegram_app!(fn main() -> Result<(), wasm_bindgen::JsValue> {
+ telegram_router!();
+ Ok(())
+});
+```
+
+When running outside Telegram in debug builds, `telegram_app!` loads mock
+settings from `telegram-webapp.toml`.
+- Configurable mock `Telegram.WebApp` for local development and testing.
+- API helpers for user interactions, storage, device sensors and more.
+
+## Table of contents
+
+- [Installation](#installation)
+- [Quick start](#quick-start)
+- [Mock environment](#mock-environment)
+- [User interactions](#user-interactions)
+- [Keyboard control](#keyboard-control)
+- [API coverage](#api-coverage)
+- [Changelog](#changelog)
+- [License](#license)
+
+## Installation
+
+Add the crate to your `Cargo.toml`:
+
+```toml
+[dependencies]
+telegram-webapp-sdk = "0.2"
+```
+
+Enable optional features as needed:
+
+```toml
+telegram-webapp-sdk = { version = "0.2", features = ["macros", "yew", "mock"] }
+```
+
+- `macros` — enables `telegram_app!`, `telegram_page!`, and `telegram_router!`.
+- `yew` — exposes a `use_telegram_context` hook.
+- `leptos` — integrates the context into the Leptos reactive system.
+- `mock` — installs a configurable mock `Telegram.WebApp` for local development.
+
+## Quick start
+
+### Yew
+
+```rust,ignore
+use telegram_webapp_sdk::yew::use_telegram_context;
+use yew::prelude::*;
+
+#[function_component(App)]
+fn app() -> Html {
+ let ctx = use_telegram_context().expect("context");
+ html! { { ctx.init_data.auth_date } }
+}
+```
+
+### Leptos
+
+```rust,ignore
+use leptos::prelude::*;
+use telegram_webapp_sdk::leptos::provide_telegram_context;
+
+#[component]
+fn App() -> impl IntoView {
+ provide_telegram_context().expect("context");
+ let ctx = use_context::()
+ .expect("context");
+ view! { { ctx.init_data.auth_date } }
+}
+```
+
+## Mock environment
+
+The `mock` feature simulates a `Telegram.WebApp` instance, enabling local development without Telegram:
+
+```rust,ignore
+let config = telegram_webapp_sdk::mock::MockConfig::default();
+let ctx = telegram_webapp_sdk::mock::install(config)?;
+```
+
+## User interactions
+
+Request access to sensitive user data or open the contact interface:
+
+```rust,no_run
+use telegram_webapp_sdk::api::user::{request_contact, request_phone_number, open_contact};
+use telegram_webapp_sdk::webapp::TelegramWebApp;
+
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+request_contact()?;
+request_phone_number()?;
+open_contact()?;
+
+let app = TelegramWebApp::try_instance()?;
+app.request_write_access(|granted| {
+ let _ = granted;
+})?;
+# Ok(())
+# }
+```
+
+These calls require the user's explicit permission before any information is shared.
+
+## Keyboard control
+
+Hide the native keyboard when it's no longer required:
+
+```rust,no_run
+use telegram_webapp_sdk::webapp::TelegramWebApp;
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let app = TelegramWebApp::try_instance()?;
+app.hide_keyboard()?;
+# Ok(())
+# }
+```
+
+## Closing confirmation
+
+Prompt users before the Mini App closes:
+
+```rust,no_run
+use telegram_webapp_sdk::webapp::TelegramWebApp;
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let app = TelegramWebApp::try_instance()?;
+app.enable_closing_confirmation()?;
+assert!(app.is_closing_confirmation_enabled());
+// later
+app.disable_closing_confirmation()?;
+# Ok(())
+# }
+```
+## Invoice payments
+
+Open invoices and react to the final payment status:
+
+```rust,no_run
+use telegram_webapp_sdk::webapp::TelegramWebApp;
+
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let app = TelegramWebApp::try_instance()?;
+let handle = app.on_invoice_closed(|status| {
+ let _ = status;
+})?;
+app.open_invoice("https://invoice", |_status| {})?;
+app.off_event(handle)?;
+# Ok(())
+# }
+```
+## Sharing
+
+Share links, prepared messages, or stories and join voice chats:
+
+```rust,no_run
+use js_sys::Object;
+use telegram_webapp_sdk::webapp::TelegramWebApp;
+
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let app = TelegramWebApp::try_instance()?;
+app.share_url("https://example.com", Some("Check this out"))?;
+app.join_voice_chat("chat", None)?;
+app.share_message("msg-id", |sent| {
+ let _ = sent;
+})?;
+let params = Object::new();
+app.share_to_story("https://example.com/image.png", Some(¶ms.into()))?;
+# Ok(())
+# }
+```
+
+## Settings button
+
+Control the Telegram client's settings button and handle user clicks:
+
+```rust,no_run
+use telegram_webapp_sdk::api::settings_button::{show, hide, on_click, off_click};
+use wasm_bindgen::prelude::Closure;
+
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let cb = Closure::wrap(Box::new(|| {}) as Box);
+on_click(&cb)?;
+show()?;
+hide()?;
+off_click(&cb)?;
+# Ok(())
+# }
+```
+
+## Cloud storage
+
+Persist small key-value pairs in Telegram's cloud using `CloudStorage`:
+
+```rust,no_run
+use js_sys::Reflect;
+use telegram_webapp_sdk::api::cloud_storage::{get_items, set_items};
+use wasm_bindgen_futures::JsFuture;
+
+# async fn run() -> Result<(), wasm_bindgen::JsValue> {
+JsFuture::from(set_items(&[("counter", "1")])?).await?;
+let obj = JsFuture::from(get_items(&["counter"])?).await?;
+let value = Reflect::get(&obj, &"counter".into())?.as_string();
+assert_eq!(value, Some("1".into()));
+# Ok(())
+# }
+```
+
+All functions return a `Promise` and require the Web App to run inside Telegram.
+
+## Home screen
+
+Prompt users to add the app to their home screen and check the current status:
+
+```rust,no_run
+use telegram_webapp_sdk::webapp::TelegramWebApp;
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let app = TelegramWebApp::try_instance()?;
+let _shown = app.add_to_home_screen()?;
+app.check_home_screen_status(|status| {
+ let _ = status;
+})?;
+# Ok(())
+# }
+```
+
+## Event callbacks
+
+Callback registration methods return an `EventHandle` for later deregistration.
+
+```rust,no_run
+use telegram_webapp_sdk::webapp::TelegramWebApp;
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let app = TelegramWebApp::try_instance()?;
+let handle = app.on_event("my_event", |value| {
+ let _ = value;
+})?;
+app.off_event(handle)?;
+# Ok(())
+# }
+```
+
+### Background events
+
+Some Telegram events may fire while the Mini App is in the background. Register
+callbacks for these with `on_background_event`:
+
+```rust,no_run
+use telegram_webapp_sdk::webapp::{BackgroundEvent, TelegramWebApp};
+
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let app = TelegramWebApp::try_instance()?;
+let handle = app.on_background_event(BackgroundEvent::MainButtonClicked, |_| {})?;
+app.off_event(handle)?;
+# Ok(())
+# }
+```
+
+Supported background events:
+
+| Event | Payload |
+|-------|---------|
+| `mainButtonClicked` | none |
+| `backButtonClicked` | none |
+| `settingsButtonClicked` | none |
+| `writeAccessRequested` | `bool` granted flag |
+| `contactRequested` | `bool` shared flag |
+| `phoneRequested` | `bool` shared flag |
+| `invoiceClosed` | status `String` |
+| `popupClosed` | object `{ button_id: Option }` |
+| `qrTextReceived` | scanned text `String` |
+| `clipboardTextReceived` | clipboard text `String` |
+
+## Appearance
+
+Customize colors and react to theme or safe area updates:
+## Fullscreen and orientation
+
+Control the Mini App display and screen orientation:
+
+```rust,no_run
+use telegram_webapp_sdk::webapp::TelegramWebApp;
+# fn run() -> Result<(), wasm_bindgen::JsValue> {
+let app = TelegramWebApp::try_instance()?;
+app.set_header_color("#000000")?;
+app.set_background_color("#ffffff")?;
+app.set_bottom_bar_color("#cccccc")?;
+let theme_handle = app.on_theme_changed(|| {})?;
+let safe_handle = app.on_safe_area_changed(|| {})?;
+let content_handle = app.on_content_safe_area_changed(|| {})?;
+// later: app.off_event(theme_handle)?; etc.
+
+app.request_fullscreen()?;
+app.lock_orientation("portrait")?;
+// later...
+app.unlock_orientation()?;
+app.exit_fullscreen()?;
+# Ok(())
+# }
+```
+
+## Haptic feedback
+
+Trigger device vibrations through Telegram's [HapticFeedback](https://core.telegram.org/bots/webapps#hapticfeedback) API:
+
+```rust,no_run
+use telegram_webapp_sdk::api::haptic::{
+ impact_occurred, notification_occurred, selection_changed,
+ HapticImpactStyle, HapticNotificationType,
+};
+
+impact_occurred(HapticImpactStyle::Light)?;
+notification_occurred(HapticNotificationType::Success)?;
+selection_changed()?;
+# Ok::<(), wasm_bindgen::JsValue>(())
+```
+
+## Device storage
+
+Persist lightweight data on the user's device:
+
+```rust,no_run
+use telegram_webapp_sdk::api::device_storage::{set, get};
+
+# async fn run() -> Result<(), wasm_bindgen::JsValue> {
+set("theme", "dark").await?;
+let value = get("theme").await?;
+# Ok(())
+# }
+```
+
+## Secure storage
+
+Store sensitive data encrypted and restorable:
+
+```rust,no_run
+use telegram_webapp_sdk::api::secure_storage::{set, restore};
+
+# async fn run() -> Result<(), wasm_bindgen::JsValue> {
+set("token", "secret").await?;
+let _ = restore("token").await?;
+# Ok(())
+# }
+```
+
+## Location manager
+
+Retrieve user location and react to related events via Telegram's location manager:
+
+```rust,no_run
+use telegram_webapp_sdk::api::location_manager::{
+ init, get_location, open_settings, on_location_requested,
+};
+use wasm_bindgen::closure::Closure;
+
+init()?;
+let _ = get_location();
+open_settings()?;
+
+let cb = Closure::wrap(Box::new(|| {}) as Box);
+on_location_requested(&cb)?;
+cb.forget();
+# Ok::<(), wasm_bindgen::JsValue>(())
+```
+
+## Device sensors
+
+Access motion sensors if the user's device exposes them.
+
+```rust,no_run
+use telegram_webapp_sdk::api::accelerometer::{start, get_acceleration, stop};
+
+start()?;
+let reading = get_acceleration();
+stop()?;
+# Ok::<(), wasm_bindgen::JsValue>(())
+```
+
+Callbacks for sensor lifecycle events are available through `on_started`,
+`on_changed`, `on_stopped`, and `on_failed` functions for accelerometer,
+gyroscope, and device orientation sensors.
+## Init data validation
+
+Validate the integrity of the `Telegram.WebApp.initData` payload on the server.
+The `validate_init_data` module is re-exported at the crate root and can be
+used directly or through the `TelegramWebApp::validate_init_data` helper:
+
+```rust,no_run
+use telegram_webapp_sdk::{
+ validate_init_data::ValidationKey,
+ TelegramWebApp
+};
+
+let bot_token = "123456:ABC";
+let query = "user=alice&auth_date=1&hash=48f4c0e9d3dd46a5734bf2c5d4df9f4ec52a3cd612f6482a7d2c68e84e702ee2";
+TelegramWebApp::validate_init_data(query, ValidationKey::BotToken(bot_token))?;
+
+// For Ed25519-signed data
+# use ed25519_dalek::{Signer, SigningKey};
+# let sk = SigningKey::from_bytes(&[1u8;32]);
+# let pk = sk.verifying_key();
+# let sig = sk.sign(b"a=1\nb=2");
+# let init_data = format!("a=1&b=2&signature={}", base64::encode(sig.to_bytes()));
+TelegramWebApp::validate_init_data(
+ &init_data,
+ ValidationKey::Ed25519PublicKey(pk.as_bytes())
+)?;
+
+# Ok::<(), Box>(())
+```
+
+## API coverage
+
+See [WEBAPP_API.md](./WEBAPP_API.md) for a checklist of supported Telegram WebApp JavaScript API methods and features.
+
+## Changelog
+
+See [CHANGELOG.md](./CHANGELOG.md) for release notes.
+
+## License
+
+`telegram-webapp-sdk` is licensed under either of
+
+- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or )
+- MIT license ([LICENSE-MIT](LICENSE-MIT) or )
+
+at your option.
diff --git a/telegram-webapp-sdk/WEBAPP_API.md b/telegram-webapp-sdk/WEBAPP_API.md
new file mode 100644
index 0000000..eab28c1
--- /dev/null
+++ b/telegram-webapp-sdk/WEBAPP_API.md
@@ -0,0 +1,170 @@
+# Telegram WebApp API Coverage
+
+This checklist tracks support for the [Telegram Web Apps JavaScript API](https://core.telegram.org/bots/webapps). Mark items as they are implemented.
+
+## Methods
+
+- [x] ready ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] expand ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] close ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] sendData ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] openLink ([840ace1](https://github.com/RAprogramm/telegram-webapp-sdk/commit/840ace1))
+- [x] openTelegramLink ([840ace1](https://github.com/RAprogramm/telegram-webapp-sdk/commit/840ace1))
+- [x] openInvoice ([840ace1](https://github.com/RAprogramm/telegram-webapp-sdk/commit/840ace1))
+- [x] switchInlineQuery ([a098e00](https://github.com/RAprogramm/telegram-webapp-sdk/commit/a098e00))
+- [x] showAlert ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] showConfirm ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] showPopup ([840ace1](https://github.com/RAprogramm/telegram-webapp-sdk/commit/840ace1))
+- [x] showScanQrPopup ([840ace1](https://github.com/RAprogramm/telegram-webapp-sdk/commit/840ace1))
+- [x] closeScanQrPopup ([840ace1](https://github.com/RAprogramm/telegram-webapp-sdk/commit/840ace1))
+- [x] shareURL ([a098e00](https://github.com/RAprogramm/telegram-webapp-sdk/commit/a098e00))
+- [x] shareMessage ([4b10c98](https://github.com/RAprogramm/telegram-webapp-sdk/commit/4b10c98))
+- [x] shareToStory ([4b10c98](https://github.com/RAprogramm/telegram-webapp-sdk/commit/4b10c98))
+- [x] joinVoiceChat ([a098e00](https://github.com/RAprogramm/telegram-webapp-sdk/commit/a098e00))
+- [x] requestWriteAccess ([a098e00](https://github.com/RAprogramm/telegram-webapp-sdk/commit/a098e00))
+- [x] requestContact ([d595540](https://github.com/RAprogramm/telegram-webapp-sdk/commit/d595540))
+- [x] requestPhoneNumber ([d595540](https://github.com/RAprogramm/telegram-webapp-sdk/commit/d595540))
+- [x] openContact ([d595540](https://github.com/RAprogramm/telegram-webapp-sdk/commit/d595540))
+- [x] enableVerticalSwipes ([8e60df3](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8e60df3))
+- [x] disableVerticalSwipes ([8e60df3](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8e60df3))
+- [x] hideKeyboard ([94ff585](https://github.com/RAprogramm/telegram-webapp-sdk/commit/94ff585))
+- [x] downloadFile ([3092094](https://github.com/RAprogramm/telegram-webapp-sdk/commit/3092094))
+- [x] readTextFromClipboard ([fd1c84e](https://github.com/RAprogramm/telegram-webapp-sdk/commit/fd1c84e))
+- [x] setEmojiStatus ([12cfbd0](https://github.com/RAprogramm/telegram-webapp-sdk/commit/12cfbd0))
+- [x] requestEmojiStatusAccess ([12cfbd0](https://github.com/RAprogramm/telegram-webapp-sdk/commit/12cfbd0))
+- [x] setHeaderColor ([58a73cb](https://github.com/RAprogramm/telegram-webapp-sdk/commit/58a73cb))
+- [x] setBackgroundColor ([58a73cb](https://github.com/RAprogramm/telegram-webapp-sdk/commit/58a73cb))
+- [x] setBottomBarColor ([58a73cb](https://github.com/RAprogramm/telegram-webapp-sdk/commit/58a73cb))
+- [x] addToHomeScreen ([e709edb](https://github.com/RAprogramm/telegram-webapp-sdk/commit/e709edb))
+- [x] checkHomeScreenStatus ([e709edb](https://github.com/RAprogramm/telegram-webapp-sdk/commit/e709edb))
+- [x] enableClosingConfirmation ([8fe4dec](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8fe4dec))
+- [x] disableClosingConfirmation ([8fe4dec](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8fe4dec))
+- [x] isClosingConfirmationEnabled (unreleased)
+- [x] requestFullscreen ([4364008](https://github.com/RAprogramm/telegram-webapp-sdk/commit/4364008))
+- [x] exitFullscreen ([4364008](https://github.com/RAprogramm/telegram-webapp-sdk/commit/4364008))
+- [x] lockOrientation ([4364008](https://github.com/RAprogramm/telegram-webapp-sdk/commit/4364008))
+- [x] unlockOrientation ([4364008](https://github.com/RAprogramm/telegram-webapp-sdk/commit/4364008))
+
+## Objects
+
+### BottomButton (Main & Secondary)
+- [x] show ([7d524fd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7d524fd))
+- [x] hide ([7d524fd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7d524fd))
+- [x] setText ([7d524fd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7d524fd))
+- [x] setColor ([7d524fd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7d524fd))
+- [x] setTextColor ([7d524fd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7d524fd))
+- [x] onClick ([7d524fd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7d524fd))
+- [x] offClick ([7d524fd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7d524fd))
+
+### MainButton
+- [x] show ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] hide ([f0a108d](https://github.com/RAprogramm/telegram-webapp-sdk/commit/f0a108d))
+- [x] setText ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] onClick ([0a42d7b](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0a42d7b))
+- [x] offClick ([0a42d7b](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0a42d7b))
+
+### BackButton
+- [x] show ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] hide ([bcce132](https://github.com/RAprogramm/telegram-webapp-sdk/commit/bcce132))
+- [x] onClick ([0a42d7b](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0a42d7b))
+- [x] offClick ([0a42d7b](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0a42d7b))
+
+### SettingsButton
+- [x] show ([885be03](https://github.com/RAprogramm/telegram-webapp-sdk/commit/885be03))
+- [x] hide ([885be03](https://github.com/RAprogramm/telegram-webapp-sdk/commit/885be03))
+- [x] onClick ([885be03](https://github.com/RAprogramm/telegram-webapp-sdk/commit/885be03))
+- [x] offClick ([885be03](https://github.com/RAprogramm/telegram-webapp-sdk/commit/885be03))
+
+### HapticFeedback
+- [x] impactOccurred ([9896d92](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9896d92))
+- [x] notificationOccurred ([9896d92](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9896d92))
+- [x] selectionChanged ([9896d92](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9896d92))
+
+## Sensors
+
+### Accelerometer
+- [x] start ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+- [x] stop ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+- [x] getAcceleration ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+
+### Gyroscope
+- [x] start ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+- [x] stop ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+- [x] getAngularVelocity ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+
+### DeviceOrientation
+- [x] start ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+- [x] stop ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+- [x] getOrientation ([9428d51](https://github.com/RAprogramm/telegram-webapp-sdk/commit/9428d51))
+
+### LocationManager
+- [x] init ([10ca55c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/10ca55c))
+- [x] getLocation ([10ca55c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/10ca55c))
+- [x] openSettings ([10ca55c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/10ca55c))
+- [x] onLocationManagerUpdated ([10ca55c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/10ca55c))
+- [x] onLocationRequested ([10ca55c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/10ca55c))
+
+### BiometricManager
+- [x] init ([8c34fbd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8c34fbd))
+- [x] requestAccess ([8c34fbd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8c34fbd))
+- [x] authenticate ([8c34fbd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8c34fbd))
+- [x] updateBiometricToken ([8c34fbd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8c34fbd))
+- [x] openSettings ([8c34fbd](https://github.com/RAprogramm/telegram-webapp-sdk/commit/8c34fbd))
+- [x] isInited ([7a2555c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7a2555c))
+- [x] isBiometricAvailable ([7a2555c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7a2555c))
+- [x] isAccessRequested ([7a2555c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7a2555c))
+- [x] isAccessGranted ([7a2555c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7a2555c))
+- [x] isBiometricTokenSaved ([7a2555c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7a2555c))
+- [x] deviceId ([7a2555c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/7a2555c))
+
+## Storages
+
+### CloudStorage
+- [x] getItem ([ae2a302](https://github.com/RAprogramm/telegram-webapp-sdk/commit/ae2a302))
+- [x] setItem ([ae2a302](https://github.com/RAprogramm/telegram-webapp-sdk/commit/ae2a302))
+- [x] removeItem ([ae2a302](https://github.com/RAprogramm/telegram-webapp-sdk/commit/ae2a302))
+- [x] getItems ([ae2a302](https://github.com/RAprogramm/telegram-webapp-sdk/commit/ae2a302))
+- [x] setItems
+- [x] removeItems ([ae2a302](https://github.com/RAprogramm/telegram-webapp-sdk/commit/ae2a302))
+- [x] getKeys ([ae2a302](https://github.com/RAprogramm/telegram-webapp-sdk/commit/ae2a302))
+- [x] clear ([ae2a302](https://github.com/RAprogramm/telegram-webapp-sdk/commit/ae2a302))
+
+### DeviceStorage
+- [x] set ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+- [x] get ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+- [x] remove ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+- [x] clear ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+
+### SecureStorage
+- [x] set ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+- [x] get ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+- [x] restore ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+- [x] remove ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+- [x] clear ([0905616](https://github.com/RAprogramm/telegram-webapp-sdk/commit/0905616))
+
+## Background events
+
+| Event | Payload |
+|-------|---------|
+| `mainButtonClicked` | none |
+| `backButtonClicked` | none |
+| `settingsButtonClicked` | none |
+| `writeAccessRequested` | `bool` granted flag |
+| `contactRequested` | `bool` shared flag |
+| `phoneRequested` | `bool` shared flag |
+| `invoiceClosed` | status `String` |
+| `popupClosed` | object `{ button_id: Option }` |
+| `qrTextReceived` | scanned text `String` |
+| `clipboardTextReceived` | clipboard text `String` |
+
+## Remaining WebApp Features
+
+The following features are not yet covered by the SDK:
+
+- [x] Init data validation (unreleased)
+- [x] Theme and safe area change events ([58a73cb](https://github.com/RAprogramm/telegram-webapp-sdk/commit/58a73cb))
+- [x] Viewport management
+- [x] Clipboard access ([fd1c84e](https://github.com/RAprogramm/telegram-webapp-sdk/commit/fd1c84e))
+- [x] Location access ([10ca55c](https://github.com/RAprogramm/telegram-webapp-sdk/commit/10ca55c))
+- [x] Invoice payments (unreleased)
+- [x] Background events (unreleased)
diff --git a/telegram-webapp-sdk/src/api.rs b/telegram-webapp-sdk/src/api.rs
new file mode 100644
index 0000000..6f4a1d7
--- /dev/null
+++ b/telegram-webapp-sdk/src/api.rs
@@ -0,0 +1,14 @@
+pub mod accelerometer;
+pub mod biometric;
+pub mod cloud_storage;
+pub mod device_orientation;
+pub mod device_storage;
+pub mod events;
+pub mod gyroscope;
+pub mod haptic;
+pub mod location_manager;
+pub mod secure_storage;
+pub mod settings_button;
+pub mod theme;
+pub mod user;
+pub mod viewport;
diff --git a/telegram-webapp-sdk/src/api/accelerometer.rs b/telegram-webapp-sdk/src/api/accelerometer.rs
new file mode 100644
index 0000000..4c45fa0
--- /dev/null
+++ b/telegram-webapp-sdk/src/api/accelerometer.rs
@@ -0,0 +1,209 @@
+use js_sys::{Function, Reflect};
+use wasm_bindgen::{JsCast, prelude::*};
+use web_sys::window;
+
+use super::events;
+
+/// Three-dimensional acceleration in meters per second squared.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Acceleration {
+ /// Acceleration along the X axis.
+ pub x: f64,
+ /// Acceleration along the Y axis.
+ pub y: f64,
+ /// Acceleration along the Z axis.
+ pub z: f64
+}
+
+impl Acceleration {
+ /// Creates a new [`Acceleration`] instance.
+ #[allow(dead_code)]
+ const fn new(x: f64, y: f64, z: f64) -> Self {
+ Self {
+ x,
+ y,
+ z
+ }
+ }
+}
+
+/// Starts the accelerometer.
+///
+/// # Errors
+/// Returns [`JsValue`] if the underlying JavaScript call fails or the sensor is
+/// unavailable.
+///
+/// # Examples
+/// ```no_run
+/// # use telegram_webapp_sdk::api::accelerometer::start;
+/// start()?;
+/// # Ok::<(), wasm_bindgen::JsValue>(())
+/// ```
+pub fn start() -> Result<(), JsValue> {
+ let accel = accelerometer_object()?;
+ let func = Reflect::get(&accel, &"start".into())?.dyn_into::()?;
+ func.call0(&accel)?;
+ Ok(())
+}
+
+/// Stops the accelerometer.
+///
+/// # Errors
+/// Returns [`JsValue`] if the underlying JavaScript call fails or the sensor is
+/// unavailable.
+///
+/// # Examples
+/// ```no_run
+/// # use telegram_webapp_sdk::api::accelerometer::stop;
+/// stop()?;
+/// # Ok::<(), wasm_bindgen::JsValue>(())
+/// ```
+pub fn stop() -> Result<(), JsValue> {
+ let accel = accelerometer_object()?;
+ let func = Reflect::get(&accel, &"stop".into())?.dyn_into::()?;
+ func.call0(&accel)?;
+ Ok(())
+}
+
+/// Reads the current acceleration values.
+///
+/// # Examples
+/// ```no_run
+/// # use telegram_webapp_sdk::api::accelerometer::get_acceleration;
+/// let _ = get_acceleration();
+/// ```
+pub fn get_acceleration() -> Option {
+ let accel = accelerometer_object().ok()?;
+ let x = Reflect::get(&accel, &"x".into()).ok()?.as_f64()?;
+ let y = Reflect::get(&accel, &"y".into()).ok()?.as_f64()?;
+ let z = Reflect::get(&accel, &"z".into()).ok()?.as_f64()?;
+ Some(Acceleration {
+ x,
+ y,
+ z
+ })
+}
+
+/// Registers a callback for `accelerometerStarted` event.
+///
+/// ⚠️ The closure must be kept alive for as long as it is needed.
+pub fn on_started(callback: &Closure) -> Result<(), JsValue> {
+ events::on_event("accelerometerStarted", callback)
+}
+
+/// Registers a callback for `accelerometerChanged` event.
+///
+/// ⚠️ The closure must be kept alive for as long as it is needed.
+pub fn on_changed(callback: &Closure) -> Result<(), JsValue> {
+ events::on_event("accelerometerChanged", callback)
+}
+
+/// Registers a callback for `accelerometerStopped` event.
+///
+/// ⚠️ The closure must be kept alive for as long as it is needed.
+pub fn on_stopped(callback: &Closure) -> Result<(), JsValue> {
+ events::on_event("accelerometerStopped", callback)
+}
+
+/// Registers a callback for `accelerometerFailed` event.
+///
+/// ⚠️ The closure must be kept alive for as long as it is needed.
+pub fn on_failed(callback: &Closure) -> Result<(), JsValue> {
+ events::on_event("accelerometerFailed", callback)
+}
+
+fn accelerometer_object() -> Result {
+ let win = window().ok_or_else(|| JsValue::from_str("no window"))?;
+ let tg = Reflect::get(&win, &"Telegram".into())?;
+ let webapp = Reflect::get(&tg, &"WebApp".into())?;
+ Reflect::get(&webapp, &"Accelerometer".into())
+}
+
+#[cfg(test)]
+#[allow(dead_code)]
+mod tests {
+ use js_sys::{Function, Object, Reflect};
+ use wasm_bindgen::{JsValue, closure::Closure};
+ use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
+ use web_sys::window;
+
+ use super::*;
+
+ wasm_bindgen_test_configure!(run_in_browser);
+
+ #[allow(dead_code)]
+ fn setup_accelerometer() -> (Object, Object) {
+ let win = window().unwrap();
+ let telegram = Object::new();
+ let webapp = Object::new();
+ let accel = Object::new();
+ let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
+ let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
+ let _ = Reflect::set(&webapp, &"Accelerometer".into(), &accel);
+ (webapp, accel)
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(clippy::unused_unit)]
+ fn start_ok() {
+ let (_webapp, accel) = setup_accelerometer();
+ let func = Function::new_no_args("this.called = true;");
+ let _ = Reflect::set(&accel, &"start".into(), &func);
+ assert!(start().is_ok());
+ let called = Reflect::get(&accel, &"called".into()).unwrap();
+ assert_eq!(called.as_bool(), Some(true));
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(clippy::unused_unit)]
+ fn start_err() {
+ let (_webapp, accel) = setup_accelerometer();
+ let _ = Reflect::set(&accel, &"start".into(), &JsValue::from_f64(1.0));
+ assert!(start().is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(clippy::unused_unit)]
+ fn stop_ok() {
+ let (_webapp, accel) = setup_accelerometer();
+ let func = Function::new_no_args("this.stopped = true;");
+ let _ = Reflect::set(&accel, &"stop".into(), &func);
+ assert!(stop().is_ok());
+ let stopped = Reflect::get(&accel, &"stopped".into()).unwrap();
+ assert_eq!(stopped.as_bool(), Some(true));
+ }
+
+ #[wasm_bindgen_test]
+ fn get_acceleration_ok() {
+ let (_webapp, accel) = setup_accelerometer();
+ let _ = Reflect::set(&accel, &"x".into(), &JsValue::from_f64(1.0));
+ let _ = Reflect::set(&accel, &"y".into(), &JsValue::from_f64(2.0));
+ let _ = Reflect::set(&accel, &"z".into(), &JsValue::from_f64(3.0));
+ let result = get_acceleration().unwrap();
+ assert_eq!(
+ result,
+ Acceleration {
+ x: 1.0,
+ y: 2.0,
+ z: 3.0
+ }
+ );
+ }
+
+ #[wasm_bindgen_test]
+ fn registers_callbacks() {
+ let (webapp, _accel) = setup_accelerometer();
+ let on_event = Function::new_with_args("name, cb", "this[name] = cb;");
+ let _ = Reflect::set(&webapp, &"onEvent".into(), &on_event);
+ let cb = Closure::wrap(Box::new(|| {}) as Box);
+ on_started(&cb).expect("on_started");
+ on_changed(&cb).expect("on_changed");
+ on_stopped(&cb).expect("on_stopped");
+ on_failed(&cb).expect("on_failed");
+ assert!(Reflect::has(&webapp, &"accelerometerStarted".into()).unwrap());
+ assert!(Reflect::has(&webapp, &"accelerometerChanged".into()).unwrap());
+ assert!(Reflect::has(&webapp, &"accelerometerStopped".into()).unwrap());
+ assert!(Reflect::has(&webapp, &"accelerometerFailed".into()).unwrap());
+ cb.forget();
+ }
+}
diff --git a/telegram-webapp-sdk/src/api/biometric.rs b/telegram-webapp-sdk/src/api/biometric.rs
new file mode 100644
index 0000000..4e4ab37
--- /dev/null
+++ b/telegram-webapp-sdk/src/api/biometric.rs
@@ -0,0 +1,508 @@
+use js_sys::{Function, Reflect};
+use wasm_bindgen::{JsCast, prelude::*};
+use web_sys::window;
+
+/// Calls `Telegram.WebApp.BiometricManager.init()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if `BiometricManager` or the method is unavailable,
+/// or if the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::init;
+///
+/// let _ = init();
+/// ```
+pub fn init() -> Result<(), JsValue> {
+ let biom = biometric_object()?;
+ let func = Reflect::get(&biom, &JsValue::from_str("init"))?.dyn_into::()?;
+ func.call0(&biom)?;
+ Ok(())
+}
+
+/// Calls `Telegram.WebApp.BiometricManager.requestAccess(auth_key, reason,
+/// options)`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if `BiometricManager` or the method is unavailable,
+/// or if the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::request_access;
+///
+/// let _ = request_access("auth-key", None, None);
+/// ```
+pub fn request_access(
+ auth_key: &str,
+ reason: Option<&str>,
+ options: Option<&JsValue>
+) -> Result<(), JsValue> {
+ let biom = biometric_object()?;
+ let func = Reflect::get(&biom, &JsValue::from_str("requestAccess"))?.dyn_into::()?;
+ let key = JsValue::from_str(auth_key);
+ match (reason, options) {
+ (Some(r), Some(o)) => {
+ let r = JsValue::from_str(r);
+ func.call3(&biom, &key, &r, o)?;
+ }
+ (Some(r), None) => {
+ let r = JsValue::from_str(r);
+ func.call2(&biom, &key, &r)?;
+ }
+ (None, Some(o)) => {
+ func.call3(&biom, &key, &JsValue::UNDEFINED, o)?;
+ }
+ (None, None) => {
+ func.call1(&biom, &key)?;
+ }
+ }
+ Ok(())
+}
+
+/// Calls `Telegram.WebApp.BiometricManager.authenticate(auth_key, reason,
+/// options)`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if `BiometricManager` or the method is unavailable,
+/// or if the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::authenticate;
+///
+/// let _ = authenticate("auth-key", None, None);
+/// ```
+pub fn authenticate(
+ auth_key: &str,
+ reason: Option<&str>,
+ options: Option<&JsValue>
+) -> Result<(), JsValue> {
+ let biom = biometric_object()?;
+ let func = Reflect::get(&biom, &JsValue::from_str("authenticate"))?.dyn_into::()?;
+ let key = JsValue::from_str(auth_key);
+ match (reason, options) {
+ (Some(r), Some(o)) => {
+ let r = JsValue::from_str(r);
+ func.call3(&biom, &key, &r, o)?;
+ }
+ (Some(r), None) => {
+ let r = JsValue::from_str(r);
+ func.call2(&biom, &key, &r)?;
+ }
+ (None, Some(o)) => {
+ func.call3(&biom, &key, &JsValue::UNDEFINED, o)?;
+ }
+ (None, None) => {
+ func.call1(&biom, &key)?;
+ }
+ }
+ Ok(())
+}
+
+/// Calls `Telegram.WebApp.BiometricManager.updateBiometricToken(token)`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if `BiometricManager` or the method is unavailable,
+/// or if the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::update_biometric_token;
+///
+/// let _ = update_biometric_token("token");
+/// ```
+pub fn update_biometric_token(token: &str) -> Result<(), JsValue> {
+ let biom = biometric_object()?;
+ let func =
+ Reflect::get(&biom, &JsValue::from_str("updateBiometricToken"))?.dyn_into::()?;
+ func.call1(&biom, &JsValue::from_str(token))?;
+ Ok(())
+}
+
+/// Calls `Telegram.WebApp.BiometricManager.openSettings()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if `BiometricManager` or the method is unavailable,
+/// or if the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::open_settings;
+///
+/// let _ = open_settings();
+/// ```
+pub fn open_settings() -> Result<(), JsValue> {
+ let biom = biometric_object()?;
+ let func = Reflect::get(&biom, &JsValue::from_str("openSettings"))?.dyn_into::()?;
+ func.call0(&biom)?;
+ Ok(())
+}
+
+/// Returns `Telegram.WebApp.BiometricManager.isInited`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if the property is unavailable or not a boolean.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::is_inited;
+///
+/// let _ = is_inited();
+/// ```
+pub fn is_inited() -> Result {
+ let biom = biometric_object()?;
+ let value = Reflect::get(&biom, &JsValue::from_str("isInited"))?;
+ value
+ .as_bool()
+ .ok_or_else(|| JsValue::from_str("isInited not a bool"))
+}
+
+/// Returns `Telegram.WebApp.BiometricManager.isBiometricAvailable`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if the property is unavailable or not a boolean.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::is_biometric_available;
+///
+/// let _ = is_biometric_available();
+/// ```
+pub fn is_biometric_available() -> Result {
+ let biom = biometric_object()?;
+ let value = Reflect::get(&biom, &JsValue::from_str("isBiometricAvailable"))?;
+ value
+ .as_bool()
+ .ok_or_else(|| JsValue::from_str("isBiometricAvailable not a bool"))
+}
+
+/// Returns `Telegram.WebApp.BiometricManager.isAccessRequested`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if the property is unavailable or not a boolean.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::is_access_requested;
+///
+/// let _ = is_access_requested();
+/// ```
+pub fn is_access_requested() -> Result {
+ let biom = biometric_object()?;
+ let value = Reflect::get(&biom, &JsValue::from_str("isAccessRequested"))?;
+ value
+ .as_bool()
+ .ok_or_else(|| JsValue::from_str("isAccessRequested not a bool"))
+}
+
+/// Returns `Telegram.WebApp.BiometricManager.isAccessGranted`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if the property is unavailable or not a boolean.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::is_access_granted;
+///
+/// let _ = is_access_granted();
+/// ```
+pub fn is_access_granted() -> Result {
+ let biom = biometric_object()?;
+ let value = Reflect::get(&biom, &JsValue::from_str("isAccessGranted"))?;
+ value
+ .as_bool()
+ .ok_or_else(|| JsValue::from_str("isAccessGranted not a bool"))
+}
+
+/// Returns `Telegram.WebApp.BiometricManager.isBiometricTokenSaved`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if the property is unavailable or not a boolean.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::is_biometric_token_saved;
+///
+/// let _ = is_biometric_token_saved();
+/// ```
+pub fn is_biometric_token_saved() -> Result {
+ let biom = biometric_object()?;
+ let value = Reflect::get(&biom, &JsValue::from_str("isBiometricTokenSaved"))?;
+ value
+ .as_bool()
+ .ok_or_else(|| JsValue::from_str("isBiometricTokenSaved not a bool"))
+}
+
+/// Returns `Telegram.WebApp.BiometricManager.deviceId`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if the property is unavailable or not a string.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::biometric::device_id;
+///
+/// let _ = device_id();
+/// ```
+pub fn device_id() -> Result {
+ let biom = biometric_object()?;
+ let value = Reflect::get(&biom, &JsValue::from_str("deviceId"))?;
+ value
+ .as_string()
+ .ok_or_else(|| JsValue::from_str("deviceId not a string"))
+}
+
+fn biometric_object() -> Result {
+ let win = window().ok_or_else(|| JsValue::from_str("no window"))?;
+ let tg = Reflect::get(&win, &JsValue::from_str("Telegram"))?;
+ let webapp = Reflect::get(&tg, &JsValue::from_str("WebApp"))?;
+ Reflect::get(&webapp, &JsValue::from_str("BiometricManager"))
+}
+
+#[cfg(test)]
+mod tests {
+ use js_sys::{Function, Object, Reflect};
+ use wasm_bindgen::JsValue;
+ use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
+ use web_sys::window;
+
+ use super::*;
+
+ wasm_bindgen_test_configure!(run_in_browser);
+
+ #[allow(dead_code)]
+ fn setup_biometric() -> Object {
+ let win = window().expect("window should be available");
+ let telegram = Object::new();
+ let webapp = Object::new();
+ let biom = Object::new();
+ let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
+ let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
+ let _ = Reflect::set(&webapp, &"BiometricManager".into(), &biom);
+ biom
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn init_ok() {
+ let biom = setup_biometric();
+ let func = Function::new_no_args("this.called = true;");
+ let _ = Reflect::set(&biom, &"init".into(), &func);
+ assert!(init().is_ok());
+ assert!(
+ Reflect::get(&biom, &"called".into())
+ .unwrap()
+ .as_bool()
+ .unwrap()
+ );
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn init_err() {
+ let _ = setup_biometric();
+ assert!(init().is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn request_access_ok() {
+ let biom = setup_biometric();
+ let func = Function::new_with_args("key", "this.called = true; this.key = key;");
+ let _ = Reflect::set(&biom, &"requestAccess".into(), &func);
+ assert!(request_access("abc", None, None).is_ok());
+ assert!(
+ Reflect::get(&biom, &"called".into())
+ .unwrap()
+ .as_bool()
+ .unwrap()
+ );
+ assert_eq!(
+ Reflect::get(&biom, &"key".into())
+ .unwrap()
+ .as_string()
+ .unwrap(),
+ "abc"
+ );
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn request_access_err() {
+ let _ = setup_biometric();
+ assert!(request_access("abc", None, None).is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn authenticate_ok() {
+ let biom = setup_biometric();
+ let func = Function::new_with_args("key", "this.called = true; this.key = key;");
+ let _ = Reflect::set(&biom, &"authenticate".into(), &func);
+ assert!(authenticate("abc", None, None).is_ok());
+ assert!(
+ Reflect::get(&biom, &"called".into())
+ .unwrap()
+ .as_bool()
+ .unwrap()
+ );
+ assert_eq!(
+ Reflect::get(&biom, &"key".into())
+ .unwrap()
+ .as_string()
+ .unwrap(),
+ "abc"
+ );
+ assert_eq!(
+ Reflect::get(&biom, &"reason".into())
+ .unwrap()
+ .as_string()
+ .unwrap(),
+ "why"
+ );
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn authenticate_err() {
+ let _ = setup_biometric();
+ assert!(authenticate("abc", None, None).is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn update_biometric_token_ok() {
+ let biom = setup_biometric();
+ let func = Function::new_with_args("token", "this.token = token;");
+ let _ = Reflect::set(&biom, &"updateBiometricToken".into(), &func);
+ assert!(update_biometric_token("abc").is_ok());
+ assert_eq!(
+ Reflect::get(&biom, &"token".into())
+ .unwrap()
+ .as_string()
+ .unwrap(),
+ "abc"
+ );
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn update_biometric_token_err() {
+ let _ = setup_biometric();
+ assert!(update_biometric_token("abc").is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn open_settings_ok() {
+ let biom = setup_biometric();
+ let func = Function::new_no_args("this.called = true;");
+ let _ = Reflect::set(&biom, &"openSettings".into(), &func);
+ assert!(open_settings().is_ok());
+ assert!(
+ Reflect::get(&biom, &"called".into())
+ .unwrap()
+ .as_bool()
+ .unwrap()
+ );
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn open_settings_err() {
+ let _ = setup_biometric();
+ assert!(open_settings().is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_inited_ok() {
+ let biom = setup_biometric();
+ let _ = Reflect::set(&biom, &"isInited".into(), &JsValue::from(true));
+ assert!(is_inited().expect("is_inited"));
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_inited_err() {
+ let _ = setup_biometric();
+ assert!(is_inited().is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_biometric_available_ok() {
+ let biom = setup_biometric();
+ let _ = Reflect::set(&biom, &"isBiometricAvailable".into(), &JsValue::from(true));
+ assert!(is_biometric_available().expect("is_biometric_available"));
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_biometric_available_err() {
+ let _ = setup_biometric();
+ assert!(is_biometric_available().is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_access_requested_ok() {
+ let biom = setup_biometric();
+ let _ = Reflect::set(&biom, &"isAccessRequested".into(), &JsValue::from(true));
+ assert!(is_access_requested().expect("is_access_requested"));
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_access_requested_err() {
+ let _ = setup_biometric();
+ assert!(is_access_requested().is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_access_granted_ok() {
+ let biom = setup_biometric();
+ let _ = Reflect::set(&biom, &"isAccessGranted".into(), &JsValue::from(true));
+ assert!(is_access_granted().expect("is_access_granted"));
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_access_granted_err() {
+ let _ = setup_biometric();
+ assert!(is_access_granted().is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_biometric_token_saved_ok() {
+ let biom = setup_biometric();
+ let _ = Reflect::set(&biom, &"isBiometricTokenSaved".into(), &JsValue::from(true));
+ assert!(is_biometric_token_saved().expect("is_biometric_token_saved"));
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn is_biometric_token_saved_err() {
+ let _ = setup_biometric();
+ assert!(is_biometric_token_saved().is_err());
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn device_id_ok() {
+ let biom = setup_biometric();
+ let _ = Reflect::set(&biom, &"deviceId".into(), &JsValue::from_str("id123"));
+ assert_eq!(device_id().expect("device_id"), "id123");
+ }
+
+ #[wasm_bindgen_test]
+ #[allow(dead_code, clippy::unused_unit)]
+ fn device_id_err() {
+ let _ = setup_biometric();
+ assert!(device_id().is_err());
+ }
+}
diff --git a/telegram-webapp-sdk/src/api/cloud_storage.rs b/telegram-webapp-sdk/src/api/cloud_storage.rs
new file mode 100644
index 0000000..7e2a77d
--- /dev/null
+++ b/telegram-webapp-sdk/src/api/cloud_storage.rs
@@ -0,0 +1,400 @@
+use js_sys::{Array, Function, Promise, Reflect};
+use wasm_bindgen::{JsCast, prelude::*};
+use web_sys::window;
+
+/// Returns the `Telegram.WebApp.CloudStorage` object.
+fn cloud_storage_object() -> Result {
+ let win = window().ok_or_else(|| JsValue::from_str("no window"))?;
+ let tg = Reflect::get(&win, &JsValue::from_str("Telegram"))?;
+ let webapp = Reflect::get(&tg, &JsValue::from_str("WebApp"))?;
+ Reflect::get(&webapp, &JsValue::from_str("CloudStorage"))
+}
+
+/// Calls `Telegram.WebApp.CloudStorage.getItem()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if CloudStorage or the method is unavailable, or if
+/// the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::cloud_storage::get_item;
+/// use wasm_bindgen_futures::JsFuture;
+/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
+/// let value = JsFuture::from(get_item("key")?).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub fn get_item(key: &str) -> Result {
+ let storage = cloud_storage_object()?;
+ let func = Reflect::get(&storage, &JsValue::from_str("getItem"))?.dyn_into::()?;
+ func.call1(&storage, &JsValue::from_str(key))?
+ .dyn_into::()
+}
+
+/// Calls `Telegram.WebApp.CloudStorage.setItem()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if CloudStorage or the method is unavailable, or if
+/// the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::cloud_storage::set_item;
+/// use wasm_bindgen_futures::JsFuture;
+/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
+/// JsFuture::from(set_item("key", "value")?).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub fn set_item(key: &str, value: &str) -> Result {
+ let storage = cloud_storage_object()?;
+ let func = Reflect::get(&storage, &JsValue::from_str("setItem"))?.dyn_into::()?;
+ func.call2(&storage, &JsValue::from_str(key), &JsValue::from_str(value))?
+ .dyn_into::()
+}
+
+/// Calls `Telegram.WebApp.CloudStorage.removeItem()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if CloudStorage or the method is unavailable, or if
+/// the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::cloud_storage::remove_item;
+/// use wasm_bindgen_futures::JsFuture;
+/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
+/// JsFuture::from(remove_item("key")?).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub fn remove_item(key: &str) -> Result {
+ let storage = cloud_storage_object()?;
+ let func = Reflect::get(&storage, &JsValue::from_str("removeItem"))?.dyn_into::()?;
+ func.call1(&storage, &JsValue::from_str(key))?
+ .dyn_into::()
+}
+
+/// Calls `Telegram.WebApp.CloudStorage.getItems()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if CloudStorage or the method is unavailable, or if
+/// the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::cloud_storage::get_items;
+/// use wasm_bindgen_futures::JsFuture;
+/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
+/// let _ = JsFuture::from(get_items(&["a", "b"])?).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub fn get_items(keys: &[&str]) -> Result {
+ let storage = cloud_storage_object()?;
+ let func = Reflect::get(&storage, &JsValue::from_str("getItems"))?.dyn_into::()?;
+ let array = Array::new();
+ for key in keys {
+ array.push(&JsValue::from_str(key));
+ }
+ func.call1(&storage, &array.into())?.dyn_into::()
+}
+
+/// Calls `Telegram.WebApp.CloudStorage.setItems()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if CloudStorage or the method is unavailable, or if
+/// the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::cloud_storage::set_items;
+/// use wasm_bindgen_futures::JsFuture;
+/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
+/// JsFuture::from(set_items(&[("a", "1"), ("b", "2")])?).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub fn set_items(items: &[(&str, &str)]) -> Result {
+ let storage = cloud_storage_object()?;
+ let func = Reflect::get(&storage, &JsValue::from_str("setItems"))?.dyn_into::()?;
+ let obj = js_sys::Object::new();
+ for (key, value) in items {
+ Reflect::set(&obj, &JsValue::from_str(key), &JsValue::from_str(value))?;
+ }
+ func.call1(&storage, &obj.into())?.dyn_into::()
+}
+
+/// Calls `Telegram.WebApp.CloudStorage.removeItems()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if CloudStorage or the method is unavailable, or if
+/// the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::cloud_storage::remove_items;
+/// use wasm_bindgen_futures::JsFuture;
+/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
+/// JsFuture::from(remove_items(&["a", "b"])?).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub fn remove_items(keys: &[&str]) -> Result {
+ let storage = cloud_storage_object()?;
+ let func =
+ Reflect::get(&storage, &JsValue::from_str("removeItems"))?.dyn_into::()?;
+ let array = Array::new();
+ for key in keys {
+ array.push(&JsValue::from_str(key));
+ }
+ func.call1(&storage, &array.into())?.dyn_into::()
+}
+
+/// Calls `Telegram.WebApp.CloudStorage.getKeys()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if CloudStorage or the method is unavailable, or if
+/// the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::cloud_storage::get_keys;
+/// use wasm_bindgen_futures::JsFuture;
+/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
+/// let _ = JsFuture::from(get_keys()?).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub fn get_keys() -> Result {
+ let storage = cloud_storage_object()?;
+ let func = Reflect::get(&storage, &JsValue::from_str("getKeys"))?.dyn_into::()?;
+ func.call0(&storage)?.dyn_into::()
+}
+
+/// Calls `Telegram.WebApp.CloudStorage.clear()`.
+///
+/// # Errors
+/// Returns `Err(JsValue)` if CloudStorage or the method is unavailable, or if
+/// the call fails.
+///
+/// # Examples
+/// ```no_run
+/// use telegram_webapp_sdk::api::cloud_storage::clear;
+/// use wasm_bindgen_futures::JsFuture;
+/// # async fn run() -> Result<(), wasm_bindgen::JsValue> {
+/// JsFuture::from(clear()?).await?;
+/// # Ok(())
+/// # }
+/// ```
+pub fn clear() -> Result {
+ let storage = cloud_storage_object()?;
+ let func = Reflect::get(&storage, &JsValue::from_str("clear"))?.dyn_into::()?;
+ func.call0(&storage)?.dyn_into::()
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(dead_code)]
+ use js_sys::{Array, Function, Object, Reflect};
+ use wasm_bindgen_futures::JsFuture;
+ use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
+ use web_sys::window;
+
+ use super::*;
+
+ wasm_bindgen_test_configure!(run_in_browser);
+
+ fn setup_cloud_storage() -> Object {
+ let win = window().unwrap();
+ let telegram = Object::new();
+ let webapp = Object::new();
+ let storage = Object::new();
+ let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
+ let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
+ let _ = Reflect::set(&webapp, &"CloudStorage".into(), &storage);
+ storage
+ }
+
+ #[wasm_bindgen_test(async)]
+ async fn get_item_ok() {
+ let storage = setup_cloud_storage();
+ let func =
+ Function::new_with_args("key", "this.called = key; return Promise.resolve('val');");
+ let _ = Reflect::set(&storage, &"getItem".into(), &func);
+ let value = JsFuture::from(get_item("test").unwrap()).await.unwrap();
+ assert_eq!(value.as_string(), Some("val".to_string()));
+ assert_eq!(
+ Reflect::get(&storage, &"called".into())
+ .unwrap()
+ .as_string(),
+ Some("test".into())
+ );
+ }
+
+ #[wasm_bindgen_test]
+ fn get_item_err() {
+ let _ = setup_cloud_storage();
+ assert!(get_item("test").is_err());
+ }
+
+ #[wasm_bindgen_test(async)]
+ async fn set_item_ok() {
+ let storage = setup_cloud_storage();
+ let func = Function::new_with_args(
+ "key, value",
+ "this.called = key + ':' + value; return Promise.resolve();"
+ );
+ let _ = Reflect::set(&storage, &"setItem".into(), &func);
+ JsFuture::from(set_item("a", "b").unwrap()).await.unwrap();
+ assert_eq!(
+ Reflect::get(&storage, &"called".into())
+ .unwrap()
+ .as_string(),
+ Some("a:b".into())
+ );
+ }
+
+ #[wasm_bindgen_test]
+ fn set_item_err() {
+ let _ = setup_cloud_storage();
+ assert!(set_item("a", "b").is_err());
+ }
+
+ #[wasm_bindgen_test(async)]
+ async fn remove_item_ok() {
+ let storage = setup_cloud_storage();
+ let func = Function::new_with_args("key", "this.called = key; return Promise.resolve();");
+ let _ = Reflect::set(&storage, &"removeItem".into(), &func);
+ JsFuture::from(remove_item("k").unwrap()).await.unwrap();
+ assert_eq!(
+ Reflect::get(&storage, &"called".into())
+ .unwrap()
+ .as_string(),
+ Some("k".into())
+ );
+ }
+
+ #[wasm_bindgen_test]
+ fn remove_item_err() {
+ let _ = setup_cloud_storage();
+ assert!(remove_item("k").is_err());
+ }
+
+ #[wasm_bindgen_test(async)]
+ async fn get_items_ok() {
+ let storage = setup_cloud_storage();
+ let func = Function::new_with_args(
+ "keys",
+ "this.called = keys; return Promise.resolve({a: '1', b: '2'});"
+ );
+ let _ = Reflect::set(&storage, &"getItems".into(), &func);
+ let result = JsFuture::from(get_items(&["a", "b"]).unwrap())
+ .await
+ .unwrap();
+ let obj = result.dyn_into::