commit 97acb226f9ecf55b8fa17854fc746660817a2cef
parent 856a14feb6b5a8d0826c83c1059e065d09a4be87
Author: triesap <tyson@radroots.org>
Date: Fri, 17 Apr 2026 16:14:54 +0000
app: initialize gpui app shell
- archive the previous egui implementation on legacy/egui
- replace the app repo with the new radroots_app gpui bootstrap
- add the minimal native shell with transparent titlebar and centered placeholder view
- align startup boundaries with the canonical app bootstrap and run flow
Diffstat:
136 files changed, 4548 insertions(+), 25035 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -5,22 +5,18 @@
- This file applies to the full repository.
- Keep this file concise, durable, and repository-specific.
- If a closer directory-level `AGENTS.md` is added later, it overrides this file for that subtree.
-- Put detailed procedures, examples, and temporary migration notes in other checked-in docs, not here.
## 2. Repository identity
- This repository is the standalone Rad Roots application repository.
-- Optimize for durable application structure, explicit boundaries, portability, and clear runtime behavior.
-- Treat this as a public open-source application project: commits, docs, and handoff language should read clearly to contributors who only know this repository.
-- Preserve the repository’s top-level identity files and keep the workspace easy to understand from the root.
+- Treat it as a public open-source repository with a direct GPUI-native application focus.
+- Preserve the repository's top-level identity files and keep the root easy to understand without mount-path context.
## 3. Change discipline
- Prefer the smallest coherent change that fully addresses the request.
-- Do not mix unrelated cleanup, speculative refactors, or roadmap work into the same change.
-- Prefer clean target-state changes over temporary compatibility layers unless compatibility is explicitly required.
-- Remove obsolete code, dependencies, and scaffolding when they are clearly replaced.
-- Do not leave hidden task trackers in source comments, markdown checklists, or stray notes.
+- Do not mix unrelated cleanup or speculative refactors into the same change.
+- Remove obsolete code and scaffolding when they are clearly replaced.
## 4. Before editing
@@ -28,62 +24,44 @@ Before making substantial changes:
- Read this file, `README.md`, and `CONTRIBUTING.md`.
- Inspect `git status --short` before broad edits, refactors, or file removals.
-- Read the current implementation and nearby tests before changing behavior.
-- Use checked-in documentation and commands as the source of truth.
-- Do not assume contributor-specific local tooling or machine setup beyond what the repository documents.
-- Surface blockers early when the task depends on unresolved product decisions, missing prerequisites, or unclear architecture boundaries.
+- Read the current implementation before changing behavior.
+- Use checked-in commands and docs as the source of truth.
## 5. Validation and command surface
- Run validation from the repository root.
-- Prefer the narrowest relevant validation for the files or crate being changed.
-- Use documented commands first. When no narrower repo-specific command is documented, use standard Cargo commands such as:
+- Prefer the narrowest relevant validation first.
+- Use documented commands before inventing new ones.
+- Current canonical commands are:
- `cargo metadata --format-version 1 --no-deps`
- - `cargo check`
- - targeted `cargo test`
- - targeted `cargo run -p radroots_app_desktop`
-- If validation cannot be run, report the blocker clearly.
-
-## 6. Workspace structure
-
-- Keep the repository root as the workspace root.
-- Keep reusable Rust application logic under `crates/shared/`.
-- Keep Rust host-integration adapters under `crates/bridges/`.
-- Keep runnable Rust targets under `crates/launchers/`.
-- Keep reusable platform-native bridge libraries under `native/bridges/`.
-- Keep native host projects under `platforms/`.
-- Add new crates only when they represent a durable architectural boundary.
-- Keep manifests, paths, and crate boundaries simple and intentional.
-- Do not reintroduce obsolete framework scaffolding unless the requested change explicitly requires it.
+ - `cargo check -p radroots_app`
+ - `cargo test`
+ - `cargo run -p radroots_app`
+ - `./scripts/check.sh`
+ - `./scripts/run.sh`
-## 7. Rust engineering rules
+## 6. Repository structure
-- Use Rust `1.92.0`, edition `2024`, and workspace dependency versions from the root `Cargo.toml`.
-- Prefer safe, explicit APIs and avoid `unsafe`.
-- Keep state, data flow, and side effects understandable; prefer typed models and explicit transitions over stringly APIs or loosely typed maps.
-- Avoid hidden panics in non-test code.
-- Keep module layout and manifests clean; remove dead dependencies, dead modules, and unused feature wiring when they are no longer needed.
-- Keep code readable and direct; avoid unnecessary abstraction in early-stage application code.
-- Add or update deterministic tests when behavior, invariants, parsing, or state transitions change.
+- Keep the repository root as the package root.
+- Keep the structure minimal until a durable new boundary is required.
+- Do not reintroduce deprecated egui-era scaffolding.
-## 8. Dependency rules
+## 7. Rust engineering rules
-- Prefer root workspace dependencies where possible.
-- Use canonical upstream crate names in manifests and code.
-- Prefer dependency choices that align with the existing Rad Roots Rust ecosystem when practical.
-- Introduce new dependencies only when they are justified by a clear product or architectural need.
+- Use Rust `1.92.0`, edition `2024`, and safe Rust only.
+- Keep state, data flow, and side effects explicit.
+- Avoid hidden panics in non-test code.
+- Keep code readable and direct.
-## 9. Commit and handoff rules
+## 8. Commit and handoff rules
- Format commits as `<scope>: <imperative summary>`.
- Use lowercase scopes.
-- Split unrelated changes into separate commits.
-- Keep commit messages and handoff summaries clear and standalone.
+- Keep handoff summaries clear and standalone.
- In handoff, state what changed, what validation ran, and any remaining risks or assumptions.
-## 10. Definition of done
+## 9. Definition of done
- The requested change is implemented.
-- Replaced or obsolete scaffolding is removed when no longer needed.
+- Obsolete scaffolding is removed when clearly replaced.
- Relevant validation ran, or a concrete blocker is reported.
-- Any affected documentation or structural context is updated with the code change when necessary.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
@@ -1,131 +1,40 @@
# Contributing
-Rad Roots is an open-source application. Contributions are welcome, including bug fixes, usability improvements, documentation updates, tests, and new features.
+Rad Roots is an open-source application repository.
+Contributions are welcome, including bug fixes, UI improvements, tests, and documentation updates.
## Scope
-This repository is the standalone Rad Roots application repository. Reusable Rust application logic is organized under `crates/shared/`, Rust host bridges under `crates/bridges/`, runnable Rust targets under `crates/launchers/`, native bridge implementations under `native/bridges/`, and native host projects under `platforms/`.
+This repository currently ships a single-package GPUI bootstrap application named `radroots_app`.
+Keep the filetree small and direct until a larger application boundary is justified.
## Prerequisites
-Install the Rust toolchain used by this repository:
+Install the pinned Rust toolchain:
```bash
rustup toolchain install 1.92.0
-rustup target add wasm32-unknown-unknown
-```
-
-Install Trunk for the wasm target:
-
-```bash
-cargo install trunk
+rustup override set 1.92.0
```
-On hosts that will build or run the Android shell, ensure Java 17 or newer is available. The Android scripts bootstrap the local Gradle, SDK, NDK, `cargo-ndk`, and emulator resources into `platforms/android/.tooling` on demand.
-
-On macOS, ensure the Apple Swift toolchain is available. The desktop target links the shared Apple native security package during build.
-
Confirm your environment:
```bash
cargo --version
rustc --version
-trunk --version
-java --version
-```
-
-On macOS, also confirm:
-
-```bash
-swift --version
-```
-
-## Getting Started
-
-Clone your fork and enter the repository root:
-
-```bash
-git clone https://github.com/<YOUR-USERNAME>/app.git
-cd app
-```
-
-To use the repository-pinned toolchain:
-
-```bash
-rustup override set 1.92.0
```
## Development Commands
Run these commands from the repository root.
-Inspect workspace metadata:
-
```bash
cargo metadata --format-version 1 --no-deps
-```
-
-Check the application:
-
-```bash
-cargo check
-```
-
-Run tests:
-
-```bash
+cargo check -p radroots_app
cargo test
-```
-
-Run the native application:
-
-```bash
-cargo run -p radroots_app_desktop
-```
-
-Check the Android target:
-
-```bash
-./scripts/check-android-target.sh
-```
-
-Build the Android host:
-
-```bash
-./scripts/build-android-host.sh
-```
-
-Run the Android app in the emulator:
-
-```bash
-./scripts/run-android-emulator.sh
-```
-
-Check the wasm application:
-
-```bash
-./scripts/with-wasm-toolchain.sh env -u NO_COLOR cargo check -p radroots_app_web --target wasm32-unknown-unknown
-```
-
-Build the wasm application:
-
-```bash
-cd crates/launchers/web
-../../../scripts/with-wasm-toolchain.sh env -u NO_COLOR trunk build
-```
-
-Run the wasm application:
-
-```bash
-cd crates/launchers/web
-../../../scripts/with-wasm-toolchain.sh env -u NO_COLOR trunk serve --open
-```
-
-Test the Apple native security package:
-
-```bash
-cd native/bridges/apple/security/swift/RadRootsAppleSecurity
-swift test
+cargo run -p radroots_app
+./scripts/check.sh
+./scripts/run.sh
```
## Contribution Guidelines
@@ -133,12 +42,9 @@ swift test
- Keep changes scoped to a single coherent change.
- Prefer small, reviewable commits.
- Update tests when behavior changes.
-- Update documentation when commands, structure, or contributor workflow changes.
-- Use repo-relative paths in docs, comments, and contributor-facing text.
-- Keep documentation path references relative to this repository root.
-- Do not use absolute filesystem paths or home-directory path forms in repository docs.
+- Update documentation when commands or structure change.
+- Use repository-relative paths in contributor-facing text.
- Remove obsolete code and dependencies when they are clearly replaced.
-- Use workspace-managed dependency versions from the root `Cargo.toml`.
## Reporting Issues
@@ -149,7 +55,6 @@ When reporting a bug, include:
- the command you ran
- the observed behavior
- the expected behavior
-- logs, screenshots, or backtraces if available
## Submitting Changes
@@ -157,11 +62,3 @@ When reporting a bug, include:
2. Make the smallest coherent update that solves the issue.
3. Run the relevant validation commands from this document.
4. Open a pull request with a clear summary of what changed and how it was verified.
-
-## Code of Conduct
-
-Be respectful, direct, and constructive in issues and reviews.
-
-## License
-
-By contributing to this repository, you agree that your contributions will be distributed under the repository's license. See [LICENSE](LICENSE).
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,48 +3,12 @@
version = 4
[[package]]
-name = "ab_glyph"
-version = "0.2.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
-dependencies = [
- "ab_glyph_rasterizer",
- "owned_ttf_parser",
-]
-
-[[package]]
-name = "ab_glyph_rasterizer"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
-
-[[package]]
-name = "accesskit"
-version = "0.21.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99"
-dependencies = [
- "enumn",
- "serde",
-]
-
-[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
-name = "aead"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
-dependencies = [
- "crypto-common",
- "generic-array",
-]
-
-[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -53,6 +17,7 @@ dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
+ "zeroize",
]
[[package]]
@@ -62,9 +27,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
- "getrandom 0.3.4",
+ "const-random",
"once_cell",
- "serde",
"version_check",
"zerocopy",
]
@@ -79,62 +43,21 @@ dependencies = [
]
[[package]]
-name = "allocator-api2"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
-
-[[package]]
-name = "android-activity"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
-dependencies = [
- "android-properties",
- "bitflags 2.11.0",
- "cc",
- "cesu8",
- "jni 0.21.1",
- "jni-sys 0.3.0",
- "libc",
- "log",
- "ndk",
- "ndk-context",
- "ndk-sys",
- "num_enum",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "android-properties"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
-
-[[package]]
-name = "android_log-sys"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
-
-[[package]]
-name = "android_logger"
-version = "0.15.1"
+name = "aligned"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
+checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
dependencies = [
- "android_log-sys",
- "env_filter",
- "log",
+ "as-slice",
]
[[package]]
-name = "android_system_properties"
-version = "0.1.5"
+name = "aligned-vec"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
dependencies = [
- "libc",
+ "equator",
]
[[package]]
@@ -144,30 +67,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
-name = "arboard"
-version = "3.6.1"
+name = "ar_archive_writer"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
+checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
dependencies = [
- "clipboard-win",
- "image",
- "log",
- "objc2 0.6.4",
- "objc2-app-kit 0.3.2",
- "objc2-core-foundation",
- "objc2-core-graphics",
- "objc2-foundation 0.3.2",
- "parking_lot",
- "percent-encoding",
- "windows-sys 0.60.2",
- "x11rb",
+ "object",
]
[[package]]
-name = "arraydeque"
-version = "0.5.1"
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+
+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
[[package]]
name = "arrayref"
@@ -188,6 +111,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]]
+name = "as-slice"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
name = "ash"
version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -197,3845 +129,5042 @@ dependencies = [
]
[[package]]
-name = "async-trait"
-version = "0.1.89"
+name = "ash-window"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "ash",
+ "raw-window-handle",
+ "raw-window-metal",
]
[[package]]
-name = "async-utility"
-version = "0.3.1"
+name = "ashpd"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
+checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
dependencies = [
+ "async-fs",
+ "async-net",
+ "enumflags2",
+ "futures-channel",
"futures-util",
- "gloo-timers",
- "tokio",
- "wasm-bindgen-futures",
+ "rand 0.9.4",
+ "serde",
+ "serde_repr",
+ "url",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols 0.32.12",
+ "zbus",
]
[[package]]
-name = "async-wsocket"
-version = "0.13.2"
+name = "ashpd"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069"
+checksum = "33a3c86f3fd70c0ffa500ed189abfa90b5a52398a45d5dc372fcc38ebeb7a645"
dependencies = [
- "async-utility",
- "futures",
+ "async-fs",
+ "async-net",
+ "enumflags2",
+ "futures-channel",
"futures-util",
- "js-sys",
- "tokio",
- "tokio-rustls",
- "tokio-socks",
- "tokio-tungstenite",
+ "rand 0.9.4",
+ "serde",
+ "serde_repr",
"url",
- "wasm-bindgen",
- "web-sys",
+ "zbus",
]
[[package]]
-name = "atomic-destructor"
-version = "0.3.0"
+name = "async-broadcast"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener 5.4.1",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
[[package]]
-name = "atomic-waker"
-version = "1.1.2"
+name = "async-channel"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
[[package]]
-name = "autocfg"
-version = "1.5.0"
+name = "async-channel"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
[[package]]
-name = "base64"
-version = "0.21.7"
+name = "async-compression"
+version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
+dependencies = [
+ "compression-codecs",
+ "compression-core",
+ "futures-io",
+ "pin-project-lite",
+]
[[package]]
-name = "base64"
-version = "0.22.1"
+name = "async-executor"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand 2.4.1",
+ "futures-lite 2.6.1",
+ "pin-project-lite",
+ "slab",
+]
[[package]]
-name = "base64ct"
-version = "1.8.3"
+name = "async-fs"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
+dependencies = [
+ "async-lock",
+ "blocking",
+ "futures-lite 2.6.1",
+]
[[package]]
-name = "bech32"
-version = "0.11.1"
+name = "async-global-executor"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite 2.6.1",
+ "once_cell",
+]
[[package]]
-name = "bip39"
-version = "2.2.2"
+name = "async-io"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
- "bitcoin_hashes",
- "serde",
- "unicode-normalization",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite 2.6.1",
+ "parking",
+ "polling",
+ "rustix 1.1.4",
+ "slab",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "bit-set"
-version = "0.8.0"
+name = "async-lock"
+version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
- "bit-vec",
+ "event-listener 5.4.1",
+ "event-listener-strategy",
+ "pin-project-lite",
]
[[package]]
-name = "bit-vec"
-version = "0.8.0"
+name = "async-net"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
+dependencies = [
+ "async-io",
+ "blocking",
+ "futures-lite 2.6.1",
+]
[[package]]
-name = "bitcoin-io"
-version = "0.1.4"
+name = "async-process"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener 5.4.1",
+ "futures-lite 2.6.1",
+ "rustix 1.1.4",
+]
[[package]]
-name = "bitcoin_hashes"
-version = "0.14.1"
+name = "async-recursion"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
- "bitcoin-io",
- "hex-conservative",
- "serde",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "bitflags"
-version = "1.3.2"
+name = "async-signal"
+version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.4",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
[[package]]
-name = "bitflags"
-version = "2.11.0"
+name = "async-std"
+version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
dependencies = [
- "serde_core",
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite 2.6.1",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
]
[[package]]
-name = "block"
-version = "0.1.6"
+name = "async-task"
+version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
-name = "block-buffer"
-version = "0.10.4"
+name = "async-trait"
+version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
- "generic-array",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "block-padding"
-version = "0.3.3"
+name = "async_zip"
+version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [
- "generic-array",
+ "async-compression",
+ "crc32fast",
+ "futures-lite 2.6.1",
+ "pin-project",
+ "thiserror 1.0.69",
]
[[package]]
-name = "block2"
-version = "0.5.1"
+name = "atomic"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
-dependencies = [
- "objc2 0.5.2",
-]
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
-name = "bumpalo"
-version = "3.20.2"
+name = "atomic-waker"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
-name = "bytemuck"
-version = "1.25.0"
+name = "autocfg"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
-dependencies = [
- "bytemuck_derive",
-]
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
-name = "bytemuck_derive"
-version = "1.10.2"
+name = "av-scenechange"
+version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
+checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "aligned",
+ "anyhow",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "log",
+ "num-rational",
+ "num-traits",
+ "pastey",
+ "rayon",
+ "thiserror 2.0.18",
+ "v_frame",
+ "y4m",
]
[[package]]
-name = "byteorder"
-version = "1.5.0"
+name = "av1-grain"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
-
-[[package]]
-name = "byteorder-lite"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
-
-[[package]]
-name = "bytes"
-version = "1.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
-
-[[package]]
-name = "calloop"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
+checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
dependencies = [
- "bitflags 2.11.0",
+ "anyhow",
+ "arrayvec",
"log",
- "polling",
- "rustix 0.38.44",
- "slab",
- "thiserror 1.0.69",
+ "nom 8.0.0",
+ "num-rational",
+ "v_frame",
]
[[package]]
-name = "calloop"
-version = "0.14.4"
+name = "avif-serialize"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
+checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
dependencies = [
- "bitflags 2.11.0",
- "polling",
- "rustix 1.1.4",
- "slab",
- "tracing",
+ "arrayvec",
]
[[package]]
-name = "calloop-wayland-source"
-version = "0.3.0"
+name = "base64"
+version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
-dependencies = [
- "calloop 0.13.0",
- "rustix 0.38.44",
- "wayland-backend",
- "wayland-client",
-]
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
-name = "calloop-wayland-source"
-version = "0.4.1"
+name = "bindgen"
+version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
- "calloop 0.14.4",
- "rustix 1.1.4",
- "wayland-backend",
- "wayland-client",
+ "bitflags 2.11.1",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 2.1.2",
+ "shlex",
+ "syn 2.0.117",
]
[[package]]
-name = "cbc"
-version = "0.1.2"
+name = "bit-set"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
- "cipher",
+ "bit-vec",
]
[[package]]
-name = "cc"
-version = "1.2.57"
+name = "bit-vec"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
-dependencies = [
- "find-msvc-tools",
- "jobserver",
- "libc",
- "shlex",
-]
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
-name = "cesu8"
-version = "1.1.0"
+name = "bit_field"
+version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
-name = "cfg-if"
-version = "1.0.4"
+name = "bitflags"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
-name = "cfg_aliases"
-version = "0.2.1"
+name = "bitflags"
+version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
-name = "cgl"
-version = "0.3.2"
+name = "bitstream-io"
+version = "4.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
+checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f"
dependencies = [
- "libc",
+ "no_std_io2",
]
[[package]]
-name = "chacha20"
-version = "0.9.1"
+name = "blade-graphics"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+checksum = "e71cfb73b98eb9f58ee84048aa1bdf4e7497fd20c141b57523499fa066b48fed"
dependencies = [
- "cfg-if",
- "cipher",
- "cpufeatures",
+ "ash",
+ "ash-window",
+ "bitflags 2.11.1",
+ "bytemuck",
+ "codespan-reporting",
+ "glow",
+ "gpu-alloc",
+ "gpu-alloc-ash",
+ "hidden-trait",
+ "js-sys",
+ "khronos-egl",
+ "libloading",
+ "log",
+ "mint",
+ "naga",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-metal",
+ "objc2-quartz-core",
+ "objc2-ui-kit",
+ "once_cell",
+ "raw-window-handle",
+ "slab",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
-name = "chacha20poly1305"
-version = "0.10.1"
+name = "blade-macros"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca"
dependencies = [
- "aead",
- "chacha20",
- "cipher",
- "poly1305",
- "zeroize",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "chrono"
-version = "0.4.44"
+name = "blade-util"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+checksum = "3a6be3a82c001ba7a17b6f8e413ede5d1004e6047213f8efaf0ffc15b5c4904c"
dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "windows-link",
+ "blade-graphics",
+ "bytemuck",
+ "log",
+ "profiling",
]
[[package]]
-name = "cipher"
-version = "0.4.4"
+name = "block"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
-dependencies = [
- "crypto-common",
- "inout",
- "zeroize",
-]
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
-name = "clipboard-win"
-version = "5.4.1"
+name = "block-buffer"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
- "error-code",
+ "generic-array",
]
[[package]]
-name = "codespan-reporting"
-version = "0.12.0"
+name = "block-padding"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
- "serde",
- "termcolor",
- "unicode-width",
+ "generic-array",
]
[[package]]
-name = "combine"
-version = "4.6.7"
+name = "block2"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
- "bytes",
- "memchr",
+ "objc2",
]
[[package]]
-name = "concurrent-queue"
-version = "2.5.0"
+name = "blocking"
+version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
- "crossbeam-utils",
+ "async-channel 2.5.0",
+ "async-task",
+ "futures-io",
+ "futures-lite 2.6.1",
+ "piper",
]
[[package]]
-name = "config"
-version = "0.14.1"
+name = "bstr"
+version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
- "async-trait",
- "convert_case",
- "json5",
- "nom",
- "pathdiff",
- "ron",
- "rust-ini",
+ "memchr",
"serde",
- "serde_json",
- "toml",
- "yaml-rust2",
]
[[package]]
-name = "const-random"
-version = "0.1.18"
+name = "built"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
-dependencies = [
- "const-random-macro",
-]
+checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
[[package]]
-name = "const-random-macro"
-version = "0.1.16"
+name = "bumpalo"
+version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
-dependencies = [
- "getrandom 0.2.17",
- "once_cell",
- "tiny-keccak",
-]
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
-name = "convert_case"
-version = "0.6.0"
+name = "bytemuck"
+version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
dependencies = [
- "unicode-segmentation",
+ "bytemuck_derive",
]
[[package]]
-name = "core-foundation"
-version = "0.9.4"
+name = "bytemuck_derive"
+version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
- "core-foundation-sys",
- "libc",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "core-foundation"
-version = "0.10.1"
+name = "byteorder"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
+name = "byteorder-lite"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
-name = "core-graphics"
-version = "0.23.2"
+name = "bytes"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "calloop"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [
- "bitflags 1.3.2",
- "core-foundation 0.9.4",
- "core-graphics-types 0.1.3",
- "foreign-types 0.5.0",
- "libc",
+ "bitflags 2.11.1",
+ "log",
+ "polling",
+ "rustix 0.38.44",
+ "slab",
+ "thiserror 1.0.69",
]
[[package]]
-name = "core-graphics-types"
-version = "0.1.3"
+name = "calloop-wayland-source"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [
- "bitflags 1.3.2",
- "core-foundation 0.9.4",
- "libc",
+ "calloop",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-client",
]
[[package]]
-name = "core-graphics-types"
-version = "0.2.0"
+name = "cbc"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
- "bitflags 2.11.0",
- "core-foundation 0.10.1",
- "libc",
+ "cipher",
]
[[package]]
-name = "cpufeatures"
-version = "0.2.17"
+name = "cbindgen"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff"
dependencies = [
- "libc",
+ "heck 0.4.1",
+ "indexmap",
+ "log",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn 2.0.117",
+ "tempfile",
+ "toml 0.8.23",
]
[[package]]
-name = "crc32fast"
-version = "1.5.0"
+name = "cc"
+version = "1.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
- "cfg-if",
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
]
[[package]]
-name = "crossbeam-channel"
-version = "0.5.15"
+name = "cexpr"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
- "crossbeam-utils",
+ "nom 7.1.3",
]
[[package]]
-name = "crossbeam-utils"
-version = "0.8.21"
+name = "cfg-if"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
-name = "crunchy"
-version = "0.2.4"
+name = "cfg_aliases"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
-name = "crypto-common"
-version = "0.1.7"
+name = "cgl"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
dependencies = [
- "generic-array",
- "rand_core 0.6.4",
- "typenum",
+ "libc",
]
[[package]]
-name = "cursor-icon"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
-
-[[package]]
-name = "data-encoding"
-version = "2.10.0"
+name = "cipher"
+version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+ "zeroize",
+]
[[package]]
-name = "dbus"
-version = "0.9.10"
+name = "clang-sys"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
+ "glob",
"libc",
- "libdbus-sys",
- "windows-sys 0.59.0",
+ "libloading",
]
[[package]]
-name = "dbus-secret-service"
-version = "4.1.0"
+name = "cocoa"
+version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
+checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
- "dbus",
- "openssl",
- "zeroize",
+ "bitflags 1.3.2",
+ "block",
+ "cocoa-foundation 0.1.2",
+ "core-foundation 0.9.4",
+ "core-graphics 0.23.2",
+ "foreign-types",
+ "libc",
+ "objc",
]
[[package]]
-name = "deranged"
-version = "0.5.8"
+name = "cocoa"
+version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
dependencies = [
- "powerfmt",
+ "bitflags 2.11.1",
+ "block",
+ "cocoa-foundation 0.2.0",
+ "core-foundation 0.10.0",
+ "core-graphics 0.24.0",
+ "foreign-types",
+ "libc",
+ "objc",
]
[[package]]
-name = "digest"
-version = "0.10.7"
+name = "cocoa-foundation"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
- "block-buffer",
- "crypto-common",
- "subtle",
+ "bitflags 1.3.2",
+ "block",
+ "core-foundation 0.9.4",
+ "core-graphics-types 0.1.3",
+ "libc",
+ "objc",
]
[[package]]
-name = "dispatch"
+name = "cocoa-foundation"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
+dependencies = [
+ "bitflags 2.11.1",
+ "block",
+ "core-foundation 0.10.0",
+ "core-graphics-types 0.2.0",
+ "libc",
+ "objc",
+]
[[package]]
-name = "dispatch2"
-version = "0.3.1"
+name = "codespan-reporting"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
- "bitflags 2.11.0",
- "objc2 0.6.4",
+ "serde",
+ "termcolor",
+ "unicode-width",
]
[[package]]
-name = "displaydoc"
-version = "0.2.5"
+name = "color_quant"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "command-fds"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b60b5124979fccd9addd89d8b97a1d6eebb4950694520c75ddd722535ea443f"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "nix 0.31.2",
+ "thiserror 2.0.18",
]
[[package]]
-name = "dlib"
-version = "0.5.3"
+name = "compression-codecs"
+version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
+checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
dependencies = [
- "libloading",
+ "compression-core",
+ "deflate64",
+ "flate2",
+ "memchr",
]
[[package]]
-name = "dlv-list"
-version = "0.5.2"
+name = "compression-core"
+version = "0.4.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
- "const-random",
+ "crossbeam-utils",
]
[[package]]
-name = "document-features"
-version = "0.2.12"
+name = "const-random"
+version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
- "litrs",
+ "const-random-macro",
]
[[package]]
-name = "downcast-rs"
-version = "1.2.1"
+name = "const-random-macro"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "tiny-keccak",
+]
[[package]]
-name = "dpi"
-version = "0.1.2"
+name = "convert_case"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
-name = "ecolor"
-version = "0.33.3"
+name = "core-foundation"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
- "bytemuck",
- "emath",
- "serde",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "eframe"
-version = "0.33.3"
+name = "core-foundation"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6"
+checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
dependencies = [
- "ahash",
- "bytemuck",
- "document-features",
- "egui",
- "egui-wgpu",
- "egui-winit",
- "egui_glow",
- "glow",
- "glutin",
- "glutin-winit",
- "image",
- "js-sys",
- "log",
- "objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
- "parking_lot",
- "percent-encoding",
- "pollster",
- "profiling",
- "raw-window-handle",
- "static_assertions",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "web-time",
- "wgpu",
- "windows-sys 0.61.2",
- "winit",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "egui"
-version = "0.33.3"
+name = "core-foundation-sys"
+version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3"
-dependencies = [
- "accesskit",
- "ahash",
- "bitflags 2.11.0",
- "emath",
- "epaint",
- "log",
- "nohash-hasher",
- "profiling",
- "serde",
- "smallvec",
- "unicode-segmentation",
-]
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
-name = "egui-wgpu"
-version = "0.33.3"
+name = "core-graphics"
+version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
- "ahash",
- "bytemuck",
- "document-features",
- "egui",
- "epaint",
- "log",
- "profiling",
- "thiserror 2.0.18",
- "type-map",
- "web-time",
- "wgpu",
- "winit",
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "core-graphics-types 0.1.3",
+ "foreign-types",
+ "libc",
]
[[package]]
-name = "egui-winit"
-version = "0.33.3"
+name = "core-graphics"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29"
+checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
- "arboard",
- "bytemuck",
- "egui",
- "log",
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
- "objc2-ui-kit",
- "profiling",
- "raw-window-handle",
- "smithay-clipboard",
- "web-time",
- "webbrowser",
- "winit",
+ "bitflags 2.11.1",
+ "core-foundation 0.10.0",
+ "core-graphics-types 0.2.0",
+ "foreign-types",
+ "libc",
]
[[package]]
-name = "egui_glow"
-version = "0.33.3"
+name = "core-graphics-helmer-fork"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6420863ea1d90e750f75075231a260030ad8a9f30a7cef82cdc966492dc4c4eb"
+checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5"
dependencies = [
- "bytemuck",
- "egui",
- "glow",
- "log",
- "memoffset",
- "profiling",
- "wasm-bindgen",
- "web-sys",
- "winit",
+ "bitflags 2.11.1",
+ "core-foundation 0.9.4",
+ "core-graphics-types 0.1.3",
+ "foreign-types",
+ "libc",
]
[[package]]
-name = "either"
-version = "1.15.0"
+name = "core-graphics-types"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "libc",
+]
[[package]]
-name = "emath"
-version = "0.33.3"
+name = "core-graphics-types"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
- "bytemuck",
- "serde",
+ "bitflags 2.11.1",
+ "core-foundation 0.10.0",
+ "libc",
]
[[package]]
-name = "encoding_rs"
-version = "0.8.35"
+name = "core-graphics2"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d"
dependencies = [
+ "bitflags 2.11.1",
+ "block",
"cfg-if",
+ "core-foundation 0.10.0",
+ "libc",
]
[[package]]
-name = "enumn"
-version = "0.1.14"
+name = "core-text"
+version = "21.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
+checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "core-foundation 0.10.0",
+ "core-graphics 0.24.0",
+ "foreign-types",
+ "libc",
]
[[package]]
-name = "env_filter"
-version = "0.1.4"
+name = "core-video"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
+checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef"
dependencies = [
- "log",
- "regex",
+ "block",
+ "core-foundation 0.10.0",
+ "core-graphics2",
+ "io-surface",
+ "libc",
+ "metal",
]
[[package]]
-name = "epaint"
-version = "0.33.3"
+name = "core_maths"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62"
+checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
- "ab_glyph",
- "ahash",
- "bytemuck",
- "ecolor",
- "emath",
- "epaint_default_fonts",
- "log",
- "nohash-hasher",
- "parking_lot",
- "profiling",
- "serde",
+ "libm",
]
[[package]]
-name = "epaint_default_fonts"
-version = "0.33.3"
+name = "cosmic-text"
+version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862"
-
-[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8"
+dependencies = [
+ "bitflags 2.11.1",
+ "fontdb 0.16.2",
+ "log",
+ "rangemap",
+ "rustc-hash 1.1.0",
+ "rustybuzz 0.14.1",
+ "self_cell",
+ "smol_str",
+ "swash",
+ "sys-locale",
+ "ttf-parser 0.21.1",
+ "unicode-bidi",
+ "unicode-linebreak",
+ "unicode-script",
+ "unicode-segmentation",
+]
[[package]]
-name = "errno"
-version = "0.3.14"
+name = "cpufeatures"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
- "windows-sys 0.61.2",
]
[[package]]
-name = "error-code"
-version = "3.3.2"
+name = "crc32fast"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
[[package]]
-name = "fallible-iterator"
-version = "0.3.0"
+name = "crossbeam-deque"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
[[package]]
-name = "fallible-streaming-iterator"
-version = "0.1.9"
+name = "crossbeam-epoch"
+version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
[[package]]
-name = "fastrand"
-version = "2.3.0"
+name = "crossbeam-queue"
+version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
[[package]]
-name = "fax"
-version = "0.2.6"
+name = "crossbeam-utils"
+version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
-dependencies = [
- "fax_derive",
-]
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
-name = "fax_derive"
-version = "0.2.0"
+name = "crunchy"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "generic-array",
+ "rand_core 0.6.4",
+ "typenum",
]
[[package]]
-name = "fdeflate"
-version = "0.3.7"
+name = "ctor"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6"
dependencies = [
- "simd-adler32",
+ "ctor-proc-macro",
+ "dtor",
]
[[package]]
-name = "find-msvc-tools"
-version = "0.1.9"
+name = "ctor-proc-macro"
+version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
[[package]]
-name = "flate2"
-version = "1.1.9"
+name = "data-url"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "derive_more"
+version = "0.99.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
- "crc32fast",
- "miniz_oxide",
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.117",
]
[[package]]
-name = "foldhash"
-version = "0.1.5"
+name = "digest"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
[[package]]
-name = "foldhash"
-version = "0.2.0"
+name = "dirs"
+version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys 0.3.7",
+]
[[package]]
-name = "foreign-types"
-version = "0.3.2"
+name = "dirs"
+version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
- "foreign-types-shared 0.1.1",
+ "dirs-sys 0.4.1",
]
[[package]]
-name = "foreign-types"
-version = "0.5.0"
+name = "dirs-sys"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
- "foreign-types-macros",
- "foreign-types-shared 0.3.1",
+ "libc",
+ "redox_users",
+ "winapi",
]
[[package]]
-name = "foreign-types-macros"
-version = "0.2.3"
+name = "dirs-sys"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
]
[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
+name = "dispatch"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
-name = "foreign-types-shared"
+name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.11.1",
+ "objc2",
+]
[[package]]
-name = "form_urlencoded"
-version = "1.2.2"
+name = "displaydoc"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
- "percent-encoding",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "futures"
-version = "0.3.32"
+name = "dlib"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
+ "libloading",
]
[[package]]
-name = "futures-channel"
-version = "0.3.32"
+name = "downcast-rs"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "dtor"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8"
dependencies = [
- "futures-core",
- "futures-sink",
+ "dtor-proc-macro",
]
[[package]]
-name = "futures-core"
-version = "0.3.32"
+name = "dtor-proc-macro"
+version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055"
[[package]]
-name = "futures-io"
-version = "0.3.32"
+name = "dunce"
+version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
-name = "futures-sink"
-version = "0.3.32"
+name = "dwrote"
+version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "winapi",
+ "wio",
+]
[[package]]
-name = "futures-task"
-version = "0.3.32"
+name = "dyn-clone"
+version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
-name = "futures-util"
-version = "0.3.32"
+name = "either"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "embed-resource"
+version = "3.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45"
dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-sink",
- "futures-task",
+ "cc",
"memchr",
- "pin-project-lite",
- "slab",
+ "rustc_version",
+ "toml 0.9.12+spec-1.1.0",
+ "vswhom",
+ "winreg",
]
[[package]]
-name = "generic-array"
-version = "0.14.7"
+name = "encoding_rs"
+version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
- "typenum",
- "version_check",
+ "cfg-if",
]
[[package]]
-name = "gethostname"
-version = "1.1.0"
+name = "endi"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
-dependencies = [
- "rustix 1.1.4",
- "windows-link",
-]
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
-name = "getrandom"
-version = "0.2.17"
+name = "enumflags2"
+version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "wasi",
- "wasm-bindgen",
+ "enumflags2_derive",
+ "serde",
]
[[package]]
-name = "getrandom"
-version = "0.3.4"
+name = "enumflags2_derive"
+version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
- "cfg-if",
- "libc",
- "r-efi 5.3.0",
- "wasip2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "getrandom"
+name = "equator"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
dependencies = [
- "cfg-if",
- "libc",
- "r-efi 6.0.0",
- "wasip2",
- "wasip3",
+ "equator-macro",
]
[[package]]
-name = "gl_generator"
-version = "0.14.0"
+name = "equator-macro"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
dependencies = [
- "khronos_api",
- "log",
- "xml-rs",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "gloo-timers"
-version = "0.3.0"
+name = "equivalent"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
-dependencies = [
- "futures-channel",
- "futures-core",
- "js-sys",
- "wasm-bindgen",
-]
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
-name = "glow"
-version = "0.16.0"
+name = "erased-serde"
+version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
+checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
dependencies = [
- "js-sys",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "etagere"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342"
+dependencies = [
+ "euclid",
+ "svg_fmt",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[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 5.4.1",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "exr"
+version = "1.74.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fax"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
+dependencies = [
+ "fax_derive",
+]
+
+[[package]]
+name = "fax_derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "filedescriptor"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
+dependencies = [
+ "libc",
+ "thiserror 1.0.69",
+ "winapi",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+
+[[package]]
+name = "float-ord"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
+
+[[package]]
+name = "float_next_after"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "nanorand",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "font-types"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "fontconfig-parser"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
+dependencies = [
+ "roxmltree",
+]
+
+[[package]]
+name = "fontdb"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3"
+dependencies = [
+ "fontconfig-parser",
+ "log",
+ "memmap2",
"slotmap",
- "wasm-bindgen",
- "web-sys",
+ "tinyvec",
+ "ttf-parser 0.20.0",
]
[[package]]
-name = "glutin"
-version = "0.32.3"
+name = "fontdb"
+version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325"
+checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
dependencies = [
- "bitflags 2.11.0",
- "cfg_aliases",
- "cgl",
- "dispatch2",
- "glutin_egl_sys",
- "glutin_glx_sys",
- "glutin_wgl_sys",
- "libloading",
- "objc2 0.6.4",
- "objc2-app-kit 0.3.2",
- "objc2-core-foundation",
- "objc2-foundation 0.3.2",
- "once_cell",
- "raw-window-handle",
- "wayland-sys",
- "windows-sys 0.52.0",
- "x11-dl",
+ "fontconfig-parser",
+ "log",
+ "memmap2",
+ "slotmap",
+ "tinyvec",
+ "ttf-parser 0.25.1",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[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 = "freetype-sys"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
]
[[package]]
-name = "glutin-winit"
-version = "0.5.0"
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand 1.9.0",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand 2.4.1",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "gethostname"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
+dependencies = [
+ "rustix 1.1.4",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gif"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "glow"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
+dependencies = [
+ "js-sys",
+ "slotmap",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gpu-alloc"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
+dependencies = [
+ "bitflags 2.11.1",
+ "gpu-alloc-types",
+]
+
+[[package]]
+name = "gpu-alloc-ash"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbda7a18a29bc98c2e0de0435c347df935bf59489935d0cbd0b73f1679b6f79a"
+dependencies = [
+ "ash",
+ "gpu-alloc-types",
+ "tinyvec",
+]
+
+[[package]]
+name = "gpu-alloc-types"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "gpui"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "979b45cfa6ec723b6f42330915a1b3769b930d02b2d505f9697f8ca602bee707"
+dependencies = [
+ "anyhow",
+ "as-raw-xcb-connection",
+ "ashpd 0.11.1",
+ "async-task",
+ "bindgen",
+ "blade-graphics",
+ "blade-macros",
+ "blade-util",
+ "block",
+ "bytemuck",
+ "calloop",
+ "calloop-wayland-source",
+ "cbindgen",
+ "cocoa 0.26.0",
+ "cocoa-foundation 0.2.0",
+ "core-foundation 0.10.0",
+ "core-foundation-sys",
+ "core-graphics 0.24.0",
+ "core-text",
+ "core-video",
+ "cosmic-text",
+ "ctor",
+ "derive_more",
+ "embed-resource",
+ "etagere",
+ "filedescriptor",
+ "flume",
+ "foreign-types",
+ "futures",
+ "gpui-macros",
+ "gpui_collections",
+ "gpui_http_client",
+ "gpui_media",
+ "gpui_refineable",
+ "gpui_semantic_version",
+ "gpui_sum_tree",
+ "gpui_util",
+ "gpui_util_macros",
+ "image",
+ "inventory",
+ "itertools 0.14.0",
+ "libc",
+ "log",
+ "lyon",
+ "metal",
+ "naga",
+ "num_cpus",
+ "objc",
+ "oo7",
+ "open",
+ "parking",
+ "parking_lot",
+ "pathfinder_geometry",
+ "pin-project",
+ "postage",
+ "profiling",
+ "rand 0.9.4",
+ "raw-window-handle",
+ "resvg",
+ "schemars",
+ "seahash",
+ "serde",
+ "serde_json",
+ "slotmap",
+ "smallvec",
+ "smol",
+ "stacksafe",
+ "strum 0.27.2",
+ "taffy",
+ "thiserror 2.0.18",
+ "usvg",
+ "uuid",
+ "waker-fn",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-cursor",
+ "wayland-protocols 0.31.2",
+ "wayland-protocols-plasma",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+ "windows-numerics",
+ "windows-registry 0.5.3",
+ "x11-clipboard",
+ "x11rb",
+ "xkbcommon",
+ "zed-font-kit",
+ "zed-scap",
+ "zed-xim",
+]
+
+[[package]]
+name = "gpui-macros"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcb02dd63a2859714ac7b6b476937617c3c744157af1b49f7c904023a79039be"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "gpui_collections"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae39dc6d3d201be97e4bc08d96dbef2bc5b5c3d5734e05786e8cc3043342351c"
+dependencies = [
+ "indexmap",
+ "rustc-hash 2.1.2",
+]
+
+[[package]]
+name = "gpui_derive_refineable"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644de174341a87b3478bd65b66bca38af868bcf2b2e865700523734f83cfc664"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "gpui_http_client"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23822b0a6d2c5e6a42507980a0ab3848610ea908942c8ef98187f646f690335e"
+dependencies = [
+ "anyhow",
+ "async-compression",
+ "async-fs",
+ "bytes",
+ "derive_more",
+ "futures",
+ "gpui_util",
+ "http",
+ "http-body",
+ "log",
+ "parking_lot",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tempfile",
+ "url",
+ "zed-async-tar",
+ "zed-reqwest",
+]
+
+[[package]]
+name = "gpui_media"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05cb8912ae17371725132d2b7eec6797a255accc95d58ee5c1134b529810f14b"
+dependencies = [
+ "anyhow",
+ "bindgen",
+ "core-foundation 0.10.0",
+ "core-video",
+ "ctor",
+ "foreign-types",
+ "metal",
+ "objc",
+]
+
+[[package]]
+name = "gpui_perf"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40a0961dcf598955130e867f4b731150a20546427b41b1a63767c1037a86d77"
+dependencies = [
+ "gpui_collections",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "gpui_refineable"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "258cb099254e9468181aee5614410fba61db4ae115fc1d51b4a0b985f60d6641"
+dependencies = [
+ "gpui_derive_refineable",
+]
+
+[[package]]
+name = "gpui_semantic_version"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201e45eff7b695528fb3af6560a534943fbc2db5323d755b9d198bd743948e35"
+dependencies = [
+ "anyhow",
+ "serde",
+]
+
+[[package]]
+name = "gpui_sum_tree"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4f3bedd573fafafa13d1200b356c588cf094fb2786e3684bb3f5ea59b549fa9"
+dependencies = [
+ "arrayvec",
+ "log",
+ "rayon",
+]
+
+[[package]]
+name = "gpui_util"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68faea25903ae524de9af83990b9aa51bcbc8dd085929ac0aea7fd41905e05c3"
+dependencies = [
+ "anyhow",
+ "async-fs",
+ "async_zip",
+ "command-fds",
+ "dirs 4.0.0",
+ "dunce",
+ "futures",
+ "futures-lite 1.13.0",
+ "globset",
+ "gpui_collections",
+ "itertools 0.14.0",
+ "libc",
+ "log",
+ "nix 0.29.0",
+ "regex",
+ "rust-embed",
+ "schemars",
+ "serde",
+ "serde_json",
+ "serde_json_lenient",
+ "shlex",
+ "smol",
+ "take-until",
+ "tempfile",
+ "tendril",
+ "unicase",
+ "walkdir",
+ "which",
+]
+
+[[package]]
+name = "gpui_util_macros"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c28f65ef47fb97e21e82fd4dd75ccc2506eda010c846dc8054015ea234f1a22"
+dependencies = [
+ "gpui_perf",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "grid"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681"
+
+[[package]]
+name = "h2"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "num-traits",
+ "zerocopy",
+]
+
+[[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"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hexf-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
+
+[[package]]
+name = "hidden-trait"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ed9e850438ac849bec07e7d09fbe9309cbd396a5988c30b010580ce08860df"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-native-certs",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "libc",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
- "cfg_aliases",
- "glutin",
- "raw-window-handle",
- "winit",
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
]
[[package]]
-name = "glutin_egl_sys"
-version = "0.7.1"
+name = "id-arena"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2"
-dependencies = [
- "gl_generator",
- "windows-sys 0.52.0",
-]
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
-name = "glutin_glx_sys"
-version = "0.6.1"
+name = "idna"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
- "gl_generator",
- "x11-dl",
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
]
[[package]]
-name = "glutin_wgl_sys"
-version = "0.6.1"
+name = "idna_adapter"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
- "gl_generator",
+ "icu_normalizer",
+ "icu_properties",
]
[[package]]
-name = "gpu-alloc"
-version = "0.6.0"
+name = "image"
+version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
- "bitflags 2.11.0",
- "gpu-alloc-types",
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "exr",
+ "gif",
+ "image-webp",
+ "moxcms",
+ "num-traits",
+ "png 0.18.1",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
+ "zune-core",
+ "zune-jpeg",
]
[[package]]
-name = "gpu-alloc-types"
-version = "0.3.0"
+name = "image-webp"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
+checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
dependencies = [
- "bitflags 2.11.0",
+ "byteorder-lite",
+ "quick-error",
]
[[package]]
-name = "gpu-allocator"
-version = "0.27.0"
+name = "imagesize"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd"
-dependencies = [
- "log",
- "presser",
- "thiserror 1.0.69",
- "windows",
-]
+checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]]
-name = "gpu-descriptor"
-version = "0.3.2"
+name = "imgref"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
- "bitflags 2.11.0",
- "gpu-descriptor-types",
- "hashbrown 0.15.5",
+ "equivalent",
+ "hashbrown 0.17.0",
+ "serde",
+ "serde_core",
]
[[package]]
-name = "gpu-descriptor-types"
-version = "0.2.0"
+name = "inout"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
- "bitflags 2.11.0",
+ "block-padding",
+ "generic-array",
]
[[package]]
-name = "half"
-version = "2.7.1"
+name = "instant"
+version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
- "crunchy",
- "num-traits",
- "zerocopy",
]
[[package]]
-name = "hashbrown"
-version = "0.14.5"
+name = "interpolate_name"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
- "ahash",
- "allocator-api2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "hashbrown"
-version = "0.15.5"
+name = "inventory"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
dependencies = [
- "foldhash 0.1.5",
+ "rustversion",
]
[[package]]
-name = "hashbrown"
+name = "io-surface"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e"
dependencies = [
- "foldhash 0.2.0",
+ "cgl",
+ "core-foundation 0.10.0",
+ "core-foundation-sys",
+ "leaky-cow",
]
[[package]]
-name = "hashlink"
-version = "0.8.4"
+name = "ipnet"
+version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
-dependencies = [
- "hashbrown 0.14.5",
-]
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
-name = "heck"
-version = "0.5.0"
+name = "is-docker"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
+dependencies = [
+ "once_cell",
+]
[[package]]
-name = "hermit-abi"
-version = "0.5.2"
+name = "is-wsl"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
+dependencies = [
+ "is-docker",
+ "once_cell",
+]
[[package]]
-name = "hex"
-version = "0.4.3"
+name = "itertools"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
[[package]]
-name = "hex-conservative"
-version = "0.2.2"
+name = "itertools"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
- "arrayvec",
+ "either",
]
[[package]]
-name = "hexf-parse"
-version = "0.2.1"
+name = "itoa"
+version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
-name = "hmac"
-version = "0.12.1"
+name = "jobserver"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
- "digest",
+ "getrandom 0.3.4",
+ "libc",
]
[[package]]
-name = "http"
-version = "1.4.0"
+name = "js-sys"
+version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
- "bytes",
- "itoa",
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
]
[[package]]
-name = "httparse"
-version = "1.10.1"
+name = "khronos-egl"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
+dependencies = [
+ "libc",
+ "libloading",
+]
[[package]]
-name = "iana-time-zone"
-version = "0.1.65"
+name = "kurbo"
+version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core",
+ "arrayvec",
+ "euclid",
+ "smallvec",
]
[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
+name = "kv-log-macro"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
dependencies = [
- "cc",
+ "log",
]
[[package]]
-name = "icu_collections"
-version = "2.1.1"
+name = "lazy_static"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
- "displaydoc",
- "potential_utf",
- "yoke",
- "zerofrom",
- "zerovec",
+ "spin",
]
[[package]]
-name = "icu_locale_core"
-version = "2.1.1"
+name = "leak"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
-dependencies = [
- "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
-]
+checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73"
[[package]]
-name = "icu_normalizer"
-version = "2.1.1"
+name = "leaky-cow"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc"
dependencies = [
- "icu_collections",
- "icu_normalizer_data",
- "icu_properties",
- "icu_provider",
- "smallvec",
- "zerovec",
+ "leak",
]
[[package]]
-name = "icu_normalizer_data"
-version = "2.1.1"
+name = "leb128fmt"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
-name = "icu_properties"
-version = "2.1.2"
+name = "lebe"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
-dependencies = [
- "icu_collections",
- "icu_locale_core",
- "icu_properties_data",
- "icu_provider",
- "zerotrie",
- "zerovec",
-]
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
-name = "icu_properties_data"
-version = "2.1.2"
+name = "libc"
+version = "0.2.185"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
-name = "icu_provider"
-version = "2.1.1"
+name = "libfuzzer-sys"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
dependencies = [
- "displaydoc",
- "icu_locale_core",
- "writeable",
- "yoke",
- "zerofrom",
- "zerotrie",
- "zerovec",
+ "arbitrary",
+ "cc",
]
[[package]]
-name = "id-arena"
-version = "2.3.0"
+name = "libloading"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link 0.2.1",
+]
[[package]]
-name = "idna"
-version = "1.1.0"
+name = "libm"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
dependencies = [
- "idna_adapter",
- "smallvec",
- "utf8_iter",
+ "bitflags 2.11.1",
+ "libc",
+ "plain",
+ "redox_syscall 0.7.4",
]
[[package]]
-name = "idna_adapter"
-version = "1.2.1"
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
-dependencies = [
- "icu_normalizer",
- "icu_properties",
-]
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
-name = "image"
-version = "0.25.10"
+name = "litemap"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
-dependencies = [
- "bytemuck",
- "byteorder-lite",
- "moxcms",
- "num-traits",
- "png",
- "tiff",
-]
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
-name = "indexmap"
-version = "2.13.0"
+name = "lock_api"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
- "equivalent",
- "hashbrown 0.16.1",
- "serde",
- "serde_core",
+ "scopeguard",
]
[[package]]
-name = "inout"
-version = "0.1.4"
+name = "log"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
dependencies = [
- "block-padding",
- "generic-array",
+ "serde_core",
+ "value-bag",
]
[[package]]
-name = "instant"
-version = "0.1.13"
+name = "loop9"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
dependencies = [
- "cfg-if",
- "js-sys",
- "wasm-bindgen",
- "web-sys",
+ "imgref",
]
[[package]]
-name = "itoa"
-version = "1.0.18"
+name = "lru-slab"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
-name = "jni"
-version = "0.21.1"
+name = "lyon"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7"
dependencies = [
- "cesu8",
- "cfg-if",
- "combine",
- "jni-sys 0.3.0",
- "log",
- "thiserror 1.0.69",
- "walkdir",
- "windows-sys 0.45.0",
+ "lyon_algorithms",
+ "lyon_tessellation",
]
[[package]]
-name = "jni"
-version = "0.22.4"
+name = "lyon_algorithms"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+checksum = "9815fac08e6fd96733a11dce4f9d15a3f338e96a2e2311ee21e1b738efc2bc0f"
dependencies = [
- "cfg-if",
- "combine",
- "jni-macros",
- "jni-sys 0.4.1",
- "log",
- "simd_cesu8",
- "thiserror 2.0.18",
- "walkdir",
- "windows-link",
+ "lyon_path",
+ "num-traits",
]
[[package]]
-name = "jni-macros"
-version = "0.22.4"
+name = "lyon_geom"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92"
dependencies = [
- "proc-macro2",
- "quote",
- "rustc_version",
- "simd_cesu8",
- "syn",
+ "arrayvec",
+ "euclid",
+ "num-traits",
]
[[package]]
-name = "jni-sys"
-version = "0.3.0"
+name = "lyon_path"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e"
+dependencies = [
+ "lyon_geom",
+ "num-traits",
+]
[[package]]
-name = "jni-sys"
-version = "0.4.1"
+name = "lyon_tessellation"
+version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552"
dependencies = [
- "jni-sys-macros",
+ "float_next_after",
+ "lyon_path",
+ "num-traits",
]
[[package]]
-name = "jni-sys-macros"
-version = "0.4.1"
+name = "mac"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
-dependencies = [
- "quote",
- "syn",
-]
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
-name = "jobserver"
-version = "0.1.34"
+name = "malloc_buf"
+version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
- "getrandom 0.3.4",
"libc",
]
[[package]]
-name = "js-sys"
-version = "0.3.91"
+name = "maybe-rayon"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
- "once_cell",
- "wasm-bindgen",
+ "cfg-if",
+ "rayon",
]
[[package]]
-name = "json5"
-version = "0.4.1"
+name = "md-5"
+version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
- "pest",
- "pest_derive",
- "serde",
+ "cfg-if",
+ "digest",
]
[[package]]
-name = "keyring"
-version = "3.6.3"
+name = "memchr"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
-dependencies = [
- "byteorder",
- "dbus-secret-service",
- "linux-keyutils",
- "log",
- "openssl",
- "security-framework 2.11.1",
- "security-framework 3.7.0",
- "windows-sys 0.60.2",
- "zeroize",
-]
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
-name = "khronos-egl"
-version = "6.0.0"
+name = "memmap2"
+version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
+checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
dependencies = [
"libc",
- "libloading",
- "pkg-config",
]
[[package]]
-name = "khronos_api"
-version = "3.1.0"
+name = "memoffset"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
[[package]]
-name = "lazy_static"
-version = "1.5.0"
+name = "metal"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
+dependencies = [
+ "bitflags 2.11.1",
+ "block",
+ "core-graphics-types 0.1.3",
+ "foreign-types",
+ "log",
+ "objc",
+ "paste",
+]
[[package]]
-name = "leb128fmt"
-version = "0.1.0"
+name = "mime"
+version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
-name = "libc"
-version = "0.2.183"
+name = "mime_guess"
+version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
[[package]]
-name = "libdbus-sys"
-version = "0.2.7"
+name = "minimal-lexical"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
-dependencies = [
- "cc",
- "pkg-config",
-]
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
-name = "libloading"
+name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
- "cfg-if",
- "windows-link",
+ "adler2",
+ "simd-adler32",
]
[[package]]
-name = "libm"
-version = "0.2.16"
+name = "mint"
+version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]]
-name = "libredox"
-version = "0.1.14"
+name = "mio"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
- "bitflags 2.11.0",
"libc",
- "plain",
- "redox_syscall 0.7.3",
+ "wasi",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "libsqlite3-sys"
-version = "0.37.0"
+name = "moxcms"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
- "cc",
- "pkg-config",
- "vcpkg",
+ "num-traits",
+ "pxfm",
]
[[package]]
-name = "linux-keyutils"
-version = "0.2.4"
+name = "naga"
+version = "25.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e"
+checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
dependencies = [
- "bitflags 2.11.0",
- "libc",
+ "arrayvec",
+ "bit-set",
+ "bitflags 2.11.1",
+ "cfg_aliases",
+ "codespan-reporting",
+ "half",
+ "hashbrown 0.15.5",
+ "hexf-parse",
+ "indexmap",
+ "log",
+ "num-traits",
+ "once_cell",
+ "rustc-hash 1.1.0",
+ "spirv",
+ "strum 0.26.3",
+ "thiserror 2.0.18",
+ "unicode-ident",
]
[[package]]
-name = "linux-raw-sys"
-version = "0.4.15"
+name = "nanorand"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
+dependencies = [
+ "getrandom 0.2.17",
+]
[[package]]
-name = "linux-raw-sys"
-version = "0.12.1"
+name = "new_debug_unreachable"
+version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
-name = "litemap"
-version = "0.8.1"
+name = "nix"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.11.1",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
[[package]]
-name = "litrs"
-version = "1.0.0"
+name = "nix"
+version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
+dependencies = [
+ "bitflags 2.11.1",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
[[package]]
-name = "lock_api"
-version = "0.4.14"
+name = "no_std_io2"
+version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550"
dependencies = [
- "scopeguard",
+ "memchr",
]
[[package]]
-name = "log"
-version = "0.4.29"
+name = "nom"
+version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
[[package]]
-name = "lru"
-version = "0.16.3"
+name = "noop_proc_macro"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
-name = "malloc_buf"
-version = "0.0.6"
+name = "ntapi"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
dependencies = [
- "libc",
+ "winapi",
]
[[package]]
-name = "matchers"
-version = "0.2.0"
+name = "num"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
- "regex-automata",
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
]
[[package]]
-name = "memchr"
-version = "2.8.0"
+name = "num-bigint"
+version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
[[package]]
-name = "memmap2"
-version = "0.9.10"
+name = "num-bigint-dig"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
- "libc",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.6",
+ "serde",
+ "smallvec",
+ "zeroize",
]
[[package]]
-name = "memoffset"
-version = "0.9.1"
+name = "num-complex"
+version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
- "autocfg",
+ "num-traits",
]
[[package]]
-name = "metal"
-version = "0.32.0"
+name = "num-derive"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
- "bitflags 2.11.0",
- "block",
- "core-graphics-types 0.2.0",
- "foreign-types 0.5.0",
- "log",
- "objc",
- "paste",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "miniz_oxide"
-version = "0.8.9"
+name = "num-integer"
+version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
- "adler2",
- "simd-adler32",
+ "num-traits",
]
[[package]]
-name = "mio"
-version = "1.1.1"
+name = "num-iter"
+version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
- "libc",
- "wasi",
- "windows-sys 0.61.2",
+ "autocfg",
+ "num-integer",
+ "num-traits",
]
[[package]]
-name = "moxcms"
-version = "0.8.1"
+name = "num-rational"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
+ "num-bigint",
+ "num-integer",
"num-traits",
- "pxfm",
]
[[package]]
-name = "naga"
-version = "27.0.3"
+name = "num-traits"
+version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
- "arrayvec",
- "bit-set",
- "bitflags 2.11.0",
- "cfg-if",
- "cfg_aliases",
- "codespan-reporting",
- "half",
- "hashbrown 0.16.1",
- "hexf-parse",
- "indexmap",
+ "autocfg",
"libm",
- "log",
- "num-traits",
- "once_cell",
- "rustc-hash 1.1.0",
- "spirv",
- "thiserror 2.0.18",
- "unicode-ident",
]
[[package]]
-name = "ndk"
-version = "0.9.0"
+name = "num_cpus"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
- "bitflags 2.11.0",
- "jni-sys 0.3.0",
- "log",
- "ndk-sys",
- "num_enum",
- "raw-window-handle",
- "thiserror 1.0.69",
+ "hermit-abi",
+ "libc",
]
[[package]]
-name = "ndk-context"
-version = "0.1.1"
+name = "objc"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+ "objc_exception",
+]
[[package]]
-name = "ndk-sys"
-version = "0.6.0+11769913"
+name = "objc-foundation"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
- "jni-sys 0.3.0",
+ "block",
+ "objc",
+ "objc_id",
]
[[package]]
-name = "negentropy"
-version = "0.5.0"
+name = "objc2"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+]
[[package]]
-name = "nohash-hasher"
-version = "0.2.0"
+name = "objc2-app-kit"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.11.1",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
[[package]]
-name = "nom"
-version = "7.1.3"
+name = "objc2-core-foundation"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
- "memchr",
- "minimal-lexical",
+ "bitflags 2.11.1",
+ "dispatch2",
+ "objc2",
]
[[package]]
-name = "nostr"
-version = "0.44.2"
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
- "aes",
- "base64 0.22.1",
- "bech32",
- "bip39",
- "bitcoin_hashes",
- "cbc",
- "chacha20",
- "chacha20poly1305",
- "getrandom 0.2.17",
- "hex",
- "instant",
- "scrypt",
- "secp256k1",
- "serde",
- "serde_json",
- "unicode-normalization",
- "url",
+ "bitflags 2.11.1",
+ "objc2",
+ "objc2-core-foundation",
]
[[package]]
-name = "nostr-browser-signer"
-version = "0.44.2"
+name = "objc2-metal"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e0430dc4517ea03ec055c256f06326671a214e0dfd561d46bafb224d3d31314"
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [
- "js-sys",
- "nostr",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-foundation",
]
[[package]]
-name = "nostr-database"
-version = "0.44.0"
+name = "objc2-quartz-core"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
- "lru",
- "nostr",
- "tokio",
+ "bitflags 2.11.1",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-metal",
]
[[package]]
-name = "nostr-gossip"
-version = "0.44.0"
+name = "objc2-ui-kit"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
dependencies = [
- "nostr",
+ "bitflags 2.11.1",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
]
[[package]]
-name = "nostr-relay-pool"
-version = "0.44.0"
+name = "objc_exception"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
dependencies = [
- "async-utility",
- "async-wsocket",
- "atomic-destructor",
- "hex",
- "lru",
- "negentropy",
- "nostr",
- "nostr-database",
- "tokio",
- "tracing",
+ "cc",
]
[[package]]
-name = "nostr-sdk"
-version = "0.44.1"
+name = "objc_id"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
- "async-utility",
- "nostr",
- "nostr-database",
- "nostr-gossip",
- "nostr-relay-pool",
- "tokio",
- "tracing",
+ "objc",
]
[[package]]
-name = "nu-ansi-term"
-version = "0.50.3"
+name = "object"
+version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
- "windows-sys 0.61.2",
+ "memchr",
]
[[package]]
-name = "num-conv"
-version = "0.2.0"
+name = "once_cell"
+version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
-name = "num-traits"
-version = "0.2.19"
+name = "oo7"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d"
dependencies = [
- "autocfg",
- "libm",
+ "aes",
+ "ashpd 0.12.3",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "cbc",
+ "cipher",
+ "digest",
+ "endi",
+ "futures-lite 2.6.1",
+ "futures-util",
+ "getrandom 0.3.4",
+ "hkdf",
+ "hmac",
+ "md-5",
+ "num",
+ "num-bigint-dig",
+ "pbkdf2",
+ "rand 0.9.4",
+ "serde",
+ "sha2",
+ "subtle",
+ "zbus",
+ "zbus_macros",
+ "zeroize",
+ "zvariant",
]
[[package]]
-name = "num_enum"
-version = "0.7.6"
+name = "open"
+version = "5.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
dependencies = [
- "num_enum_derive",
- "rustversion",
+ "is-wsl",
+ "libc",
+ "pathdiff",
]
[[package]]
-name = "num_enum_derive"
-version = "0.7.6"
+name = "openssl-probe"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
-name = "objc"
-version = "0.2.7"
+name = "option-ext"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
- "malloc_buf",
+ "futures-core",
+ "pin-project-lite",
]
[[package]]
-name = "objc-sys"
-version = "0.3.5"
+name = "parking"
+version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
-name = "objc2"
-version = "0.5.2"
+name = "parking_lot"
+version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
- "objc-sys",
- "objc2-encode",
+ "lock_api",
+ "parking_lot_core",
]
[[package]]
-name = "objc2"
-version = "0.6.4"
+name = "parking_lot_core"
+version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
- "objc2-encode",
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link 0.2.1",
]
[[package]]
-name = "objc2-app-kit"
-version = "0.2.2"
+name = "paste"
+version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
-dependencies = [
- "bitflags 2.11.0",
- "block2",
- "libc",
- "objc2 0.5.2",
- "objc2-core-data",
- "objc2-core-image",
- "objc2-foundation 0.2.2",
- "objc2-quartz-core",
-]
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
-name = "objc2-app-kit"
-version = "0.3.2"
+name = "pastey"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
-dependencies = [
- "bitflags 2.11.0",
- "objc2 0.6.4",
- "objc2-core-foundation",
- "objc2-core-graphics",
- "objc2-foundation 0.3.2",
-]
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
-name = "objc2-cloud-kit"
-version = "0.2.2"
+name = "pathdiff"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
-dependencies = [
- "bitflags 2.11.0",
- "block2",
- "objc2 0.5.2",
- "objc2-core-location",
- "objc2-foundation 0.2.2",
-]
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
-name = "objc2-contacts"
-version = "0.2.2"
+name = "pathfinder_geometry"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3"
dependencies = [
- "block2",
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "log",
+ "pathfinder_simd",
]
[[package]]
-name = "objc2-core-data"
-version = "0.2.2"
+name = "pathfinder_simd"
+version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57"
dependencies = [
- "bitflags 2.11.0",
- "block2",
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "rustc_version",
]
[[package]]
-name = "objc2-core-foundation"
-version = "0.3.2"
+name = "pbkdf2"
+version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
- "bitflags 2.11.0",
- "dispatch2",
- "objc2 0.6.4",
+ "digest",
+ "hmac",
]
[[package]]
-name = "objc2-core-graphics"
-version = "0.3.2"
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pico-args"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+
+[[package]]
+name = "pin-project"
+version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [
- "bitflags 2.11.0",
- "dispatch2",
- "objc2 0.6.4",
- "objc2-core-foundation",
- "objc2-io-surface",
+ "pin-project-internal",
]
[[package]]
-name = "objc2-core-image"
-version = "0.2.2"
+name = "pin-project-internal"
+version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
- "block2",
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
- "objc2-metal",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "objc2-core-location"
-version = "0.2.2"
+name = "pin-project-lite"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
-dependencies = [
- "block2",
- "objc2 0.5.2",
- "objc2-contacts",
- "objc2-foundation 0.2.2",
-]
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
-name = "objc2-encode"
-version = "4.1.0"
+name = "pin-utils"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
-name = "objc2-foundation"
-version = "0.2.2"
+name = "piper"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
- "bitflags 2.11.0",
- "block2",
- "dispatch",
- "libc",
- "objc2 0.5.2",
+ "atomic-waker",
+ "fastrand 2.4.1",
+ "futures-io",
]
[[package]]
-name = "objc2-foundation"
-version = "0.3.2"
+name = "pkg-config"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
-dependencies = [
- "bitflags 2.11.0",
- "objc2 0.6.4",
- "objc2-core-foundation",
-]
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
-name = "objc2-io-surface"
-version = "0.3.2"
+name = "plain"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
-dependencies = [
- "bitflags 2.11.0",
- "objc2 0.6.4",
- "objc2-core-foundation",
-]
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
-name = "objc2-link-presentation"
-version = "0.2.2"
+name = "png"
+version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
- "block2",
- "objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
]
[[package]]
-name = "objc2-metal"
-version = "0.2.2"
+name = "png"
+version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
- "bitflags 2.11.0",
- "block2",
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "bitflags 2.11.1",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
]
[[package]]
-name = "objc2-quartz-core"
-version = "0.2.2"
+name = "polling"
+version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
- "bitflags 2.11.0",
- "block2",
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
- "objc2-metal",
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "objc2-symbols"
-version = "0.2.2"
+name = "pollster"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
-dependencies = [
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
-]
+checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
[[package]]
-name = "objc2-ui-kit"
-version = "0.2.2"
+name = "postage"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1"
dependencies = [
- "bitflags 2.11.0",
- "block2",
- "objc2 0.5.2",
- "objc2-cloud-kit",
- "objc2-core-data",
- "objc2-core-image",
- "objc2-core-location",
- "objc2-foundation 0.2.2",
- "objc2-link-presentation",
- "objc2-quartz-core",
- "objc2-symbols",
- "objc2-uniform-type-identifiers",
- "objc2-user-notifications",
+ "atomic",
+ "crossbeam-queue",
+ "futures",
+ "log",
+ "parking_lot",
+ "pin-project",
+ "pollster",
+ "static_assertions",
+ "thiserror 1.0.69",
]
[[package]]
-name = "objc2-uniform-type-identifiers"
-version = "0.2.2"
+name = "potential_utf"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
- "block2",
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
+ "zerovec",
]
[[package]]
-name = "objc2-user-notifications"
-version = "0.2.2"
+name = "ppv-lite86"
+version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
- "bitflags 2.11.0",
- "block2",
- "objc2 0.5.2",
- "objc2-core-location",
- "objc2-foundation 0.2.2",
+ "zerocopy",
]
[[package]]
-name = "once_cell"
-version = "1.21.4"
+name = "prettyplease"
+version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.117",
+]
[[package]]
-name = "opaque-debug"
-version = "0.3.1"
+name = "proc-macro-crate"
+version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit 0.25.11+spec-1.1.0",
+]
[[package]]
-name = "openssl"
-version = "0.10.76"
+name = "proc-macro-error-attr2"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
- "bitflags 2.11.0",
- "cfg-if",
- "foreign-types 0.3.2",
- "libc",
- "once_cell",
- "openssl-macros",
- "openssl-sys",
+ "proc-macro2",
+ "quote",
]
[[package]]
-name = "openssl-macros"
-version = "0.1.1"
+name = "proc-macro-error2"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
+ "proc-macro-error-attr2",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
-name = "openssl-src"
-version = "300.5.5+3.5.5"
+name = "proc-macro2"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
- "cc",
+ "unicode-ident",
]
[[package]]
-name = "openssl-sys"
-version = "0.9.112"
+name = "profiling"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
+checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
dependencies = [
- "cc",
- "libc",
- "openssl-src",
- "pkg-config",
- "vcpkg",
+ "profiling-procmacros",
]
[[package]]
-name = "orbclient"
-version = "0.3.51"
+name = "profiling-procmacros"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6"
+checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
dependencies = [
- "libc",
- "libredox",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "ordered-float"
-version = "5.1.0"
+name = "psm"
+version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d"
+checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8"
dependencies = [
- "num-traits",
+ "ar_archive_writer",
+ "cc",
]
[[package]]
-name = "ordered-multimap"
-version = "0.7.3"
+name = "pxfm"
+version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
-dependencies = [
- "dlv-list",
- "hashbrown 0.14.5",
-]
+checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
-name = "owned_ttf_parser"
-version = "0.25.1"
+name = "qoi"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
- "ttf-parser",
+ "bytemuck",
]
[[package]]
-name = "parking_lot"
-version = "0.12.5"
+name = "quick-error"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quick-xml"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
- "lock_api",
- "parking_lot_core",
+ "memchr",
]
[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
+name = "quick-xml"
+version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall 0.5.18",
- "smallvec",
- "windows-link",
+ "memchr",
]
[[package]]
-name = "password-hash"
-version = "0.5.0"
+name = "quinn"
+version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
- "base64ct",
- "rand_core 0.6.4",
- "subtle",
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash 2.1.2",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
]
[[package]]
-name = "paste"
-version = "1.0.15"
+name = "quinn-proto"
+version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.4",
+ "ring",
+ "rustc-hash 2.1.2",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
[[package]]
-name = "pathdiff"
-version = "0.2.3"
+name = "quinn-udp"
+version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
[[package]]
-name = "pbkdf2"
-version = "0.12.2"
+name = "quote"
+version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
- "digest",
- "hmac",
+ "proc-macro2",
]
[[package]]
-name = "percent-encoding"
-version = "2.3.2"
+name = "r-efi"
+version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
-name = "pest"
-version = "2.8.6"
+name = "r-efi"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
-dependencies = [
- "memchr",
- "ucd-trie",
-]
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
-name = "pest_derive"
-version = "2.8.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77"
+name = "radroots_app"
+version = "0.1.0"
dependencies = [
- "pest",
- "pest_generator",
+ "gpui",
]
[[package]]
-name = "pest_generator"
-version = "2.8.6"
+name = "rand"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn",
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
]
[[package]]
-name = "pest_meta"
-version = "2.8.6"
+name = "rand"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
- "pest",
- "sha2",
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
]
[[package]]
-name = "pin-project"
-version = "1.1.11"
+name = "rand_chacha"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
- "pin-project-internal",
+ "ppv-lite86",
+ "rand_core 0.6.4",
]
[[package]]
-name = "pin-project-internal"
-version = "1.1.11"
+name = "rand_chacha"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "ppv-lite86",
+ "rand_core 0.9.5",
]
[[package]]
-name = "pin-project-lite"
-version = "0.2.17"
+name = "rand_core"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
[[package]]
-name = "pkg-config"
-version = "0.3.32"
+name = "rand_core"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
[[package]]
-name = "plain"
-version = "0.2.3"
+name = "rangemap"
+version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68"
[[package]]
-name = "png"
-version = "0.18.1"
+name = "rav1e"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
dependencies = [
- "bitflags 2.11.0",
- "crc32fast",
- "fdeflate",
- "flate2",
- "miniz_oxide",
+ "aligned-vec",
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av-scenechange",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if",
+ "interpolate_name",
+ "itertools 0.14.0",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "profiling",
+ "rand 0.9.4",
+ "rand_chacha 0.9.0",
+ "simd_helpers",
+ "thiserror 2.0.18",
+ "v_frame",
+ "wasm-bindgen",
]
[[package]]
-name = "polling"
-version = "3.11.0"
+name = "ravif"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [
- "cfg-if",
- "concurrent-queue",
- "hermit-abi",
- "pin-project-lite",
- "rustix 1.1.4",
- "windows-sys 0.61.2",
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
]
[[package]]
-name = "pollster"
-version = "0.4.0"
+name = "raw-window-handle"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
-name = "poly1305"
-version = "0.8.0"
+name = "raw-window-metal"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1"
dependencies = [
- "cpufeatures",
- "opaque-debug",
- "universal-hash",
+ "cocoa 0.25.0",
+ "core-graphics 0.23.2",
+ "objc",
+ "raw-window-handle",
]
[[package]]
-name = "portable-atomic"
-version = "1.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
-
-[[package]]
-name = "portable-atomic-util"
-version = "0.2.6"
+name = "rayon"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
- "portable-atomic",
+ "either",
+ "rayon-core",
]
[[package]]
-name = "potential_utf"
-version = "0.1.4"
+name = "rayon-core"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
- "zerovec",
+ "crossbeam-deque",
+ "crossbeam-utils",
]
[[package]]
-name = "powerfmt"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
-
-[[package]]
-name = "ppv-lite86"
-version = "0.2.21"
+name = "read-fonts"
+version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5"
dependencies = [
- "zerocopy",
+ "bytemuck",
+ "font-types",
]
[[package]]
-name = "presser"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
-
-[[package]]
-name = "prettyplease"
-version = "0.2.37"
+name = "redox_syscall"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
- "proc-macro2",
- "syn",
+ "bitflags 1.3.2",
]
[[package]]
-name = "proc-macro-crate"
-version = "3.5.0"
+name = "redox_syscall"
+version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "toml_edit 0.25.5+spec-1.1.0",
+ "bitflags 2.11.1",
]
[[package]]
-name = "proc-macro2"
-version = "1.0.106"
+name = "redox_syscall"
+version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
dependencies = [
- "unicode-ident",
+ "bitflags 2.11.1",
]
[[package]]
-name = "profiling"
-version = "1.0.17"
+name = "redox_users"
+version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 1.0.69",
+]
[[package]]
-name = "pxfm"
-version = "0.1.28"
+name = "ref-cast"
+version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
[[package]]
-name = "quick-error"
-version = "2.0.1"
+name = "ref-cast-impl"
+version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
[[package]]
-name = "quick-xml"
-version = "0.39.2"
+name = "regex"
+version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
+ "aho-corasick",
"memchr",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
-name = "quote"
-version = "1.0.45"
+name = "regex-automata"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
- "proc-macro2",
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
]
[[package]]
-name = "r-efi"
-version = "5.3.0"
+name = "regex-syntax"
+version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
-name = "r-efi"
-version = "6.0.0"
+name = "resvg"
+version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
-
-[[package]]
-name = "radroots_app_android"
-version = "0.1.0"
+checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43"
dependencies = [
- "android_logger",
- "eframe",
"log",
- "radroots_app_android_security",
- "radroots_app_core",
- "radroots_app_remote_signer",
- "radroots_app_test_support",
- "radroots_geocoder",
- "radroots_identity",
- "radroots_nostr_accounts",
- "radroots_runtime_paths",
- "wgpu",
- "winit",
- "zeroize",
+ "pico-args",
+ "rgb",
+ "svgtypes",
+ "tiny-skia",
+ "usvg",
]
[[package]]
-name = "radroots_app_android_security"
-version = "0.1.0"
+name = "rgb"
+version = "0.8.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
- "jni 0.21.1",
- "ndk-context",
- "radroots_nostr_accounts",
- "radroots_secret_vault",
- "zeroize",
+ "bytemuck",
]
[[package]]
-name = "radroots_app_apple_security"
-version = "0.1.0"
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
- "radroots_nostr_accounts",
- "radroots_secret_vault",
- "zeroize",
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
]
[[package]]
-name = "radroots_app_core"
-version = "0.1.0"
-dependencies = [
- "eframe",
- "egui",
- "radroots_app_test_support",
- "radroots_runtime_paths",
- "zeroize",
-]
+name = "roxmltree"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
-name = "radroots_app_desktop"
-version = "0.1.0"
+name = "rust-embed"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
dependencies = [
- "eframe",
- "egui",
- "image",
- "log",
- "objc2-foundation 0.3.2",
- "radroots_app_apple_security",
- "radroots_app_core",
- "radroots_app_remote_signer",
- "radroots_app_test_support",
- "radroots_geocoder",
- "radroots_identity",
- "radroots_nostr_accounts",
- "radroots_runtime_paths",
- "wgpu",
- "zeroize",
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
]
[[package]]
-name = "radroots_app_ios"
-version = "0.1.0"
+name = "rust-embed-impl"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
dependencies = [
- "eframe",
- "log",
- "radroots_app_apple_security",
- "radroots_app_core",
- "radroots_app_remote_signer",
- "radroots_app_test_support",
- "radroots_geocoder",
- "radroots_identity",
- "radroots_nostr_accounts",
- "radroots_runtime_paths",
- "wgpu",
- "zeroize",
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.117",
+ "walkdir",
]
[[package]]
-name = "radroots_app_remote_signer"
-version = "0.1.0"
+name = "rust-embed-utils"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [
- "nostr",
- "radroots_app_test_support",
- "radroots_identity",
- "radroots_nostr",
- "radroots_nostr_accounts",
- "radroots_nostr_connect",
- "serde",
- "serde_json",
- "tempfile",
- "tokio",
- "url",
+ "globset",
+ "sha2",
+ "walkdir",
]
[[package]]
-name = "radroots_app_test_support"
-version = "0.1.0"
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
- "radroots_identity",
+ "semver",
]
[[package]]
-name = "radroots_app_web"
-version = "0.1.0"
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
- "eframe",
- "js-sys",
- "log",
- "nostr",
- "nostr-browser-signer",
- "radroots_app_core",
- "radroots_geocoder",
- "wasm-bindgen-futures",
- "web-sys",
- "wgpu",
+ "bitflags 2.11.1",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
]
[[package]]
-name = "radroots_core"
-version = "0.1.0-alpha.2"
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
- "rust_decimal",
- "rust_decimal_macros",
- "serde",
+ "bitflags 2.11.1",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "radroots_events"
-version = "0.1.0-alpha.2"
+name = "rustls"
+version = "0.23.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
dependencies = [
- "radroots_core",
- "serde",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
]
[[package]]
-name = "radroots_geocoder"
-version = "0.1.0-alpha.2"
+name = "rustls-native-certs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
- "rusqlite",
- "serde",
- "thiserror 1.0.69",
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
]
[[package]]
-name = "radroots_identity"
-version = "0.1.0-alpha.2"
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
- "nostr",
- "radroots_events",
- "radroots_runtime",
- "radroots_runtime_paths",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "tracing",
+ "rustls-pki-types",
]
[[package]]
-name = "radroots_log"
-version = "0.1.0-alpha.2"
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
- "chrono",
- "thiserror 1.0.69",
- "tracing",
- "tracing-appender",
- "tracing-subscriber",
+ "web-time",
+ "zeroize",
]
[[package]]
-name = "radroots_nostr"
-version = "0.1.0-alpha.2"
+name = "rustls-webpki"
+version = "0.103.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
dependencies = [
- "nostr",
- "nostr-sdk",
- "radroots_identity",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
]
[[package]]
-name = "radroots_nostr_accounts"
-version = "0.1.0-alpha.2"
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "rustybuzz"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
dependencies = [
- "radroots_identity",
- "radroots_nostr_signer",
- "radroots_runtime",
- "radroots_secret_vault",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "zeroize",
+ "bitflags 2.11.1",
+ "bytemuck",
+ "libm",
+ "smallvec",
+ "ttf-parser 0.21.1",
+ "unicode-bidi-mirroring 0.2.0",
+ "unicode-ccc 0.2.0",
+ "unicode-properties",
+ "unicode-script",
]
[[package]]
-name = "radroots_nostr_connect"
-version = "0.1.0-alpha.2"
+name = "rustybuzz"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
dependencies = [
- "nostr",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "url",
+ "bitflags 2.11.1",
+ "bytemuck",
+ "core_maths",
+ "log",
+ "smallvec",
+ "ttf-parser 0.25.1",
+ "unicode-bidi-mirroring 0.4.0",
+ "unicode-ccc 0.4.0",
+ "unicode-properties",
+ "unicode-script",
]
[[package]]
-name = "radroots_nostr_signer"
-version = "0.1.0-alpha.2"
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
- "hex",
- "nostr",
- "radroots_identity",
- "radroots_nostr",
- "radroots_nostr_connect",
- "radroots_runtime",
- "serde",
- "serde_json",
- "sha2",
- "thiserror 1.0.69",
- "url",
- "uuid",
+ "winapi-util",
]
[[package]]
-name = "radroots_protected_store"
-version = "0.1.0-alpha.2"
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
- "chacha20poly1305",
- "getrandom 0.2.17",
- "radroots_secret_vault",
- "serde",
- "serde_json",
- "zeroize",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "radroots_runtime"
-version = "0.1.0-alpha.2"
+name = "schemars"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
dependencies = [
- "anyhow",
- "chacha20poly1305",
- "config",
- "getrandom 0.2.17",
- "radroots_log",
- "radroots_protected_store",
- "radroots_runtime_paths",
- "radroots_secret_vault",
+ "dyn-clone",
+ "indexmap",
+ "ref-cast",
+ "schemars_derive",
"serde",
"serde_json",
- "tempfile",
- "thiserror 1.0.69",
- "tokio",
- "toml",
- "tracing",
- "zeroize",
]
[[package]]
-name = "radroots_runtime_paths"
-version = "0.1.0-alpha.2"
+name = "schemars_derive"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
dependencies = [
- "thiserror 1.0.69",
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.117",
]
[[package]]
-name = "radroots_secret_vault"
-version = "0.1.0-alpha.2"
-dependencies = [
- "keyring",
-]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
-name = "rand"
-version = "0.8.5"
+name = "scopeguard"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
-dependencies = [
- "libc",
- "rand_chacha 0.3.1",
- "rand_core 0.6.4",
-]
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
-name = "rand"
-version = "0.9.2"
+name = "screencapturekit"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e"
dependencies = [
- "rand_chacha 0.9.0",
- "rand_core 0.9.5",
+ "screencapturekit-sys",
]
[[package]]
-name = "rand_chacha"
-version = "0.3.1"
+name = "screencapturekit-sys"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60"
dependencies = [
- "ppv-lite86",
- "rand_core 0.6.4",
+ "block",
+ "dispatch",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "once_cell",
]
[[package]]
-name = "rand_chacha"
-version = "0.9.0"
+name = "seahash"
+version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.9.5",
-]
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
-name = "rand_core"
-version = "0.6.4"
+name = "security-framework"
+version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
- "getrandom 0.2.17",
+ "bitflags 2.11.1",
+ "core-foundation 0.10.0",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
]
[[package]]
-name = "rand_core"
-version = "0.9.5"
+name = "security-framework-sys"
+version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
- "getrandom 0.3.4",
+ "core-foundation-sys",
+ "libc",
]
[[package]]
-name = "range-alloc"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
-
-[[package]]
-name = "raw-window-handle"
-version = "0.6.2"
+name = "self_cell"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
[[package]]
-name = "redox_syscall"
-version = "0.4.1"
+name = "semver"
+version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
-dependencies = [
- "bitflags 1.3.2",
-]
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
-name = "redox_syscall"
-version = "0.5.18"
+name = "serde"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
- "bitflags 2.11.0",
+ "serde_core",
+ "serde_derive",
]
[[package]]
-name = "redox_syscall"
-version = "0.7.3"
+name = "serde_core"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
- "bitflags 2.11.0",
+ "serde_derive",
]
[[package]]
-name = "regex"
-version = "1.12.3"
+name = "serde_derive"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata",
- "regex-syntax",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "regex-automata"
-version = "0.4.14"
+name = "serde_derive_internals"
+version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "regex-syntax"
-version = "0.8.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
-
-[[package]]
-name = "renderdoc-sys"
+name = "serde_fmt"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
-
-[[package]]
-name = "ring"
-version = "0.17.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed"
dependencies = [
- "cc",
- "cfg-if",
- "getrandom 0.2.17",
- "libc",
- "untrusted",
- "windows-sys 0.52.0",
+ "serde_core",
]
[[package]]
-name = "ron"
-version = "0.8.1"
+name = "serde_json"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
- "base64 0.21.7",
- "bitflags 2.11.0",
+ "indexmap",
+ "itoa",
+ "memchr",
"serde",
- "serde_derive",
+ "serde_core",
+ "zmij",
]
[[package]]
-name = "rsqlite-vfs"
-version = "0.1.0"
+name = "serde_json_lenient"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
+checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540"
dependencies = [
- "hashbrown 0.16.1",
- "thiserror 2.0.18",
+ "indexmap",
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
]
[[package]]
-name = "rusqlite"
-version = "0.39.0"
+name = "serde_repr"
+version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
- "bitflags 2.11.0",
- "fallible-iterator",
- "fallible-streaming-iterator",
- "libsqlite3-sys",
- "smallvec",
- "sqlite-wasm-rs",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "rust-ini"
-version = "0.20.0"
+name = "serde_spanned"
+version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
- "cfg-if",
- "ordered-multimap",
+ "serde",
]
[[package]]
-name = "rust_decimal"
-version = "1.40.0"
+name = "serde_spanned"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
- "arrayvec",
- "num-traits",
- "serde",
+ "serde_core",
]
[[package]]
-name = "rust_decimal_macros"
-version = "1.40.0"
+name = "serde_urlencoded"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
- "quote",
- "syn",
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
]
[[package]]
-name = "rustc-hash"
-version = "1.1.0"
+name = "sha1_smol"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
-name = "rustc-hash"
-version = "2.1.1"
+name = "sha2"
+version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
[[package]]
-name = "rustc_version"
-version = "0.4.1"
+name = "shlex"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
-dependencies = [
- "semver",
-]
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
-name = "rustix"
-version = "0.38.44"
+name = "signal-hook-registry"
+version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
- "bitflags 2.11.0",
"errno",
"libc",
- "linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
]
[[package]]
-name = "rustix"
-version = "1.1.4"
+name = "simd-adler32"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
-dependencies = [
- "bitflags 2.11.0",
- "errno",
- "libc",
- "linux-raw-sys 0.12.1",
- "windows-sys 0.61.2",
-]
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
-name = "rustls"
-version = "0.23.37"
+name = "simd_helpers"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
- "once_cell",
- "ring",
- "rustls-pki-types",
- "rustls-webpki",
- "subtle",
- "zeroize",
+ "quote",
]
[[package]]
-name = "rustls-pki-types"
-version = "1.14.0"
+name = "simplecss"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
dependencies = [
- "zeroize",
+ "log",
]
[[package]]
-name = "rustls-webpki"
-version = "0.103.10"
+name = "siphasher"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
+
+[[package]]
+name = "skrifa"
+version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
+checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac"
dependencies = [
- "ring",
- "rustls-pki-types",
- "untrusted",
+ "bytemuck",
+ "read-fonts",
]
[[package]]
-name = "rustversion"
-version = "1.0.22"
+name = "slab"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
-name = "salsa20"
-version = "0.10.2"
+name = "slotmap"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
dependencies = [
- "cipher",
+ "version_check",
]
[[package]]
-name = "same-file"
-version = "1.0.6"
+name = "smallvec"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
-dependencies = [
- "winapi-util",
-]
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
-name = "scoped-tls"
-version = "1.0.1"
+name = "smol"
+version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f"
+dependencies = [
+ "async-channel 2.5.0",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-net",
+ "async-process",
+ "blocking",
+ "futures-lite 2.6.1",
+]
[[package]]
-name = "scopeguard"
-version = "1.2.0"
+name = "smol_str"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
[[package]]
-name = "scrypt"
-version = "0.11.0"
+name = "socket2"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
- "password-hash",
- "pbkdf2",
- "salsa20",
- "sha2",
+ "libc",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "sctk-adwaita"
-version = "0.10.1"
+name = "spin"
+version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
- "ab_glyph",
- "log",
- "memmap2",
- "smithay-client-toolkit 0.19.2",
- "tiny-skia",
+ "lock_api",
]
[[package]]
-name = "secp256k1"
-version = "0.29.1"
+name = "spirv"
+version = "0.3.0+sdk-1.3.268.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
dependencies = [
- "rand 0.8.5",
- "secp256k1-sys",
- "serde",
+ "bitflags 2.11.1",
]
[[package]]
-name = "secp256k1-sys"
-version = "0.10.1"
+name = "stable_deref_trait"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
-dependencies = [
- "cc",
-]
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
-name = "security-framework"
-version = "2.11.1"
+name = "stacker"
+version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013"
dependencies = [
- "bitflags 2.11.0",
- "core-foundation 0.9.4",
- "core-foundation-sys",
+ "cc",
+ "cfg-if",
"libc",
- "security-framework-sys",
+ "psm",
+ "windows-sys 0.59.0",
]
[[package]]
-name = "security-framework"
-version = "3.7.0"
+name = "stacksafe"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090"
dependencies = [
- "bitflags 2.11.0",
- "core-foundation 0.10.1",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
+ "stacker",
+ "stacksafe-macro",
]
[[package]]
-name = "security-framework-sys"
-version = "2.17.0"
+name = "stacksafe-macro"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69"
dependencies = [
- "core-foundation-sys",
- "libc",
+ "proc-macro-error2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "semver"
-version = "1.0.27"
+name = "static_assertions"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
-name = "serde"
-version = "1.0.228"
+name = "strict-num"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
dependencies = [
- "serde_core",
- "serde_derive",
+ "float-cmp",
]
[[package]]
-name = "serde_core"
-version = "1.0.228"
+name = "strum"
+version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
- "serde_derive",
+ "strum_macros 0.26.4",
]
[[package]]
-name = "serde_derive"
-version = "1.0.228"
+name = "strum"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "strum_macros 0.27.2",
]
[[package]]
-name = "serde_json"
-version = "1.0.149"
+name = "strum_macros"
+version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.117",
]
[[package]]
-name = "serde_spanned"
-version = "0.6.9"
+name = "strum_macros"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
- "serde",
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "sha1"
-version = "0.10.6"
+name = "subtle"
+version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
-name = "sha2"
-version = "0.10.9"
+name = "sval"
+version = "2.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
+checksum = "2eb9318255ebd817902d7e279d8f8e39b35b1b9954decd5eb9ea0e30e5fd2b6a"
[[package]]
-name = "sharded-slab"
-version = "0.1.7"
+name = "sval_buffer"
+version = "2.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+checksum = "12571299185e653fdb0fbfe36cd7f6529d39d4e747a60b15a3f34574b7b97c61"
dependencies = [
- "lazy_static",
+ "sval",
+ "sval_ref",
]
[[package]]
-name = "shlex"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-[[package]]
-name = "signal-hook-registry"
-version = "1.4.8"
+name = "sval_dynamic"
+version = "2.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+checksum = "39526f24e997706c0de7f03fb7371f7f5638b66a504ded508e20ad173d0a3677"
dependencies = [
- "errno",
- "libc",
+ "sval",
]
[[package]]
-name = "simd-adler32"
-version = "0.3.8"
+name = "sval_fmt"
+version = "2.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+checksum = "933dd3bb26965d682280fcc49400ac2a05036f4ee1e6dbd61bf8402d5a5c3a54"
+dependencies = [
+ "itoa",
+ "ryu",
+ "sval",
+]
[[package]]
-name = "simd_cesu8"
-version = "1.1.1"
+name = "sval_json"
+version = "2.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+checksum = "a0cda08f6d5c9948024a6551077557b1fdcc3880ff2f20ae839667d2ec2d87ed"
dependencies = [
- "rustc_version",
- "simdutf8",
+ "itoa",
+ "ryu",
+ "sval",
]
[[package]]
-name = "simdutf8"
-version = "0.1.5"
+name = "sval_nested"
+version = "2.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+checksum = "88d49d5e6c1f9fd0e53515819b03a97ca4eb1bff5c8ee097c43391c09ecfb19f"
+dependencies = [
+ "sval",
+ "sval_buffer",
+ "sval_ref",
+]
[[package]]
-name = "slab"
-version = "0.4.12"
+name = "sval_ref"
+version = "2.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+checksum = "14f876c5a78405375b4e19cbb9554407513b59c93dea12dc6a4af4e1d30899ca"
+dependencies = [
+ "sval",
+]
[[package]]
-name = "slotmap"
-version = "1.1.1"
+name = "sval_serde"
+version = "2.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
+checksum = "5f9ccd3b7f7200239a655e517dd3fd48d960b9111ad24bd6a5e055bef17607c7"
dependencies = [
- "version_check",
+ "serde_core",
+ "sval",
+ "sval_nested",
]
[[package]]
-name = "smallvec"
-version = "1.15.1"
+name = "svg_fmt"
+version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
[[package]]
-name = "smithay-client-toolkit"
-version = "0.19.2"
+name = "svgtypes"
+version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
+checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
dependencies = [
- "bitflags 2.11.0",
- "calloop 0.13.0",
- "calloop-wayland-source 0.3.0",
- "cursor-icon",
- "libc",
- "log",
- "memmap2",
- "rustix 0.38.44",
- "thiserror 1.0.69",
- "wayland-backend",
- "wayland-client",
- "wayland-csd-frame",
- "wayland-cursor",
- "wayland-protocols",
- "wayland-protocols-wlr",
- "wayland-scanner",
- "xkeysym",
+ "kurbo",
+ "siphasher",
]
[[package]]
-name = "smithay-client-toolkit"
-version = "0.20.0"
+name = "swash"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
+checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64"
dependencies = [
- "bitflags 2.11.0",
- "calloop 0.14.4",
- "calloop-wayland-source 0.4.1",
- "cursor-icon",
- "libc",
- "log",
- "memmap2",
- "rustix 1.1.4",
- "thiserror 2.0.18",
- "wayland-backend",
- "wayland-client",
- "wayland-csd-frame",
- "wayland-cursor",
- "wayland-protocols",
- "wayland-protocols-experimental",
- "wayland-protocols-misc",
- "wayland-protocols-wlr",
- "wayland-scanner",
- "xkeysym",
+ "skrifa",
+ "yazi",
+ "zeno",
]
[[package]]
-name = "smithay-clipboard"
-version = "0.7.3"
+name = "syn"
+version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
- "libc",
- "smithay-client-toolkit 0.20.0",
- "wayland-backend",
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
]
[[package]]
-name = "smol_str"
-version = "0.2.2"
+name = "syn"
+version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
- "serde",
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
]
[[package]]
-name = "socket2"
-version = "0.6.3"
+name = "sync_wrapper"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
- "libc",
- "windows-sys 0.60.2",
+ "futures-core",
]
[[package]]
-name = "spirv"
-version = "0.3.0+sdk-1.3.268.0"
+name = "synstructure"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
- "bitflags 2.11.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "sqlite-wasm-rs"
-version = "0.5.2"
+name = "sys-locale"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
dependencies = [
- "cc",
- "js-sys",
- "rsqlite-vfs",
- "wasm-bindgen",
+ "libc",
]
[[package]]
-name = "stable_deref_trait"
-version = "1.2.1"
+name = "sysinfo"
+version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+ "memchr",
+ "ntapi",
+ "rayon",
+ "windows 0.57.0",
+]
[[package]]
-name = "static_assertions"
-version = "1.1.0"
+name = "system-configuration"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.11.1",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
[[package]]
-name = "strict-num"
-version = "0.1.1"
+name = "system-configuration-sys"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
[[package]]
-name = "subtle"
-version = "2.6.1"
+name = "taffy"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004"
+dependencies = [
+ "arrayvec",
+ "grid",
+ "serde",
+ "slotmap",
+]
[[package]]
-name = "syn"
-version = "2.0.117"
+name = "take-until"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
+checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
[[package]]
-name = "synstructure"
-version = "0.13.2"
+name = "tao-core-video-sys"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "cfg-if",
+ "core-foundation-sys",
+ "libc",
+ "objc",
]
[[package]]
@@ -4044,14 +5173,25 @@ version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
- "fastrand",
- "getrandom 0.3.4",
+ "fastrand 2.4.1",
+ "getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
]
[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4086,7 +5226,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -4097,16 +5237,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn",
-]
-
-[[package]]
-name = "thread_local"
-version = "1.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
-dependencies = [
- "cfg-if",
+ "syn 2.0.117",
]
[[package]]
@@ -4124,37 +5255,6 @@ dependencies = [
]
[[package]]
-name = "time"
-version = "0.3.47"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
-dependencies = [
- "deranged",
- "itoa",
- "num-conv",
- "powerfmt",
- "serde_core",
- "time-core",
- "time-macros",
-]
-
-[[package]]
-name = "time-core"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
-
-[[package]]
-name = "time-macros"
-version = "0.2.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
-dependencies = [
- "num-conv",
- "time-core",
-]
-
-[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4174,6 +5274,7 @@ dependencies = [
"bytemuck",
"cfg-if",
"log",
+ "png 0.17.16",
"tiny-skia-path",
]
@@ -4190,9 +5291,9 @@ dependencies = [
[[package]]
name = "tinystr"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [
"displaydoc",
"zerovec",
@@ -4215,32 +5316,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.50.0"
+version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
+checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"bytes",
"libc",
"mio",
"pin-project-lite",
- "signal-hook-registry",
"socket2",
- "tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
-name = "tokio-macros"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4263,19 +5351,16 @@ dependencies = [
]
[[package]]
-name = "tokio-tungstenite"
-version = "0.26.2"
+name = "tokio-util"
+version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
- "futures-util",
- "log",
- "rustls",
- "rustls-pki-types",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
"tokio",
- "tokio-rustls",
- "tungstenite",
- "webpki-roots 0.26.11",
]
[[package]]
@@ -4285,12 +5370,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
- "serde_spanned",
+ "serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4301,9 +5401,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
-version = "1.0.1+spec-1.1.0"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
@@ -4316,7 +5425,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
- "serde_spanned",
+ "serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
@@ -4324,23 +5433,23 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.25.5+spec-1.1.0"
+version = "0.25.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
+checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
dependencies = [
"indexmap",
- "toml_datetime 1.0.1+spec-1.1.0",
+ "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
- "winnow 1.0.0",
+ "winnow 1.0.1",
]
[[package]]
name = "toml_parser"
-version = "1.0.10+spec-1.1.0"
+version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
- "winnow 1.0.0",
+ "winnow 1.0.1",
]
[[package]]
@@ -4350,26 +5459,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
- "log",
"pin-project-lite",
+ "tracing-attributes",
"tracing-core",
]
[[package]]
-name = "tracing-appender"
-version = "0.2.4"
+name = "tracing-attributes"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
- "crossbeam-channel",
- "thiserror 2.0.18",
- "time",
- "tracing-subscriber",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
@@ -4379,71 +5520,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
- "valuable",
]
[[package]]
-name = "tracing-log"
-version = "0.2.0"
+name = "try-lock"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
-dependencies = [
- "log",
- "once_cell",
- "tracing-core",
-]
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
-name = "tracing-subscriber"
-version = "0.3.23"
+name = "ttf-parser"
+version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
-dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex-automata",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log",
-]
+checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4"
[[package]]
name = "ttf-parser"
-version = "0.25.1"
+version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
[[package]]
-name = "tungstenite"
-version = "0.26.2"
+name = "ttf-parser"
+version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
dependencies = [
- "bytes",
- "data-encoding",
- "http",
- "httparse",
- "log",
- "rand 0.9.2",
- "rustls",
- "rustls-pki-types",
- "sha1",
- "thiserror 2.0.18",
- "utf-8",
+ "core_maths",
]
[[package]]
-name = "type-map"
-version = "0.5.1"
+name = "typeid"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
-dependencies = [
- "rustc-hash 2.1.1",
-]
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
@@ -4452,10 +5562,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
-name = "ucd-trie"
-version = "0.1.7"
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86"
+
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]]
name = "unicode-ident"
@@ -4464,19 +5615,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
-name = "unicode-normalization"
-version = "0.1.25"
+name = "unicode-linebreak"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
-dependencies = [
- "tinyvec",
-]
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+
+[[package]]
+name = "unicode-script"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
[[package]]
name = "unicode-segmentation"
-version = "1.12.0"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-vo"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]]
name = "unicode-width"
@@ -4491,16 +5657,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
-name = "universal-hash"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
-dependencies = [
- "crypto-common",
- "subtle",
-]
-
-[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4520,6 +5676,33 @@ dependencies = [
]
[[package]]
+name = "usvg"
+version = "0.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"
+dependencies = [
+ "base64",
+ "data-url",
+ "flate2",
+ "fontdb 0.23.0",
+ "imagesize",
+ "kurbo",
+ "log",
+ "pico-args",
+ "roxmltree",
+ "rustybuzz 0.20.1",
+ "simplecss",
+ "siphasher",
+ "strict-num",
+ "svgtypes",
+ "tiny-skia-path",
+ "unicode-bidi",
+ "unicode-script",
+ "unicode-vo",
+ "xmlwriter",
+]
+
+[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4533,26 +5716,63 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
-version = "1.22.0"
+version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
+checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"getrandom 0.4.2",
"js-sys",
+ "serde_core",
+ "sha1_smol",
"wasm-bindgen",
]
[[package]]
-name = "valuable"
-version = "0.1.1"
+name = "v_frame"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
[[package]]
-name = "vcpkg"
-version = "0.2.15"
+name = "value-bag"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
+dependencies = [
+ "value-bag-serde1",
+ "value-bag-sval2",
+]
+
+[[package]]
+name = "value-bag-serde1"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5"
+dependencies = [
+ "erased-serde",
+ "serde_core",
+ "serde_fmt",
+]
+
+[[package]]
+name = "value-bag-sval2"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f"
+dependencies = [
+ "sval",
+ "sval_buffer",
+ "sval_dynamic",
+ "sval_fmt",
+ "sval_json",
+ "sval_ref",
+ "sval_serde",
+]
[[package]]
name = "version_check"
@@ -4561,6 +5781,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "waker-fn"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
+
+[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4571,6 +5817,15 @@ dependencies = [
]
[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4596,9 +5851,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.114"
+version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
+checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [
"cfg-if",
"once_cell",
@@ -4609,23 +5864,19 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.64"
+version = "0.4.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
+checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
dependencies = [
- "cfg-if",
- "futures-util",
"js-sys",
- "once_cell",
"wasm-bindgen",
- "web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.114"
+version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
+checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4633,22 +5884,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.114"
+version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
+checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.114"
+version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
+checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [
"unicode-ident",
]
@@ -4676,12 +5927,25 @@ dependencies = [
]
[[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 = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
- "bitflags 2.11.0",
+ "bitflags 2.11.1",
"hashbrown 0.15.5",
"indexmap",
"semver",
@@ -4689,9 +5953,9 @@ dependencies = [
[[package]]
name = "wayland-backend"
-version = "0.3.14"
+version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406"
+checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
dependencies = [
"cc",
"downcast-rs",
@@ -4703,32 +5967,21 @@ dependencies = [
[[package]]
name = "wayland-client"
-version = "0.31.13"
+version = "0.31.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3"
+checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
dependencies = [
- "bitflags 2.11.0",
+ "bitflags 2.11.1",
"rustix 1.1.4",
"wayland-backend",
"wayland-scanner",
]
[[package]]
-name = "wayland-csd-frame"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
-dependencies = [
- "bitflags 2.11.0",
- "cursor-icon",
- "wayland-backend",
-]
-
-[[package]]
name = "wayland-cursor"
-version = "0.31.13"
+version = "0.31.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091"
+checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d"
dependencies = [
"rustix 1.1.4",
"wayland-client",
@@ -4737,84 +5990,57 @@ dependencies = [
[[package]]
name = "wayland-protocols"
-version = "0.32.11"
+version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7"
+checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
- "bitflags 2.11.0",
+ "bitflags 2.11.1",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
-name = "wayland-protocols-experimental"
-version = "20250721.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
-dependencies = [
- "bitflags 2.11.0",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-protocols-misc"
-version = "0.3.11"
+name = "wayland-protocols"
+version = "0.32.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "429b99200febaf95d4f4e46deff6fe4382bcff3280ee16a41cf887b3c3364984"
+checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
dependencies = [
- "bitflags 2.11.0",
+ "bitflags 2.11.1",
"wayland-backend",
"wayland-client",
- "wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-plasma"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c"
-dependencies = [
- "bitflags 2.11.0",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-protocols-wlr"
-version = "0.3.11"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235"
+checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
dependencies = [
- "bitflags 2.11.0",
+ "bitflags 2.11.1",
"wayland-backend",
"wayland-client",
- "wayland-protocols",
+ "wayland-protocols 0.31.2",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
-version = "0.31.9"
+version = "0.31.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3"
+checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
dependencies = [
"proc-macro2",
- "quick-xml",
+ "quick-xml 0.39.2",
"quote",
]
[[package]]
name = "wayland-sys"
-version = "0.31.10"
+version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17"
+checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
dependencies = [
"dlib",
"log",
@@ -4824,9 +6050,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.91"
+version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
+checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4843,281 +6069,266 @@ dependencies = [
]
[[package]]
-name = "webbrowser"
-version = "1.2.0"
+name = "weezl"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16"
-dependencies = [
- "core-foundation 0.10.1",
- "jni 0.22.4",
- "log",
- "ndk-context",
- "objc2 0.6.4",
- "objc2-foundation 0.3.2",
- "url",
- "web-sys",
-]
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
-name = "webpki-roots"
-version = "0.26.11"
+name = "which"
+version = "6.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
dependencies = [
- "webpki-roots 1.0.6",
+ "either",
+ "home",
+ "rustix 0.38.44",
+ "winsafe",
]
[[package]]
-name = "webpki-roots"
-version = "1.0.6"
+name = "winapi"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
- "rustls-pki-types",
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
]
[[package]]
-name = "weezl"
-version = "0.1.12"
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
-name = "wgpu"
-version = "27.0.1"
+name = "winapi-util"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "arrayvec",
- "bitflags 2.11.0",
- "cfg-if",
- "cfg_aliases",
- "document-features",
- "hashbrown 0.16.1",
- "js-sys",
- "log",
- "naga",
- "portable-atomic",
- "profiling",
- "raw-window-handle",
- "smallvec",
- "static_assertions",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "wgpu-core",
- "wgpu-hal",
- "wgpu-types",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "wgpu-core"
-version = "27.0.3"
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
+checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
- "arrayvec",
- "bit-set",
- "bit-vec",
- "bitflags 2.11.0",
- "bytemuck",
- "cfg_aliases",
- "document-features",
- "hashbrown 0.16.1",
- "indexmap",
- "log",
- "naga",
- "once_cell",
- "parking_lot",
- "portable-atomic",
- "profiling",
- "raw-window-handle",
- "rustc-hash 1.1.0",
- "smallvec",
- "thiserror 2.0.18",
- "wgpu-core-deps-apple",
- "wgpu-core-deps-emscripten",
- "wgpu-core-deps-windows-linux-android",
- "wgpu-hal",
- "wgpu-types",
+ "windows-core 0.57.0",
+ "windows-targets 0.52.6",
]
[[package]]
-name = "wgpu-core-deps-apple"
-version = "27.0.0"
+name = "windows"
+version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
- "wgpu-hal",
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link 0.1.3",
+ "windows-numerics",
]
[[package]]
-name = "wgpu-core-deps-emscripten"
-version = "27.0.0"
+name = "windows-capture"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5"
+checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24"
dependencies = [
- "wgpu-hal",
+ "parking_lot",
+ "rayon",
+ "thiserror 2.0.18",
+ "windows 0.61.3",
+ "windows-future",
]
[[package]]
-name = "wgpu-core-deps-windows-linux-android"
-version = "27.0.0"
+name = "windows-collections"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
- "wgpu-hal",
+ "windows-core 0.61.2",
]
[[package]]
-name = "wgpu-hal"
-version = "27.0.4"
+name = "windows-core"
+version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce"
+checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
- "android_system_properties",
- "arrayvec",
- "ash",
- "bit-set",
- "bitflags 2.11.0",
- "block",
- "bytemuck",
- "cfg-if",
- "cfg_aliases",
- "core-graphics-types 0.2.0",
- "glow",
- "glutin_wgl_sys",
- "gpu-alloc",
- "gpu-allocator",
- "gpu-descriptor",
- "hashbrown 0.16.1",
- "js-sys",
- "khronos-egl",
- "libc",
- "libloading",
- "log",
- "metal",
- "naga",
- "ndk-sys",
- "objc",
- "once_cell",
- "ordered-float",
- "parking_lot",
- "portable-atomic",
- "portable-atomic-util",
- "profiling",
- "range-alloc",
- "raw-window-handle",
- "renderdoc-sys",
- "smallvec",
- "thiserror 2.0.18",
- "wasm-bindgen",
- "web-sys",
- "wgpu-types",
- "windows",
- "windows-core",
+ "windows-implement 0.57.0",
+ "windows-interface 0.57.0",
+ "windows-result 0.1.2",
+ "windows-targets 0.52.6",
]
[[package]]
-name = "wgpu-types"
-version = "27.0.1"
+name = "windows-core"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
- "bitflags 2.11.0",
- "bytemuck",
- "js-sys",
- "log",
- "thiserror 2.0.18",
- "web-sys",
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
]
[[package]]
-name = "winapi-util"
-version = "0.1.11"
+name = "windows-future"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+ "windows-threading",
]
[[package]]
-name = "windows"
-version = "0.58.0"
+name = "windows-implement"
+version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
+checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
- "windows-core",
- "windows-targets 0.52.6",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "windows-core"
-version = "0.58.0"
+name = "windows-implement"
+version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-result",
- "windows-strings",
- "windows-targets 0.52.6",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
]
[[package]]
-name = "windows-implement"
-version = "0.58.0"
+name = "windows-interface"
+version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
+checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
name = "windows-interface"
-version = "0.58.0"
+version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
-name = "windows-result"
+name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-registry"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
+dependencies = [
+ "windows-result 0.3.4",
+ "windows-strings 0.3.1",
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
name = "windows-strings"
-version = "0.1.0"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
- "windows-result",
- "windows-targets 0.52.6",
+ "windows-link 0.1.3",
]
[[package]]
name = "windows-sys"
-version = "0.45.0"
+version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
- "windows-targets 0.42.2",
+ "windows-targets 0.48.5",
]
[[package]]
@@ -5153,22 +6364,22 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows-link",
+ "windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
- "windows_aarch64_gnullvm 0.42.2",
- "windows_aarch64_msvc 0.42.2",
- "windows_i686_gnu 0.42.2",
- "windows_i686_msvc 0.42.2",
- "windows_x86_64_gnu 0.42.2",
- "windows_x86_64_gnullvm 0.42.2",
- "windows_x86_64_msvc 0.42.2",
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
]
[[package]]
@@ -5193,7 +6404,7 @@ version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
- "windows-link",
+ "windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
@@ -5205,10 +6416,19 @@ dependencies = [
]
[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
@@ -5224,9 +6444,9 @@ checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
@@ -5242,9 +6462,9 @@ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
@@ -5272,9 +6492,9 @@ checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
@@ -5290,9 +6510,9 @@ checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
@@ -5308,9 +6528,9 @@ checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
@@ -5326,9 +6546,9 @@ checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.2"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
@@ -5343,73 +6563,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
-name = "winit"
-version = "0.30.13"
+name = "winnow"
+version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
- "ahash",
- "android-activity",
- "atomic-waker",
- "bitflags 2.11.0",
- "block2",
- "bytemuck",
- "calloop 0.13.0",
- "cfg_aliases",
- "concurrent-queue",
- "core-foundation 0.9.4",
- "core-graphics",
- "cursor-icon",
- "dpi",
- "js-sys",
- "libc",
- "memmap2",
- "ndk",
- "objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
- "objc2-ui-kit",
- "orbclient",
- "percent-encoding",
- "pin-project",
- "raw-window-handle",
- "redox_syscall 0.4.1",
- "rustix 0.38.44",
- "sctk-adwaita",
- "smithay-client-toolkit 0.19.2",
- "smol_str",
- "tracing",
- "unicode-segmentation",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "wayland-protocols-plasma",
- "web-sys",
- "web-time",
- "windows-sys 0.52.0",
- "x11-dl",
- "x11rb",
- "xkbcommon-dl",
+ "memchr",
]
[[package]]
name = "winnow"
-version = "0.7.15"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
dependencies = [
"memchr",
]
[[package]]
-name = "winnow"
-version = "1.0.0"
+name = "winreg"
+version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
- "memchr",
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winsafe"
+version = "0.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
+
+[[package]]
+name = "wio"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5"
+dependencies = [
+ "winapi",
]
[[package]]
@@ -5428,7 +6621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
- "heck",
+ "heck 0.5.0",
"wit-parser",
]
@@ -5439,10 +6632,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
- "heck",
+ "heck 0.5.0",
"indexmap",
"prettyplease",
- "syn",
+ "syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -5458,7 +6651,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -5470,7 +6663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
- "bitflags 2.11.0",
+ "bitflags 2.11.1",
"indexmap",
"log",
"serde",
@@ -5502,22 +6695,31 @@ dependencies = [
[[package]]
name = "writeable"
-version = "0.6.2"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
-name = "x11-dl"
+name = "x11"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
dependencies = [
"libc",
- "once_cell",
"pkg-config",
]
[[package]]
+name = "x11-clipboard"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3"
+dependencies = [
+ "libc",
+ "x11rb",
+]
+
+[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5526,10 +6728,9 @@ dependencies = [
"as-raw-xcb-connection",
"gethostname",
"libc",
- "libloading",
- "once_cell",
"rustix 1.1.4",
"x11rb-protocol",
+ "xcursor",
]
[[package]]
@@ -5539,21 +6740,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
+name = "xattr"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "xcb"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+ "quick-xml 0.30.0",
+ "x11",
+]
+
+[[package]]
name = "xcursor"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
[[package]]
-name = "xkbcommon-dl"
-version = "0.4.2"
+name = "xim-ctext"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
+checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c"
dependencies = [
- "bitflags 2.11.0",
- "dlib",
- "log",
- "once_cell",
+ "encoding_rs",
+]
+
+[[package]]
+name = "xim-parser"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "xkbcommon"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9"
+dependencies = [
+ "as-raw-xcb-connection",
+ "libc",
+ "memmap2",
"xkeysym",
]
@@ -5564,27 +6803,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
-name = "xml-rs"
-version = "0.8.28"
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
+[[package]]
+name = "y4m"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]]
-name = "yaml-rust2"
-version = "0.8.1"
+name = "yazi"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
+
+[[package]]
+name = "yeslogic-fontconfig-sys"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
+checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd"
dependencies = [
- "arraydeque",
- "encoding_rs",
- "hashlink",
+ "dlib",
+ "once_cell",
+ "pkg-config",
]
[[package]]
name = "yoke"
-version = "0.8.1"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
dependencies = [
"stable_deref_trait",
"yoke-derive",
@@ -5593,54 +6844,246 @@ dependencies = [
[[package]]
name = "yoke-derive"
-version = "0.8.1"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"synstructure",
]
[[package]]
+name = "zbus"
+version = "5.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener 5.4.1",
+ "futures-core",
+ "futures-lite 2.6.1",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix 1.1.4",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 0.7.15",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
+dependencies = [
+ "serde",
+ "winnow 0.7.15",
+ "zvariant",
+]
+
+[[package]]
+name = "zed-async-tar"
+version = "0.5.0-zed"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cf4b5f655e29700e473cb1acd914ab112b37b62f96f7e642d5fc6a0c02eb881"
+dependencies = [
+ "async-std",
+ "filetime",
+ "libc",
+ "pin-project",
+ "redox_syscall 0.2.16",
+ "xattr",
+]
+
+[[package]]
+name = "zed-font-kit"
+version = "0.14.1-zed"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5"
+dependencies = [
+ "bitflags 2.11.1",
+ "byteorder",
+ "core-foundation 0.10.0",
+ "core-graphics 0.24.0",
+ "core-text",
+ "dirs 5.0.1",
+ "dwrote",
+ "float-ord",
+ "freetype-sys",
+ "lazy_static",
+ "libc",
+ "log",
+ "pathfinder_geometry",
+ "pathfinder_simd",
+ "walkdir",
+ "winapi",
+ "yeslogic-fontconfig-sys",
+]
+
+[[package]]
+name = "zed-reqwest"
+version = "0.12.15-zed"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac2d05756ff48539950c3282ad7acf3817ad3f08797c205ad1c34a2ce03b9970"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "mime_guess",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-rustls",
+ "tokio-socks",
+ "tokio-util",
+ "tower",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "windows-registry 0.4.0",
+]
+
+[[package]]
+name = "zed-scap"
+version = "0.0.8-zed"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8"
+dependencies = [
+ "anyhow",
+ "cocoa 0.25.0",
+ "core-graphics-helmer-fork",
+ "log",
+ "objc",
+ "rand 0.8.6",
+ "screencapturekit",
+ "screencapturekit-sys",
+ "sysinfo",
+ "tao-core-video-sys",
+ "windows 0.61.3",
+ "windows-capture",
+ "x11",
+ "xcb",
+]
+
+[[package]]
+name = "zed-xim"
+version = "0.4.0-zed"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642"
+dependencies = [
+ "ahash",
+ "hashbrown 0.14.5",
+ "log",
+ "x11rb",
+ "xim-ctext",
+ "xim-parser",
+]
+
+[[package]]
+name = "zeno"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
+
+[[package]]
name = "zerocopy"
-version = "0.8.47"
+version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.47"
+version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
name = "zerofrom"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
"synstructure",
]
@@ -5661,14 +7104,14 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
name = "zerotrie"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
dependencies = [
"displaydoc",
"yoke",
@@ -5677,9 +7120,9 @@ dependencies = [
[[package]]
name = "zerovec"
-version = "0.11.5"
+version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [
"yoke",
"zerofrom",
@@ -5688,13 +7131,13 @@ dependencies = [
[[package]]
name = "zerovec-derive"
-version = "0.11.2"
+version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.117",
]
[[package]]
@@ -5710,10 +7153,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
name = "zune-jpeg"
-version = "0.5.13"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
+
+[[package]]
+name = "zvariant"
+version = "5.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "url",
+ "winnow 0.7.15",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.117",
+ "winnow 0.7.15",
+]
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,18 +1,5 @@
-[workspace]
-members = [
- "crates/shared/core",
- "crates/shared/remote_signer",
- "crates/shared/test_support",
- "crates/bridges/android/security",
- "crates/bridges/apple/security",
- "crates/launchers/android",
- "crates/launchers/desktop",
- "crates/launchers/ios",
- "crates/launchers/web",
-]
-resolver = "2"
-
-[workspace.package]
+[package]
+name = "radroots_app"
version = "0.1.0"
edition = "2024"
authors = ["Radroots Authors"]
@@ -21,42 +8,12 @@ license = "GPL-3.0"
repository = "https://github.com/radrootslabs/app"
homepage = "https://radroots.org"
readme = "README.md"
+publish = false
-[workspace.dependencies]
-android_logger = "0.15.1"
-eframe = { version = "0.33.3", default-features = false, features = ["default_fonts"] }
-egui = { version = "0.33.3", features = ["serde"] }
-image = { version = "0.25.10", default-features = false, features = ["ico", "png"] }
-jni = "0.21.1"
-log = "0.4.28"
-ndk-context = "0.1.1"
-nostr = { version = "0.44.1", default-features = false, features = ["std"] }
-nostr-browser-signer = "0.44.1"
-objc2-foundation = { version = "0.3.2", default-features = false, features = ["std"] }
-radroots_app_android_security = { path = "crates/bridges/android/security" }
-radroots_app_apple_security = { path = "crates/bridges/apple/security" }
-radroots_geocoder = { path = "../lib/crates/geocoder" }
-radroots_identity = { path = "../lib/crates/identity", default-features = false, features = ["std", "nip49"] }
-radroots_nostr = { path = "../lib/crates/nostr", default-features = false, features = ["std", "client"] }
-radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts", default-features = false, features = ["std", "file-store", "os-keyring"] }
-radroots_nostr_connect = { path = "../lib/crates/nostr_connect" }
-radroots_runtime_paths = { path = "../lib/crates/runtime_paths" }
-radroots_secret_vault = { path = "../lib/crates/secret_vault", default-features = false, features = ["std"] }
-serde = { version = "1.0.228", features = ["derive"] }
-serde_json = "1.0.145"
-tokio = { version = "1.48.0", features = ["rt", "sync", "time"] }
-url = "2.5.7"
-wasm-bindgen-futures = "0.4.50"
-web-sys = { version = "0.3.91", features = ["Document", "HtmlCanvasElement", "Window"] }
-wgpu = { version = "27.0.1", default-features = false }
-winit = { version = "0.30.13", features = ["android-game-activity"] }
-zeroize = "1.8.2"
+[workspace]
-[workspace.lints.rust]
-unsafe_code = "forbid"
+[dependencies]
+gpui = "0.2.2"
-[profile.release]
-codegen-units = 1
-lto = true
-opt-level = "z"
-strip = true
+[lints.rust]
+unsafe_code = "forbid"
diff --git a/assets/geocoder/.gitkeep b/assets/geocoder/.gitkeep
@@ -1 +0,0 @@
-
diff --git a/crates/bridges/android/security/Cargo.toml b/crates/bridges/android/security/Cargo.toml
@@ -1,20 +0,0 @@
-[package]
-name = "radroots_app_android_security"
-authors.workspace = true
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots Android security bridge"
-publish = false
-
-[dependencies]
-radroots_nostr_accounts.workspace = true
-radroots_secret_vault.workspace = true
-zeroize.workspace = true
-
-[target.'cfg(target_os = "android")'.dependencies]
-jni.workspace = true
-ndk-context.workspace = true
diff --git a/crates/bridges/android/security/src/lib.rs b/crates/bridges/android/security/src/lib.rs
@@ -1,9 +0,0 @@
-mod security;
-mod vault;
-
-pub use security::{
- ANDROID_NOSTR_SERVICE, AndroidUserPresenceVerificationResult,
- begin_user_presence_verification, is_user_presence_verification_pending,
- resolve_radroots_base_root, take_user_presence_verification_result,
-};
-pub use vault::RadrootsAndroidKeystoreVault;
diff --git a/crates/bridges/android/security/src/security.rs b/crates/bridges/android/security/src/security.rs
@@ -1,553 +0,0 @@
-#![cfg_attr(not(target_os = "android"), allow(dead_code))]
-
-use radroots_nostr_accounts::prelude::RadrootsNostrAccountsError;
-use std::path::PathBuf;
-
-pub const ANDROID_NOSTR_SERVICE: &str = "org.radroots.app.nostr";
-pub(crate) const ANDROID_NOSTR_NAMESPACE: &str = "nostr";
-
-#[cfg(target_os = "android")]
-use jni::objects::{JByteArray, JClass, JObject, JString, JValue};
-#[cfg(target_os = "android")]
-use jni::sys::{jboolean, jobject};
-#[cfg(target_os = "android")]
-use jni::{JNIEnv, JavaVM};
-
-#[cfg(target_os = "android")]
-const ANDROID_SECURITY_BRIDGE_CLASS: &str =
- "org.radroots.app.android.security.RadRootsAndroidSecurityBridge";
-
-#[cfg(target_os = "android")]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum AndroidSecretStatus {
- Success,
- NotFound,
- InvalidInput,
- Error,
-}
-
-#[cfg(target_os = "android")]
-impl AndroidSecretStatus {
- fn from_raw(value: i32) -> Result<Self, RadrootsNostrAccountsError> {
- match value {
- 0 => Ok(Self::Success),
- 1 => Ok(Self::NotFound),
- 2 => Ok(Self::InvalidInput),
- 3 => Ok(Self::Error),
- other => Err(RadrootsNostrAccountsError::Vault(format!(
- "unknown android security bridge status {other}"
- ))),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum AndroidUserPresenceVerificationResult {
- Verified,
- Failed(String),
-}
-
-#[cfg(target_os = "android")]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum AndroidUserPresenceResultStatus {
- None,
- Success,
- Error,
-}
-
-#[cfg(target_os = "android")]
-impl AndroidUserPresenceResultStatus {
- fn from_raw(value: i32) -> Result<Self, RadrootsNostrAccountsError> {
- match value {
- 0 => Ok(Self::None),
- 1 => Ok(Self::Success),
- 2 => Ok(Self::Error),
- other => Err(RadrootsNostrAccountsError::Vault(format!(
- "unknown android user presence status {other}"
- ))),
- }
- }
-}
-
-#[cfg(target_os = "android")]
-pub(crate) fn store_secret(
- service: &str,
- namespace: &str,
- name: &str,
- value: &[u8],
- device_local_only: bool,
- user_presence_required: bool,
- prefer_strong_box: bool,
-) -> Result<(), RadrootsNostrAccountsError> {
- let java_vm = android_java_vm()?;
- let mut env = java_vm.attach_current_thread().map_err(jni_error)?;
- let bridge_class = bridge_class(&mut env)?;
- let service = java_string_arg(&mut env, service)?;
- let namespace = java_string_arg(&mut env, namespace)?;
- let name = java_string_arg(&mut env, name)?;
- let value = env.byte_array_from_slice(value).map_err(jni_error)?;
- let value = JObject::from(value);
-
- let status = env
- .call_static_method(
- &bridge_class,
- "putSecret",
- "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BZZZ)I",
- &[
- JValue::Object(&service),
- JValue::Object(&namespace),
- JValue::Object(&name),
- JValue::Object(&value),
- JValue::Bool(bool_to_jboolean(device_local_only)),
- JValue::Bool(bool_to_jboolean(user_presence_required)),
- JValue::Bool(bool_to_jboolean(prefer_strong_box)),
- ],
- )
- .and_then(|value| value.i())
- .map_err(jni_error)?;
-
- match AndroidSecretStatus::from_raw(status)? {
- AndroidSecretStatus::Success => Ok(()),
- AndroidSecretStatus::NotFound => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android security bridge reported not found during store",
- )),
- AndroidSecretStatus::InvalidInput => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android security bridge rejected the store request",
- )),
- AndroidSecretStatus::Error => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android keystore store failed",
- )),
- }
-}
-
-#[cfg(not(target_os = "android"))]
-pub(crate) fn store_secret(
- service: &str,
- namespace: &str,
- name: &str,
- value: &[u8],
- device_local_only: bool,
- user_presence_required: bool,
- prefer_strong_box: bool,
-) -> Result<(), RadrootsNostrAccountsError> {
- let _ = (
- service,
- namespace,
- name,
- value,
- device_local_only,
- user_presence_required,
- prefer_strong_box,
- );
- Err(RadrootsNostrAccountsError::Vault(
- "android keystore storage is only available on android".to_owned(),
- ))
-}
-
-#[cfg(target_os = "android")]
-pub(crate) fn load_secret(
- service: &str,
- namespace: &str,
- name: &str,
-) -> Result<Option<Vec<u8>>, RadrootsNostrAccountsError> {
- let java_vm = android_java_vm()?;
- let mut env = java_vm.attach_current_thread().map_err(jni_error)?;
- let bridge_class = bridge_class(&mut env)?;
- let service = java_string_arg(&mut env, service)?;
- let namespace = java_string_arg(&mut env, namespace)?;
- let name = java_string_arg(&mut env, name)?;
-
- let value = env
- .call_static_method(
- &bridge_class,
- "getSecret",
- "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B",
- &[
- JValue::Object(&service),
- JValue::Object(&namespace),
- JValue::Object(&name),
- ],
- )
- .and_then(|value| value.l())
- .map_err(jni_error)?;
-
- if value.is_null() {
- let Some(message) = take_last_error_message(&mut env, &bridge_class)? else {
- return Ok(None);
- };
- return Err(RadrootsNostrAccountsError::Vault(message));
- }
-
- let value = JByteArray::from(value);
- env.convert_byte_array(&value).map(Some).map_err(jni_error)
-}
-
-#[cfg(not(target_os = "android"))]
-pub(crate) fn load_secret(
- service: &str,
- namespace: &str,
- name: &str,
-) -> Result<Option<Vec<u8>>, RadrootsNostrAccountsError> {
- let _ = (service, namespace, name);
- Err(RadrootsNostrAccountsError::Vault(
- "android keystore storage is only available on android".to_owned(),
- ))
-}
-
-#[cfg(target_os = "android")]
-pub(crate) fn remove_secret(
- service: &str,
- namespace: &str,
- name: &str,
-) -> Result<(), RadrootsNostrAccountsError> {
- let java_vm = android_java_vm()?;
- let mut env = java_vm.attach_current_thread().map_err(jni_error)?;
- let bridge_class = bridge_class(&mut env)?;
- let service = java_string_arg(&mut env, service)?;
- let namespace = java_string_arg(&mut env, namespace)?;
- let name = java_string_arg(&mut env, name)?;
-
- let status = env
- .call_static_method(
- &bridge_class,
- "deleteSecret",
- "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
- &[
- JValue::Object(&service),
- JValue::Object(&namespace),
- JValue::Object(&name),
- ],
- )
- .and_then(|value| value.i())
- .map_err(jni_error)?;
-
- match AndroidSecretStatus::from_raw(status)? {
- AndroidSecretStatus::Success | AndroidSecretStatus::NotFound => Ok(()),
- AndroidSecretStatus::InvalidInput => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android security bridge rejected the delete request",
- )),
- AndroidSecretStatus::Error => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android keystore delete failed",
- )),
- }
-}
-
-#[cfg(not(target_os = "android"))]
-pub(crate) fn remove_secret(
- service: &str,
- namespace: &str,
- name: &str,
-) -> Result<(), RadrootsNostrAccountsError> {
- let _ = (service, namespace, name);
- Err(RadrootsNostrAccountsError::Vault(
- "android keystore storage is only available on android".to_owned(),
- ))
-}
-
-#[cfg(target_os = "android")]
-pub(crate) fn remove_secret_namespace(
- service: &str,
- namespace: &str,
-) -> Result<(), RadrootsNostrAccountsError> {
- let java_vm = android_java_vm()?;
- let mut env = java_vm.attach_current_thread().map_err(jni_error)?;
- let bridge_class = bridge_class(&mut env)?;
- let service = java_string_arg(&mut env, service)?;
- let namespace = java_string_arg(&mut env, namespace)?;
-
- let status = env
- .call_static_method(
- &bridge_class,
- "deleteSecretNamespace",
- "(Ljava/lang/String;Ljava/lang/String;)I",
- &[JValue::Object(&service), JValue::Object(&namespace)],
- )
- .and_then(|value| value.i())
- .map_err(jni_error)?;
-
- match AndroidSecretStatus::from_raw(status)? {
- AndroidSecretStatus::Success | AndroidSecretStatus::NotFound => Ok(()),
- AndroidSecretStatus::InvalidInput => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android security bridge rejected the namespace delete request",
- )),
- AndroidSecretStatus::Error => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android keystore namespace delete failed",
- )),
- }
-}
-
-#[cfg(not(target_os = "android"))]
-pub(crate) fn remove_secret_namespace(
- service: &str,
- namespace: &str,
-) -> Result<(), RadrootsNostrAccountsError> {
- let _ = (service, namespace);
- Err(RadrootsNostrAccountsError::Vault(
- "android keystore storage is only available on android".to_owned(),
- ))
-}
-
-#[cfg(target_os = "android")]
-pub fn resolve_radroots_base_root() -> Result<PathBuf, RadrootsNostrAccountsError> {
- let java_vm = android_java_vm()?;
- let mut env = java_vm.attach_current_thread().map_err(jni_error)?;
- let bridge_class = bridge_class(&mut env)?;
- let value = env
- .call_static_method(
- &bridge_class,
- "resolveRadrootsBaseRoot",
- "()Ljava/lang/String;",
- &[],
- )
- .and_then(|value| value.l())
- .map_err(jni_error)?;
-
- if value.is_null() {
- return Err(bridge_store_error(
- &mut env,
- &bridge_class,
- "android security bridge returned no storage root",
- ));
- }
-
- let value = JString::from(value);
- let path: String = env.get_string(&value).map_err(jni_error)?.into();
- Ok(PathBuf::from(path))
-}
-
-#[cfg(target_os = "android")]
-pub fn begin_user_presence_verification(
- reason: &str,
-) -> Result<(), RadrootsNostrAccountsError> {
- let java_vm = android_java_vm()?;
- let mut env = java_vm.attach_current_thread().map_err(jni_error)?;
- let bridge_class = bridge_class(&mut env)?;
- let reason = java_string_arg(&mut env, reason)?;
-
- let status = env
- .call_static_method(
- &bridge_class,
- "beginUserPresenceVerification",
- "(Ljava/lang/String;)I",
- &[JValue::Object(&reason)],
- )
- .and_then(|value| value.i())
- .map_err(jni_error)?;
-
- match AndroidSecretStatus::from_raw(status)? {
- AndroidSecretStatus::Success => Ok(()),
- AndroidSecretStatus::NotFound => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android security bridge reported no user presence result",
- )),
- AndroidSecretStatus::InvalidInput => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android security bridge rejected the user presence request",
- )),
- AndroidSecretStatus::Error => Err(bridge_vault_error(
- &mut env,
- &bridge_class,
- "android user presence verification failed to start",
- )),
- }
-}
-
-#[cfg(not(target_os = "android"))]
-pub fn begin_user_presence_verification(
- reason: &str,
-) -> Result<(), RadrootsNostrAccountsError> {
- let _ = reason;
- Err(RadrootsNostrAccountsError::Vault(
- "android user presence verification is only available on android".to_owned(),
- ))
-}
-
-#[cfg(target_os = "android")]
-pub fn is_user_presence_verification_pending() -> Result<bool, RadrootsNostrAccountsError> {
- let java_vm = android_java_vm()?;
- let mut env = java_vm.attach_current_thread().map_err(jni_error)?;
- let bridge_class = bridge_class(&mut env)?;
-
- env.call_static_method(
- &bridge_class,
- "isUserPresenceVerificationPending",
- "()Z",
- &[],
- )
- .and_then(|value| value.z())
- .map_err(jni_error)
-}
-
-#[cfg(not(target_os = "android"))]
-pub fn is_user_presence_verification_pending() -> Result<bool, RadrootsNostrAccountsError> {
- Err(RadrootsNostrAccountsError::Vault(
- "android user presence verification is only available on android".to_owned(),
- ))
-}
-
-#[cfg(target_os = "android")]
-pub fn take_user_presence_verification_result()
--> Result<Option<AndroidUserPresenceVerificationResult>, RadrootsNostrAccountsError> {
- let java_vm = android_java_vm()?;
- let mut env = java_vm.attach_current_thread().map_err(jni_error)?;
- let bridge_class = bridge_class(&mut env)?;
-
- let status = env
- .call_static_method(
- &bridge_class,
- "takeUserPresenceVerificationResult",
- "()I",
- &[],
- )
- .and_then(|value| value.i())
- .map_err(jni_error)?;
-
- match AndroidUserPresenceResultStatus::from_raw(status)? {
- AndroidUserPresenceResultStatus::None => Ok(None),
- AndroidUserPresenceResultStatus::Success => {
- Ok(Some(AndroidUserPresenceVerificationResult::Verified))
- }
- AndroidUserPresenceResultStatus::Error => {
- Ok(Some(AndroidUserPresenceVerificationResult::Failed(
- take_last_error_message(&mut env, &bridge_class)?
- .unwrap_or_else(|| "android device authentication failed".to_owned()),
- )))
- }
- }
-}
-
-#[cfg(not(target_os = "android"))]
-pub fn take_user_presence_verification_result()
--> Result<Option<AndroidUserPresenceVerificationResult>, RadrootsNostrAccountsError> {
- Err(RadrootsNostrAccountsError::Vault(
- "android user presence verification is only available on android".to_owned(),
- ))
-}
-
-#[cfg(not(target_os = "android"))]
-#[allow(dead_code)]
-pub fn resolve_radroots_base_root() -> Result<PathBuf, RadrootsNostrAccountsError> {
- Err(RadrootsNostrAccountsError::Store(
- "android mobile base storage root is only available on android".to_owned(),
- ))
-}
-
-#[cfg(target_os = "android")]
-#[allow(unsafe_code)]
-fn android_java_vm() -> Result<JavaVM, RadrootsNostrAccountsError> {
- let context = ndk_context::android_context();
- // SAFETY: ndk_context is initialized by the Android runtime before this code runs and
- // returns a stable JavaVM pointer for the current process.
- unsafe { JavaVM::from_raw(context.vm().cast()) }.map_err(jni_error)
-}
-
-#[cfg(target_os = "android")]
-#[allow(unsafe_code)]
-fn bridge_class<'local>(
- env: &mut JNIEnv<'local>,
-) -> Result<JClass<'local>, RadrootsNostrAccountsError> {
- let context = ndk_context::android_context();
- // SAFETY: ndk_context stores a live process-wide Context jobject for the active Android app.
- let context = unsafe { JObject::from_raw(context.context() as jobject) };
- let context = env.new_local_ref(&context).map_err(jni_error)?;
- let class_loader = env
- .call_method(&context, "getClassLoader", "()Ljava/lang/ClassLoader;", &[])
- .and_then(|value| value.l())
- .map_err(jni_error)?;
- let class_name = env
- .new_string(ANDROID_SECURITY_BRIDGE_CLASS)
- .map_err(jni_error)?;
- let class_name = JObject::from(class_name);
- let bridge_class = env
- .call_method(
- &class_loader,
- "loadClass",
- "(Ljava/lang/String;)Ljava/lang/Class;",
- &[JValue::Object(&class_name)],
- )
- .and_then(|value| value.l())
- .map_err(jni_error)?;
- Ok(JClass::from(bridge_class))
-}
-
-#[cfg(target_os = "android")]
-fn java_string_arg<'local>(
- env: &mut JNIEnv<'local>,
- value: &str,
-) -> Result<JObject<'local>, RadrootsNostrAccountsError> {
- env.new_string(value).map(JObject::from).map_err(jni_error)
-}
-
-#[cfg(target_os = "android")]
-fn take_last_error_message(
- env: &mut JNIEnv<'_>,
- bridge_class: &JClass<'_>,
-) -> Result<Option<String>, RadrootsNostrAccountsError> {
- let value = env
- .call_static_method(
- bridge_class,
- "takeLastErrorMessage",
- "()Ljava/lang/String;",
- &[],
- )
- .and_then(|value| value.l())
- .map_err(jni_error)?;
- if value.is_null() {
- return Ok(None);
- }
- let value = JString::from(value);
- let value: String = env.get_string(&value).map_err(jni_error)?.into();
- Ok(Some(value))
-}
-
-#[cfg(target_os = "android")]
-fn bridge_vault_error(
- env: &mut JNIEnv<'_>,
- bridge_class: &JClass<'_>,
- fallback: &str,
-) -> RadrootsNostrAccountsError {
- let message = take_last_error_message(env, bridge_class)
- .ok()
- .flatten()
- .unwrap_or_else(|| fallback.to_owned());
- RadrootsNostrAccountsError::Vault(message)
-}
-
-#[cfg(target_os = "android")]
-fn bridge_store_error(
- env: &mut JNIEnv<'_>,
- bridge_class: &JClass<'_>,
- fallback: &str,
-) -> RadrootsNostrAccountsError {
- let message = take_last_error_message(env, bridge_class)
- .ok()
- .flatten()
- .unwrap_or_else(|| fallback.to_owned());
- RadrootsNostrAccountsError::Store(message)
-}
-
-#[cfg(target_os = "android")]
-fn jni_error(error: jni::errors::Error) -> RadrootsNostrAccountsError {
- RadrootsNostrAccountsError::Vault(format!("android jni error: {error}"))
-}
-
-#[cfg(target_os = "android")]
-fn bool_to_jboolean(value: bool) -> jboolean {
- if value { 1 } else { 0 }
-}
diff --git a/crates/bridges/android/security/src/vault.rs b/crates/bridges/android/security/src/vault.rs
@@ -1,197 +0,0 @@
-use crate::security::{
- ANDROID_NOSTR_NAMESPACE, load_secret, remove_secret, remove_secret_namespace, store_secret,
-};
-use radroots_secret_vault::{
- RadrootsHostVaultCapabilities, RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy,
- RadrootsHostVaultResidency, RadrootsHostVaultUserPresencePolicy, RadrootsSecretVault,
- RadrootsSecretVaultAccessError,
-};
-use zeroize::Zeroizing;
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAndroidKeystoreVault {
- service_name: String,
- namespace: String,
-}
-
-impl RadrootsAndroidKeystoreVault {
- #[must_use]
- pub fn new(service_name: impl Into<String>) -> Self {
- Self::new_with_namespace(service_name, ANDROID_NOSTR_NAMESPACE)
- }
-
- #[must_use]
- pub fn new_with_namespace(
- service_name: impl Into<String>,
- namespace: impl Into<String>,
- ) -> Self {
- Self {
- service_name: service_name.into(),
- namespace: namespace.into(),
- }
- }
-
- #[must_use]
- pub const fn secure_local_policy() -> RadrootsHostVaultPolicy {
- RadrootsHostVaultPolicy {
- residency: RadrootsHostVaultResidency::DeviceLocalOnly,
- user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired,
- hardware: RadrootsHostVaultHardwarePolicy::PreferHardwareBacked,
- }
- }
-
- fn capabilities() -> RadrootsHostVaultCapabilities {
- #[cfg(target_os = "android")]
- {
- RadrootsHostVaultCapabilities {
- available: true,
- supports_device_local_only: true,
- supports_user_presence: true,
- supports_hardware_backed: true,
- }
- }
-
- #[cfg(not(target_os = "android"))]
- {
- RadrootsHostVaultCapabilities::unavailable()
- }
- }
-
- fn validate_policy(
- policy: RadrootsHostVaultPolicy,
- ) -> Result<(), RadrootsSecretVaultAccessError> {
- Self::capabilities()
- .validate(policy)
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))
- }
-
- fn security_flags(policy: RadrootsHostVaultPolicy) -> (bool, bool, bool) {
- (
- matches!(
- policy.residency,
- RadrootsHostVaultResidency::DeviceLocalOnly
- ),
- matches!(
- policy.user_presence,
- RadrootsHostVaultUserPresencePolicy::Required
- ),
- !matches!(policy.hardware, RadrootsHostVaultHardwarePolicy::Any),
- )
- }
-
- pub fn store_secret_with_policy(
- &self,
- slot: &str,
- secret: &str,
- policy: RadrootsHostVaultPolicy,
- ) -> Result<(), RadrootsSecretVaultAccessError> {
- Self::validate_policy(policy)?;
- let secret = Zeroizing::new(secret.to_owned());
- let (device_local_only, user_presence_required, prefer_strong_box) =
- Self::security_flags(policy);
- store_secret(
- self.service_name.as_str(),
- self.namespace.as_str(),
- slot,
- secret.as_bytes(),
- device_local_only,
- user_presence_required,
- prefer_strong_box,
- )
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))
- }
-
- #[cfg_attr(not(target_os = "android"), allow(dead_code))]
- pub fn purge_namespace(&self) -> Result<(), RadrootsSecretVaultAccessError> {
- remove_secret_namespace(self.service_name.as_str(), self.namespace.as_str())
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))
- }
-}
-
-impl RadrootsSecretVault for RadrootsAndroidKeystoreVault {
- fn store_secret(&self, slot: &str, secret: &str) -> Result<(), RadrootsSecretVaultAccessError> {
- self.store_secret_with_policy(slot, secret, Self::secure_local_policy())
- }
-
- fn load_secret(&self, slot: &str) -> Result<Option<String>, RadrootsSecretVaultAccessError> {
- let Some(secret) =
- load_secret(self.service_name.as_str(), self.namespace.as_str(), slot)
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))?
- else {
- return Ok(None);
- };
-
- let secret = Zeroizing::new(secret);
- let secret = std::str::from_utf8(secret.as_slice()).map_err(|source| {
- RadrootsSecretVaultAccessError::Backend(format!(
- "android keystore secret was not valid utf-8: {source}"
- ))
- })?;
- Ok(Some(secret.to_owned()))
- }
-
- fn remove_secret(&self, slot: &str) -> Result<(), RadrootsSecretVaultAccessError> {
- remove_secret(self.service_name.as_str(), self.namespace.as_str(), slot)
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_secret_vault::{
- RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy, RadrootsHostVaultResidency,
- RadrootsHostVaultUserPresencePolicy,
- };
-
- #[test]
- fn secure_local_policy_prefers_device_local_hardware_backed_storage() {
- assert_eq!(
- RadrootsAndroidKeystoreVault::secure_local_policy(),
- RadrootsHostVaultPolicy {
- residency: RadrootsHostVaultResidency::DeviceLocalOnly,
- user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired,
- hardware: RadrootsHostVaultHardwarePolicy::PreferHardwareBacked,
- }
- );
- }
-
- #[test]
- fn security_flags_request_strong_box_for_hardware_backed_policies() {
- assert_eq!(
- RadrootsAndroidKeystoreVault::security_flags(RadrootsHostVaultPolicy {
- residency: RadrootsHostVaultResidency::UserProfile,
- user_presence: RadrootsHostVaultUserPresencePolicy::Required,
- hardware: RadrootsHostVaultHardwarePolicy::Any,
- }),
- (false, true, false)
- );
- assert_eq!(
- RadrootsAndroidKeystoreVault::security_flags(RadrootsHostVaultPolicy {
- residency: RadrootsHostVaultResidency::DeviceLocalOnly,
- user_presence: RadrootsHostVaultUserPresencePolicy::Required,
- hardware: RadrootsHostVaultHardwarePolicy::RequireHardwareBacked,
- }),
- (true, true, true)
- );
- }
-
- #[cfg(not(target_os = "android"))]
- #[test]
- fn vault_operations_report_unavailable_off_android() {
- let vault = RadrootsAndroidKeystoreVault::new(crate::security::ANDROID_NOSTR_SERVICE);
-
- let load = vault.load_secret("alice").expect_err("load off android");
- assert!(load.to_string().starts_with("secret vault access error:"));
-
- let store = vault
- .store_secret("alice", "deadbeef")
- .expect_err("store off android");
- assert!(store.to_string().starts_with("secret vault access error:"));
-
- let remove = vault
- .remove_secret("alice")
- .expect_err("remove off android");
- assert!(remove.to_string().starts_with("secret vault access error:"));
- }
-}
diff --git a/crates/bridges/apple/security/Cargo.toml b/crates/bridges/apple/security/Cargo.toml
@@ -1,19 +0,0 @@
-[package]
-name = "radroots_app_apple_security"
-authors.workspace = true
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots Apple security bridge"
-publish = false
-
-[dependencies]
-radroots_nostr_accounts.workspace = true
-radroots_secret_vault.workspace = true
-zeroize.workspace = true
-
-[lints.rust]
-unsafe_code = { level = "allow", priority = 1 }
diff --git a/crates/bridges/apple/security/build.rs b/crates/bridges/apple/security/build.rs
@@ -1,164 +0,0 @@
-use std::env;
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-
-fn main() {
- println!("cargo:rerun-if-changed=build.rs");
-
- let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
- if target_os != "macos" && target_os != "ios" {
- return;
- }
-
- let package_dir = swift_package_dir();
- println!(
- "cargo:rerun-if-changed={}",
- package_dir.join("Package.swift").display()
- );
- println!(
- "cargo:rerun-if-changed={}",
- package_dir.join("Sources").display()
- );
- println!(
- "cargo:rerun-if-changed={}",
- package_dir.join("Tests").display()
- );
-
- let ffi_library = "libRadRootsAppleSecurityFFIDynamic.dylib";
- run_swift_build(package_dir.as_path(), "RadRootsAppleSecurityFFIDynamic");
-
- let build_dir = find_library_dir(package_dir.join(".build"), ffi_library)
- .expect("swift ffi library dir");
- let copied_library_dir = target_profile_dir();
- fs::create_dir_all(&copied_library_dir).expect("create target profile dir");
- fs::copy(
- build_dir.join(ffi_library),
- copied_library_dir.join(ffi_library),
- )
- .expect("copy swift ffi library into cargo target dir");
- let swift_runtime_dir = swift_runtime_dir(target_os.as_str());
- println!(
- "cargo:rustc-link-search=native={}",
- copied_library_dir.display()
- );
- println!(
- "cargo:rustc-link-search=native={}",
- swift_runtime_dir.display()
- );
- println!(
- "cargo:rustc-link-arg=-Wl,-rpath,{}",
- copied_library_dir.display()
- );
- println!(
- "cargo:rustc-link-arg=-Wl,-rpath,{}",
- swift_runtime_dir.display()
- );
- println!("cargo:rustc-link-lib=dylib=RadRootsAppleSecurityFFIDynamic");
- println!("cargo:rustc-link-lib=framework=CoreFoundation");
- println!("cargo:rustc-link-lib=framework=Foundation");
- println!("cargo:rustc-link-lib=framework=LocalAuthentication");
- println!("cargo:rustc-link-lib=framework=Security");
- println!("cargo:rustc-link-lib=dylib=objc");
-}
-
-fn swift_package_dir() -> PathBuf {
- PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("manifest dir"))
- .join("../../../../native/bridges/apple/security/swift/RadRootsAppleSecurity")
-}
-
-fn swift_runtime_dir(target_os: &str) -> PathBuf {
- let swift_bin = run_stdout(Command::new("xcrun").arg("--toolchain").arg("swift").arg("--find").arg("swift"));
- let swift_bin = PathBuf::from(swift_bin.trim());
- let toolchain_dir = swift_bin
- .parent()
- .and_then(Path::parent)
- .and_then(Path::parent)
- .expect("swift toolchain dir");
- find_swift_runtime_dir(toolchain_dir.join("usr/lib"), target_os).expect("swift runtime dir")
-}
-
-fn find_swift_runtime_dir(root: PathBuf, target_os: &str) -> Option<PathBuf> {
- let platform_dir = match target_os {
- "macos" => "macosx",
- "ios" => "iphoneos",
- other => other,
- };
- let mut stack = vec![root];
- while let Some(dir) = stack.pop() {
- let entries = std::fs::read_dir(&dir).ok()?;
- for entry in entries.flatten() {
- let path = entry.path();
- if path.is_dir() {
- stack.push(path);
- continue;
- }
- if path.file_name().is_some_and(|name| name == "libswift_Concurrency.dylib")
- && path
- .components()
- .any(|component| component.as_os_str() == platform_dir)
- {
- return path.parent().map(Path::to_path_buf);
- }
- }
- }
- None
-}
-
-fn find_library_dir(root: PathBuf, library_name: &str) -> Option<PathBuf> {
- let mut stack = vec![root];
- while let Some(dir) = stack.pop() {
- let entries = std::fs::read_dir(&dir).ok()?;
- for entry in entries.flatten() {
- let path = entry.path();
- if path.is_dir() {
- stack.push(path);
- continue;
- }
- if path.file_name().is_some_and(|name| name == library_name) {
- return path.parent().map(Path::to_path_buf);
- }
- }
- }
- None
-}
-
-fn target_profile_dir() -> PathBuf {
- let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"));
- out_dir
- .ancestors()
- .nth(3)
- .unwrap_or_else(|| panic!("unexpected cargo OUT_DIR layout: {}", out_dir.display()))
- .to_path_buf()
-}
-
-fn run_swift_build(package_dir: &Path, product: &str) {
- let output = Command::new("swift")
- .arg("build")
- .arg("--product")
- .arg(product)
- .current_dir(package_dir)
- .output()
- .expect("failed to run swift build");
-
- if output.status.success() {
- return;
- }
-
- let stdout = String::from_utf8_lossy(&output.stdout);
- let stderr = String::from_utf8_lossy(&output.stderr);
- panic!(
- "swift build --product {product} failed in {}:\nstdout:\n{stdout}\nstderr:\n{stderr}",
- package_dir.display()
- );
-}
-
-fn run_stdout(command: &mut Command) -> String {
- let output = command.output().expect("failed to run command");
- if output.status.success() {
- return String::from_utf8(output.stdout).expect("utf-8 stdout");
- }
-
- let stderr = String::from_utf8_lossy(&output.stderr);
- panic!("command failed: {command:?}\nstderr:\n{stderr}");
-}
diff --git a/crates/bridges/apple/security/src/lib.rs b/crates/bridges/apple/security/src/lib.rs
@@ -1,7 +0,0 @@
-#![allow(unsafe_code)]
-
-mod security;
-mod vault;
-
-pub use security::{APPLE_NOSTR_NAMESPACE, APPLE_NOSTR_SERVICE, verify_user_presence};
-pub use vault::RadrootsAppleKeychainVault;
diff --git a/crates/bridges/apple/security/src/security.rs b/crates/bridges/apple/security/src/security.rs
@@ -1,454 +0,0 @@
-use radroots_nostr_accounts::prelude::RadrootsNostrAccountsError;
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-use std::ffi::CStr;
-use std::ffi::CString;
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-use std::os::raw::{c_char, c_int};
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-use std::ptr;
-
-pub const APPLE_NOSTR_SERVICE: &str = "org.radroots.app.nostr";
-pub const APPLE_NOSTR_NAMESPACE: &str = "nostr";
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-#[repr(i32)]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum AppleSecretStatus {
- Success = 0,
- NotFound = 1,
- InvalidInput = 2,
- Error = 3,
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-impl AppleSecretStatus {
- fn from_raw(value: i32) -> Result<Self, RadrootsNostrAccountsError> {
- match value {
- 0 => Ok(Self::Success),
- 1 => Ok(Self::NotFound),
- 2 => Ok(Self::InvalidInput),
- 3 => Ok(Self::Error),
- other => Err(RadrootsNostrAccountsError::Vault(format!(
- "unknown apple security ffi status {other}"
- ))),
- }
- }
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-unsafe extern "C" {
- fn radroots_apple_secret_store_put(
- service_prefix: *const c_char,
- namespace: *const c_char,
- name: *const c_char,
- value_ptr: *const u8,
- value_len: isize,
- accessibility_raw: i32,
- device_local_only_raw: i32,
- user_presence_required_raw: i32,
- error_out: *mut *mut c_char,
- ) -> i32;
-
- fn radroots_apple_secret_store_get(
- service_prefix: *const c_char,
- namespace: *const c_char,
- name: *const c_char,
- value_out: *mut *mut u8,
- value_len_out: *mut isize,
- error_out: *mut *mut c_char,
- ) -> i32;
-
- fn radroots_apple_secret_store_delete(
- service_prefix: *const c_char,
- namespace: *const c_char,
- name: *const c_char,
- error_out: *mut *mut c_char,
- ) -> i32;
-
- fn radroots_apple_secret_store_delete_namespace(
- service_prefix: *const c_char,
- namespace: *const c_char,
- error_out: *mut *mut c_char,
- ) -> i32;
-
- fn radroots_apple_user_presence_verify(
- reason: *const c_char,
- error_out: *mut *mut c_char,
- ) -> i32;
-
- fn radroots_apple_buffer_free(buffer: *mut u8, length: isize);
- fn radroots_apple_c_string_free(string: *mut c_char);
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-struct FfiErrorString {
- ptr: *mut c_char,
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-impl FfiErrorString {
- fn new() -> Self {
- Self {
- ptr: ptr::null_mut(),
- }
- }
-
- fn as_mut_ptr(&mut self) -> *mut *mut c_char {
- &mut self.ptr
- }
-
- fn message(&self) -> Option<String> {
- if self.ptr.is_null() {
- return None;
- }
- // SAFETY: the Swift FFI returns a null-terminated string pointer that remains valid
- // until released through the paired free function.
- unsafe { Some(CStr::from_ptr(self.ptr).to_string_lossy().into_owned()) }
- }
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-impl Drop for FfiErrorString {
- fn drop(&mut self) {
- if self.ptr.is_null() {
- return;
- }
- // SAFETY: the pointer originated from the Swift FFI string allocator.
- unsafe {
- radroots_apple_c_string_free(self.ptr);
- }
- }
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-struct FfiDataBuffer {
- ptr: *mut u8,
- len: isize,
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-impl FfiDataBuffer {
- fn new() -> Self {
- Self {
- ptr: ptr::null_mut(),
- len: 0,
- }
- }
-
- fn as_mut_ptr(&mut self) -> *mut *mut u8 {
- &mut self.ptr
- }
-
- fn len_mut_ptr(&mut self) -> *mut isize {
- &mut self.len
- }
-
- fn to_vec(&self) -> Result<Vec<u8>, RadrootsNostrAccountsError> {
- if self.len < 0 {
- return Err(RadrootsNostrAccountsError::Vault(
- "apple security ffi returned a negative buffer length".to_owned(),
- ));
- }
- if self.ptr.is_null() {
- if self.len == 0 {
- return Ok(Vec::new());
- }
- return Err(RadrootsNostrAccountsError::Vault(
- "apple security ffi returned a null buffer pointer".to_owned(),
- ));
- }
- // SAFETY: the pointer and length pair came from the Swift FFI and stays valid until
- // released by the paired free function. We copy into an owned Vec before dropping.
- unsafe { Ok(std::slice::from_raw_parts(self.ptr, self.len as usize).to_vec()) }
- }
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-impl Drop for FfiDataBuffer {
- fn drop(&mut self) {
- if self.ptr.is_null() {
- return;
- }
- // SAFETY: the pointer originated from the Swift FFI buffer allocator.
- unsafe {
- radroots_apple_buffer_free(self.ptr, self.len);
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum AppleSecretAccessibility {
- WhenUnlocked = 0,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct AppleSecretAccessPolicy {
- pub accessibility: AppleSecretAccessibility,
- pub device_local_only: bool,
- pub user_presence_required: bool,
-}
-
-impl AppleSecretAccessPolicy {
- #[cfg_attr(not(test), allow(dead_code))]
- pub const SECURE_LOCAL_SECRET: Self = Self {
- accessibility: AppleSecretAccessibility::WhenUnlocked,
- device_local_only: true,
- user_presence_required: false,
- };
-}
-
-pub fn store_secret(
- service: &str,
- namespace: &str,
- name: &str,
- value: &[u8],
- policy: AppleSecretAccessPolicy,
-) -> Result<(), RadrootsNostrAccountsError> {
- #[cfg(any(target_os = "ios", target_os = "macos"))]
- {
- let service = c_string(service)?;
- let namespace = c_string(namespace)?;
- let name = c_string(name)?;
- let mut ffi_error = FfiErrorString::new();
- let status = unsafe {
- // SAFETY: all pointers are derived from live CString values and valid slices.
- radroots_apple_secret_store_put(
- service.as_ptr(),
- namespace.as_ptr(),
- name.as_ptr(),
- value.as_ptr(),
- value.len() as isize,
- policy.accessibility as i32,
- bool_to_c_int(policy.device_local_only),
- bool_to_c_int(policy.user_presence_required),
- ffi_error.as_mut_ptr(),
- )
- };
- return match AppleSecretStatus::from_raw(status)? {
- AppleSecretStatus::Success => Ok(()),
- AppleSecretStatus::NotFound => Err(vault_error(
- ffi_error,
- "apple security ffi reported not found during store",
- )),
- AppleSecretStatus::InvalidInput => Err(vault_error(
- ffi_error,
- "apple security ffi rejected the store request",
- )),
- AppleSecretStatus::Error => Err(vault_error(ffi_error, "apple keychain store failed")),
- };
- }
-
- #[cfg(not(any(target_os = "ios", target_os = "macos")))]
- {
- let _ = (service, namespace, name, value, policy);
- Err(RadrootsNostrAccountsError::Vault(
- "apple keychain storage is only available on ios and macos".to_owned(),
- ))
- }
-}
-
-pub fn load_secret(
- service: &str,
- namespace: &str,
- name: &str,
-) -> Result<Option<Vec<u8>>, RadrootsNostrAccountsError> {
- #[cfg(any(target_os = "ios", target_os = "macos"))]
- {
- let service = c_string(service)?;
- let namespace = c_string(namespace)?;
- let name = c_string(name)?;
- let mut ffi_error = FfiErrorString::new();
- let mut ffi_buffer = FfiDataBuffer::new();
- let status = unsafe {
- // SAFETY: all output pointers reference live local storage for the duration
- // of the call, and all input strings are backed by live CString values.
- radroots_apple_secret_store_get(
- service.as_ptr(),
- namespace.as_ptr(),
- name.as_ptr(),
- ffi_buffer.as_mut_ptr(),
- ffi_buffer.len_mut_ptr(),
- ffi_error.as_mut_ptr(),
- )
- };
- return match AppleSecretStatus::from_raw(status)? {
- AppleSecretStatus::Success => ffi_buffer.to_vec().map(Some),
- AppleSecretStatus::NotFound => Ok(None),
- AppleSecretStatus::InvalidInput => Err(vault_error(
- ffi_error,
- "apple security ffi rejected the load request",
- )),
- AppleSecretStatus::Error => Err(vault_error(ffi_error, "apple keychain load failed")),
- };
- }
-
- #[cfg(not(any(target_os = "ios", target_os = "macos")))]
- {
- let _ = (service, namespace, name);
- Err(RadrootsNostrAccountsError::Vault(
- "apple keychain storage is only available on ios and macos".to_owned(),
- ))
- }
-}
-
-pub fn remove_secret(
- service: &str,
- namespace: &str,
- name: &str,
-) -> Result<(), RadrootsNostrAccountsError> {
- #[cfg(any(target_os = "ios", target_os = "macos"))]
- {
- let service = c_string(service)?;
- let namespace = c_string(namespace)?;
- let name = c_string(name)?;
- let mut ffi_error = FfiErrorString::new();
- let status = unsafe {
- // SAFETY: all pointers are backed by live CString values for the duration
- // of the call.
- radroots_apple_secret_store_delete(
- service.as_ptr(),
- namespace.as_ptr(),
- name.as_ptr(),
- ffi_error.as_mut_ptr(),
- )
- };
- return match AppleSecretStatus::from_raw(status)? {
- AppleSecretStatus::Success | AppleSecretStatus::NotFound => Ok(()),
- AppleSecretStatus::InvalidInput => Err(vault_error(
- ffi_error,
- "apple security ffi rejected the delete request",
- )),
- AppleSecretStatus::Error => Err(vault_error(ffi_error, "apple keychain delete failed")),
- };
- }
-
- #[cfg(not(any(target_os = "ios", target_os = "macos")))]
- {
- let _ = (service, namespace, name);
- Err(RadrootsNostrAccountsError::Vault(
- "apple keychain storage is only available on ios and macos".to_owned(),
- ))
- }
-}
-
-pub fn remove_secret_namespace(
- service: &str,
- namespace: &str,
-) -> Result<(), RadrootsNostrAccountsError> {
- #[cfg(any(target_os = "ios", target_os = "macos"))]
- {
- let service = c_string(service)?;
- let namespace = c_string(namespace)?;
- let mut ffi_error = FfiErrorString::new();
- let status = unsafe {
- // SAFETY: all pointers are backed by live CString values for the duration
- // of the call.
- radroots_apple_secret_store_delete_namespace(
- service.as_ptr(),
- namespace.as_ptr(),
- ffi_error.as_mut_ptr(),
- )
- };
- return match AppleSecretStatus::from_raw(status)? {
- AppleSecretStatus::Success | AppleSecretStatus::NotFound => Ok(()),
- AppleSecretStatus::InvalidInput => Err(vault_error(
- ffi_error,
- "apple security ffi rejected the namespace delete request",
- )),
- AppleSecretStatus::Error => Err(vault_error(
- ffi_error,
- "apple keychain namespace delete failed",
- )),
- };
- }
-
- #[cfg(not(any(target_os = "ios", target_os = "macos")))]
- {
- let _ = (service, namespace);
- Err(RadrootsNostrAccountsError::Vault(
- "apple keychain storage is only available on ios and macos".to_owned(),
- ))
- }
-}
-
-pub fn verify_user_presence(reason: &str) -> Result<(), RadrootsNostrAccountsError> {
- #[cfg(any(target_os = "ios", target_os = "macos"))]
- {
- let reason = c_string(reason)?;
- let mut ffi_error = FfiErrorString::new();
- let status = unsafe {
- // SAFETY: the reason pointer is derived from a live CString and the error output
- // references live local storage for the duration of the call.
- radroots_apple_user_presence_verify(reason.as_ptr(), ffi_error.as_mut_ptr())
- };
- return match AppleSecretStatus::from_raw(status)? {
- AppleSecretStatus::Success => Ok(()),
- AppleSecretStatus::NotFound => Err(vault_error(
- ffi_error,
- "apple security ffi reported not found during user presence verification",
- )),
- AppleSecretStatus::InvalidInput => Err(vault_error(
- ffi_error,
- "apple security ffi rejected the user presence request",
- )),
- AppleSecretStatus::Error => Err(vault_error(
- ffi_error,
- "apple user presence verification failed",
- )),
- };
- }
-
- #[cfg(not(any(target_os = "ios", target_os = "macos")))]
- {
- let _ = reason;
- Err(RadrootsNostrAccountsError::Vault(
- "apple user presence verification is only available on ios and macos".to_owned(),
- ))
- }
-}
-
-fn c_string(value: &str) -> Result<CString, RadrootsNostrAccountsError> {
- CString::new(value).map_err(|_| {
- RadrootsNostrAccountsError::Vault(
- "apple security ffi input contained an interior nul".into(),
- )
- })
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-fn bool_to_c_int(value: bool) -> c_int {
- if value { 1 } else { 0 }
-}
-
-#[cfg(any(target_os = "ios", target_os = "macos"))]
-fn vault_error(
- ffi_error: FfiErrorString,
- fallback: impl Into<String>,
-) -> RadrootsNostrAccountsError {
- let fallback = fallback.into();
- let message = ffi_error.message().unwrap_or(fallback);
- RadrootsNostrAccountsError::Vault(message)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn secure_local_secret_policy_defaults_to_when_unlocked_device_local() {
- let policy = AppleSecretAccessPolicy::SECURE_LOCAL_SECRET;
-
- assert!(matches!(
- policy.accessibility,
- AppleSecretAccessibility::WhenUnlocked
- ));
- assert!(policy.device_local_only);
- assert!(!policy.user_presence_required);
- }
-
- #[test]
- fn c_string_rejects_interior_nul() {
- let err = c_string("bad\0value").expect_err("interior nul");
- assert!(err.to_string().starts_with("vault error:"));
- }
-}
diff --git a/crates/bridges/apple/security/src/vault.rs b/crates/bridges/apple/security/src/vault.rs
@@ -1,225 +0,0 @@
-use crate::security::{
- APPLE_NOSTR_NAMESPACE, AppleSecretAccessPolicy, AppleSecretAccessibility, load_secret,
- remove_secret, remove_secret_namespace, store_secret,
-};
-use radroots_secret_vault::{
- RadrootsHostVaultCapabilities, RadrootsHostVaultPolicy, RadrootsHostVaultResidency,
- RadrootsHostVaultUserPresencePolicy, RadrootsSecretVault, RadrootsSecretVaultAccessError,
-};
-use zeroize::Zeroizing;
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppleKeychainVault {
- service_name: String,
- namespace: String,
- default_policy: RadrootsHostVaultPolicy,
-}
-
-impl RadrootsAppleKeychainVault {
- #[must_use]
- pub fn new_desktop(service_name: impl Into<String>) -> Self {
- Self::new_with_namespace_desktop(service_name, APPLE_NOSTR_NAMESPACE)
- }
-
- #[must_use]
- pub fn new_device_local(service_name: impl Into<String>) -> Self {
- Self::new_with_namespace_device_local(service_name, APPLE_NOSTR_NAMESPACE)
- }
-
- #[must_use]
- pub fn new_with_namespace_desktop(
- service_name: impl Into<String>,
- namespace: impl Into<String>,
- ) -> Self {
- Self::new_with_namespace_and_policy(service_name, namespace, Self::desktop_policy())
- }
-
- #[must_use]
- pub fn new_with_namespace_device_local(
- service_name: impl Into<String>,
- namespace: impl Into<String>,
- ) -> Self {
- Self::new_with_namespace_and_policy(service_name, namespace, Self::device_local_policy())
- }
-
- fn new_with_namespace_and_policy(
- service_name: impl Into<String>,
- namespace: impl Into<String>,
- default_policy: RadrootsHostVaultPolicy,
- ) -> Self {
- Self {
- service_name: service_name.into(),
- namespace: namespace.into(),
- default_policy,
- }
- }
-
- #[must_use]
- pub const fn desktop_policy() -> RadrootsHostVaultPolicy {
- RadrootsHostVaultPolicy::desktop()
- }
-
- #[must_use]
- pub const fn device_local_policy() -> RadrootsHostVaultPolicy {
- RadrootsHostVaultPolicy::device_local()
- }
-
- fn capabilities() -> RadrootsHostVaultCapabilities {
- #[cfg(any(target_os = "ios", target_os = "macos"))]
- {
- RadrootsHostVaultCapabilities {
- available: true,
- supports_device_local_only: true,
- supports_user_presence: true,
- supports_hardware_backed: false,
- }
- }
-
- #[cfg(not(any(target_os = "ios", target_os = "macos")))]
- {
- RadrootsHostVaultCapabilities::unavailable()
- }
- }
-
- fn validate_policy(
- policy: RadrootsHostVaultPolicy,
- ) -> Result<(), RadrootsSecretVaultAccessError> {
- Self::capabilities()
- .validate(policy)
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))
- }
-
- fn access_policy(policy: RadrootsHostVaultPolicy) -> AppleSecretAccessPolicy {
- AppleSecretAccessPolicy {
- accessibility: AppleSecretAccessibility::WhenUnlocked,
- device_local_only: matches!(
- policy.residency,
- RadrootsHostVaultResidency::DeviceLocalOnly
- ),
- user_presence_required: matches!(
- policy.user_presence,
- RadrootsHostVaultUserPresencePolicy::Required
- ),
- }
- }
-
- pub fn store_secret_with_policy(
- &self,
- slot: &str,
- secret: &str,
- policy: RadrootsHostVaultPolicy,
- ) -> Result<(), RadrootsSecretVaultAccessError> {
- Self::validate_policy(policy)?;
- let secret = Zeroizing::new(secret.to_owned());
- store_secret(
- self.service_name.as_str(),
- self.namespace.as_str(),
- slot,
- secret.as_bytes(),
- Self::access_policy(policy),
- )
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))
- }
-
- pub fn purge_namespace(&self) -> Result<(), RadrootsSecretVaultAccessError> {
- remove_secret_namespace(self.service_name.as_str(), self.namespace.as_str())
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))
- }
-}
-
-impl RadrootsSecretVault for RadrootsAppleKeychainVault {
- fn store_secret(&self, slot: &str, secret: &str) -> Result<(), RadrootsSecretVaultAccessError> {
- self.store_secret_with_policy(slot, secret, self.default_policy)
- }
-
- fn load_secret(&self, slot: &str) -> Result<Option<String>, RadrootsSecretVaultAccessError> {
- let Some(secret) =
- load_secret(self.service_name.as_str(), self.namespace.as_str(), slot)
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))?
- else {
- return Ok(None);
- };
-
- let secret = Zeroizing::new(secret);
- let secret = std::str::from_utf8(secret.as_slice()).map_err(|source| {
- RadrootsSecretVaultAccessError::Backend(format!(
- "apple keychain secret was not valid utf-8: {source}"
- ))
- })?;
- Ok(Some(secret.to_owned()))
- }
-
- fn remove_secret(&self, slot: &str) -> Result<(), RadrootsSecretVaultAccessError> {
- remove_secret(self.service_name.as_str(), self.namespace.as_str(), slot)
- .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_secret_vault::{
- RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy, RadrootsHostVaultResidency,
- RadrootsHostVaultUserPresencePolicy,
- };
-
- #[test]
- fn desktop_policy_matches_shared_desktop_contract() {
- assert_eq!(
- RadrootsAppleKeychainVault::desktop_policy(),
- RadrootsHostVaultPolicy {
- residency: RadrootsHostVaultResidency::UserProfile,
- user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired,
- hardware: RadrootsHostVaultHardwarePolicy::Any,
- }
- );
- }
-
- #[test]
- fn device_local_policy_matches_shared_mobile_contract() {
- assert_eq!(
- RadrootsAppleKeychainVault::device_local_policy(),
- RadrootsHostVaultPolicy {
- residency: RadrootsHostVaultResidency::DeviceLocalOnly,
- user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired,
- hardware: RadrootsHostVaultHardwarePolicy::Any,
- }
- );
- }
-
- #[cfg(not(any(target_os = "ios", target_os = "macos")))]
- #[test]
- fn vault_operations_report_unavailable_off_apple() {
- let vault = RadrootsAppleKeychainVault::new_desktop(crate::APPLE_NOSTR_SERVICE);
-
- let load = vault.load_secret("alice").expect_err("load off apple");
- assert!(load.to_string().starts_with("secret vault access error:"));
-
- let store = vault
- .store_secret("alice", "deadbeef")
- .expect_err("store off apple");
- assert!(store.to_string().starts_with("secret vault access error:"));
-
- let remove = vault.remove_secret("alice").expect_err("remove off apple");
- assert!(remove.to_string().starts_with("secret vault access error:"));
- }
-
- #[cfg(any(target_os = "ios", target_os = "macos"))]
- #[test]
- fn hardware_backed_requirement_reports_unsupported() {
- let vault = RadrootsAppleKeychainVault::new_device_local(crate::APPLE_NOSTR_SERVICE);
- let error = vault
- .store_secret_with_policy(
- "alice",
- "deadbeef",
- RadrootsHostVaultPolicy {
- residency: RadrootsHostVaultResidency::DeviceLocalOnly,
- user_presence: RadrootsHostVaultUserPresencePolicy::Required,
- hardware: RadrootsHostVaultHardwarePolicy::RequireHardwareBacked,
- },
- )
- .expect_err("apple adapter should reject hardware-backed requirement");
-
- assert!(error.to_string().contains("hardware_backed"));
- }
-}
diff --git a/crates/launchers/android/Cargo.toml b/crates/launchers/android/Cargo.toml
@@ -1,35 +0,0 @@
-[package]
-name = "radroots_app_android"
-authors.workspace = true
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots Android launcher"
-publish = false
-
-[lib]
-path = "src/lib.rs"
-crate-type = ["cdylib", "rlib"]
-
-[dependencies]
-eframe = { workspace = true, features = ["android-game-activity", "glow"] }
-log.workspace = true
-radroots_app_android_security.workspace = true
-radroots_app_core = { path = "../../shared/core" }
-radroots_app_remote_signer = { path = "../../shared/remote_signer" }
-radroots_geocoder.workspace = true
-radroots_identity.workspace = true
-radroots_nostr_accounts = { workspace = true, features = ["memory-vault"] }
-radroots_runtime_paths.workspace = true
-zeroize.workspace = true
-
-[target.'cfg(target_os = "android")'.dependencies]
-android_logger.workspace = true
-wgpu = { workspace = true, features = ["vulkan", "gles", "wgsl"] }
-winit.workspace = true
-
-[dev-dependencies]
-radroots_app_test_support = { path = "../../shared/test_support" }
diff --git a/crates/launchers/android/src/country_lookup.rs b/crates/launchers/android/src/country_lookup.rs
@@ -1,179 +0,0 @@
-#![cfg_attr(not(target_os = "android"), allow(dead_code))]
-
-#[cfg(target_os = "android")]
-use crate::offline_geocoder;
-use radroots_app_core::{
- RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult,
- RadrootsLocationResolverError, RadrootsOfflineGeocoderState,
-};
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-#[derive(Clone, Default)]
-pub(crate) struct AndroidCountryLookup {
- country_list_result: Arc<Mutex<Option<RadrootsLocationCountryListResult>>>,
- country_list_changed: Arc<AtomicBool>,
- country_list_pending: Arc<AtomicBool>,
- country_center_result: Arc<Mutex<Option<RadrootsLocationCountryCenterLookupResult>>>,
- country_center_changed: Arc<AtomicBool>,
- country_center_pending: Arc<AtomicBool>,
-}
-
-impl AndroidCountryLookup {
- pub(crate) fn new() -> Self {
- Self::default()
- }
-
- #[cfg(target_os = "android")]
- pub(crate) fn begin_list(
- &self,
- geocoder_state: RadrootsOfflineGeocoderState,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.country_list_pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline country list query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.country_list_result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.country_list_result);
- let changed = Arc::clone(&self.country_list_changed);
- let pending = Arc::clone(&self.country_list_pending);
- std::thread::spawn(move || {
- let lookup_result = offline_geocoder::list_countries(&geocoder_state);
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "android"))]
- pub(crate) fn begin_list(
- &self,
- _geocoder_state: RadrootsOfflineGeocoderState,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- #[cfg(target_os = "android")]
- pub(crate) fn begin_center(
- &self,
- geocoder_state: RadrootsOfflineGeocoderState,
- country_id: String,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.country_center_pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline country center query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.country_center_result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.country_center_result);
- let changed = Arc::clone(&self.country_center_changed);
- let pending = Arc::clone(&self.country_center_pending);
- std::thread::spawn(move || {
- let lookup_result = offline_geocoder::country_center(&geocoder_state, &country_id);
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "android"))]
- pub(crate) fn begin_center(
- &self,
- _geocoder_state: RadrootsOfflineGeocoderState,
- _country_id: String,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- pub(crate) fn take_list_update(&self) -> Option<RadrootsLocationCountryListResult> {
- if !self.country_list_changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.country_list_result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "android country list result lock poisoned".to_owned(),
- })),
- }
- }
-
- pub(crate) fn take_center_update(&self) -> Option<RadrootsLocationCountryCenterLookupResult> {
- if !self.country_center_changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.country_center_result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "android country center result lock poisoned".to_owned(),
- })),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_core::{RadrootsLocationCountry, RadrootsLocationPoint};
-
- fn sample_countries() -> RadrootsLocationCountryListResult {
- Ok(vec![RadrootsLocationCountry {
- country_id: "BR".to_owned(),
- country_name: Some("Brazil".to_owned()),
- center: RadrootsLocationPoint {
- lat: -14.235,
- lng: -51.9253,
- },
- }])
- }
-
- #[test]
- fn take_list_update_is_none_until_tracker_changes() {
- let tracker = AndroidCountryLookup::new();
-
- assert_eq!(tracker.take_list_update(), None);
- }
-
- #[test]
- fn take_list_update_returns_queued_result_once() {
- let tracker = AndroidCountryLookup::new();
- *tracker.country_list_result.lock().unwrap() = Some(sample_countries());
- tracker.country_list_changed.store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_list_update(), Some(Ok(results)) if results.len() == 1));
- assert_eq!(tracker.take_list_update(), None);
- }
-
- #[test]
- fn take_center_update_returns_queued_result_once() {
- let tracker = AndroidCountryLookup::new();
- *tracker.country_center_result.lock().unwrap() = Some(Ok(RadrootsLocationPoint {
- lat: -14.235,
- lng: -51.9253,
- }));
- tracker
- .country_center_changed
- .store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_center_update(), Some(Ok(point)) if point.lat == -14.235));
- assert_eq!(tracker.take_center_update(), None);
- }
-}
diff --git a/crates/launchers/android/src/lib.rs b/crates/launchers/android/src/lib.rs
@@ -1,1228 +0,0 @@
-#[cfg(target_os = "android")]
-use android_logger::Config;
-#[cfg(target_os = "android")]
-use eframe::egui::ViewportBuilder;
-#[cfg(target_os = "android")]
-use radroots_app_android_security as android_security;
-#[cfg(any(target_os = "android", test))]
-use radroots_app_core::RadrootsAppBackend;
-#[cfg(target_os = "android")]
-use radroots_app_core::{APP_NAME, RadrootsApp};
-#[cfg(any(target_os = "android", test))]
-use radroots_app_core::{
- HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, ImportActionState,
- RadrootsAccountCustody, RadrootsAccountSummary, RadrootsLocationCountry,
- RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult,
- RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions,
- RadrootsOfflineGeocoderState, RadrootsResolvedLocation, RadrootsReverseLocationLookupResult,
- RadrootsSecretImportMode, RadrootsSecretImportRequest, SetupActionState,
-};
-#[cfg(any(target_os = "android", test))]
-use radroots_identity::RadrootsIdentity;
-#[cfg(test)]
-use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord;
-#[cfg(any(target_os = "android", test))]
-use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager;
-#[cfg(any(target_os = "android", test))]
-use radroots_nostr_accounts::prelude::RadrootsNostrSelectedAccountStatus;
-#[cfg(any(target_os = "android", test))]
-use std::path::Path;
-#[cfg(any(target_os = "android", test))]
-use std::sync::Mutex;
-#[cfg(target_os = "android")]
-use winit::platform::android::activity::AndroidApp;
-#[cfg(any(target_os = "android", test))]
-use zeroize::Zeroizing;
-
-#[cfg(any(target_os = "android", test))]
-mod country_lookup;
-#[cfg(any(target_os = "android", test))]
-mod offline_geocoder;
-#[cfg(target_os = "android")]
-mod remote_signer;
-#[cfg(any(target_os = "android", test))]
-mod reverse_lookup;
-#[cfg(any(target_os = "android", test))]
-mod storage;
-
-#[cfg(any(target_os = "android", test))]
-#[cfg_attr(not(target_os = "android"), allow(dead_code))]
-struct AndroidBackend {
- country_lookup: country_lookup::AndroidCountryLookup,
- offline_geocoder: offline_geocoder::AndroidOfflineGeocoder,
- #[cfg(target_os = "android")]
- remote_signer: remote_signer::AndroidRemoteSigner,
- reverse_lookup: reverse_lookup::AndroidReverseLookup,
-}
-
-#[cfg(any(target_os = "android", test))]
-#[cfg_attr(not(target_os = "android"), allow(dead_code))]
-enum PendingSecretKeyExport {
- EncryptedBackup { password: Zeroizing<String> },
- RawReveal,
-}
-
-#[cfg(any(target_os = "android", test))]
-#[cfg_attr(not(target_os = "android"), allow(dead_code))]
-static PENDING_SECRET_KEY_EXPORT: Mutex<Option<PendingSecretKeyExport>> = Mutex::new(None);
-
-#[cfg(any(target_os = "android", test))]
-impl RadrootsAppBackend for AndroidBackend {
- fn load_identity_state(&self) -> Result<IdentityGateState, String> {
- #[cfg(target_os = "android")]
- {
- let manager = Self::accounts_manager()?;
- let status = manager
- .selected_account_status()
- .map_err(|source| source.to_string())?;
- return remote_signer::identity_state_from_status(status);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Ok(Self::unsupported_identity_state())
- }
- }
-
- fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> {
- #[cfg(target_os = "android")]
- {
- let manager = Self::accounts_manager()?;
- return Self::account_roster_from_manager(&manager);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Ok(Vec::new())
- }
- }
-
- fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> {
- Some(self.offline_geocoder.current_state())
- }
-
- fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> {
- Ok(self.offline_geocoder.take_update())
- }
-
- fn reverse_location(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
- #[cfg(target_os = "android")]
- {
- return offline_geocoder::reverse_location(
- &self.offline_geocoder.current_state(),
- point,
- options,
- );
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = (point, options);
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn request_reverse_location_lookup(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- #[cfg(target_os = "android")]
- {
- return self.reverse_lookup.begin(
- self.offline_geocoder.current_state(),
- point,
- options,
- );
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = (point, options);
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn poll_reverse_location_lookup_result(
- &self,
- ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> {
- Ok(self.reverse_lookup.take_update())
- }
-
- fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> {
- #[cfg(target_os = "android")]
- {
- return self
- .country_lookup
- .begin_list(self.offline_geocoder.current_state());
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn poll_location_country_list_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryListResult>, String> {
- Ok(self.country_lookup.take_list_update())
- }
-
- fn request_location_country_center_lookup(
- &self,
- country_id: &str,
- ) -> Result<(), RadrootsLocationResolverError> {
- #[cfg(target_os = "android")]
- {
- return self
- .country_lookup
- .begin_center(self.offline_geocoder.current_state(), country_id.to_owned());
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = country_id;
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn poll_location_country_center_lookup_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> {
- Ok(self.country_lookup.take_center_update())
- }
-
- fn list_location_countries(
- &self,
- ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- #[cfg(target_os = "android")]
- {
- return offline_geocoder::list_countries(&self.offline_geocoder.current_state());
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn location_country_center(
- &self,
- country_id: &str,
- ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- #[cfg(target_os = "android")]
- {
- return offline_geocoder::country_center(
- &self.offline_geocoder.current_state(),
- country_id,
- );
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = country_id;
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn setup_action_state(&self) -> SetupActionState {
- #[cfg(target_os = "android")]
- {
- return Self::enabled_setup_action_state();
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Self::unsupported_setup_action_state()
- }
- }
-
- fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "android")]
- {
- let manager = Self::accounts_manager()?;
- return Self::generate_local_identity(&manager).map(Some);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Ok(Some(Self::unsupported_identity_state()))
- }
- }
-
- fn home_setup_action_state(&self) -> Option<SetupActionState> {
- Some(self.setup_action_state())
- }
-
- fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- self.request_setup_action()
- }
-
- fn import_action_state(&self) -> Option<ImportActionState> {
- #[cfg(target_os = "android")]
- {
- return Some(ImportActionState {
- label: "Import Secret Key".to_owned(),
- enabled: true,
- pending: false,
- });
- }
-
- #[cfg(not(target_os = "android"))]
- {
- None
- }
- }
-
- fn request_import_action(
- &self,
- request: &RadrootsSecretImportRequest,
- ) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "android")]
- {
- let manager = Self::accounts_manager()?;
- return Self::import_local_identity(&manager, request).map(Some);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = request;
- Ok(None)
- }
- }
-
- fn request_select_account(
- &self,
- account_id: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "android")]
- {
- let manager = Self::accounts_manager()?;
- let account_id = radroots_identity::RadrootsIdentityId::try_from(account_id)
- .map_err(|_| "invalid account id".to_owned())?;
- manager
- .select_account(&account_id)
- .map_err(|source| source.to_string())?;
- return self.load_identity_state().map(Some);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = account_id;
- Ok(None)
- }
- }
-
- fn remote_signer_action_state(&self) -> Option<SetupActionState> {
- #[cfg(target_os = "android")]
- {
- return Some(
- self.remote_signer
- .action_state()
- .unwrap_or_else(|_| SetupActionState {
- label: "Connect Remote Signer".to_owned(),
- enabled: !self.remote_signer.is_connecting(),
- pending: self.remote_signer.is_connecting(),
- }),
- );
- }
-
- #[cfg(not(target_os = "android"))]
- {
- None
- }
- }
-
- fn preview_remote_signer_connection(
- &self,
- input: &str,
- ) -> Result<radroots_app_core::RadrootsRemoteSignerPreview, String> {
- #[cfg(target_os = "android")]
- {
- return remote_signer::preview_connection(input);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = input;
- Err("remote signer onboarding is not available in this build".to_owned())
- }
- }
-
- fn request_remote_signer_connection(
- &self,
- input: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "android")]
- {
- self.remote_signer.begin_connect(input)?;
- return Ok(None);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = input;
- Ok(None)
- }
- }
-
- fn pending_remote_signer_connection(
- &self,
- ) -> Result<Option<radroots_app_core::RadrootsPendingRemoteSignerConnection>, String> {
- #[cfg(target_os = "android")]
- {
- return self.remote_signer.pending_connection();
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Ok(None)
- }
- }
-
- fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> {
- #[cfg(target_os = "android")]
- {
- return remote_signer::cancel_pending_connection();
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Ok(())
- }
- }
-
- fn remote_signer_note_action_state(&self) -> Option<SetupActionState> {
- #[cfg(target_os = "android")]
- {
- return Some(
- self.remote_signer
- .note_action_state()
- .unwrap_or(SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: false,
- pending: false,
- }),
- );
- }
-
- #[cfg(not(target_os = "android"))]
- {
- None
- }
- }
-
- fn request_remote_signer_note_action(&self, content: &str) -> Result<(), String> {
- #[cfg(target_os = "android")]
- {
- return self.remote_signer.begin_sign_kind1_note_selected(content);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = content;
- Ok(())
- }
- }
-
- fn poll_remote_signer_note_action_result(
- &self,
- ) -> Result<Option<radroots_app_core::RadrootsRemoteSignerSignedNote>, String> {
- #[cfg(target_os = "android")]
- {
- return self
- .remote_signer
- .take_note_update()
- .transpose()
- .map(|result| result.flatten());
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Ok(None)
- }
- }
-
- fn home_action_states(&self) -> Vec<HomeActionState> {
- #[cfg(target_os = "android")]
- {
- let secret_key_export_pending = Self::secret_key_export_pending();
- let Ok(manager) = Self::accounts_manager() else {
- return Vec::new();
- };
- let Ok(status) = manager
- .selected_account_status()
- .map_err(|source| source.to_string())
- else {
- return Vec::new();
- };
-
- return match status {
- RadrootsNostrSelectedAccountStatus::NotConfigured => Vec::new(),
- RadrootsNostrSelectedAccountStatus::PublicOnly { account } => {
- if matches!(
- remote_signer::custody_for_account_id(account.account_id.as_str()),
- Ok(RadrootsAccountCustody::RemoteSigner)
- ) {
- vec![HomeActionState {
- kind: HomeActionKind::DisconnectSigner,
- label: "Disconnect Remote Signer".to_owned(),
- enabled: true,
- pending: false,
- }]
- } else {
- Vec::new()
- }
- }
- RadrootsNostrSelectedAccountStatus::Ready { .. } => vec![
- HomeActionState {
- kind: HomeActionKind::BackupSecretKey,
- label: "Back Up Secret Key".to_owned(),
- enabled: !secret_key_export_pending,
- pending: secret_key_export_pending,
- },
- HomeActionState {
- kind: HomeActionKind::RevealRawSecretKey,
- label: "Reveal Raw Secret Key".to_owned(),
- enabled: !secret_key_export_pending,
- pending: secret_key_export_pending,
- },
- HomeActionState {
- kind: HomeActionKind::RemoveLocalKey,
- label: "Remove Key From This Device".to_owned(),
- enabled: true,
- pending: false,
- },
- HomeActionState {
- kind: HomeActionKind::ResetDevice,
- label: "Reset This Device".to_owned(),
- enabled: true,
- pending: false,
- },
- ],
- };
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Vec::new()
- }
- }
-
- fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> {
- #[cfg(target_os = "android")]
- {
- return match action {
- HomeActionKind::BackupSecretKey => Ok(HomeActionResult::None),
- HomeActionKind::RevealRawSecretKey => {
- Self::begin_raw_secret_key_reveal().map(|()| HomeActionResult::None)
- }
- HomeActionKind::RemoveLocalKey => {
- let manager = Self::accounts_manager()?;
- Self::remove_selected_local_identity(&manager)
- .map(HomeActionResult::IdentityState)
- }
- HomeActionKind::ResetDevice => {
- let manager = Self::accounts_manager()?;
- let accounts_path = storage::accounts_path()?;
- Self::reset_local_device_state(&manager, accounts_path.as_path())
- .map(HomeActionResult::IdentityState)
- }
- HomeActionKind::DisconnectSigner => {
- let manager = Self::accounts_manager()?;
- remote_signer::disconnect_selected_remote_signer(&manager)
- .map(HomeActionResult::IdentityState)
- }
- };
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = action;
- Ok(HomeActionResult::None)
- }
- }
-
- fn request_secret_key_backup_action(&self, password: &str) -> Result<HomeActionResult, String> {
- #[cfg(target_os = "android")]
- {
- return Self::begin_encrypted_secret_key_backup(password)
- .map(|()| HomeActionResult::None);
- }
-
- #[cfg(not(target_os = "android"))]
- {
- let _ = password;
- Ok(HomeActionResult::None)
- }
- }
-
- fn poll_home_action_result(&self) -> Result<Option<HomeActionResult>, String> {
- #[cfg(target_os = "android")]
- {
- return Self::poll_secret_key_export();
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Ok(None)
- }
- }
-
- fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "android")]
- {
- return self
- .remote_signer
- .take_update()
- .transpose()
- .map(|state| state.flatten());
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Ok(None)
- }
- }
-}
-
-#[cfg(any(target_os = "android", test))]
-#[cfg_attr(not(target_os = "android"), allow(dead_code))]
-impl AndroidBackend {
- fn new() -> Self {
- #[cfg(target_os = "android")]
- let offline_geocoder = offline_geocoder::AndroidOfflineGeocoder::start();
-
- #[cfg(not(target_os = "android"))]
- let offline_geocoder = offline_geocoder::AndroidOfflineGeocoder::from_state(
- RadrootsOfflineGeocoderState::unavailable(
- radroots_app_core::RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- radroots_app_core::RadrootsOfflineGeocoderPlatform::Android,
- "android offline geocoder initialization is only wired on android targets",
- ),
- );
-
- Self {
- country_lookup: country_lookup::AndroidCountryLookup::new(),
- offline_geocoder,
- #[cfg(target_os = "android")]
- remote_signer: remote_signer::AndroidRemoteSigner::new(),
- reverse_lookup: reverse_lookup::AndroidReverseLookup::new(),
- }
- }
-
- #[cfg(target_os = "android")]
- fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> {
- #[cfg(target_os = "android")]
- {
- return storage::accounts_manager();
- }
- }
-
- #[cfg(test)]
- fn unsupported_identity_state() -> IdentityGateState {
- IdentityGateState::Unsupported {
- reason: ANDROID_SETUP_UNAVAILABLE_REASON.to_owned(),
- }
- }
-
- #[cfg(test)]
- fn unsupported_setup_action_state() -> SetupActionState {
- SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: false,
- pending: false,
- }
- }
-
- fn enabled_setup_action_state() -> SetupActionState {
- SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: true,
- pending: false,
- }
- }
-
- fn map_status(status: RadrootsNostrSelectedAccountStatus) -> IdentityGateState {
- match status {
- RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- },
- RadrootsNostrSelectedAccountStatus::NotConfigured
- | RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing,
- }
- }
-
- fn identity_state_from_manager(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- let status = manager
- .selected_account_status()
- .map_err(|source| source.to_string())?;
- Ok(Self::map_status(status))
- }
-
- fn account_roster_from_manager(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<Vec<RadrootsAccountSummary>, String> {
- manager
- .list_accounts()
- .map_err(|source| source.to_string())?
- .into_iter()
- .map(|record| {
- #[cfg(target_os = "android")]
- let custody = remote_signer::custody_for_account_id(record.account_id.as_str())?;
- #[cfg(not(target_os = "android"))]
- let custody = RadrootsAccountCustody::LocalManaged;
- Ok(RadrootsAccountSummary {
- account_id: record.account_id.to_string(),
- npub: record.public_identity.public_key_npub,
- label: record.label,
- custody,
- })
- })
- .collect()
- }
-
- fn generate_local_identity(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- manager
- .generate_identity(Some("local".to_owned()), true)
- .map_err(|source| source.to_string())?;
- Self::identity_state_from_manager(manager)
- }
-
- fn export_selected_local_encrypted_secret_key(
- manager: &RadrootsNostrAccountsManager,
- password: &str,
- ) -> Result<String, String> {
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Err("no selected local identity is available to back up".to_owned());
- };
-
- let Some(secret_key_hex) = manager
- .export_secret_hex(&account_id)
- .map_err(|source| source.to_string())?
- else {
- return Err("selected local identity does not have an exportable secret".to_owned());
- };
-
- let secret_key_hex = Zeroizing::new(secret_key_hex);
- let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())
- .map_err(|source| source.to_string())?;
- identity
- .encrypt_secret_key_ncryptsec(password)
- .map_err(|source| source.to_string())
- }
-
- fn export_selected_local_raw_secret_key(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<String, String> {
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Err("no selected local identity is available to back up".to_owned());
- };
-
- let Some(secret_key_hex) = manager
- .export_secret_hex(&account_id)
- .map_err(|source| source.to_string())?
- else {
- return Err("selected local identity does not have an exportable secret".to_owned());
- };
-
- let secret_key_hex = Zeroizing::new(secret_key_hex);
- let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())
- .map_err(|source| source.to_string())?;
- Ok(identity.nsec())
- }
-
- fn import_local_identity(
- manager: &RadrootsNostrAccountsManager,
- request: &RadrootsSecretImportRequest,
- ) -> Result<IdentityGateState, String> {
- let identity = match request.mode {
- RadrootsSecretImportMode::EncryptedSecretKey => {
- let Some(password) = request.password.as_deref() else {
- return Err("password is required to import an encrypted secret key".to_owned());
- };
- RadrootsIdentity::from_encrypted_secret_key_str(
- request.secret_text.as_str(),
- password,
- )
- .map_err(|_| "invalid encrypted secret key or password".to_owned())?
- }
- RadrootsSecretImportMode::RawSecretKey => {
- RadrootsIdentity::from_secret_key_str(request.secret_text.as_str())
- .map_err(|_| "invalid raw secret key".to_owned())?
- }
- };
-
- manager
- .upsert_identity(&identity, None, true)
- .map_err(|source| source.to_string())?;
-
- Self::identity_state_from_manager(manager)
- }
-
- #[cfg(target_os = "android")]
- fn begin_encrypted_secret_key_backup(password: &str) -> Result<(), String> {
- *PENDING_SECRET_KEY_EXPORT
- .lock()
- .map_err(|_| "failed to store pending encrypted secret key backup".to_owned())? =
- Some(PendingSecretKeyExport::EncryptedBackup {
- password: Zeroizing::new(password.to_owned()),
- });
- if let Err(source) =
- android_security::begin_user_presence_verification("back up the current secret key")
- {
- *PENDING_SECRET_KEY_EXPORT
- .lock()
- .map_err(|_| "failed to clear pending encrypted secret key backup".to_owned())? =
- None;
- return Err(source.to_string());
- }
- Ok(())
- }
-
- #[cfg(not(target_os = "android"))]
- fn begin_encrypted_secret_key_backup(password: &str) -> Result<(), String> {
- let _ = password;
- Ok(())
- }
-
- #[cfg(target_os = "android")]
- fn begin_raw_secret_key_reveal() -> Result<(), String> {
- *PENDING_SECRET_KEY_EXPORT
- .lock()
- .map_err(|_| "failed to store pending raw secret key reveal".to_owned())? =
- Some(PendingSecretKeyExport::RawReveal);
- if let Err(source) =
- android_security::begin_user_presence_verification("reveal the current secret key")
- {
- *PENDING_SECRET_KEY_EXPORT
- .lock()
- .map_err(|_| "failed to clear pending raw secret key reveal".to_owned())? = None;
- return Err(source.to_string());
- }
- Ok(())
- }
-
- #[cfg(not(target_os = "android"))]
- fn begin_raw_secret_key_reveal() -> Result<(), String> {
- Ok(())
- }
-
- #[cfg(target_os = "android")]
- fn secret_key_export_pending() -> bool {
- android_security::is_user_presence_verification_pending().unwrap_or(false)
- }
-
- #[cfg(not(target_os = "android"))]
- fn secret_key_export_pending() -> bool {
- false
- }
-
- #[cfg(target_os = "android")]
- fn poll_secret_key_export() -> Result<Option<HomeActionResult>, String> {
- match android_security::take_user_presence_verification_result()
- .map_err(|source| source.to_string())?
- {
- Some(android_security::AndroidUserPresenceVerificationResult::Verified) => {
- let manager = Self::accounts_manager()?;
- let pending_export = PENDING_SECRET_KEY_EXPORT
- .lock()
- .map_err(|_| "failed to take pending secret key export".to_owned())?
- .take();
- match pending_export {
- Some(PendingSecretKeyExport::EncryptedBackup { password }) => {
- Self::export_selected_local_encrypted_secret_key(
- &manager,
- password.as_str(),
- )
- .map(|ncryptsec| {
- Some(HomeActionResult::RevealEncryptedSecretKey { ncryptsec })
- })
- }
- Some(PendingSecretKeyExport::RawReveal) => {
- Self::export_selected_local_raw_secret_key(&manager)
- .map(|nsec| Some(HomeActionResult::RevealRawSecretKey { nsec }))
- }
- None => Err("missing pending secret key export request".to_owned()),
- }
- }
- Some(android_security::AndroidUserPresenceVerificationResult::Failed(message)) => {
- *PENDING_SECRET_KEY_EXPORT
- .lock()
- .map_err(|_| "failed to clear pending secret key export".to_owned())? = None;
- Err(message)
- }
- None => Ok(None),
- }
- }
-
- #[cfg(not(target_os = "android"))]
- fn poll_secret_key_export() -> Result<Option<HomeActionResult>, String> {
- Ok(None)
- }
-
- fn remove_selected_local_identity(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Ok(IdentityGateState::Missing);
- };
-
- manager
- .remove_account(&account_id)
- .map_err(|source| source.to_string())?;
- Self::identity_state_from_manager(manager)
- }
-
- fn remove_all_local_identities(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- let account_ids = manager
- .list_accounts()
- .map_err(|source| source.to_string())?
- .into_iter()
- .map(|record| record.account_id)
- .collect::<Vec<_>>();
-
- for account_id in account_ids {
- manager
- .remove_account(&account_id)
- .map_err(|source| source.to_string())?;
- }
-
- Self::identity_state_from_manager(manager)
- }
-
- fn remove_accounts_file_if_present(accounts_path: &Path) -> Result<(), String> {
- match std::fs::remove_file(accounts_path) {
- Ok(()) => Ok(()),
- Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
- Err(source) => Err(format!("failed to remove android accounts file: {source}")),
- }
- }
-
- #[cfg(target_os = "android")]
- fn reset_local_device_state(
- manager: &RadrootsNostrAccountsManager,
- accounts_path: &Path,
- ) -> Result<IdentityGateState, String> {
- remote_signer::purge_all_custody_state()?;
- let state = Self::remove_all_local_identities(manager)?;
- Self::remove_accounts_file_if_present(accounts_path)?;
- Ok(state)
- }
-}
-
-#[cfg(any(target_os = "android", test))]
-#[cfg(test)]
-const ANDROID_SETUP_UNAVAILABLE_REASON: &str = "Secure onboarding is not yet available on Android.";
-
-#[cfg(target_os = "android")]
-fn native_options(android_app: AndroidApp) -> eframe::NativeOptions {
- eframe::NativeOptions {
- renderer: eframe::Renderer::Glow,
- android_app: Some(android_app),
- viewport: ViewportBuilder::default().with_title(APP_NAME),
- ..Default::default()
- }
-}
-
-#[cfg(target_os = "android")]
-fn run_android_app(android_app: AndroidApp) -> Result<(), String> {
- android_logger::init_once(Config::default().with_max_level(log::LevelFilter::Info));
- eframe::run_native(
- APP_NAME,
- native_options(android_app),
- Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(AndroidBackend::new()))))),
- )
- .map_err(|err| err.to_string())
-}
-
-#[cfg(target_os = "android")]
-#[allow(improper_ctypes_definitions)]
-#[allow(unsafe_code)]
-#[unsafe(no_mangle)]
-pub extern "C" fn android_main(android_app: AndroidApp) {
- if let Err(err) = run_android_app(android_app) {
- log::error!("android launcher failed: {err}");
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_test_support::{
- FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD, fixture_identity_ncryptsec,
- };
-
- #[test]
- fn android_backend_reports_android_disabled_state_off_target() {
- assert_eq!(
- AndroidBackend::unsupported_identity_state(),
- IdentityGateState::Unsupported {
- reason: ANDROID_SETUP_UNAVAILABLE_REASON.to_owned(),
- }
- );
- assert_eq!(
- AndroidBackend::unsupported_setup_action_state(),
- SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: false,
- pending: false,
- }
- );
- }
-
- #[test]
- fn android_backend_enables_setup_action_when_android_keygen_is_wired() {
- assert_eq!(
- AndroidBackend::enabled_setup_action_state(),
- SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: true,
- pending: false,
- }
- );
- }
-
- #[test]
- fn android_backend_maps_ready_account_to_ready_state() {
- let identity = RadrootsIdentity::generate();
- let account =
- RadrootsNostrAccountRecord::new(identity.to_public(), Some("local".into()), 0);
-
- let state = AndroidBackend::map_status(RadrootsNostrSelectedAccountStatus::Ready {
- account: account.clone(),
- });
-
- assert_eq!(
- state,
- IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- }
- );
- }
-
- #[test]
- fn android_backend_maps_fresh_and_public_only_accounts_to_missing() {
- let public_only_identity = RadrootsIdentity::generate();
- let public_only_account =
- RadrootsNostrAccountRecord::new(public_only_identity.to_public(), None, 0);
-
- assert_eq!(
- AndroidBackend::map_status(RadrootsNostrSelectedAccountStatus::NotConfigured),
- IdentityGateState::Missing
- );
- assert_eq!(
- AndroidBackend::map_status(RadrootsNostrSelectedAccountStatus::PublicOnly {
- account: public_only_account,
- }),
- IdentityGateState::Missing
- );
- }
-
- #[test]
- fn fresh_android_manager_starts_in_setup_state() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
-
- assert_eq!(
- AndroidBackend::identity_state_from_manager(&manager),
- Ok(IdentityGateState::Missing)
- );
- }
-
- #[test]
- fn local_identity_generation_transitions_android_to_ready() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
-
- let state = AndroidBackend::generate_local_identity(&manager).expect("generate identity");
- let IdentityGateState::Ready { account_id } = state else {
- panic!("expected ready identity state");
- };
-
- assert!(!account_id.is_empty());
- }
-
- #[test]
- fn local_identity_removal_transitions_android_back_to_missing() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
-
- AndroidBackend::generate_local_identity(&manager).expect("generate identity");
- let state = AndroidBackend::remove_selected_local_identity(&manager)
- .expect("remove selected account");
-
- assert_eq!(state, IdentityGateState::Missing);
- assert_eq!(
- manager.selected_account_id().expect("selected account"),
- None
- );
- }
-
- #[test]
- fn remove_all_local_identities_clears_every_account() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
-
- manager
- .generate_identity(Some("first".into()), true)
- .expect("generate first");
- manager
- .generate_identity(Some("second".into()), false)
- .expect("generate second");
-
- let state = AndroidBackend::remove_all_local_identities(&manager).expect("reset state");
-
- assert_eq!(state, IdentityGateState::Missing);
- assert_eq!(manager.list_accounts().expect("list accounts").len(), 0);
- assert_eq!(manager.selected_account_id().expect("selected"), None);
- }
-
- #[test]
- fn export_selected_local_raw_secret_key_returns_nsec() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let identity = RadrootsIdentity::generate();
-
- manager
- .upsert_identity(&identity, Some("primary".into()), true)
- .expect("store identity");
-
- let nsec =
- AndroidBackend::export_selected_local_raw_secret_key(&manager).expect("export secret");
-
- assert_eq!(nsec, identity.nsec());
- assert!(nsec.starts_with("nsec1"));
- }
-
- #[test]
- fn export_selected_local_encrypted_secret_key_returns_ncryptsec() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let fixture_identity =
- RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture");
-
- manager
- .upsert_identity(&fixture_identity, Some("primary".into()), true)
- .expect("store identity");
-
- let ncryptsec = AndroidBackend::export_selected_local_encrypted_secret_key(
- &manager,
- FIXTURE_BACKUP_PASSWORD,
- )
- .expect("export encrypted secret");
-
- let restored = RadrootsIdentity::from_encrypted_secret_key_str(
- ncryptsec.as_str(),
- FIXTURE_BACKUP_PASSWORD,
- )
- .expect("restore encrypted secret");
-
- assert_eq!(restored.secret_key_hex(), FIXTURE_ALICE.secret_key_hex);
- }
-
- #[test]
- fn import_local_identity_imports_raw_secret_key_and_selects_account() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let identity = RadrootsIdentity::generate();
-
- let state = AndroidBackend::import_local_identity(
- &manager,
- &RadrootsSecretImportRequest {
- mode: RadrootsSecretImportMode::RawSecretKey,
- secret_text: identity.nsec(),
- password: None,
- },
- )
- .expect("import");
-
- assert_eq!(
- state,
- IdentityGateState::Ready {
- account_id: identity.id().to_string(),
- }
- );
- assert_eq!(
- manager.selected_account_id().expect("selected"),
- Some(identity.id())
- );
- assert_eq!(manager.list_accounts().expect("list").len(), 1);
- assert_eq!(
- manager
- .export_secret_hex(&identity.id())
- .expect("export secret"),
- Some(identity.secret_key_hex())
- );
- }
-
- #[test]
- fn import_local_identity_imports_encrypted_secret_key_and_selects_account() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let encrypted_secret_key =
- fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD)
- .expect("fixture encrypted secret key");
- let fixture_identity =
- RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture");
- let fixture_account_id = fixture_identity.id();
-
- let state = AndroidBackend::import_local_identity(
- &manager,
- &RadrootsSecretImportRequest {
- mode: RadrootsSecretImportMode::EncryptedSecretKey,
- secret_text: encrypted_secret_key,
- password: Some(FIXTURE_BACKUP_PASSWORD.to_owned()),
- },
- )
- .expect("import");
-
- assert_eq!(
- state,
- IdentityGateState::Ready {
- account_id: fixture_account_id.to_string(),
- }
- );
- assert_eq!(
- manager.selected_account_id().expect("selected"),
- Some(fixture_account_id.clone())
- );
- assert_eq!(manager.list_accounts().expect("list").len(), 1);
- assert_eq!(
- manager
- .export_secret_hex(&fixture_account_id)
- .expect("export secret"),
- Some(FIXTURE_ALICE.secret_key_hex.to_owned())
- );
- }
-
- #[test]
- fn remove_accounts_file_if_present_deletes_existing_file() {
- let unique = format!(
- "radroots-android-reset-{}-{}.json",
- std::process::id(),
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .expect("system time")
- .as_nanos()
- );
- let path = std::env::temp_dir().join(unique);
- std::fs::write(&path, b"{}").expect("write accounts file");
-
- AndroidBackend::remove_accounts_file_if_present(path.as_path()).expect("remove file");
-
- assert!(!path.exists());
- }
-}
diff --git a/crates/launchers/android/src/offline_geocoder.rs b/crates/launchers/android/src/offline_geocoder.rs
@@ -1,600 +0,0 @@
-#![cfg_attr(not(target_os = "android"), allow(dead_code))]
-
-use radroots_app_core::{
- RadrootsLocationCountry, RadrootsLocationPoint, RadrootsLocationResolverError,
- RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState,
- RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation,
-};
-#[cfg(any(target_os = "android", test))]
-use radroots_geocoder::{
- Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions,
- GeocoderReverseResult,
-};
-use std::path::Path;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-#[cfg(target_os = "android")]
-use jni::objects::{JClass, JObject, JString};
-#[cfg(target_os = "android")]
-use jni::sys::jobject;
-#[cfg(target_os = "android")]
-use jni::{JNIEnv, JavaVM};
-#[cfg(target_os = "android")]
-use radroots_nostr_accounts::prelude::RadrootsNostrAccountsError;
-
-#[cfg(target_os = "android")]
-const ANDROID_APP_BRIDGE_CLASS: &str = "org.radroots.app.android.RadRootsAndroidAppBridge";
-
-#[derive(Clone)]
-pub(crate) struct AndroidOfflineGeocoder {
- current: Arc<Mutex<RadrootsOfflineGeocoderState>>,
- changed: Arc<AtomicBool>,
-}
-
-impl AndroidOfflineGeocoder {
- pub(crate) fn from_state(state: RadrootsOfflineGeocoderState) -> Self {
- Self {
- current: Arc::new(Mutex::new(state)),
- changed: Arc::new(AtomicBool::new(false)),
- }
- }
-
- #[cfg(target_os = "android")]
- pub(crate) fn start() -> Self {
- let tracker = Self::from_state(RadrootsOfflineGeocoderState::Initializing);
- let current = Arc::clone(&tracker.current);
- let changed = Arc::clone(&tracker.changed);
-
- std::thread::spawn(move || {
- let state = initialize_offline_geocoder();
- if let RadrootsOfflineGeocoderState::Unavailable { debug_message, .. } = &state {
- log::warn!("android offline geocoder unavailable: {debug_message}");
- }
- if let Ok(mut slot) = current.lock() {
- *slot = state;
- changed.store(true, Ordering::Release);
- }
- });
-
- tracker
- }
-
- pub(crate) fn current_state(&self) -> RadrootsOfflineGeocoderState {
- self.current
- .lock()
- .map(|state| state.clone())
- .unwrap_or_else(|_| {
- RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- RadrootsOfflineGeocoderPlatform::Android,
- "android offline geocoder state lock poisoned",
- )
- })
- }
-
- pub(crate) fn take_update(&self) -> Option<RadrootsOfflineGeocoderState> {
- if self.changed.swap(false, Ordering::AcqRel) {
- Some(self.current_state())
- } else {
- None
- }
- }
-}
-
-#[cfg(any(target_os = "android", test))]
-pub(crate) fn reverse_location(
- state: &RadrootsOfflineGeocoderState,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
-) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(state)?;
- let options = options.map(|options| GeocoderReverseOptions {
- limit: options.limit,
- degree_offset: options.degree_offset,
- });
- geocoder
- .reverse(
- GeocoderPoint {
- lat: point.lat,
- lng: point.lng,
- },
- options,
- )
- .map(|results| results.into_iter().map(map_reverse_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- })
-}
-
-#[cfg(any(target_os = "android", test))]
-pub(crate) fn list_countries(
- state: &RadrootsOfflineGeocoderState,
-) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(state)?;
- geocoder
- .country_list()
- .map(|results| results.into_iter().map(map_country_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- })
-}
-
-#[cfg(any(target_os = "android", test))]
-pub(crate) fn country_center(
- state: &RadrootsOfflineGeocoderState,
- country_id: &str,
-) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(state)?;
- geocoder
- .country_center(country_id)
- .map(|point| RadrootsLocationPoint {
- lat: point.lat,
- lng: point.lng,
- })
- .map_err(map_country_center_error)
-}
-
-#[cfg(target_os = "android")]
-fn initialize_offline_geocoder() -> RadrootsOfflineGeocoderState {
- match initialize_offline_geocoder_inner() {
- Ok(()) => RadrootsOfflineGeocoderState::Ready,
- Err((kind, asset_revision, debug_message)) => match asset_revision {
- Some(asset_revision) => RadrootsOfflineGeocoderState::unavailable_with_revision(
- kind,
- RadrootsOfflineGeocoderPlatform::Android,
- asset_revision,
- debug_message,
- ),
- None => RadrootsOfflineGeocoderState::unavailable(
- kind,
- RadrootsOfflineGeocoderPlatform::Android,
- debug_message,
- ),
- },
- }
-}
-
-#[cfg(target_os = "android")]
-fn initialize_offline_geocoder_inner() -> Result<
- (),
- (
- RadrootsOfflineGeocoderUnavailableKind,
- Option<String>,
- String,
- ),
-> {
- let staged_path = stage_offline_geocoder_asset()
- .map_err(|(kind, debug_message)| (kind, None, debug_message))?;
- let asset_revision = staged_asset_revision(staged_path.as_str()).map_err(|debug_message| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- None,
- debug_message,
- )
- })?;
- Geocoder::open_path(staged_path.as_str()).map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- Some(asset_revision.clone()),
- format!("failed to open staged android geocoder db: {source}"),
- )
- })?;
- let _ = prune_stale_revisions(staged_path.as_str());
- Ok(())
-}
-
-#[cfg(any(target_os = "android", test))]
-fn geocoder_for_queries(
- state: &RadrootsOfflineGeocoderState,
-) -> Result<Geocoder, RadrootsLocationResolverError> {
- match state {
- RadrootsOfflineGeocoderState::Initializing => {
- return Err(RadrootsLocationResolverError::Initializing);
- }
- RadrootsOfflineGeocoderState::Unavailable { .. } => {
- return Err(RadrootsLocationResolverError::Unavailable);
- }
- RadrootsOfflineGeocoderState::Ready => {}
- }
-
- #[cfg(target_os = "android")]
- {
- let staged_path = stage_offline_geocoder_asset()
- .map_err(|(_, message)| RadrootsLocationResolverError::QueryFailed { message })?;
- Geocoder::open_path(staged_path.as_str()).map_err(|source| {
- RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- }
- })
- }
-
- #[cfg(not(target_os = "android"))]
- {
- Err(RadrootsLocationResolverError::QueryFailed {
- message: "android location resolver queries are only available on android runtime"
- .to_owned(),
- })
- }
-}
-
-#[cfg(target_os = "android")]
-fn stage_offline_geocoder_asset() -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)>
-{
- let java_vm = android_java_vm().map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- source.to_string(),
- )
- })?;
- let mut env = java_vm
- .attach_current_thread()
- .map_err(jni_error)
- .map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- source.to_string(),
- )
- })?;
- let bridge_class = bridge_class(&mut env).map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- source.to_string(),
- )
- })?;
- let value = env
- .call_static_method(
- &bridge_class,
- "stageOfflineGeocoderAsset",
- "()Ljava/lang/String;",
- &[],
- )
- .and_then(|value| value.l())
- .map_err(jni_error)
- .map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- source.to_string(),
- )
- })?;
-
- if value.is_null() {
- let error_kind = take_last_error_kind(&mut env, &bridge_class).map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- source.to_string(),
- )
- })?;
- let debug_message = take_last_error_message(&mut env, &bridge_class)
- .map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- source.to_string(),
- )
- })?
- .unwrap_or_else(|| "android app bridge returned no staged geocoder path".to_owned());
- return Err((error_kind, debug_message));
- }
-
- let value = JString::from(value);
- env.get_string(&value)
- .map(|value| value.into())
- .map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- jni_error(source).to_string(),
- )
- })
-}
-
-#[cfg(target_os = "android")]
-#[allow(unsafe_code)]
-fn android_java_vm() -> Result<JavaVM, RadrootsNostrAccountsError> {
- let context = ndk_context::android_context();
- // SAFETY: ndk_context is initialized by the Android runtime before this code runs and
- // returns a stable JavaVM pointer for the current process.
- unsafe { JavaVM::from_raw(context.vm().cast()) }.map_err(jni_error)
-}
-
-#[cfg(target_os = "android")]
-#[allow(unsafe_code)]
-fn bridge_class<'local>(
- env: &mut JNIEnv<'local>,
-) -> Result<JClass<'local>, RadrootsNostrAccountsError> {
- let context = ndk_context::android_context();
- // SAFETY: ndk_context stores a live process-wide Context jobject for the active Android app.
- let context = unsafe { JObject::from_raw(context.context() as jobject) };
- let context = env.new_local_ref(&context).map_err(jni_error)?;
- let class_loader = env
- .call_method(&context, "getClassLoader", "()Ljava/lang/ClassLoader;", &[])
- .and_then(|value| value.l())
- .map_err(jni_error)?;
- let class_name = env
- .new_string(ANDROID_APP_BRIDGE_CLASS)
- .map_err(jni_error)?;
- let class_name = JObject::from(class_name);
- let bridge_class = env
- .call_method(
- &class_loader,
- "loadClass",
- "(Ljava/lang/String;)Ljava/lang/Class;",
- &[jni::objects::JValue::Object(&class_name)],
- )
- .and_then(|value| value.l())
- .map_err(jni_error)?;
- Ok(JClass::from(bridge_class))
-}
-
-#[cfg(target_os = "android")]
-fn take_last_error_kind(
- env: &mut JNIEnv<'_>,
- bridge_class: &JClass<'_>,
-) -> Result<RadrootsOfflineGeocoderUnavailableKind, RadrootsNostrAccountsError> {
- let value = env
- .call_static_method(bridge_class, "takeLastErrorKind", "()I", &[])
- .and_then(|value| value.i())
- .map_err(jni_error)?;
- match value {
- 1 => Ok(RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset),
- 2 => Ok(RadrootsOfflineGeocoderUnavailableKind::InitializationFailed),
- 3 => Ok(RadrootsOfflineGeocoderUnavailableKind::InternalError),
- _ => Ok(RadrootsOfflineGeocoderUnavailableKind::InitializationFailed),
- }
-}
-
-#[cfg(target_os = "android")]
-fn take_last_error_message(
- env: &mut JNIEnv<'_>,
- bridge_class: &JClass<'_>,
-) -> Result<Option<String>, RadrootsNostrAccountsError> {
- let value = env
- .call_static_method(
- bridge_class,
- "takeLastErrorMessage",
- "()Ljava/lang/String;",
- &[],
- )
- .and_then(|value| value.l())
- .map_err(jni_error)?;
- if value.is_null() {
- return Ok(None);
- }
- let value = JString::from(value);
- let value: String = env.get_string(&value).map_err(jni_error)?.into();
- Ok(Some(value))
-}
-
-#[cfg(target_os = "android")]
-fn jni_error(error: jni::errors::Error) -> RadrootsNostrAccountsError {
- RadrootsNostrAccountsError::Store(format!("android jni error: {error}"))
-}
-
-fn prune_stale_revisions(staged_path: &str) -> Result<(), String> {
- let staged_path = Path::new(staged_path);
- let Some(active_revision_dir) = staged_path.parent() else {
- return Err("android staged geocoder path did not have a revision directory".to_owned());
- };
- let Some(staged_root) = active_revision_dir.parent() else {
- return Err(
- "android staged geocoder path did not have a geocoder root directory".to_owned(),
- );
- };
- let Some(active_revision) = active_revision_dir.file_name() else {
- return Err("android staged geocoder revision directory did not have a name".to_owned());
- };
-
- if !staged_root.is_dir() {
- return Ok(());
- }
-
- for entry in std::fs::read_dir(staged_root)
- .map_err(|source| format!("failed to list android geocoder revisions: {source}"))?
- {
- let entry = entry.map_err(|source| {
- format!("failed to read android geocoder revision entry: {source}")
- })?;
- if entry.file_name() == active_revision {
- continue;
- }
-
- let path = entry.path();
- if entry
- .file_type()
- .map_err(|source| {
- format!("failed to inspect android geocoder revision entry: {source}")
- })?
- .is_dir()
- {
- std::fs::remove_dir_all(path.as_path()).map_err(|source| {
- format!(
- "failed to remove stale android geocoder revision {}: {source}",
- path.display()
- )
- })?;
- } else {
- std::fs::remove_file(path.as_path()).map_err(|source| {
- format!(
- "failed to remove stale android geocoder revision file {}: {source}",
- path.display()
- )
- })?;
- }
- }
-
- Ok(())
-}
-
-fn staged_asset_revision(staged_path: &str) -> Result<String, String> {
- let staged_path = Path::new(staged_path);
- let Some(active_revision_dir) = staged_path.parent() else {
- return Err("android staged geocoder path did not have a revision directory".to_owned());
- };
- let Some(active_revision) = active_revision_dir.file_name() else {
- return Err("android staged geocoder revision directory did not have a name".to_owned());
- };
- let revision = active_revision.to_string_lossy();
- if revision.len() != 64 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) {
- return Err(
- "android staged geocoder revision directory name was not a sha256 hex revision"
- .to_owned(),
- );
- }
- Ok(revision.into_owned())
-}
-
-fn map_reverse_result(result: GeocoderReverseResult) -> RadrootsResolvedLocation {
- RadrootsResolvedLocation {
- id: result.id,
- name: result.name,
- admin1_id: result.admin1_id,
- admin1_name: result.admin1_name,
- country_id: result.country_id,
- country_name: result.country_name,
- point: RadrootsLocationPoint {
- lat: result.latitude,
- lng: result.longitude,
- },
- }
-}
-
-fn map_country_result(result: GeocoderCountryListResult) -> RadrootsLocationCountry {
- RadrootsLocationCountry {
- country_id: result.country_id,
- country_name: result.country,
- center: RadrootsLocationPoint {
- lat: result.lat,
- lng: result.lng,
- },
- }
-}
-
-fn map_country_center_error(source: GeocoderError) -> RadrootsLocationResolverError {
- match source {
- GeocoderError::CountryCenterNotFound { country_id } => {
- RadrootsLocationResolverError::CountryCenterNotFound { country_id }
- }
- other => RadrootsLocationResolverError::QueryFailed {
- message: other.to_string(),
- },
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::time::{SystemTime, UNIX_EPOCH};
-
- #[test]
- fn missing_asset_maps_to_build_unavailable_message() {
- let state = RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Android,
- "android bundled geocoder asset missing at assets/geocoder/geonames.db",
- );
-
- assert_eq!(
- state,
- RadrootsOfflineGeocoderState::Unavailable {
- kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- platform: RadrootsOfflineGeocoderPlatform::Android,
- asset_revision: None,
- debug_message:
- "android bundled geocoder asset missing at assets/geocoder/geonames.db"
- .to_owned(),
- }
- );
- }
-
- #[test]
- fn staged_asset_revision_reads_sha256_directory_name() {
- let revision = "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c";
- let staged_path = format!("/tmp/radroots/android/geocoder/{revision}/geonames.db");
-
- assert_eq!(
- staged_asset_revision(staged_path.as_str()).unwrap(),
- revision
- );
- }
-
- #[test]
- fn prune_stale_revisions_keeps_active_revision_only() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-android-geocoder-prune-test-{}",
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
- let staged_root = temp_root.join("geocoder");
- let active_dir = staged_root.join("active");
- let stale_dir = staged_root.join("stale");
- let stale_file = staged_root.join("orphan.txt");
- let staged_path = active_dir.join("geonames.db");
-
- std::fs::create_dir_all(active_dir.as_path()).unwrap();
- std::fs::create_dir_all(stale_dir.as_path()).unwrap();
- std::fs::write(staged_path.as_path(), b"active").unwrap();
- std::fs::write(stale_dir.join("geonames.db"), b"stale").unwrap();
- std::fs::write(stale_file.as_path(), b"orphan").unwrap();
-
- prune_stale_revisions(staged_path.to_str().unwrap()).unwrap();
-
- assert!(active_dir.exists());
- assert!(!stale_dir.exists());
- assert!(!stale_file.exists());
-
- std::fs::remove_dir_all(temp_root.as_path()).unwrap();
- }
-
- #[test]
- fn reverse_result_mapping_preserves_location_fields() {
- let resolved = map_reverse_result(GeocoderReverseResult {
- id: 123,
- name: "Lusaka".to_owned(),
- admin1_id: Some(456),
- admin1_name: Some("Lusaka".to_owned()),
- country_id: "ZM".to_owned(),
- country_name: Some("Zambia".to_owned()),
- latitude: -15.4167,
- longitude: 28.2833,
- });
-
- assert_eq!(
- resolved,
- RadrootsResolvedLocation {
- id: 123,
- name: "Lusaka".to_owned(),
- admin1_id: Some(456),
- admin1_name: Some("Lusaka".to_owned()),
- country_id: "ZM".to_owned(),
- country_name: Some("Zambia".to_owned()),
- point: RadrootsLocationPoint {
- lat: -15.4167,
- lng: 28.2833,
- },
- }
- );
- }
-
- #[test]
- fn unavailable_state_blocks_queries_until_ready() {
- let state = RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Android,
- "missing android geocoder asset",
- );
-
- assert_eq!(
- reverse_location(&state, RadrootsLocationPoint { lat: 0.0, lng: 0.0 }, None,),
- Err(RadrootsLocationResolverError::Unavailable)
- );
- assert_eq!(
- list_countries(&state),
- Err(RadrootsLocationResolverError::Unavailable)
- );
- assert_eq!(
- country_center(&state, "US"),
- Err(RadrootsLocationResolverError::Unavailable)
- );
- }
-}
diff --git a/crates/launchers/android/src/remote_signer.rs b/crates/launchers/android/src/remote_signer.rs
@@ -1,471 +0,0 @@
-use crate::storage;
-use radroots_app_android_security::{ANDROID_NOSTR_SERVICE, RadrootsAndroidKeystoreVault};
-use radroots_app_core::{
- IdentityGateState, RadrootsAccountCustody, RadrootsPendingRemoteSignerConnection,
- RadrootsRemoteSignerPreview, RadrootsRemoteSignerSignedNote, SetupActionState,
-};
-use radroots_app_remote_signer::{
- RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerActionController,
- RadrootsAppRemoteSignerActionControllerHooks, RadrootsAppRemoteSignerActionState,
- RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks,
- RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerPendingState,
- RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState,
- RadrootsAppRemoteSignerSignedEvent, radroots_app_remote_signer_clear_pending_session,
- radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview,
- radroots_app_remote_signer_purge_all_custody_state,
- radroots_app_remote_signer_reconcile_startup,
-};
-use radroots_identity::RadrootsIdentityId;
-use radroots_nostr_accounts::prelude::{
- RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, RadrootsSecretVault,
- account_secret_slot,
-};
-use std::path::{Path, PathBuf};
-
-const REMOTE_SIGNER_LABEL: &str = "remote signer";
-
-#[derive(Clone, Copy)]
-struct AndroidRemoteSignerHooks;
-
-impl RadrootsAppRemoteSignerControllerHooks for AndroidRemoteSignerHooks {
- type ReadyState = IdentityGateState;
-
- fn reconcile_startup_state(&self) -> Result<(), String> {
- let manager = crate::storage::accounts_manager()?;
- let store_path = sessions_path()?;
- radroots_app_remote_signer_reconcile_startup(
- &manager,
- store_path.as_path(),
- REMOTE_SIGNER_LABEL,
- load_client_secret,
- remove_client_secret,
- purge_client_secret_namespace,
- )
- }
-
- fn store_pending_session(
- &self,
- pending: &RadrootsAppRemoteSignerPendingSession,
- ) -> Result<(), String> {
- let client_account_id = pending.record.client_account_id().to_owned();
- store_client_secret(
- client_account_id.as_str(),
- pending.client_secret_key_hex.as_str(),
- )?;
- let store_path = sessions_path()?;
- let mut state = load_sessions(store_path.as_path())?;
- if let Err(error) = state.upsert_pending(pending.record.clone()) {
- let _ = remove_client_secret(client_account_id.as_str());
- return Err(error.to_string());
- }
- if let Err(error) = save_sessions(store_path.as_path(), &state) {
- let _ = remove_client_secret(client_account_id.as_str());
- return Err(error);
- }
- Ok(())
- }
-
- fn pending_session_record(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- pending_session_record()
- }
-
- fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String> {
- load_client_secret(client_account_id)
- }
-
- fn activate_pending_session(
- &self,
- client_account_id: &str,
- approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession,
- ) -> Result<Self::ReadyState, String> {
- activate_remote_session(client_account_id, approved)
- }
-
- fn clear_pending_session(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- radroots_app_remote_signer_clear_pending_session(store_path.as_path(), remove_client_secret)
- }
-}
-
-#[derive(Clone)]
-pub(crate) struct AndroidRemoteSigner {
- controller: RadrootsAppRemoteSignerController<AndroidRemoteSignerHooks>,
- action_controller: RadrootsAppRemoteSignerActionController<AndroidRemoteSignerHooks>,
-}
-
-impl AndroidRemoteSigner {
- pub(crate) fn new() -> Self {
- Self {
- controller: RadrootsAppRemoteSignerController::new(AndroidRemoteSignerHooks),
- action_controller: RadrootsAppRemoteSignerActionController::new(
- AndroidRemoteSignerHooks,
- ),
- }
- }
-
- pub(crate) fn take_update(&self) -> Option<Result<Option<IdentityGateState>, String>> {
- self.controller.take_update()
- }
-
- pub(crate) fn is_connecting(&self) -> bool {
- self.controller.is_connecting()
- }
-
- pub(crate) fn action_state(&self) -> Result<SetupActionState, String> {
- if self.is_connecting() {
- return Ok(SetupActionState {
- label: "Connecting Remote Signer...".to_owned(),
- enabled: false,
- pending: true,
- });
- }
-
- if self.pending_connection()?.is_some() {
- return Ok(match self.controller.pending_state() {
- RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState {
- label: "Remote Signer Approval Check Retrying".to_owned(),
- enabled: false,
- pending: false,
- },
- RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { .. } => {
- SetupActionState {
- label: "Authorize Remote Signer to Continue".to_owned(),
- enabled: false,
- pending: false,
- }
- }
- RadrootsAppRemoteSignerPendingState::Idle
- | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState {
- label: "Remote Signer Waiting for Approval".to_owned(),
- enabled: false,
- pending: false,
- },
- });
- }
-
- Ok(SetupActionState {
- label: "Connect Remote Signer".to_owned(),
- enabled: true,
- pending: false,
- })
- }
-
- pub(crate) fn begin_connect(&self, input: &str) -> Result<(), String> {
- self.controller.begin_connect(input)
- }
-
- pub(crate) fn pending_connection(
- &self,
- ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> {
- Ok(
- pending_session_record()?.map(|record| RadrootsPendingRemoteSignerConnection {
- signer_npub: record.signer_identity.public_key_npub,
- relays: record.relays,
- auth_url: match self.controller.pending_state() {
- RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { url } => Some(url),
- _ => None,
- },
- }),
- )
- }
-
- pub(crate) fn note_action_state(&self) -> Result<SetupActionState, String> {
- if selected_remote_signer_account()?.is_none() {
- return Ok(SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: false,
- pending: false,
- });
- }
-
- Ok(match self.action_controller.state() {
- RadrootsAppRemoteSignerActionState::Idle => SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: true,
- pending: false,
- },
- RadrootsAppRemoteSignerActionState::Signing => SetupActionState {
- label: "Signing Remote Kind 1 Note...".to_owned(),
- enabled: false,
- pending: true,
- },
- RadrootsAppRemoteSignerActionState::AwaitingAuthorization { .. } => SetupActionState {
- label: "Authorize Remote Signer to Continue".to_owned(),
- enabled: false,
- pending: false,
- },
- })
- }
-
- pub(crate) fn begin_sign_kind1_note_selected(&self, content: &str) -> Result<(), String> {
- self.action_controller.begin_sign_kind1_note(content)
- }
-
- pub(crate) fn take_note_update(
- &self,
- ) -> Option<Result<Option<RadrootsRemoteSignerSignedNote>, String>> {
- self.action_controller.take_update()
- }
-}
-
-pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> {
- let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?;
- let requested_permissions = preview.requested_permission_labels();
- Ok(RadrootsRemoteSignerPreview {
- source_label: preview.source_label().to_owned(),
- signer_npub: preview.signer_identity.public_key_npub,
- relays: preview.relays,
- requested_permissions,
- })
-}
-
-pub(crate) fn identity_state_from_status(
- status: RadrootsNostrSelectedAccountStatus,
-) -> Result<IdentityGateState, String> {
- match status {
- RadrootsNostrSelectedAccountStatus::NotConfigured => Ok(IdentityGateState::Missing),
- RadrootsNostrSelectedAccountStatus::Ready { account } => Ok(IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- }),
- RadrootsNostrSelectedAccountStatus::PublicOnly { account } => {
- if active_session_for_account_id(account.account_id.as_str())?.is_some() {
- Ok(IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- })
- } else {
- Ok(IdentityGateState::Missing)
- }
- }
- }
-}
-
-pub(crate) fn custody_for_account_id(account_id: &str) -> Result<RadrootsAccountCustody, String> {
- if active_session_for_account_id(account_id)?.is_some() {
- Ok(RadrootsAccountCustody::RemoteSigner)
- } else {
- Ok(RadrootsAccountCustody::LocalManaged)
- }
-}
-
-pub(crate) fn disconnect_selected_remote_signer(
- manager: &RadrootsNostrAccountsManager,
-) -> Result<IdentityGateState, String> {
- let store_path = sessions_path()?;
- let status = radroots_app_remote_signer_disconnect_selected(
- manager,
- store_path.as_path(),
- remove_client_secret,
- )?;
- identity_state_from_status(status)
-}
-
-pub(crate) fn cancel_pending_connection() -> Result<(), String> {
- let store_path = sessions_path()?;
- let _ = radroots_app_remote_signer_clear_pending_session(
- store_path.as_path(),
- remove_client_secret,
- )?;
- Ok(())
-}
-
-pub(crate) fn purge_all_custody_state() -> Result<(), String> {
- let store_path = sessions_path()?;
- radroots_app_remote_signer_purge_all_custody_state(
- store_path.as_path(),
- remove_client_secret,
- purge_client_secret_namespace,
- )
-}
-
-fn activate_remote_session(
- client_account_id: &str,
- approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession,
-) -> Result<IdentityGateState, String> {
- let manager = crate::storage::accounts_manager()?;
- manager
- .upsert_public_identity(
- approved.user_identity.clone(),
- Some(REMOTE_SIGNER_LABEL.to_owned()),
- true,
- )
- .map_err(|source| source.to_string())?;
- let store_path = sessions_path()?;
- let activation_result = (|| -> Result<(), String> {
- let mut state = load_sessions(store_path.as_path())?;
- state
- .activate_session(
- client_account_id,
- approved.user_identity.clone(),
- approved.relays.clone(),
- approved.approved_permissions.clone(),
- )
- .ok_or_else(|| {
- "pending remote signer session disappeared before activation".to_owned()
- })?;
- save_sessions(store_path.as_path(), &state)
- })();
- if let Err(error) = activation_result {
- if let Err(rollback_error) = manager.remove_account(&approved.user_identity.id) {
- return Err(format!(
- "{error}. remote signer account rollback needs retry: {rollback_error}"
- ));
- }
- return Err(error);
- }
- Ok(IdentityGateState::Ready {
- account_id: approved.user_identity.id.to_string(),
- })
-}
-
-fn selected_remote_signer_account() -> Result<Option<String>, String> {
- let manager = crate::storage::accounts_manager()?;
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Ok(None);
- };
- if active_session_for_account_id(account_id.as_str())?.is_some() {
- Ok(Some(account_id.to_string()))
- } else {
- Ok(None)
- }
-}
-
-fn update_active_session_relays(account_id: &str, relays: Vec<String>) -> Result<(), String> {
- let store_path = sessions_path()?;
- let mut state = load_sessions(store_path.as_path())?;
- let Some(mut session) = state.active_session_for_account_id(account_id).cloned() else {
- return Err("active remote signer session disappeared before relay update".to_owned());
- };
- if session.relays == relays {
- return Ok(());
- }
- session.relays = relays;
- state.remove_active_session_for_account_id(account_id);
- state.sessions.push(session);
- save_sessions(store_path.as_path(), &state)
-}
-
-impl RadrootsAppRemoteSignerActionControllerHooks for AndroidRemoteSignerHooks {
- type ReadyState = RadrootsRemoteSignerSignedNote;
-
- fn selected_active_session(
- &self,
- ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String> {
- let Some(account_id) = selected_remote_signer_account()? else {
- return Ok(None);
- };
- let Some(record) = active_session_for_account_id(account_id.as_str())? else {
- return Ok(None);
- };
- let secret = load_client_secret(record.client_account_id())?;
- Ok(Some((record, secret)))
- }
-
- fn complete_sign_event(
- &self,
- signed_event: RadrootsAppRemoteSignerSignedEvent,
- ) -> Result<Self::ReadyState, String> {
- let Some(account_id) = selected_remote_signer_account()? else {
- return Err("remote signer account is no longer selected".to_owned());
- };
- update_active_session_relays(account_id.as_str(), signed_event.relays.clone())?;
- Ok(RadrootsRemoteSignerSignedNote {
- event_id_hex: signed_event.event_id_hex,
- })
- }
-}
-
-fn pending_session_record() -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- let state = load_sessions(store_path.as_path())?;
- Ok(state.pending_session().cloned())
-}
-
-fn active_session_for_account_id(
- account_id: &str,
-) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- let state = load_sessions(store_path.as_path())?;
- Ok(state.active_session_for_account_id(account_id).cloned())
-}
-
-fn load_sessions(path: &Path) -> Result<RadrootsAppRemoteSignerSessionStoreState, String> {
- RadrootsAppRemoteSignerSessionStoreState::load(path).map_err(|error| error.to_string())
-}
-
-fn save_sessions(
- path: &Path,
- state: &RadrootsAppRemoteSignerSessionStoreState,
-) -> Result<(), String> {
- state.save(path).map_err(|error| error.to_string())
-}
-
-fn sessions_path() -> Result<PathBuf, String> {
- Ok(storage::app_data_root()?
- .join("nostr")
- .join("remote-signer-sessions.json"))
-}
-
-fn client_secret_vault() -> RadrootsAndroidKeystoreVault {
- RadrootsAndroidKeystoreVault::new_with_namespace(
- ANDROID_NOSTR_SERVICE,
- RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE,
- )
-}
-
-fn legacy_client_secret_vault() -> RadrootsAndroidKeystoreVault {
- RadrootsAndroidKeystoreVault::new(ANDROID_NOSTR_SERVICE)
-}
-
-fn client_secret_slot(client_account_id: &str) -> Result<String, String> {
- let account_id = RadrootsIdentityId::try_from(client_account_id)
- .map_err(|_| "invalid remote signer client account id".to_owned())?;
- Ok(account_secret_slot(&account_id))
-}
-
-fn store_client_secret(client_account_id: &str, secret_key_hex: &str) -> Result<(), String> {
- let slot = client_secret_slot(client_account_id)?;
- client_secret_vault()
- .store_secret(slot.as_str(), secret_key_hex)
- .map_err(|source| source.to_string())
-}
-
-fn load_client_secret(client_account_id: &str) -> Result<String, String> {
- let slot = client_secret_slot(client_account_id)?;
- if let Some(secret) = client_secret_vault()
- .load_secret(slot.as_str())
- .map_err(|source| source.to_string())?
- {
- return Ok(secret);
- }
-
- let secret = legacy_client_secret_vault()
- .load_secret(slot.as_str())
- .map_err(|source| source.to_string())?
- .ok_or_else(|| "remote signer session secret is missing".to_owned())?;
- let _ = client_secret_vault().store_secret(slot.as_str(), secret.as_str());
- let _ = legacy_client_secret_vault().remove_secret(slot.as_str());
- Ok(secret)
-}
-
-fn remove_client_secret(client_account_id: &str) -> Result<(), String> {
- let slot = client_secret_slot(client_account_id)?;
- client_secret_vault()
- .remove_secret(slot.as_str())
- .map_err(|source| source.to_string())?;
- legacy_client_secret_vault()
- .remove_secret(slot.as_str())
- .map_err(|source| source.to_string())
-}
-
-fn purge_client_secret_namespace() -> Result<(), String> {
- client_secret_vault()
- .purge_namespace()
- .map_err(|source| source.to_string())
-}
diff --git a/crates/launchers/android/src/reverse_lookup.rs b/crates/launchers/android/src/reverse_lookup.rs
@@ -1,113 +0,0 @@
-#![cfg_attr(not(target_os = "android"), allow(dead_code))]
-
-#[cfg(target_os = "android")]
-use crate::offline_geocoder;
-use radroots_app_core::{
- RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions,
- RadrootsOfflineGeocoderState, RadrootsReverseLocationLookupResult,
-};
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-#[derive(Clone, Default)]
-pub(crate) struct AndroidReverseLookup {
- result: Arc<Mutex<Option<RadrootsReverseLocationLookupResult>>>,
- changed: Arc<AtomicBool>,
- pending: Arc<AtomicBool>,
-}
-
-impl AndroidReverseLookup {
- pub(crate) fn new() -> Self {
- Self::default()
- }
-
- #[cfg(target_os = "android")]
- pub(crate) fn begin(
- &self,
- geocoder_state: RadrootsOfflineGeocoderState,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline location query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.result);
- let changed = Arc::clone(&self.changed);
- let pending = Arc::clone(&self.pending);
- std::thread::spawn(move || {
- let lookup_result = offline_geocoder::reverse_location(&geocoder_state, point, options);
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "android"))]
- pub(crate) fn begin(
- &self,
- _geocoder_state: RadrootsOfflineGeocoderState,
- _point: RadrootsLocationPoint,
- _options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- pub(crate) fn take_update(&self) -> Option<RadrootsReverseLocationLookupResult> {
- if !self.changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "android reverse lookup result lock poisoned".to_owned(),
- })),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_core::RadrootsResolvedLocation;
-
- fn sample_result() -> RadrootsReverseLocationLookupResult {
- Ok(vec![RadrootsResolvedLocation {
- id: 7,
- name: "example".to_owned(),
- admin1_id: None,
- admin1_name: None,
- country_id: "US".to_owned(),
- country_name: Some("United States".to_owned()),
- point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 },
- }])
- }
-
- #[test]
- fn take_update_is_none_until_tracker_changes() {
- let tracker = AndroidReverseLookup::new();
-
- assert_eq!(tracker.take_update(), None);
- }
-
- #[test]
- fn take_update_returns_queued_result_once() {
- let tracker = AndroidReverseLookup::new();
- *tracker.result.lock().unwrap() = Some(sample_result());
- tracker.changed.store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_update(), Some(Ok(results)) if results.len() == 1));
- assert_eq!(tracker.take_update(), None);
- }
-}
diff --git a/crates/launchers/android/src/storage.rs b/crates/launchers/android/src/storage.rs
@@ -1,107 +0,0 @@
-#[cfg(target_os = "android")]
-use radroots_app_android_security::{
- ANDROID_NOSTR_SERVICE, RadrootsAndroidKeystoreVault, resolve_radroots_base_root,
-};
-use radroots_app_core::mobile_native_app_storage_layout;
-#[cfg(target_os = "android")]
-use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager;
-use radroots_runtime_paths::{RadrootsPaths, RadrootsPlatform};
-use std::path::{Path, PathBuf};
-
-fn app_paths_from_base_root(base_root: &Path) -> Result<RadrootsPaths, String> {
- Ok(mobile_native_app_storage_layout(RadrootsPlatform::Android, base_root)?.app_paths)
-}
-
-#[cfg(target_os = "android")]
-pub(crate) fn app_data_root() -> Result<PathBuf, String> {
- let base_root = resolve_radroots_base_root().map_err(|source| source.to_string())?;
- let root = app_data_root_from_base_root(base_root.as_path())?;
- ensure_directory_tree(root.as_path())?;
- Ok(root)
-}
-
-#[cfg(target_os = "android")]
-pub(crate) fn accounts_path() -> Result<PathBuf, String> {
- let base_root = resolve_radroots_base_root().map_err(|source| source.to_string())?;
- let accounts_path = accounts_path_from_base_root(base_root.as_path())?;
- if let Some(parent) = accounts_path.parent() {
- ensure_directory_tree(parent)?;
- }
- Ok(accounts_path)
-}
-
-#[cfg(target_os = "android")]
-pub(crate) fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> {
- RadrootsNostrAccountsManager::new_file_backed_with_vault(
- accounts_path()?,
- RadrootsAndroidKeystoreVault::new(ANDROID_NOSTR_SERVICE),
- )
- .map_err(|source| source.to_string())
-}
-
-pub(crate) fn app_data_root_from_base_root(base_root: &Path) -> Result<PathBuf, String> {
- Ok(app_paths_from_base_root(base_root)?.data)
-}
-
-pub(crate) fn accounts_path_from_base_root(base_root: &Path) -> Result<PathBuf, String> {
- Ok(app_data_root_from_base_root(base_root)?
- .join("nostr")
- .join("accounts.json"))
-}
-
-#[cfg(target_os = "android")]
-fn ensure_directory_tree(path: &Path) -> Result<(), String> {
- std::fs::create_dir_all(path)
- .map_err(|source| format!("failed to create android app data directory: {source}"))?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn accounts_path_uses_android_mobile_native_layout() {
- let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots");
-
- assert_eq!(
- accounts_path_from_base_root(base_root.as_path()).expect("accounts path"),
- PathBuf::from(
- "/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app/nostr/accounts.json"
- )
- );
- }
-
- #[test]
- fn app_data_root_uses_android_mobile_native_layout() {
- let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots");
-
- assert_eq!(
- app_data_root_from_base_root(base_root.as_path()).expect("app data root"),
- PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app")
- );
- }
-
- #[test]
- fn mobile_paths_follow_shared_logical_root_model() {
- let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots");
- let paths = app_paths_from_base_root(base_root.as_path()).expect("mobile paths");
-
- assert_eq!(
- paths.config,
- PathBuf::from(
- "/data/user/0/org.radroots.app.android/no_backup/RadRoots/config/apps/app"
- )
- );
- assert_eq!(
- paths.data,
- PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app")
- );
- assert_eq!(
- paths.secrets,
- PathBuf::from(
- "/data/user/0/org.radroots.app.android/no_backup/RadRoots/secrets/apps/app"
- )
- );
- }
-}
diff --git a/crates/launchers/desktop/Cargo.toml b/crates/launchers/desktop/Cargo.toml
@@ -1,44 +0,0 @@
-[package]
-name = "radroots_app_desktop"
-authors.workspace = true
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots desktop launcher"
-publish = false
-build = "build.rs"
-
-[lints]
-workspace = true
-
-[dependencies]
-eframe = { workspace = true, features = ["wgpu", "wayland", "x11"] }
-egui.workspace = true
-image.workspace = true
-log.workspace = true
-radroots_app_core = { path = "../../shared/core" }
-radroots_app_remote_signer = { path = "../../shared/remote_signer" }
-radroots_geocoder.workspace = true
-radroots_nostr_accounts = { workspace = true, features = ["memory-vault"] }
-radroots_runtime_paths.workspace = true
-zeroize.workspace = true
-
-[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
-wgpu = { workspace = true, features = ["metal", "wgsl"] }
-
-[target.'cfg(target_os = "macos")'.dependencies]
-objc2-foundation = { workspace = true, features = ["NSProcessInfo", "NSString"] }
-radroots_app_apple_security.workspace = true
-radroots_identity.workspace = true
-
-[target.'cfg(target_os = "windows")'.dependencies]
-wgpu = { workspace = true, features = ["dx12", "wgsl"] }
-
-[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
-wgpu = { workspace = true, features = ["vulkan", "gles", "wgsl"] }
-
-[dev-dependencies]
-radroots_app_test_support = { path = "../../shared/test_support" }
diff --git a/crates/launchers/desktop/assets/icons/radroots-logo.ico b/crates/launchers/desktop/assets/icons/radroots-logo.ico
Binary files differ.
diff --git a/crates/launchers/desktop/build.rs b/crates/launchers/desktop/build.rs
@@ -1,214 +0,0 @@
-use std::env;
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-
-const GEOCODER_DB_FILENAME: &str = "geonames.db";
-const GEOCODER_REVISION_FILENAME: &str = "geonames.revision";
-
-fn main() {
- println!("cargo:rerun-if-changed=build.rs");
- sync_optional_geocoder_assets();
-
- if env::var("CARGO_CFG_TARGET_OS").ok().as_deref() != Some("macos") {
- return;
- }
-
- let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir"));
- let package_dir =
- manifest_dir.join("../../../native/bridges/apple/security/swift/RadRootsAppleSecurity");
- let info_plist_path = manifest_dir.join("macos/Info.plist");
-
- emit_rerun_paths(&package_dir);
- println!("cargo:rerun-if-changed={}", info_plist_path.display());
-
- let configuration = if env::var("PROFILE").ok().as_deref() == Some("release") {
- "release"
- } else {
- "debug"
- };
- let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("target arch");
-
- run_swift_build(&package_dir, configuration, &arch);
- let bin_path = swift_bin_path(&package_dir, configuration, &arch);
-
- let dylib_path = bin_path.join("libRadRootsAppleSecurityFFIDynamic.dylib");
- if !dylib_path.is_file() {
- panic!(
- "swift package did not produce expected dynamic library at {}",
- dylib_path.display()
- );
- }
-
- let copied_library_dir = target_profile_dir();
- fs::copy(
- &dylib_path,
- copied_library_dir.join("libRadRootsAppleSecurityFFIDynamic.dylib"),
- )
- .unwrap_or_else(|err| {
- panic!(
- "failed to copy swift ffi library from {} into {}: {err}",
- dylib_path.display(),
- copied_library_dir.display()
- )
- });
-
- println!(
- "cargo:rustc-link-search=native={}",
- copied_library_dir.display()
- );
- println!("cargo:rustc-link-lib=dylib=RadRootsAppleSecurityFFIDynamic");
- println!("cargo:rustc-link-lib=framework=Foundation");
- println!("cargo:rustc-link-lib=framework=Security");
- println!("cargo:rustc-link-lib=framework=LocalAuthentication");
- println!(
- "cargo:rustc-link-arg=-Wl,-rpath,{}",
- copied_library_dir.display()
- );
- println!(
- "cargo:rustc-link-arg-bin=radroots_app_desktop=-Wl,-sectcreate,__TEXT,__info_plist,{}",
- info_plist_path.display()
- );
-}
-
-fn sync_optional_geocoder_assets() {
- let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir"));
- let source_db_path =
- manifest_dir.join(format!("../../../assets/geocoder/{GEOCODER_DB_FILENAME}"));
- let source_revision_path = manifest_dir.join(format!(
- "../../../assets/geocoder/{GEOCODER_REVISION_FILENAME}"
- ));
- println!("cargo:rerun-if-changed={}", source_db_path.display());
- println!("cargo:rerun-if-changed={}", source_revision_path.display());
-
- let profile_dir = target_profile_dir();
- let target_db_path = profile_dir.join(GEOCODER_DB_FILENAME);
- let target_revision_path = profile_dir.join(GEOCODER_REVISION_FILENAME);
-
- if source_db_path.is_file() {
- if !source_revision_path.is_file() {
- panic!(
- "stamped desktop geocoder revision asset missing at {}",
- source_revision_path.display()
- );
- }
-
- std::fs::copy(&source_db_path, &target_db_path).unwrap_or_else(|err| {
- panic!(
- "failed to copy optional desktop geocoder asset from {} to {}: {err}",
- source_db_path.display(),
- target_db_path.display()
- )
- });
- std::fs::copy(&source_revision_path, &target_revision_path).unwrap_or_else(|err| {
- panic!(
- "failed to copy optional desktop geocoder revision from {} to {}: {err}",
- source_revision_path.display(),
- target_revision_path.display()
- )
- });
- return;
- }
-
- if target_db_path.exists() {
- std::fs::remove_file(&target_db_path).unwrap_or_else(|err| {
- panic!(
- "failed to remove stale desktop geocoder asset at {}: {err}",
- target_db_path.display()
- )
- });
- }
- if target_revision_path.exists() {
- std::fs::remove_file(&target_revision_path).unwrap_or_else(|err| {
- panic!(
- "failed to remove stale desktop geocoder revision at {}: {err}",
- target_revision_path.display()
- )
- });
- }
-}
-
-fn target_profile_dir() -> PathBuf {
- let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"));
- out_dir
- .ancestors()
- .nth(3)
- .unwrap_or_else(|| panic!("unexpected cargo OUT_DIR layout: {}", out_dir.display()))
- .to_path_buf()
-}
-
-fn emit_rerun_paths(package_dir: &Path) {
- println!(
- "cargo:rerun-if-changed={}",
- package_dir.join("Package.swift").display()
- );
- emit_rerun_dir(&package_dir.join("Sources"));
-}
-
-fn emit_rerun_dir(dir: &Path) {
- if !dir.is_dir() {
- return;
- }
-
- let mut entries = std::fs::read_dir(dir)
- .unwrap_or_else(|err| panic!("failed to read {}: {err}", dir.display()))
- .map(|entry| entry.unwrap().path())
- .collect::<Vec<_>>();
- entries.sort();
-
- for path in entries {
- if path.is_dir() {
- emit_rerun_dir(&path);
- } else {
- println!("cargo:rerun-if-changed={}", path.display());
- }
- }
-}
-
-fn run_swift_build(package_dir: &Path, configuration: &str, arch: &str) {
- let status = Command::new("swift")
- .arg("build")
- .arg("--package-path")
- .arg(package_dir)
- .arg("--product")
- .arg("RadRootsAppleSecurityFFIDynamic")
- .arg("--configuration")
- .arg(configuration)
- .arg("--arch")
- .arg(arch)
- .status()
- .unwrap_or_else(|err| panic!("failed to run swift build: {err}"));
-
- if !status.success() {
- panic!("swift build failed for RadRootsAppleSecurityFFIDynamic");
- }
-}
-
-fn swift_bin_path(package_dir: &Path, configuration: &str, arch: &str) -> PathBuf {
- let output = Command::new("swift")
- .arg("build")
- .arg("--package-path")
- .arg(package_dir)
- .arg("--product")
- .arg("RadRootsAppleSecurityFFIDynamic")
- .arg("--configuration")
- .arg(configuration)
- .arg("--arch")
- .arg(arch)
- .arg("--show-bin-path")
- .output()
- .unwrap_or_else(|err| panic!("failed to resolve swift bin path: {err}"));
-
- if !output.status.success() {
- panic!(
- "swift build --show-bin-path failed: {}",
- String::from_utf8_lossy(&output.stderr)
- );
- }
-
- PathBuf::from(
- String::from_utf8(output.stdout)
- .expect("swift bin path utf-8")
- .trim(),
- )
-}
diff --git a/crates/launchers/desktop/macos/Info.plist b/crates/launchers/desktop/macos/Info.plist
@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>CFBundleIdentifier</key>
- <string>org.radroots.app.desktop</string>
- <key>CFBundleName</key>
- <string>Rad Roots</string>
- <key>CFBundleDisplayName</key>
- <string>Rad Roots</string>
- <key>CFBundlePackageType</key>
- <string>APPL</string>
- <key>CFBundleInfoDictionaryVersion</key>
- <string>6.0</string>
-</dict>
-</plist>
diff --git a/crates/launchers/desktop/src/country_lookup.rs b/crates/launchers/desktop/src/country_lookup.rs
@@ -1,187 +0,0 @@
-use crate::offline_geocoder;
-use radroots_app_core::{
- RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult,
- RadrootsLocationResolverError, RadrootsOfflineGeocoderState,
-};
-#[cfg(target_os = "macos")]
-use std::path::PathBuf;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-#[derive(Clone, Default)]
-pub(crate) struct DesktopCountryLookup {
- country_list_result: Arc<Mutex<Option<RadrootsLocationCountryListResult>>>,
- country_list_changed: Arc<AtomicBool>,
- country_list_pending: Arc<AtomicBool>,
- country_center_result: Arc<Mutex<Option<RadrootsLocationCountryCenterLookupResult>>>,
- country_center_changed: Arc<AtomicBool>,
- country_center_pending: Arc<AtomicBool>,
-}
-
-impl DesktopCountryLookup {
- pub(crate) fn new() -> Self {
- Self::default()
- }
-
- #[cfg(target_os = "macos")]
- pub(crate) fn begin_list(
- &self,
- app_data_root: PathBuf,
- geocoder_state: RadrootsOfflineGeocoderState,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.country_list_pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline country list query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.country_list_result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.country_list_result);
- let changed = Arc::clone(&self.country_list_changed);
- let pending = Arc::clone(&self.country_list_pending);
- std::thread::spawn(move || {
- let lookup_result =
- offline_geocoder::list_countries(app_data_root.as_path(), &geocoder_state);
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "macos"))]
- pub(crate) fn begin_list(
- &self,
- _app_data_root: std::path::PathBuf,
- _geocoder_state: RadrootsOfflineGeocoderState,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- #[cfg(target_os = "macos")]
- pub(crate) fn begin_center(
- &self,
- app_data_root: PathBuf,
- geocoder_state: RadrootsOfflineGeocoderState,
- country_id: String,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.country_center_pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline country center query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.country_center_result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.country_center_result);
- let changed = Arc::clone(&self.country_center_changed);
- let pending = Arc::clone(&self.country_center_pending);
- std::thread::spawn(move || {
- let lookup_result = offline_geocoder::country_center(
- app_data_root.as_path(),
- &geocoder_state,
- &country_id,
- );
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "macos"))]
- pub(crate) fn begin_center(
- &self,
- _app_data_root: std::path::PathBuf,
- _geocoder_state: RadrootsOfflineGeocoderState,
- _country_id: String,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- pub(crate) fn take_list_update(&self) -> Option<RadrootsLocationCountryListResult> {
- if !self.country_list_changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.country_list_result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "desktop country list result lock poisoned".to_owned(),
- })),
- }
- }
-
- pub(crate) fn take_center_update(&self) -> Option<RadrootsLocationCountryCenterLookupResult> {
- if !self.country_center_changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.country_center_result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "desktop country center result lock poisoned".to_owned(),
- })),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_core::{RadrootsLocationCountry, RadrootsLocationPoint};
-
- fn sample_countries() -> RadrootsLocationCountryListResult {
- Ok(vec![RadrootsLocationCountry {
- country_id: "BR".to_owned(),
- country_name: Some("Brazil".to_owned()),
- center: RadrootsLocationPoint {
- lat: -14.235,
- lng: -51.9253,
- },
- }])
- }
-
- #[test]
- fn take_list_update_is_none_until_tracker_changes() {
- let tracker = DesktopCountryLookup::new();
-
- assert_eq!(tracker.take_list_update(), None);
- }
-
- #[test]
- fn take_list_update_returns_queued_result_once() {
- let tracker = DesktopCountryLookup::new();
- *tracker.country_list_result.lock().unwrap() = Some(sample_countries());
- tracker.country_list_changed.store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_list_update(), Some(Ok(results)) if results.len() == 1));
- assert_eq!(tracker.take_list_update(), None);
- }
-
- #[test]
- fn take_center_update_returns_queued_result_once() {
- let tracker = DesktopCountryLookup::new();
- *tracker.country_center_result.lock().unwrap() = Some(Ok(RadrootsLocationPoint {
- lat: -14.235,
- lng: -51.9253,
- }));
- tracker
- .country_center_changed
- .store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_center_update(), Some(Ok(point)) if point.lat == -14.235));
- assert_eq!(tracker.take_center_update(), None);
- }
-}
diff --git a/crates/launchers/desktop/src/main.rs b/crates/launchers/desktop/src/main.rs
@@ -1,1279 +0,0 @@
-#![forbid(unsafe_code)]
-#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-
-use eframe::egui;
-use image::ImageFormat;
-#[cfg(all(target_os = "macos", not(test)))]
-use radroots_app_apple_security::verify_user_presence;
-#[cfg(target_os = "macos")]
-use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault};
-use radroots_app_core::{
- APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState,
- ImportActionState, RadrootsAccountCustody, RadrootsAccountSummary, RadrootsApp,
- RadrootsAppBackend, RadrootsLocationCountry, RadrootsLocationCountryCenterLookupResult,
- RadrootsLocationCountryListResult, RadrootsLocationPoint, RadrootsLocationResolverError,
- RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState,
- RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation,
- RadrootsReverseLocationLookupResult, RadrootsSecretImportMode, RadrootsSecretImportRequest,
- SetupActionState, interactive_user_app_storage_layout_with_resolver,
-};
-#[cfg(target_os = "macos")]
-use radroots_identity::RadrootsIdentity;
-#[cfg(target_os = "macos")]
-use radroots_nostr_accounts::prelude::{
- RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus,
-};
-use radroots_runtime_paths::{RadrootsPathResolver, RadrootsPaths};
-use std::path::{Path, PathBuf};
-#[cfg(target_os = "macos")]
-use zeroize::Zeroizing;
-
-mod country_lookup;
-mod offline_geocoder;
-#[cfg(target_os = "macos")]
-mod remote_signer;
-mod reverse_lookup;
-
-use country_lookup::DesktopCountryLookup;
-use offline_geocoder::DesktopOfflineGeocoder;
-#[cfg(target_os = "macos")]
-use remote_signer::DesktopRemoteSigner;
-use reverse_lookup::DesktopReverseLookup;
-
-const RADROOTS_DESKTOP_ICON_BYTES: &[u8] = include_bytes!("../assets/icons/radroots-logo.ico");
-
-#[cfg(target_os = "macos")]
-fn set_macos_app_name() {
- use objc2_foundation::{NSProcessInfo, NSString};
-
- let process_info = NSProcessInfo::processInfo();
- let process_name = NSString::from_str(APP_NAME);
- process_info.setProcessName(&process_name);
-}
-
-#[cfg(not(target_os = "macos"))]
-fn set_macos_app_name() {}
-
-fn desktop_icon() -> Option<egui::IconData> {
- let image =
- image::load_from_memory_with_format(RADROOTS_DESKTOP_ICON_BYTES, ImageFormat::Ico).ok()?;
- let image = image.into_rgba8();
- let (width, height) = image.dimensions();
- Some(egui::IconData {
- rgba: image.into_raw(),
- width,
- height,
- })
-}
-
-struct DesktopBackend {
- country_lookup: DesktopCountryLookup,
- offline_geocoder: DesktopOfflineGeocoder,
- #[cfg(target_os = "macos")]
- remote_signer: DesktopRemoteSigner,
- reverse_lookup: DesktopReverseLookup,
-}
-
-impl DesktopBackend {
- fn app_paths_with_resolver(resolver: &RadrootsPathResolver) -> Result<RadrootsPaths, String> {
- Ok(interactive_user_app_storage_layout_with_resolver(resolver)?.app_paths)
- }
-
- fn new() -> Self {
- #[cfg(target_os = "macos")]
- let offline_geocoder = match Self::app_data_root() {
- Ok(app_data_root) => DesktopOfflineGeocoder::start(app_data_root),
- Err(debug_message) => {
- DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- RadrootsOfflineGeocoderPlatform::Desktop,
- debug_message,
- ))
- }
- };
-
- #[cfg(not(target_os = "macos"))]
- let offline_geocoder =
- DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Desktop,
- "desktop offline geocoder initialization is only wired for macos",
- ));
-
- Self {
- country_lookup: DesktopCountryLookup::new(),
- offline_geocoder,
- #[cfg(target_os = "macos")]
- remote_signer: DesktopRemoteSigner::new(),
- reverse_lookup: DesktopReverseLookup::new(),
- }
- }
-
- fn radroots_root() -> Result<PathBuf, String> {
- Ok(
- interactive_user_app_storage_layout_with_resolver(&RadrootsPathResolver::current())?
- .runtime_root,
- )
- }
-
- fn app_data_root() -> Result<PathBuf, String> {
- Ok(Self::app_paths_with_resolver(&RadrootsPathResolver::current())?.data)
- }
-
- fn private_directory_chain(root: &Path, leaf: &Path) -> Result<Vec<PathBuf>, String> {
- let relative = leaf
- .strip_prefix(root)
- .map_err(|_| "private directory escaped radroots root".to_owned())?;
- let mut current = root.to_path_buf();
- let mut chain = vec![current.clone()];
- for component in relative.components() {
- current.push(component);
- chain.push(current.clone());
- }
- Ok(chain)
- }
-
- #[cfg(target_os = "macos")]
- fn ensure_private_directory_tree(leaf: &Path) -> Result<(), String> {
- use std::os::unix::fs::PermissionsExt;
-
- std::fs::create_dir_all(leaf)
- .map_err(|source| format!("failed to create accounts directory: {source}"))?;
-
- for path in Self::private_directory_chain(&Self::radroots_root()?, leaf)? {
- std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).map_err(
- |source| format!("failed to set private directory permissions: {source}"),
- )?;
- }
-
- Ok(())
- }
-
- fn accounts_path() -> Result<PathBuf, String> {
- Ok(Self::app_data_root()?.join("nostr").join("accounts.json"))
- }
-
- #[cfg(target_os = "macos")]
- fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> {
- let accounts_path = Self::accounts_path()?;
- if let Some(parent) = accounts_path.parent() {
- Self::ensure_private_directory_tree(parent)?;
- }
-
- RadrootsNostrAccountsManager::new_file_backed_with_vault(
- accounts_path,
- RadrootsAppleKeychainVault::new_desktop(APPLE_NOSTR_SERVICE),
- )
- .map_err(|source| source.to_string())
- }
-
- #[cfg(target_os = "macos")]
- fn map_status(status: RadrootsNostrSelectedAccountStatus) -> IdentityGateState {
- match status {
- RadrootsNostrSelectedAccountStatus::NotConfigured => IdentityGateState::Missing,
- RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing,
- RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- },
- }
- }
-
- #[cfg(target_os = "macos")]
- fn account_roster_from_manager(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<Vec<RadrootsAccountSummary>, String> {
- manager
- .list_accounts()
- .map_err(|source| source.to_string())?
- .into_iter()
- .map(|record| {
- let custody = remote_signer::custody_for_account_id(record.account_id.as_str())?;
- Ok(RadrootsAccountSummary {
- account_id: record.account_id.to_string(),
- npub: record.public_identity.public_key_npub,
- label: record.label,
- custody,
- })
- })
- .collect()
- }
-
- #[cfg(target_os = "macos")]
- fn remove_selected_local_identity(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Ok(IdentityGateState::Missing);
- };
-
- manager
- .remove_account(&account_id)
- .map_err(|source| source.to_string())?;
- let status = manager
- .selected_account_status()
- .map_err(|source| source.to_string())?;
- Ok(Self::map_status(status))
- }
-
- #[cfg(target_os = "macos")]
- fn export_selected_local_encrypted_secret_key(
- manager: &RadrootsNostrAccountsManager,
- password: &str,
- ) -> Result<String, String> {
- Self::authorize_secret_key_backup()?;
-
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Err("no selected local identity is available to back up".to_owned());
- };
-
- let Some(secret_key_hex) = manager
- .export_secret_hex(&account_id)
- .map_err(|source| source.to_string())?
- else {
- return Err("selected local identity does not have an exportable secret".to_owned());
- };
-
- let secret_key_hex = Zeroizing::new(secret_key_hex);
- let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())
- .map_err(|source| source.to_string())?;
- identity
- .encrypt_secret_key_ncryptsec(password)
- .map_err(|source| source.to_string())
- }
-
- #[cfg(target_os = "macos")]
- fn export_selected_local_raw_secret_key(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<String, String> {
- Self::authorize_secret_key_reveal()?;
-
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Err("no selected local identity is available to back up".to_owned());
- };
-
- let Some(secret_key_hex) = manager
- .export_secret_hex(&account_id)
- .map_err(|source| source.to_string())?
- else {
- return Err("selected local identity does not have an exportable secret".to_owned());
- };
-
- let secret_key_hex = Zeroizing::new(secret_key_hex);
- let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())
- .map_err(|source| source.to_string())?;
- Ok(identity.nsec())
- }
-
- #[cfg(all(target_os = "macos", not(test)))]
- fn authorize_secret_key_reveal() -> Result<(), String> {
- verify_user_presence("reveal the current secret key").map_err(|source| source.to_string())
- }
-
- #[cfg(any(not(target_os = "macos"), test))]
- fn authorize_secret_key_reveal() -> Result<(), String> {
- Ok(())
- }
-
- #[cfg(all(target_os = "macos", not(test)))]
- fn authorize_secret_key_backup() -> Result<(), String> {
- verify_user_presence("back up the current secret key").map_err(|source| source.to_string())
- }
-
- #[cfg(any(not(target_os = "macos"), test))]
- fn authorize_secret_key_backup() -> Result<(), String> {
- Ok(())
- }
-
- #[cfg(target_os = "macos")]
- fn import_local_identity(
- manager: &RadrootsNostrAccountsManager,
- request: &RadrootsSecretImportRequest,
- ) -> Result<IdentityGateState, String> {
- let identity = match request.mode {
- RadrootsSecretImportMode::EncryptedSecretKey => {
- let Some(password) = request.password.as_deref() else {
- return Err("password is required to import an encrypted secret key".to_owned());
- };
- RadrootsIdentity::from_encrypted_secret_key_str(
- request.secret_text.as_str(),
- password,
- )
- .map_err(|_| "invalid encrypted secret key or password".to_owned())?
- }
- RadrootsSecretImportMode::RawSecretKey => {
- RadrootsIdentity::from_secret_key_str(request.secret_text.as_str())
- .map_err(|_| "invalid raw secret key".to_owned())?
- }
- };
-
- manager
- .upsert_identity(&identity, None, true)
- .map_err(|source| source.to_string())?;
-
- let status = manager
- .selected_account_status()
- .map_err(|source| source.to_string())?;
- Ok(Self::map_status(status))
- }
-
- #[cfg(target_os = "macos")]
- fn remove_all_local_identities(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- let account_ids = manager
- .list_accounts()
- .map_err(|source| source.to_string())?
- .into_iter()
- .map(|record| record.account_id)
- .collect::<Vec<_>>();
-
- for account_id in account_ids {
- manager
- .remove_account(&account_id)
- .map_err(|source| source.to_string())?;
- }
-
- let status = manager
- .selected_account_status()
- .map_err(|source| source.to_string())?;
- Ok(Self::map_status(status))
- }
-
- #[cfg(target_os = "macos")]
- fn remove_accounts_file_if_present(accounts_path: &Path) -> Result<(), String> {
- match std::fs::remove_file(accounts_path) {
- Ok(()) => Ok(()),
- Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
- Err(source) => Err(format!("failed to remove accounts file: {source}")),
- }
- }
-
- #[cfg(target_os = "macos")]
- fn reset_local_device_state(
- manager: &RadrootsNostrAccountsManager,
- accounts_path: &Path,
- ) -> Result<IdentityGateState, String> {
- remote_signer::purge_all_custody_state()?;
- let state = Self::remove_all_local_identities(manager)?;
- Self::remove_accounts_file_if_present(accounts_path)?;
- Ok(state)
- }
-}
-
-impl RadrootsAppBackend for DesktopBackend {
- fn load_identity_state(&self) -> Result<IdentityGateState, String> {
- #[cfg(target_os = "macos")]
- {
- let manager = Self::accounts_manager()?;
- let status = manager
- .selected_account_status()
- .map_err(|source| source.to_string())?;
- return remote_signer::identity_state_from_status(status);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Ok(IdentityGateState::Unsupported {
- reason: "Local secure onboarding is only implemented for macOS in this slice."
- .to_owned(),
- })
- }
- }
-
- fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> {
- #[cfg(target_os = "macos")]
- {
- let manager = Self::accounts_manager()?;
- return Self::account_roster_from_manager(&manager);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Ok(Vec::new())
- }
- }
-
- fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> {
- Some(self.offline_geocoder.current_state())
- }
-
- fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> {
- Ok(self.offline_geocoder.take_update())
- }
-
- fn reverse_location(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
- #[cfg(target_os = "macos")]
- {
- let app_data_root = Self::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return offline_geocoder::reverse_location(
- app_data_root.as_path(),
- &self.offline_geocoder.current_state(),
- point,
- options,
- );
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = (point, options);
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn request_reverse_location_lookup(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- #[cfg(target_os = "macos")]
- {
- let app_data_root = Self::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return self.reverse_lookup.begin(
- app_data_root,
- self.offline_geocoder.current_state(),
- point,
- options,
- );
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = (point, options);
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn poll_reverse_location_lookup_result(
- &self,
- ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> {
- Ok(self.reverse_lookup.take_update())
- }
-
- fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> {
- #[cfg(target_os = "macos")]
- {
- let app_data_root = Self::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return self
- .country_lookup
- .begin_list(app_data_root, self.offline_geocoder.current_state());
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn poll_location_country_list_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryListResult>, String> {
- Ok(self.country_lookup.take_list_update())
- }
-
- fn request_location_country_center_lookup(
- &self,
- country_id: &str,
- ) -> Result<(), RadrootsLocationResolverError> {
- #[cfg(target_os = "macos")]
- {
- let app_data_root = Self::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return self.country_lookup.begin_center(
- app_data_root,
- self.offline_geocoder.current_state(),
- country_id.to_owned(),
- );
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = country_id;
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn poll_location_country_center_lookup_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> {
- Ok(self.country_lookup.take_center_update())
- }
-
- fn list_location_countries(
- &self,
- ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- #[cfg(target_os = "macos")]
- {
- let app_data_root = Self::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return offline_geocoder::list_countries(
- app_data_root.as_path(),
- &self.offline_geocoder.current_state(),
- );
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn location_country_center(
- &self,
- country_id: &str,
- ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- #[cfg(target_os = "macos")]
- {
- let app_data_root = Self::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return offline_geocoder::country_center(
- app_data_root.as_path(),
- &self.offline_geocoder.current_state(),
- country_id,
- );
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = country_id;
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn setup_action_state(&self) -> SetupActionState {
- #[cfg(target_os = "macos")]
- {
- return SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: true,
- pending: false,
- };
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: false,
- pending: false,
- }
- }
- }
-
- fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "macos")]
- {
- let manager = Self::accounts_manager()?;
- manager
- .generate_identity(Some("local".to_owned()), true)
- .map_err(|source| source.to_string())?;
- return self.load_identity_state().map(Some);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Ok(Some(IdentityGateState::Unsupported {
- reason: "Local secure onboarding is only implemented for macOS in this slice."
- .to_owned(),
- }))
- }
- }
-
- fn home_setup_action_state(&self) -> Option<SetupActionState> {
- Some(self.setup_action_state())
- }
-
- fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- self.request_setup_action()
- }
-
- fn import_action_state(&self) -> Option<ImportActionState> {
- #[cfg(target_os = "macos")]
- {
- return Some(ImportActionState {
- label: "Import Secret Key".to_owned(),
- enabled: true,
- pending: false,
- });
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- None
- }
- }
-
- fn request_import_action(
- &self,
- request: &RadrootsSecretImportRequest,
- ) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "macos")]
- {
- let manager = Self::accounts_manager()?;
- return Self::import_local_identity(&manager, request).map(Some);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = request;
- Ok(None)
- }
- }
-
- fn remote_signer_action_state(&self) -> Option<SetupActionState> {
- #[cfg(target_os = "macos")]
- {
- return Some(
- self.remote_signer
- .action_state()
- .unwrap_or_else(|_| SetupActionState {
- label: "Connect Remote Signer".to_owned(),
- enabled: !self.remote_signer.is_connecting(),
- pending: self.remote_signer.is_connecting(),
- }),
- );
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- None
- }
- }
-
- fn preview_remote_signer_connection(
- &self,
- input: &str,
- ) -> Result<radroots_app_core::RadrootsRemoteSignerPreview, String> {
- #[cfg(target_os = "macos")]
- {
- return remote_signer::preview_connection(input);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = input;
- Err("remote signer onboarding is not available in this build".to_owned())
- }
- }
-
- fn request_remote_signer_connection(
- &self,
- input: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "macos")]
- {
- self.remote_signer.begin_connect(input)?;
- return Ok(None);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = input;
- Ok(None)
- }
- }
-
- fn pending_remote_signer_connection(
- &self,
- ) -> Result<Option<radroots_app_core::RadrootsPendingRemoteSignerConnection>, String> {
- #[cfg(target_os = "macos")]
- {
- return self.remote_signer.pending_connection();
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Ok(None)
- }
- }
-
- fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> {
- #[cfg(target_os = "macos")]
- {
- return remote_signer::cancel_pending_connection();
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Ok(())
- }
- }
-
- fn remote_signer_note_action_state(&self) -> Option<SetupActionState> {
- #[cfg(target_os = "macos")]
- {
- return Some(
- self.remote_signer
- .note_action_state()
- .unwrap_or(SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: false,
- pending: false,
- }),
- );
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- None
- }
- }
-
- fn selected_remote_signer_approved_permissions(&self) -> Option<Vec<String>> {
- #[cfg(target_os = "macos")]
- {
- return remote_signer::selected_approved_permission_labels().unwrap_or(None);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- None
- }
- }
-
- fn request_remote_signer_note_action(&self, content: &str) -> Result<(), String> {
- #[cfg(target_os = "macos")]
- {
- return self.remote_signer.begin_sign_kind1_note_selected(content);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = content;
- Ok(())
- }
- }
-
- fn poll_remote_signer_note_action_result(
- &self,
- ) -> Result<Option<radroots_app_core::RadrootsRemoteSignerSignedNote>, String> {
- #[cfg(target_os = "macos")]
- {
- return self
- .remote_signer
- .take_note_update()
- .transpose()
- .map(|result| result.flatten());
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Ok(None)
- }
- }
-
- fn request_select_account(
- &self,
- account_id: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "macos")]
- {
- let manager = Self::accounts_manager()?;
- let account_id = radroots_identity::RadrootsIdentityId::try_from(account_id)
- .map_err(|_| "invalid account id".to_owned())?;
- manager
- .select_account(&account_id)
- .map_err(|source| source.to_string())?;
- return self.load_identity_state().map(Some);
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = account_id;
- Ok(None)
- }
- }
-
- fn home_action_states(&self) -> Vec<HomeActionState> {
- #[cfg(target_os = "macos")]
- {
- let Ok(manager) = Self::accounts_manager() else {
- return Vec::new();
- };
- let Ok(status) = manager
- .selected_account_status()
- .map_err(|source| source.to_string())
- else {
- return Vec::new();
- };
-
- return match status {
- RadrootsNostrSelectedAccountStatus::NotConfigured => Vec::new(),
- RadrootsNostrSelectedAccountStatus::PublicOnly { account } => {
- if matches!(
- remote_signer::custody_for_account_id(account.account_id.as_str()),
- Ok(RadrootsAccountCustody::RemoteSigner)
- ) {
- vec![HomeActionState {
- kind: HomeActionKind::DisconnectSigner,
- label: "Disconnect Remote Signer".to_owned(),
- enabled: true,
- pending: false,
- }]
- } else {
- Vec::new()
- }
- }
- RadrootsNostrSelectedAccountStatus::Ready { .. } => vec![
- HomeActionState {
- kind: HomeActionKind::BackupSecretKey,
- label: "Back Up Secret Key".to_owned(),
- enabled: true,
- pending: false,
- },
- HomeActionState {
- kind: HomeActionKind::RevealRawSecretKey,
- label: "Reveal Raw Secret Key".to_owned(),
- enabled: true,
- pending: false,
- },
- HomeActionState {
- kind: HomeActionKind::RemoveLocalKey,
- label: "Remove Key From This Device".to_owned(),
- enabled: true,
- pending: false,
- },
- HomeActionState {
- kind: HomeActionKind::ResetDevice,
- label: "Reset This Device".to_owned(),
- enabled: true,
- pending: false,
- },
- ],
- };
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Vec::new()
- }
- }
-
- fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> {
- #[cfg(target_os = "macos")]
- {
- let manager = Self::accounts_manager()?;
- return match action {
- HomeActionKind::BackupSecretKey => Ok(HomeActionResult::None),
- HomeActionKind::RevealRawSecretKey => {
- Self::export_selected_local_raw_secret_key(&manager)
- .map(|nsec| HomeActionResult::RevealRawSecretKey { nsec })
- }
- HomeActionKind::RemoveLocalKey => Self::remove_selected_local_identity(&manager)
- .map(HomeActionResult::IdentityState),
- HomeActionKind::ResetDevice => {
- let accounts_path = Self::accounts_path()?;
- Self::reset_local_device_state(&manager, accounts_path.as_path())
- .map(HomeActionResult::IdentityState)
- }
- HomeActionKind::DisconnectSigner => {
- remote_signer::disconnect_selected_remote_signer(&manager)
- .map(HomeActionResult::IdentityState)
- }
- };
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = action;
- Ok(HomeActionResult::None)
- }
- }
-
- fn request_secret_key_backup_action(&self, password: &str) -> Result<HomeActionResult, String> {
- #[cfg(target_os = "macos")]
- {
- let manager = Self::accounts_manager()?;
- return Self::export_selected_local_encrypted_secret_key(&manager, password)
- .map(|ncryptsec| HomeActionResult::RevealEncryptedSecretKey { ncryptsec });
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- let _ = password;
- Ok(HomeActionResult::None)
- }
- }
-
- fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> {
- #[cfg(target_os = "macos")]
- {
- return self
- .remote_signer
- .take_update()
- .transpose()
- .map(|state| state.flatten());
- }
-
- #[cfg(not(target_os = "macos"))]
- {
- Ok(None)
- }
- }
-}
-
-fn main() -> eframe::Result<()> {
- set_macos_app_name();
-
- let viewport = {
- let viewport = egui::ViewportBuilder::default()
- .with_inner_size([1280.0, 820.0])
- .with_min_inner_size([480.0, 320.0]);
- if let Some(icon) = desktop_icon() {
- viewport.with_icon(icon)
- } else {
- viewport
- }
- };
-
- let options = eframe::NativeOptions {
- viewport,
- ..Default::default()
- };
-
- eframe::run_native(
- APP_NAME,
- options,
- Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(DesktopBackend::new()))))),
- )
-}
-
-#[cfg(test)]
-mod path_contract_tests {
- use super::DesktopBackend;
- use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform};
- use std::path::PathBuf;
-
- #[test]
- fn desktop_app_paths_follow_linux_interactive_user_contract() {
- let resolver = RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(PathBuf::from("/home/treesap")),
- ..RadrootsHostEnvironment::default()
- },
- );
-
- let paths = DesktopBackend::app_paths_with_resolver(&resolver).expect("desktop app paths");
-
- assert_eq!(
- paths.data,
- PathBuf::from("/home/treesap/.radroots/data/apps/app")
- );
- assert_eq!(
- paths.logs,
- PathBuf::from("/home/treesap/.radroots/logs/apps/app")
- );
- assert_eq!(
- paths.secrets,
- PathBuf::from("/home/treesap/.radroots/secrets/apps/app")
- );
- }
-
- #[test]
- fn desktop_app_paths_follow_macos_interactive_user_contract() {
- let resolver = RadrootsPathResolver::new(
- RadrootsPlatform::Macos,
- RadrootsHostEnvironment {
- home_dir: Some(PathBuf::from("/Users/treesap")),
- ..RadrootsHostEnvironment::default()
- },
- );
-
- let paths = DesktopBackend::app_paths_with_resolver(&resolver).expect("desktop app paths");
-
- assert_eq!(
- paths.data,
- PathBuf::from("/Users/treesap/.radroots/data/apps/app")
- );
- assert_eq!(
- paths.logs,
- PathBuf::from("/Users/treesap/.radroots/logs/apps/app")
- );
- assert_eq!(
- paths.secrets,
- PathBuf::from("/Users/treesap/.radroots/secrets/apps/app")
- );
- }
-
- #[test]
- fn desktop_app_paths_follow_windows_interactive_user_contract() {
- let resolver = RadrootsPathResolver::new(
- RadrootsPlatform::Windows,
- RadrootsHostEnvironment {
- appdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Roaming")),
- localappdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Local")),
- ..RadrootsHostEnvironment::default()
- },
- );
-
- let paths = DesktopBackend::app_paths_with_resolver(&resolver).expect("desktop app paths");
-
- assert_eq!(
- paths.config,
- PathBuf::from(r"C:\Users\treesap\AppData\Roaming")
- .join("Radroots")
- .join("config")
- .join("apps")
- .join("app")
- );
- assert_eq!(
- paths.data,
- PathBuf::from(r"C:\Users\treesap\AppData\Local")
- .join("Radroots")
- .join("data")
- .join("apps")
- .join("app")
- );
- assert_eq!(
- paths.logs,
- PathBuf::from(r"C:\Users\treesap\AppData\Local")
- .join("Radroots")
- .join("logs")
- .join("apps")
- .join("app")
- );
- assert_eq!(
- paths.secrets,
- PathBuf::from(r"C:\Users\treesap\AppData\Roaming")
- .join("Radroots")
- .join("secrets")
- .join("apps")
- .join("app")
- );
- }
-}
-
-#[cfg(all(test, target_os = "macos"))]
-mod tests {
- use super::DesktopBackend;
- use radroots_app_apple_security::RadrootsAppleKeychainVault;
- use radroots_app_core::{
- IdentityGateState, RadrootsSecretImportMode, RadrootsSecretImportRequest,
- };
- use radroots_app_test_support::{
- FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD, fixture_identity_ncryptsec,
- };
- use radroots_identity::{RadrootsIdentity, RadrootsIdentityId};
- use radroots_nostr_accounts::prelude::{RadrootsSecretVault, account_secret_slot};
- use std::path::PathBuf;
-
- #[test]
- fn private_directory_chain_covers_only_radroots_subtree() {
- let root = PathBuf::from("/tmp/example/.radroots");
- let leaf = root.join("data").join("apps").join("app").join("nostr");
-
- let chain = DesktopBackend::private_directory_chain(&root, &leaf).unwrap();
-
- assert_eq!(
- chain,
- vec![
- PathBuf::from("/tmp/example/.radroots"),
- PathBuf::from("/tmp/example/.radroots/data"),
- PathBuf::from("/tmp/example/.radroots/data/apps"),
- PathBuf::from("/tmp/example/.radroots/data/apps/app"),
- PathBuf::from("/tmp/example/.radroots/data/apps/app/nostr"),
- ]
- );
- }
-
- #[test]
- fn apple_keychain_vault_round_trips_secret_hex() {
- let vault =
- RadrootsAppleKeychainVault::new_desktop("org.radroots.app.tests.desktop.roundtrip");
- let account_id = RadrootsIdentityId::parse(
- "3bf0c63f0f4478a288f6b67f0429dbf7f5119d4fa7218a4c40ef1378f80f7606",
- )
- .expect("account id");
- let slot = account_secret_slot(&account_id);
-
- let _ = vault.remove_secret(slot.as_str());
-
- vault
- .store_secret(
- slot.as_str(),
- "a0468b0f2f5de9db868fb563b13632eb92ec4697dd4fddbdca0488f1a1b2c3d4",
- )
- .expect("store secret");
-
- assert_eq!(
- vault.load_secret(slot.as_str()).expect("load secret"),
- Some("a0468b0f2f5de9db868fb563b13632eb92ec4697dd4fddbdca0488f1a1b2c3d4".to_owned())
- );
-
- vault.remove_secret(slot.as_str()).expect("remove secret");
- assert_eq!(
- vault.load_secret(slot.as_str()).expect("load missing"),
- None
- );
- }
-
- #[test]
- fn remove_all_local_identities_clears_every_account() {
- let manager =
- radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory();
-
- manager
- .generate_identity(Some("first".into()), true)
- .expect("generate first");
- manager
- .generate_identity(Some("second".into()), false)
- .expect("generate second");
-
- let state = DesktopBackend::remove_all_local_identities(&manager).expect("reset state");
-
- assert_eq!(state, IdentityGateState::Missing);
- assert_eq!(manager.list_accounts().expect("list accounts").len(), 0);
- assert_eq!(manager.selected_account_id().expect("selected"), None);
- }
-
- #[test]
- fn export_selected_local_raw_secret_key_returns_nsec() {
- let manager =
- radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory();
- let identity = RadrootsIdentity::generate();
-
- manager
- .upsert_identity(&identity, Some("primary".into()), true)
- .expect("store identity");
-
- let nsec =
- DesktopBackend::export_selected_local_raw_secret_key(&manager).expect("export secret");
-
- assert_eq!(nsec, identity.nsec());
- assert!(nsec.starts_with("nsec1"));
- }
-
- #[test]
- fn export_selected_local_encrypted_secret_key_returns_ncryptsec() {
- let manager =
- radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory();
- let fixture_identity =
- RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture");
-
- manager
- .upsert_identity(&fixture_identity, Some("primary".into()), true)
- .expect("store identity");
-
- let ncryptsec = DesktopBackend::export_selected_local_encrypted_secret_key(
- &manager,
- FIXTURE_BACKUP_PASSWORD,
- )
- .expect("export encrypted secret");
-
- let restored = RadrootsIdentity::from_encrypted_secret_key_str(
- ncryptsec.as_str(),
- FIXTURE_BACKUP_PASSWORD,
- )
- .expect("restore encrypted secret");
-
- assert_eq!(restored.secret_key_hex(), FIXTURE_ALICE.secret_key_hex);
- }
-
- #[test]
- fn import_local_identity_imports_raw_secret_key_and_selects_account() {
- let manager =
- radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory();
- let identity = RadrootsIdentity::generate();
-
- let state = DesktopBackend::import_local_identity(
- &manager,
- &RadrootsSecretImportRequest {
- mode: RadrootsSecretImportMode::RawSecretKey,
- secret_text: identity.nsec(),
- password: None,
- },
- )
- .expect("import identity");
-
- assert_eq!(
- state,
- IdentityGateState::Ready {
- account_id: identity.id().to_string(),
- }
- );
- assert_eq!(
- manager.selected_account_id().expect("selected"),
- Some(identity.id())
- );
- assert_eq!(manager.list_accounts().expect("list").len(), 1);
- assert_eq!(
- manager
- .export_secret_hex(&identity.id())
- .expect("export secret"),
- Some(identity.secret_key_hex())
- );
- }
-
- #[test]
- fn import_local_identity_imports_encrypted_secret_key_and_selects_account() {
- let manager =
- radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory();
- let encrypted_secret_key =
- fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD)
- .expect("fixture encrypted secret key");
- let fixture_identity =
- RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture");
- let fixture_account_id = fixture_identity.id();
-
- let state = DesktopBackend::import_local_identity(
- &manager,
- &RadrootsSecretImportRequest {
- mode: RadrootsSecretImportMode::EncryptedSecretKey,
- secret_text: encrypted_secret_key,
- password: Some(FIXTURE_BACKUP_PASSWORD.to_owned()),
- },
- )
- .expect("import identity");
-
- assert_eq!(
- state,
- IdentityGateState::Ready {
- account_id: fixture_account_id.to_string(),
- }
- );
- assert_eq!(
- manager.selected_account_id().expect("selected"),
- Some(fixture_account_id.clone())
- );
- assert_eq!(
- manager
- .export_secret_hex(&fixture_account_id)
- .expect("export secret"),
- Some(FIXTURE_ALICE.secret_key_hex.to_owned())
- );
- }
-
- #[test]
- fn remove_accounts_file_if_present_deletes_existing_file() {
- let unique = format!(
- "radroots-desktop-reset-{}-{}.json",
- std::process::id(),
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .expect("system time")
- .as_nanos()
- );
- let path = std::env::temp_dir().join(unique);
- std::fs::write(&path, b"{}").expect("write accounts file");
-
- DesktopBackend::remove_accounts_file_if_present(path.as_path()).expect("remove file");
-
- assert!(!path.exists());
- }
-}
diff --git a/crates/launchers/desktop/src/offline_geocoder.rs b/crates/launchers/desktop/src/offline_geocoder.rs
@@ -1,514 +0,0 @@
-use radroots_app_core::{
- RadrootsLocationCountry, RadrootsLocationPoint, RadrootsLocationResolverError,
- RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState,
- RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation,
-};
-use radroots_geocoder::{
- Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions,
- GeocoderReverseResult,
-};
-use std::path::{Path, PathBuf};
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-const GEOCODER_ASSET_FILENAME: &str = "geonames.db";
-const GEOCODER_REVISION_FILENAME: &str = "geonames.revision";
-
-#[derive(Clone)]
-pub(crate) struct DesktopOfflineGeocoder {
- current: Arc<Mutex<RadrootsOfflineGeocoderState>>,
- changed: Arc<AtomicBool>,
-}
-
-impl DesktopOfflineGeocoder {
- pub(crate) fn from_state(state: RadrootsOfflineGeocoderState) -> Self {
- Self {
- current: Arc::new(Mutex::new(state)),
- changed: Arc::new(AtomicBool::new(false)),
- }
- }
-
- pub(crate) fn start(app_data_root: PathBuf) -> Self {
- let tracker = Self::from_state(RadrootsOfflineGeocoderState::Initializing);
-
- let current = Arc::clone(&tracker.current);
- let changed = Arc::clone(&tracker.changed);
- std::thread::spawn(move || {
- let state = initialize_offline_geocoder(app_data_root.as_path());
- if let RadrootsOfflineGeocoderState::Unavailable { debug_message, .. } = &state {
- log::warn!("desktop offline geocoder unavailable: {debug_message}");
- }
- if let Ok(mut slot) = current.lock() {
- *slot = state;
- changed.store(true, Ordering::Release);
- }
- });
-
- tracker
- }
-
- pub(crate) fn current_state(&self) -> RadrootsOfflineGeocoderState {
- self.current
- .lock()
- .map(|state| state.clone())
- .unwrap_or_else(|_| {
- RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- RadrootsOfflineGeocoderPlatform::Desktop,
- "desktop offline geocoder state lock poisoned",
- )
- })
- }
-
- pub(crate) fn take_update(&self) -> Option<RadrootsOfflineGeocoderState> {
- if self.changed.swap(false, Ordering::AcqRel) {
- Some(self.current_state())
- } else {
- None
- }
- }
-}
-
-pub(crate) fn reverse_location(
- app_data_root: &Path,
- state: &RadrootsOfflineGeocoderState,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
-) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(app_data_root, state)?;
- let options = options.map(|options| GeocoderReverseOptions {
- limit: options.limit,
- degree_offset: options.degree_offset,
- });
- geocoder
- .reverse(
- GeocoderPoint {
- lat: point.lat,
- lng: point.lng,
- },
- options,
- )
- .map(|results| results.into_iter().map(map_reverse_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- })
-}
-
-pub(crate) fn list_countries(
- app_data_root: &Path,
- state: &RadrootsOfflineGeocoderState,
-) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(app_data_root, state)?;
- geocoder
- .country_list()
- .map(|results| results.into_iter().map(map_country_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- })
-}
-
-pub(crate) fn country_center(
- app_data_root: &Path,
- state: &RadrootsOfflineGeocoderState,
- country_id: &str,
-) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(app_data_root, state)?;
- geocoder
- .country_center(country_id)
- .map(|point| RadrootsLocationPoint {
- lat: point.lat,
- lng: point.lng,
- })
- .map_err(map_country_center_error)
-}
-
-fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState {
- let source_path = runtime_asset_path().map_err(|debug_message| {
- RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- RadrootsOfflineGeocoderPlatform::Desktop,
- debug_message,
- )
- });
- let source_path = match source_path {
- Ok(source_path) => source_path,
- Err(state) => return state,
- };
- if !source_path.is_file() {
- return RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Desktop,
- format!(
- "desktop bundled geocoder asset missing at {}",
- source_path.display()
- ),
- );
- }
-
- let revision =
- match runtime_asset_revision(source_path.parent().unwrap_or_else(|| Path::new("."))) {
- Ok(revision) => revision,
- Err((kind, debug_message)) => {
- return RadrootsOfflineGeocoderState::unavailable(
- kind,
- RadrootsOfflineGeocoderPlatform::Desktop,
- debug_message,
- );
- }
- };
- let staged_path = staged_db_path(app_data_root, revision.as_str());
- if let Err(debug_message) = stage_runtime_asset(source_path.as_path(), staged_path.as_path()) {
- return RadrootsOfflineGeocoderState::unavailable_with_revision(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Desktop,
- revision,
- debug_message,
- );
- }
- if let Err(source) = Geocoder::open_path(staged_path.as_path()) {
- return RadrootsOfflineGeocoderState::unavailable_with_revision(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Desktop,
- revision,
- format!("failed to open staged geocoder db: {source}"),
- );
- }
- let _ = prune_stale_revisions(staged_geocoder_root(app_data_root), revision.as_str());
- RadrootsOfflineGeocoderState::Ready
-}
-
-fn runtime_asset_path() -> Result<PathBuf, String> {
- let executable_path = std::env::current_exe()
- .map_err(|source| format!("failed to resolve desktop executable path: {source}"))?;
- let Some(parent) = executable_path.parent() else {
- return Err("desktop executable path did not have a parent directory".to_owned());
- };
- Ok(parent.join(GEOCODER_ASSET_FILENAME))
-}
-
-fn geocoder_for_queries(
- app_data_root: &Path,
- state: &RadrootsOfflineGeocoderState,
-) -> Result<Geocoder, RadrootsLocationResolverError> {
- match state {
- RadrootsOfflineGeocoderState::Initializing => {
- return Err(RadrootsLocationResolverError::Initializing);
- }
- RadrootsOfflineGeocoderState::Unavailable { .. } => {
- return Err(RadrootsLocationResolverError::Unavailable);
- }
- RadrootsOfflineGeocoderState::Ready => {}
- }
-
- let source_path = runtime_asset_path()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- let revision =
- runtime_asset_revision(source_path.parent().unwrap_or_else(|| Path::new(".")))
- .map_err(|(_, message)| RadrootsLocationResolverError::QueryFailed { message })?;
- let staged_path = staged_db_path(app_data_root, revision.as_str());
- stage_runtime_asset(source_path.as_path(), staged_path.as_path())
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- Geocoder::open_path(staged_path.as_path()).map_err(|source| {
- RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- }
- })
-}
-
-fn map_reverse_result(result: GeocoderReverseResult) -> RadrootsResolvedLocation {
- RadrootsResolvedLocation {
- id: result.id,
- name: result.name,
- admin1_id: result.admin1_id,
- admin1_name: result.admin1_name,
- country_id: result.country_id,
- country_name: result.country_name,
- point: RadrootsLocationPoint {
- lat: result.latitude,
- lng: result.longitude,
- },
- }
-}
-
-fn map_country_result(result: GeocoderCountryListResult) -> RadrootsLocationCountry {
- RadrootsLocationCountry {
- country_id: result.country_id,
- country_name: result.country,
- center: RadrootsLocationPoint {
- lat: result.lat,
- lng: result.lng,
- },
- }
-}
-
-fn map_country_center_error(source: GeocoderError) -> RadrootsLocationResolverError {
- match source {
- GeocoderError::CountryCenterNotFound { country_id } => {
- RadrootsLocationResolverError::CountryCenterNotFound { country_id }
- }
- other => RadrootsLocationResolverError::QueryFailed {
- message: other.to_string(),
- },
- }
-}
-
-fn runtime_asset_revision(
- asset_dir: &Path,
-) -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> {
- let revision_path = asset_dir.join(GEOCODER_REVISION_FILENAME);
- let revision = std::fs::read_to_string(revision_path.as_path()).map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- format!(
- "desktop bundled geocoder revision asset missing at {}: {source}",
- revision_path.display()
- ),
- )
- })?;
- let revision = revision.trim();
- if !is_valid_revision(revision) {
- return Err((
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- format!(
- "desktop bundled geocoder revision asset invalid at {}",
- revision_path.display()
- ),
- ));
- }
- Ok(revision.to_owned())
-}
-
-fn is_valid_revision(revision: &str) -> bool {
- revision.len() == 64 && revision.bytes().all(|byte| byte.is_ascii_hexdigit())
-}
-
-fn staged_geocoder_root(app_data_root: &Path) -> PathBuf {
- app_data_root.join("geocoder")
-}
-
-fn staged_db_path(app_data_root: &Path, revision: &str) -> PathBuf {
- staged_geocoder_root(app_data_root)
- .join(revision)
- .join(GEOCODER_ASSET_FILENAME)
-}
-
-fn stage_runtime_asset(source_path: &Path, staged_path: &Path) -> Result<bool, String> {
- let Some(parent) = staged_path.parent() else {
- return Err("staged desktop geocoder path did not have a parent directory".to_owned());
- };
- std::fs::create_dir_all(parent)
- .map_err(|source| format!("failed to create desktop geocoder directory: {source}"))?;
- if staged_path.is_file() {
- return Ok(false);
- }
- std::fs::copy(source_path, staged_path)
- .map_err(|source| format!("failed to stage desktop geocoder asset: {source}"))?;
- Ok(true)
-}
-
-fn prune_stale_revisions(staged_root: PathBuf, active_revision: &str) -> Result<(), String> {
- if !staged_root.is_dir() {
- return Ok(());
- }
-
- for entry in std::fs::read_dir(staged_root.as_path())
- .map_err(|source| format!("failed to list desktop geocoder revisions: {source}"))?
- {
- let entry = entry.map_err(|source| {
- format!("failed to read desktop geocoder revision entry: {source}")
- })?;
- if entry.file_name() == std::ffi::OsStr::new(active_revision) {
- continue;
- }
-
- let path = entry.path();
- if entry
- .file_type()
- .map_err(|source| {
- format!("failed to inspect desktop geocoder revision entry: {source}")
- })?
- .is_dir()
- {
- std::fs::remove_dir_all(path.as_path()).map_err(|source| {
- format!(
- "failed to remove stale desktop geocoder revision {}: {source}",
- path.display()
- )
- })?;
- } else {
- std::fs::remove_file(path.as_path()).map_err(|source| {
- format!(
- "failed to remove stale desktop geocoder revision file {}: {source}",
- path.display()
- )
- })?;
- }
- }
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::time::{SystemTime, UNIX_EPOCH};
-
- #[test]
- fn staged_db_path_uses_app_geocoder_directory() {
- let app_data_root = PathBuf::from("/Users/example/.radroots/data/apps/app");
-
- assert_eq!(
- staged_db_path(app_data_root.as_path(), "abcd"),
- PathBuf::from("/Users/example/.radroots/data/apps/app/geocoder/abcd/geonames.db")
- );
- }
-
- #[test]
- fn valid_revision_requires_sha256_hex() {
- assert!(is_valid_revision(
- "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"
- ));
- assert!(!is_valid_revision("abcd"));
- assert!(!is_valid_revision(
- "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079z"
- ));
- }
-
- #[test]
- fn missing_asset_maps_to_build_unavailable_message() {
- let state = RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Desktop,
- "desktop bundled geocoder asset missing at /tmp/geonames.db",
- );
-
- assert_eq!(
- state,
- RadrootsOfflineGeocoderState::Unavailable {
- kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- platform: RadrootsOfflineGeocoderPlatform::Desktop,
- asset_revision: None,
- debug_message: "desktop bundled geocoder asset missing at /tmp/geonames.db"
- .to_owned(),
- }
- );
- }
-
- #[test]
- fn stage_runtime_asset_reuses_existing_staged_copy() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-desktop-geocoder-test-{}",
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
- let source_path = temp_root.join("source.db");
- let staged_path = temp_root.join("staged").join("geonames.db");
-
- std::fs::create_dir_all(temp_root.as_path()).unwrap();
- std::fs::write(source_path.as_path(), b"source").unwrap();
- std::fs::create_dir_all(staged_path.parent().unwrap()).unwrap();
- std::fs::write(staged_path.as_path(), b"existing").unwrap();
-
- let copied = stage_runtime_asset(source_path.as_path(), staged_path.as_path()).unwrap();
-
- assert!(!copied);
- assert_eq!(std::fs::read(staged_path.as_path()).unwrap(), b"existing");
-
- std::fs::remove_dir_all(temp_root.as_path()).unwrap();
- }
-
- #[test]
- fn prune_stale_revisions_keeps_active_revision_only() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-desktop-geocoder-prune-test-{}",
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
- let staged_root = temp_root.join("geocoder");
- let active_dir = staged_root.join("active");
- let stale_dir = staged_root.join("stale");
- let stale_file = staged_root.join("orphan.txt");
-
- std::fs::create_dir_all(active_dir.as_path()).unwrap();
- std::fs::create_dir_all(stale_dir.as_path()).unwrap();
- std::fs::write(active_dir.join("geonames.db"), b"active").unwrap();
- std::fs::write(stale_dir.join("geonames.db"), b"stale").unwrap();
- std::fs::write(stale_file.as_path(), b"orphan").unwrap();
-
- prune_stale_revisions(staged_root.clone(), "active").unwrap();
-
- assert!(active_dir.exists());
- assert!(!stale_dir.exists());
- assert!(!stale_file.exists());
-
- std::fs::remove_dir_all(temp_root.as_path()).unwrap();
- }
-
- #[test]
- fn runtime_asset_revision_reads_stamped_sidecar() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-desktop-geocoder-revision-test-{}",
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
- let revision_path = temp_root.join(GEOCODER_REVISION_FILENAME);
- let revision = "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c";
-
- std::fs::create_dir_all(temp_root.as_path()).unwrap();
- std::fs::write(revision_path.as_path(), format!("{revision}\n")).unwrap();
-
- assert_eq!(
- runtime_asset_revision(temp_root.as_path()).unwrap(),
- revision.to_owned()
- );
-
- std::fs::remove_dir_all(temp_root.as_path()).unwrap();
- }
-
- #[test]
- fn reverse_result_mapping_preserves_location_fields() {
- let mapped = map_reverse_result(GeocoderReverseResult {
- id: 42,
- name: "Oslo".to_owned(),
- admin1_id: Some(12),
- admin1_name: Some("Oslo".to_owned()),
- country_id: "NO".to_owned(),
- country_name: Some("Norway".to_owned()),
- latitude: 59.9139,
- longitude: 10.7522,
- });
-
- assert_eq!(mapped.id, 42);
- assert_eq!(mapped.name, "Oslo");
- assert_eq!(mapped.admin1_id, Some(12));
- assert_eq!(mapped.admin1_name.as_deref(), Some("Oslo"));
- assert_eq!(mapped.country_id, "NO");
- assert_eq!(mapped.country_name.as_deref(), Some("Norway"));
- assert_eq!(mapped.point.lat, 59.9139);
- assert_eq!(mapped.point.lng, 10.7522);
- }
-
- #[test]
- fn unavailable_state_blocks_queries_until_ready() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-desktop-geocoder-query-state-test-{}",
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
-
- let result = list_countries(
- temp_root.as_path(),
- &RadrootsOfflineGeocoderState::Initializing,
- );
-
- assert_eq!(result, Err(RadrootsLocationResolverError::Initializing));
- }
-}
diff --git a/crates/launchers/desktop/src/remote_signer.rs b/crates/launchers/desktop/src/remote_signer.rs
@@ -1,518 +0,0 @@
-use super::DesktopBackend;
-use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault};
-use radroots_app_core::{
- IdentityGateState, RadrootsAccountCustody, RadrootsPendingRemoteSignerConnection,
- RadrootsRemoteSignerPreview, RadrootsRemoteSignerSignedNote, SetupActionState,
-};
-use radroots_app_remote_signer::{
- RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerActionController,
- RadrootsAppRemoteSignerActionControllerHooks, RadrootsAppRemoteSignerActionState,
- RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks,
- RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerPendingState,
- RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState,
- RadrootsAppRemoteSignerSignedEvent, radroots_app_remote_signer_clear_pending_session,
- radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview,
- radroots_app_remote_signer_purge_all_custody_state,
- radroots_app_remote_signer_reconcile_startup,
-};
-use radroots_identity::RadrootsIdentityId;
-use radroots_nostr_accounts::prelude::{
- RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, RadrootsSecretVault,
- account_secret_slot,
-};
-use std::path::{Path, PathBuf};
-
-const REMOTE_SIGNER_LABEL: &str = "remote signer";
-
-#[derive(Clone, Copy)]
-struct DesktopRemoteSignerHooks;
-
-impl RadrootsAppRemoteSignerControllerHooks for DesktopRemoteSignerHooks {
- type ReadyState = IdentityGateState;
-
- fn reconcile_startup_state(&self) -> Result<(), String> {
- let manager = DesktopBackend::accounts_manager()?;
- let store_path = sessions_path()?;
- radroots_app_remote_signer_reconcile_startup(
- &manager,
- store_path.as_path(),
- REMOTE_SIGNER_LABEL,
- load_client_secret,
- remove_client_secret,
- purge_client_secret_namespace,
- )
- }
-
- fn store_pending_session(
- &self,
- pending: &RadrootsAppRemoteSignerPendingSession,
- ) -> Result<(), String> {
- let client_account_id = pending.record.client_account_id().to_owned();
- store_client_secret(
- client_account_id.as_str(),
- pending.client_secret_key_hex.as_str(),
- )?;
- let store_path = sessions_path()?;
- let mut state = load_sessions(store_path.as_path())?;
- if let Err(error) = state.upsert_pending(pending.record.clone()) {
- let _ = remove_client_secret(client_account_id.as_str());
- return Err(error.to_string());
- }
- if let Err(error) = save_sessions(store_path.as_path(), &state) {
- let _ = remove_client_secret(client_account_id.as_str());
- return Err(error);
- }
- Ok(())
- }
-
- fn pending_session_record(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- pending_session_record()
- }
-
- fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String> {
- load_client_secret(client_account_id)
- }
-
- fn activate_pending_session(
- &self,
- client_account_id: &str,
- approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession,
- ) -> Result<Self::ReadyState, String> {
- activate_remote_session(client_account_id, approved)
- }
-
- fn clear_pending_session(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- radroots_app_remote_signer_clear_pending_session(store_path.as_path(), remove_client_secret)
- }
-}
-
-#[derive(Clone)]
-pub(crate) struct DesktopRemoteSigner {
- controller: RadrootsAppRemoteSignerController<DesktopRemoteSignerHooks>,
- action_controller: RadrootsAppRemoteSignerActionController<DesktopRemoteSignerHooks>,
-}
-
-impl DesktopRemoteSigner {
- pub(crate) fn new() -> Self {
- Self {
- controller: RadrootsAppRemoteSignerController::new(DesktopRemoteSignerHooks),
- action_controller: RadrootsAppRemoteSignerActionController::new(
- DesktopRemoteSignerHooks,
- ),
- }
- }
-
- pub(crate) fn take_update(&self) -> Option<Result<Option<IdentityGateState>, String>> {
- self.controller.take_update()
- }
-
- pub(crate) fn is_connecting(&self) -> bool {
- self.controller.is_connecting()
- }
-
- pub(crate) fn action_state(&self) -> Result<SetupActionState, String> {
- if self.is_connecting() {
- return Ok(SetupActionState {
- label: "Connecting Remote Signer...".to_owned(),
- enabled: false,
- pending: true,
- });
- }
-
- if self.pending_connection()?.is_some() {
- return Ok(match self.controller.pending_state() {
- RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState {
- label: "Remote Signer Approval Check Retrying".to_owned(),
- enabled: false,
- pending: false,
- },
- RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { .. } => {
- SetupActionState {
- label: "Authorize Remote Signer to Continue".to_owned(),
- enabled: false,
- pending: false,
- }
- }
- RadrootsAppRemoteSignerPendingState::Idle
- | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState {
- label: "Remote Signer Waiting for Approval".to_owned(),
- enabled: false,
- pending: false,
- },
- });
- }
-
- Ok(SetupActionState {
- label: "Connect Remote Signer".to_owned(),
- enabled: true,
- pending: false,
- })
- }
-
- pub(crate) fn begin_connect(&self, input: &str) -> Result<(), String> {
- self.controller.begin_connect(input)
- }
-
- pub(crate) fn pending_connection(
- &self,
- ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> {
- Ok(
- pending_session_record()?.map(|record| RadrootsPendingRemoteSignerConnection {
- signer_npub: record.signer_identity.public_key_npub,
- relays: record.relays,
- auth_url: match self.controller.pending_state() {
- RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { url } => Some(url),
- _ => None,
- },
- }),
- )
- }
-
- pub(crate) fn note_action_state(&self) -> Result<SetupActionState, String> {
- let Some(account_id) = selected_remote_signer_account()? else {
- return Ok(SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: false,
- pending: false,
- });
- };
- let Some(record) = active_session_for_account_id(account_id.as_str())? else {
- return Ok(SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: false,
- pending: false,
- });
- };
- if !record.allows_sign_event_kind1() {
- return Ok(SetupActionState {
- label: "Remote Signer Missing sign_event:kind:1".to_owned(),
- enabled: false,
- pending: false,
- });
- }
-
- Ok(match self.action_controller.state() {
- RadrootsAppRemoteSignerActionState::Idle => SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: true,
- pending: false,
- },
- RadrootsAppRemoteSignerActionState::Signing => SetupActionState {
- label: "Signing Remote Kind 1 Note...".to_owned(),
- enabled: false,
- pending: true,
- },
- RadrootsAppRemoteSignerActionState::AwaitingAuthorization { .. } => SetupActionState {
- label: "Authorize Remote Signer to Continue".to_owned(),
- enabled: false,
- pending: false,
- },
- })
- }
-
- pub(crate) fn begin_sign_kind1_note_selected(&self, content: &str) -> Result<(), String> {
- self.action_controller.begin_sign_kind1_note(content)
- }
-
- pub(crate) fn take_note_update(
- &self,
- ) -> Option<Result<Option<RadrootsRemoteSignerSignedNote>, String>> {
- self.action_controller.take_update()
- }
-}
-
-pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> {
- let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?;
- let requested_permissions = preview.requested_permission_labels();
- Ok(RadrootsRemoteSignerPreview {
- source_label: preview.source_label().to_owned(),
- signer_npub: preview.signer_identity.public_key_npub,
- relays: preview.relays,
- requested_permissions,
- })
-}
-
-pub(crate) fn identity_state_from_status(
- status: RadrootsNostrSelectedAccountStatus,
-) -> Result<IdentityGateState, String> {
- match status {
- RadrootsNostrSelectedAccountStatus::NotConfigured => Ok(IdentityGateState::Missing),
- RadrootsNostrSelectedAccountStatus::Ready { account } => Ok(IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- }),
- RadrootsNostrSelectedAccountStatus::PublicOnly { account } => {
- if active_session_for_account_id(account.account_id.as_str())?.is_some() {
- Ok(IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- })
- } else {
- Ok(IdentityGateState::Missing)
- }
- }
- }
-}
-
-pub(crate) fn custody_for_account_id(account_id: &str) -> Result<RadrootsAccountCustody, String> {
- if active_session_for_account_id(account_id)?.is_some() {
- Ok(RadrootsAccountCustody::RemoteSigner)
- } else {
- Ok(RadrootsAccountCustody::LocalManaged)
- }
-}
-
-pub(crate) fn disconnect_selected_remote_signer(
- manager: &RadrootsNostrAccountsManager,
-) -> Result<IdentityGateState, String> {
- let store_path = sessions_path()?;
- let status = radroots_app_remote_signer_disconnect_selected(
- manager,
- store_path.as_path(),
- remove_client_secret,
- )?;
- identity_state_from_status(status)
-}
-
-pub(crate) fn cancel_pending_connection() -> Result<(), String> {
- let store_path = sessions_path()?;
- let _ = radroots_app_remote_signer_clear_pending_session(
- store_path.as_path(),
- remove_client_secret,
- )?;
- Ok(())
-}
-
-pub(crate) fn purge_all_custody_state() -> Result<(), String> {
- let store_path = sessions_path()?;
- radroots_app_remote_signer_purge_all_custody_state(
- store_path.as_path(),
- remove_client_secret,
- purge_client_secret_namespace,
- )
-}
-
-fn activate_remote_session(
- client_account_id: &str,
- approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession,
-) -> Result<IdentityGateState, String> {
- let manager = DesktopBackend::accounts_manager()?;
- manager
- .upsert_public_identity(
- approved.user_identity.clone(),
- Some(REMOTE_SIGNER_LABEL.to_owned()),
- true,
- )
- .map_err(|source| source.to_string())?;
- let store_path = sessions_path()?;
- let activation_result = (|| -> Result<(), String> {
- let mut state = load_sessions(store_path.as_path())?;
- state
- .activate_session(
- client_account_id,
- approved.user_identity.clone(),
- approved.relays.clone(),
- approved.approved_permissions.clone(),
- )
- .ok_or_else(|| {
- "pending remote signer session disappeared before activation".to_owned()
- })?;
- save_sessions(store_path.as_path(), &state)
- })();
- if let Err(error) = activation_result {
- if let Err(rollback_error) = manager.remove_account(&approved.user_identity.id) {
- return Err(format!(
- "{error}. remote signer account rollback needs retry: {rollback_error}"
- ));
- }
- return Err(error);
- }
- Ok(IdentityGateState::Ready {
- account_id: approved.user_identity.id.to_string(),
- })
-}
-
-fn selected_remote_signer_account() -> Result<Option<String>, String> {
- let manager = DesktopBackend::accounts_manager()?;
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Ok(None);
- };
- if active_session_for_account_id(account_id.as_str())?.is_some() {
- Ok(Some(account_id.to_string()))
- } else {
- Ok(None)
- }
-}
-
-fn update_active_session_relays(account_id: &str, relays: Vec<String>) -> Result<(), String> {
- let store_path = sessions_path()?;
- let mut state = load_sessions(store_path.as_path())?;
- let Some(mut session) = state.active_session_for_account_id(account_id).cloned() else {
- return Err("active remote signer session disappeared before relay update".to_owned());
- };
- if session.relays == relays {
- return Ok(());
- }
- session.relays = relays;
- state.remove_active_session_for_account_id(account_id);
- state.sessions.push(session);
- save_sessions(store_path.as_path(), &state)
-}
-
-impl RadrootsAppRemoteSignerActionControllerHooks for DesktopRemoteSignerHooks {
- type ReadyState = RadrootsRemoteSignerSignedNote;
-
- fn selected_active_session(
- &self,
- ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String> {
- let Some(account_id) = selected_remote_signer_account()? else {
- return Ok(None);
- };
- let Some(record) = active_session_for_account_id(account_id.as_str())? else {
- return Ok(None);
- };
- if !record.allows_sign_event_kind1() {
- return Err("remote signer has not approved sign_event:kind:1".to_owned());
- }
- let secret = load_client_secret(record.client_account_id())?;
- Ok(Some((record, secret)))
- }
-
- fn complete_sign_event(
- &self,
- signed_event: RadrootsAppRemoteSignerSignedEvent,
- ) -> Result<Self::ReadyState, String> {
- let Some(account_id) = selected_remote_signer_account()? else {
- return Err("remote signer account is no longer selected".to_owned());
- };
- update_active_session_relays(account_id.as_str(), signed_event.relays.clone())?;
- Ok(RadrootsRemoteSignerSignedNote {
- event_id_hex: signed_event.event_id_hex,
- })
- }
-}
-
-fn pending_session_record() -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- let state = load_sessions(store_path.as_path())?;
- Ok(state.pending_session().cloned())
-}
-
-fn active_session_for_account_id(
- account_id: &str,
-) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- let state = load_sessions(store_path.as_path())?;
- Ok(state.active_session_for_account_id(account_id).cloned())
-}
-
-pub(crate) fn selected_approved_permission_labels() -> Result<Option<Vec<String>>, String> {
- let Some(account_id) = selected_remote_signer_account()? else {
- return Ok(None);
- };
- Ok(active_session_for_account_id(account_id.as_str())?
- .map(|record| record.approved_permission_labels()))
-}
-
-fn load_sessions(path: &Path) -> Result<RadrootsAppRemoteSignerSessionStoreState, String> {
- RadrootsAppRemoteSignerSessionStoreState::load(path).map_err(|error| error.to_string())
-}
-
-fn save_sessions(
- path: &Path,
- state: &RadrootsAppRemoteSignerSessionStoreState,
-) -> Result<(), String> {
- state.save(path).map_err(|error| error.to_string())
-}
-
-fn sessions_path() -> Result<PathBuf, String> {
- Ok(DesktopBackend::app_data_root()?
- .join("nostr")
- .join("remote-signer-sessions.json"))
-}
-
-fn client_secret_vault() -> RadrootsAppleKeychainVault {
- RadrootsAppleKeychainVault::new_with_namespace_desktop(
- APPLE_NOSTR_SERVICE,
- RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE,
- )
-}
-
-fn legacy_client_secret_vault() -> RadrootsAppleKeychainVault {
- RadrootsAppleKeychainVault::new_desktop(APPLE_NOSTR_SERVICE)
-}
-
-fn client_secret_slot(client_account_id: &str) -> Result<String, String> {
- let account_id = RadrootsIdentityId::try_from(client_account_id)
- .map_err(|_| "invalid remote signer client account id".to_owned())?;
- Ok(account_secret_slot(&account_id))
-}
-
-fn store_client_secret(client_account_id: &str, secret_key_hex: &str) -> Result<(), String> {
- let slot = client_secret_slot(client_account_id)?;
- client_secret_vault()
- .store_secret(slot.as_str(), secret_key_hex)
- .map_err(|source| source.to_string())
-}
-
-fn load_client_secret(client_account_id: &str) -> Result<String, String> {
- let slot = client_secret_slot(client_account_id)?;
- if let Some(secret) = client_secret_vault()
- .load_secret(slot.as_str())
- .map_err(|source| source.to_string())?
- {
- return Ok(secret);
- }
-
- let secret = legacy_client_secret_vault()
- .load_secret(slot.as_str())
- .map_err(|source| source.to_string())?
- .ok_or_else(|| "remote signer session secret is missing".to_owned())?;
- let _ = client_secret_vault().store_secret(slot.as_str(), secret.as_str());
- let _ = legacy_client_secret_vault().remove_secret(slot.as_str());
- Ok(secret)
-}
-
-fn remove_client_secret(client_account_id: &str) -> Result<(), String> {
- let slot = client_secret_slot(client_account_id)?;
- client_secret_vault()
- .remove_secret(slot.as_str())
- .map_err(|source| source.to_string())?;
- legacy_client_secret_vault()
- .remove_secret(slot.as_str())
- .map_err(|source| source.to_string())
-}
-
-fn purge_client_secret_namespace() -> Result<(), String> {
- client_secret_vault()
- .purge_namespace()
- .map_err(|source| source.to_string())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_test_support::FIXTURE_BOB;
-
- #[test]
- fn preview_connection_maps_signer_details() {
- let preview = preview_connection(
- "http://localhost/connect?uri=bunker%3A%2F%2Fnpub1uqnxu08mp55gd7guw06ls68nhxp8xuf7tlxe0sypvcl42x9ykwhsd55k2g%3Frelay%3Dws%253A%252F%252Flocalhost%253A8080",
- )
- .expect("preview");
-
- assert_eq!(preview.source_label, "discovery url");
- assert_eq!(preview.signer_npub, FIXTURE_BOB.npub);
- assert_eq!(preview.relays, vec!["ws://localhost:8080".to_owned()]);
- assert_eq!(
- preview.requested_permissions,
- vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned(),]
- );
- }
-}
diff --git a/crates/launchers/desktop/src/reverse_lookup.rs b/crates/launchers/desktop/src/reverse_lookup.rs
@@ -1,119 +0,0 @@
-use crate::offline_geocoder;
-use radroots_app_core::{
- RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions,
- RadrootsOfflineGeocoderState, RadrootsReverseLocationLookupResult,
-};
-#[cfg(target_os = "macos")]
-use std::path::PathBuf;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-#[derive(Clone, Default)]
-pub(crate) struct DesktopReverseLookup {
- result: Arc<Mutex<Option<RadrootsReverseLocationLookupResult>>>,
- changed: Arc<AtomicBool>,
- pending: Arc<AtomicBool>,
-}
-
-impl DesktopReverseLookup {
- pub(crate) fn new() -> Self {
- Self::default()
- }
-
- #[cfg(target_os = "macos")]
- pub(crate) fn begin(
- &self,
- app_data_root: PathBuf,
- geocoder_state: RadrootsOfflineGeocoderState,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline location query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.result);
- let changed = Arc::clone(&self.changed);
- let pending = Arc::clone(&self.pending);
- std::thread::spawn(move || {
- let lookup_result = offline_geocoder::reverse_location(
- app_data_root.as_path(),
- &geocoder_state,
- point,
- options,
- );
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "macos"))]
- pub(crate) fn begin(
- &self,
- _app_data_root: std::path::PathBuf,
- _geocoder_state: RadrootsOfflineGeocoderState,
- _point: RadrootsLocationPoint,
- _options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- pub(crate) fn take_update(&self) -> Option<RadrootsReverseLocationLookupResult> {
- if !self.changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "desktop reverse lookup result lock poisoned".to_owned(),
- })),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_core::RadrootsResolvedLocation;
-
- fn sample_result() -> RadrootsReverseLocationLookupResult {
- Ok(vec![RadrootsResolvedLocation {
- id: 7,
- name: "example".to_owned(),
- admin1_id: None,
- admin1_name: None,
- country_id: "US".to_owned(),
- country_name: Some("United States".to_owned()),
- point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 },
- }])
- }
-
- #[test]
- fn take_update_is_none_until_tracker_changes() {
- let tracker = DesktopReverseLookup::new();
-
- assert_eq!(tracker.take_update(), None);
- }
-
- #[test]
- fn take_update_returns_queued_result_once() {
- let tracker = DesktopReverseLookup::new();
- *tracker.result.lock().unwrap() = Some(sample_result());
- tracker.changed.store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_update(), Some(Ok(results)) if results.len() == 1));
- assert_eq!(tracker.take_update(), None);
- }
-}
diff --git a/crates/launchers/ios/Cargo.toml b/crates/launchers/ios/Cargo.toml
@@ -1,33 +0,0 @@
-[package]
-name = "radroots_app_ios"
-authors.workspace = true
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots iOS launcher"
-publish = false
-
-[lib]
-path = "src/lib.rs"
-crate-type = ["staticlib", "rlib"]
-
-[dependencies]
-eframe = { workspace = true, features = ["wgpu"] }
-log.workspace = true
-radroots_app_apple_security.workspace = true
-radroots_app_core = { path = "../../shared/core" }
-radroots_app_remote_signer = { path = "../../shared/remote_signer" }
-radroots_geocoder.workspace = true
-radroots_identity.workspace = true
-radroots_nostr_accounts = { workspace = true, features = ["memory-vault"] }
-radroots_runtime_paths.workspace = true
-zeroize.workspace = true
-
-[target.'cfg(target_os = "ios")'.dependencies]
-wgpu = { workspace = true, features = ["metal", "wgsl"] }
-
-[dev-dependencies]
-radroots_app_test_support = { path = "../../shared/test_support" }
diff --git a/crates/launchers/ios/src/country_lookup.rs b/crates/launchers/ios/src/country_lookup.rs
@@ -1,190 +0,0 @@
-#![cfg_attr(not(target_os = "ios"), allow(dead_code))]
-
-#[cfg(target_os = "ios")]
-use crate::offline_geocoder;
-use radroots_app_core::{
- RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult,
- RadrootsLocationResolverError, RadrootsOfflineGeocoderState,
-};
-#[cfg(target_os = "ios")]
-use std::path::PathBuf;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-#[derive(Clone, Default)]
-pub(crate) struct IosCountryLookup {
- country_list_result: Arc<Mutex<Option<RadrootsLocationCountryListResult>>>,
- country_list_changed: Arc<AtomicBool>,
- country_list_pending: Arc<AtomicBool>,
- country_center_result: Arc<Mutex<Option<RadrootsLocationCountryCenterLookupResult>>>,
- country_center_changed: Arc<AtomicBool>,
- country_center_pending: Arc<AtomicBool>,
-}
-
-impl IosCountryLookup {
- pub(crate) fn new() -> Self {
- Self::default()
- }
-
- #[cfg(target_os = "ios")]
- pub(crate) fn begin_list(
- &self,
- app_data_root: PathBuf,
- geocoder_state: RadrootsOfflineGeocoderState,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.country_list_pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline country list query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.country_list_result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.country_list_result);
- let changed = Arc::clone(&self.country_list_changed);
- let pending = Arc::clone(&self.country_list_pending);
- std::thread::spawn(move || {
- let lookup_result =
- offline_geocoder::list_countries(app_data_root.as_path(), &geocoder_state);
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "ios"))]
- pub(crate) fn begin_list(
- &self,
- _app_data_root: std::path::PathBuf,
- _geocoder_state: RadrootsOfflineGeocoderState,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- #[cfg(target_os = "ios")]
- pub(crate) fn begin_center(
- &self,
- app_data_root: PathBuf,
- geocoder_state: RadrootsOfflineGeocoderState,
- country_id: String,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.country_center_pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline country center query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.country_center_result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.country_center_result);
- let changed = Arc::clone(&self.country_center_changed);
- let pending = Arc::clone(&self.country_center_pending);
- std::thread::spawn(move || {
- let lookup_result = offline_geocoder::country_center(
- app_data_root.as_path(),
- &geocoder_state,
- &country_id,
- );
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "ios"))]
- pub(crate) fn begin_center(
- &self,
- _app_data_root: std::path::PathBuf,
- _geocoder_state: RadrootsOfflineGeocoderState,
- _country_id: String,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- pub(crate) fn take_list_update(&self) -> Option<RadrootsLocationCountryListResult> {
- if !self.country_list_changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.country_list_result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "ios country list result lock poisoned".to_owned(),
- })),
- }
- }
-
- pub(crate) fn take_center_update(&self) -> Option<RadrootsLocationCountryCenterLookupResult> {
- if !self.country_center_changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.country_center_result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "ios country center result lock poisoned".to_owned(),
- })),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_core::{RadrootsLocationCountry, RadrootsLocationPoint};
-
- fn sample_countries() -> RadrootsLocationCountryListResult {
- Ok(vec![RadrootsLocationCountry {
- country_id: "BR".to_owned(),
- country_name: Some("Brazil".to_owned()),
- center: RadrootsLocationPoint {
- lat: -14.235,
- lng: -51.9253,
- },
- }])
- }
-
- #[test]
- fn take_list_update_is_none_until_tracker_changes() {
- let tracker = IosCountryLookup::new();
-
- assert_eq!(tracker.take_list_update(), None);
- }
-
- #[test]
- fn take_list_update_returns_queued_result_once() {
- let tracker = IosCountryLookup::new();
- *tracker.country_list_result.lock().unwrap() = Some(sample_countries());
- tracker.country_list_changed.store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_list_update(), Some(Ok(results)) if results.len() == 1));
- assert_eq!(tracker.take_list_update(), None);
- }
-
- #[test]
- fn take_center_update_returns_queued_result_once() {
- let tracker = IosCountryLookup::new();
- *tracker.country_center_result.lock().unwrap() = Some(Ok(RadrootsLocationPoint {
- lat: -14.235,
- lng: -51.9253,
- }));
- tracker
- .country_center_changed
- .store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_center_update(), Some(Ok(point)) if point.lat == -14.235));
- assert_eq!(tracker.take_center_update(), None);
- }
-}
diff --git a/crates/launchers/ios/src/lib.rs b/crates/launchers/ios/src/lib.rs
@@ -1,965 +0,0 @@
-#[cfg(target_os = "ios")]
-use eframe::egui::ViewportBuilder;
-#[cfg(target_os = "ios")]
-use radroots_app_apple_security::verify_user_presence;
-#[cfg(any(target_os = "ios", test))]
-use radroots_app_core::IdentityGateState;
-#[cfg(target_os = "ios")]
-use radroots_app_core::{
- APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, ImportActionState,
- PasteActionState, RadrootsApp, RadrootsAppBackend, RadrootsLocationCountry,
- RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult,
- RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions,
- RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState,
- RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation,
- RadrootsReverseLocationLookupResult, SetupActionState,
-};
-#[cfg(any(target_os = "ios", test))]
-use radroots_app_core::{RadrootsAccountCustody, RadrootsAccountSummary};
-#[cfg(any(target_os = "ios", test))]
-use radroots_app_core::{RadrootsSecretImportMode, RadrootsSecretImportRequest};
-#[cfg(any(target_os = "ios", test))]
-use radroots_identity::RadrootsIdentity;
-#[cfg(any(target_os = "ios", test))]
-use radroots_nostr_accounts::prelude::{
- RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus,
-};
-#[cfg(any(target_os = "ios", test))]
-use std::path::Path;
-#[cfg(any(target_os = "ios", test))]
-use zeroize::Zeroizing;
-
-#[cfg(any(target_os = "ios", test))]
-mod country_lookup;
-#[cfg(any(target_os = "ios", test))]
-mod offline_geocoder;
-#[cfg(target_os = "ios")]
-mod remote_signer;
-#[cfg(any(target_os = "ios", test))]
-mod reverse_lookup;
-#[cfg(any(target_os = "ios", test))]
-mod storage;
-
-#[cfg(any(target_os = "ios", test))]
-#[cfg_attr(not(target_os = "ios"), allow(dead_code))]
-struct IosBackend {
- country_lookup: country_lookup::IosCountryLookup,
- offline_geocoder: offline_geocoder::IosOfflineGeocoder,
- #[cfg(target_os = "ios")]
- remote_signer: remote_signer::IosRemoteSigner,
- reverse_lookup: reverse_lookup::IosReverseLookup,
-}
-
-#[cfg(target_os = "ios")]
-#[allow(unsafe_code)]
-unsafe extern "C" {
- fn radroots_ios_clipboard_text_copy() -> *mut std::ffi::c_char;
- fn radroots_ios_string_free(value: *mut std::ffi::c_char);
-}
-
-#[cfg(any(target_os = "ios", test))]
-impl IosBackend {
- #[cfg(target_os = "ios")]
- fn new() -> Self {
- let offline_geocoder = match storage::app_data_root() {
- Ok(app_data_root) => offline_geocoder::IosOfflineGeocoder::start(app_data_root),
- Err(debug_message) => offline_geocoder::IosOfflineGeocoder::from_state(
- RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- RadrootsOfflineGeocoderPlatform::Ios,
- debug_message,
- ),
- ),
- };
-
- Self {
- country_lookup: country_lookup::IosCountryLookup::new(),
- offline_geocoder,
- #[cfg(target_os = "ios")]
- remote_signer: remote_signer::IosRemoteSigner::new(),
- reverse_lookup: reverse_lookup::IosReverseLookup::new(),
- }
- }
-
- #[cfg(target_os = "ios")]
- fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> {
- storage::accounts_manager()
- }
-
- fn map_status(status: RadrootsNostrSelectedAccountStatus) -> IdentityGateState {
- match status {
- RadrootsNostrSelectedAccountStatus::NotConfigured => IdentityGateState::Missing,
- RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing,
- RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- },
- }
- }
-
- fn identity_state_from_manager(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- let status = manager
- .selected_account_status()
- .map_err(|source| source.to_string())?;
- Ok(Self::map_status(status))
- }
-
- fn account_roster_from_manager(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<Vec<RadrootsAccountSummary>, String> {
- manager
- .list_accounts()
- .map_err(|source| source.to_string())?
- .into_iter()
- .map(|record| {
- #[cfg(target_os = "ios")]
- let custody = remote_signer::custody_for_account_id(record.account_id.as_str())?;
- #[cfg(not(target_os = "ios"))]
- let custody = RadrootsAccountCustody::LocalManaged;
- Ok(RadrootsAccountSummary {
- account_id: record.account_id.to_string(),
- npub: record.public_identity.public_key_npub,
- label: record.label,
- custody,
- })
- })
- .collect()
- }
-
- fn generate_local_identity(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- manager
- .generate_identity(Some("local".to_owned()), true)
- .map_err(|source| source.to_string())?;
- Self::identity_state_from_manager(manager)
- }
-
- fn remove_selected_local_identity(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Ok(IdentityGateState::Missing);
- };
-
- manager
- .remove_account(&account_id)
- .map_err(|source| source.to_string())?;
- Self::identity_state_from_manager(manager)
- }
-
- fn export_selected_local_encrypted_secret_key(
- manager: &RadrootsNostrAccountsManager,
- password: &str,
- ) -> Result<String, String> {
- Self::authorize_secret_key_backup()?;
-
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Err("no selected local identity is available to back up".to_owned());
- };
-
- let Some(secret_key_hex) = manager
- .export_secret_hex(&account_id)
- .map_err(|source| source.to_string())?
- else {
- return Err("selected local identity does not have an exportable secret".to_owned());
- };
-
- let secret_key_hex = Zeroizing::new(secret_key_hex);
- let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())
- .map_err(|source| source.to_string())?;
- identity
- .encrypt_secret_key_ncryptsec(password)
- .map_err(|source| source.to_string())
- }
-
- fn export_selected_local_raw_secret_key(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<String, String> {
- Self::authorize_secret_key_reveal()?;
-
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Err("no selected local identity is available to back up".to_owned());
- };
-
- let Some(secret_key_hex) = manager
- .export_secret_hex(&account_id)
- .map_err(|source| source.to_string())?
- else {
- return Err("selected local identity does not have an exportable secret".to_owned());
- };
-
- let secret_key_hex = Zeroizing::new(secret_key_hex);
- let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())
- .map_err(|source| source.to_string())?;
- Ok(identity.nsec())
- }
-
- fn import_local_identity(
- manager: &RadrootsNostrAccountsManager,
- request: &RadrootsSecretImportRequest,
- ) -> Result<IdentityGateState, String> {
- let identity = match request.mode {
- RadrootsSecretImportMode::EncryptedSecretKey => {
- let Some(password) = request.password.as_deref() else {
- return Err("password is required to import an encrypted secret key".to_owned());
- };
- RadrootsIdentity::from_encrypted_secret_key_str(
- request.secret_text.as_str(),
- password,
- )
- .map_err(|_| "invalid encrypted secret key or password".to_owned())?
- }
- RadrootsSecretImportMode::RawSecretKey => {
- RadrootsIdentity::from_secret_key_str(request.secret_text.as_str())
- .map_err(|_| "invalid raw secret key".to_owned())?
- }
- };
-
- manager
- .upsert_identity(&identity, None, true)
- .map_err(|source| source.to_string())?;
-
- Self::identity_state_from_manager(manager)
- }
-
- fn normalize_clipboard_secret_key_text(clipboard_text: &str) -> Result<String, String> {
- let trimmed = clipboard_text.trim();
- if trimmed.is_empty() {
- return Err("clipboard does not contain text".to_owned());
- }
-
- Ok(match trimmed.len() == clipboard_text.len() {
- true => clipboard_text.to_owned(),
- false => trimmed.to_owned(),
- })
- }
-
- #[cfg(target_os = "ios")]
- #[allow(unsafe_code)]
- fn paste_secret_key_from_clipboard() -> Result<String, String> {
- let clipboard_text_ptr = unsafe { radroots_ios_clipboard_text_copy() };
- if clipboard_text_ptr.is_null() {
- return Err("clipboard does not contain text".to_owned());
- }
-
- let clipboard_text = unsafe {
- let value = std::ffi::CStr::from_ptr(clipboard_text_ptr)
- .to_string_lossy()
- .into_owned();
- radroots_ios_string_free(clipboard_text_ptr);
- value
- };
-
- Self::normalize_clipboard_secret_key_text(&clipboard_text)
- }
-
- #[cfg(target_os = "ios")]
- fn authorize_secret_key_reveal() -> Result<(), String> {
- verify_user_presence("reveal the current secret key").map_err(|source| source.to_string())
- }
-
- #[cfg(target_os = "ios")]
- fn authorize_secret_key_backup() -> Result<(), String> {
- verify_user_presence("back up the current secret key").map_err(|source| source.to_string())
- }
-
- #[cfg(not(target_os = "ios"))]
- fn authorize_secret_key_reveal() -> Result<(), String> {
- Ok(())
- }
-
- #[cfg(not(target_os = "ios"))]
- fn authorize_secret_key_backup() -> Result<(), String> {
- Ok(())
- }
-
- fn remove_all_local_identities(
- manager: &RadrootsNostrAccountsManager,
- ) -> Result<IdentityGateState, String> {
- let account_ids = manager
- .list_accounts()
- .map_err(|source| source.to_string())?
- .into_iter()
- .map(|record| record.account_id)
- .collect::<Vec<_>>();
-
- for account_id in account_ids {
- manager
- .remove_account(&account_id)
- .map_err(|source| source.to_string())?;
- }
-
- Self::identity_state_from_manager(manager)
- }
-
- fn remove_accounts_file_if_present(accounts_path: &Path) -> Result<(), String> {
- match std::fs::remove_file(accounts_path) {
- Ok(()) => Ok(()),
- Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
- Err(source) => Err(format!("failed to remove ios accounts file: {source}")),
- }
- }
-
- #[cfg(target_os = "ios")]
- fn reset_local_device_state(
- manager: &RadrootsNostrAccountsManager,
- accounts_path: &Path,
- ) -> Result<IdentityGateState, String> {
- remote_signer::purge_all_custody_state()?;
- let state = Self::remove_all_local_identities(manager)?;
- Self::remove_accounts_file_if_present(accounts_path)?;
- Ok(state)
- }
-}
-
-#[cfg(target_os = "ios")]
-impl RadrootsAppBackend for IosBackend {
- fn load_identity_state(&self) -> Result<IdentityGateState, String> {
- let manager = Self::accounts_manager()?;
- let status = manager
- .selected_account_status()
- .map_err(|source| source.to_string())?;
- remote_signer::identity_state_from_status(status)
- }
-
- fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> {
- let manager = Self::accounts_manager()?;
- Self::account_roster_from_manager(&manager)
- }
-
- fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> {
- Some(self.offline_geocoder.current_state())
- }
-
- fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> {
- Ok(self.offline_geocoder.take_update())
- }
-
- fn reverse_location(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
- #[cfg(target_os = "ios")]
- {
- let app_data_root = storage::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return offline_geocoder::reverse_location(
- app_data_root.as_path(),
- &self.offline_geocoder.current_state(),
- point,
- options,
- );
- }
-
- #[cfg(not(target_os = "ios"))]
- {
- let _ = (point, options);
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn request_reverse_location_lookup(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- let app_data_root = storage::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- self.reverse_lookup.begin(
- app_data_root,
- self.offline_geocoder.current_state(),
- point,
- options,
- )
- }
-
- fn poll_reverse_location_lookup_result(
- &self,
- ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> {
- Ok(self.reverse_lookup.take_update())
- }
-
- fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> {
- let app_data_root = storage::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- self.country_lookup
- .begin_list(app_data_root, self.offline_geocoder.current_state())
- }
-
- fn poll_location_country_list_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryListResult>, String> {
- Ok(self.country_lookup.take_list_update())
- }
-
- fn request_location_country_center_lookup(
- &self,
- country_id: &str,
- ) -> Result<(), RadrootsLocationResolverError> {
- let app_data_root = storage::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- self.country_lookup.begin_center(
- app_data_root,
- self.offline_geocoder.current_state(),
- country_id.to_owned(),
- )
- }
-
- fn poll_location_country_center_lookup_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> {
- Ok(self.country_lookup.take_center_update())
- }
-
- fn list_location_countries(
- &self,
- ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- #[cfg(target_os = "ios")]
- {
- let app_data_root = storage::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return offline_geocoder::list_countries(
- app_data_root.as_path(),
- &self.offline_geocoder.current_state(),
- );
- }
-
- #[cfg(not(target_os = "ios"))]
- {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn location_country_center(
- &self,
- country_id: &str,
- ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- #[cfg(target_os = "ios")]
- {
- let app_data_root = storage::app_data_root()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- return offline_geocoder::country_center(
- app_data_root.as_path(),
- &self.offline_geocoder.current_state(),
- country_id,
- );
- }
-
- #[cfg(not(target_os = "ios"))]
- {
- let _ = country_id;
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn setup_action_state(&self) -> SetupActionState {
- SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: true,
- pending: false,
- }
- }
-
- fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- let manager = Self::accounts_manager()?;
- Self::generate_local_identity(&manager).map(Some)
- }
-
- fn home_setup_action_state(&self) -> Option<SetupActionState> {
- Some(self.setup_action_state())
- }
-
- fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- self.request_setup_action()
- }
-
- fn import_action_state(&self) -> Option<ImportActionState> {
- Some(ImportActionState {
- label: "Import Secret Key".to_owned(),
- enabled: true,
- pending: false,
- })
- }
-
- fn request_import_action(
- &self,
- request: &RadrootsSecretImportRequest,
- ) -> Result<Option<IdentityGateState>, String> {
- let manager = Self::accounts_manager()?;
- Self::import_local_identity(&manager, request).map(Some)
- }
-
- fn request_select_account(
- &self,
- account_id: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- let manager = Self::accounts_manager()?;
- let account_id = radroots_identity::RadrootsIdentityId::try_from(account_id)
- .map_err(|_| "invalid account id".to_owned())?;
- manager
- .select_account(&account_id)
- .map_err(|source| source.to_string())?;
- self.load_identity_state().map(Some)
- }
-
- fn remote_signer_action_state(&self) -> Option<SetupActionState> {
- Some(
- self.remote_signer
- .action_state()
- .unwrap_or_else(|_| SetupActionState {
- label: "Connect Remote Signer".to_owned(),
- enabled: !self.remote_signer.is_connecting(),
- pending: self.remote_signer.is_connecting(),
- }),
- )
- }
-
- fn preview_remote_signer_connection(
- &self,
- input: &str,
- ) -> Result<radroots_app_core::RadrootsRemoteSignerPreview, String> {
- remote_signer::preview_connection(input)
- }
-
- fn request_remote_signer_connection(
- &self,
- input: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- self.remote_signer.begin_connect(input)?;
- Ok(None)
- }
-
- fn pending_remote_signer_connection(
- &self,
- ) -> Result<Option<radroots_app_core::RadrootsPendingRemoteSignerConnection>, String> {
- self.remote_signer.pending_connection()
- }
-
- fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> {
- remote_signer::cancel_pending_connection()
- }
-
- fn remote_signer_note_action_state(&self) -> Option<SetupActionState> {
- Some(
- self.remote_signer
- .note_action_state()
- .unwrap_or(SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: false,
- pending: false,
- }),
- )
- }
-
- fn request_remote_signer_note_action(&self, content: &str) -> Result<(), String> {
- self.remote_signer.begin_sign_kind1_note_selected(content)
- }
-
- fn poll_remote_signer_note_action_result(
- &self,
- ) -> Result<Option<radroots_app_core::RadrootsRemoteSignerSignedNote>, String> {
- self.remote_signer
- .take_note_update()
- .transpose()
- .map(|result| result.flatten())
- }
-
- fn import_paste_action_state(&self) -> Option<PasteActionState> {
- Some(PasteActionState {
- label: "Paste Secret Key".to_owned(),
- enabled: true,
- pending: false,
- })
- }
-
- fn request_import_paste_action(&self) -> Result<Option<String>, String> {
- Self::paste_secret_key_from_clipboard().map(Some)
- }
-
- fn home_action_states(&self) -> Vec<HomeActionState> {
- let Ok(manager) = Self::accounts_manager() else {
- return Vec::new();
- };
- let Ok(status) = manager
- .selected_account_status()
- .map_err(|source| source.to_string())
- else {
- return Vec::new();
- };
-
- match status {
- RadrootsNostrSelectedAccountStatus::NotConfigured => Vec::new(),
- RadrootsNostrSelectedAccountStatus::PublicOnly { account } => {
- if matches!(
- remote_signer::custody_for_account_id(account.account_id.as_str()),
- Ok(RadrootsAccountCustody::RemoteSigner)
- ) {
- vec![HomeActionState {
- kind: HomeActionKind::DisconnectSigner,
- label: "Disconnect Remote Signer".to_owned(),
- enabled: true,
- pending: false,
- }]
- } else {
- Vec::new()
- }
- }
- RadrootsNostrSelectedAccountStatus::Ready { .. } => vec![
- HomeActionState {
- kind: HomeActionKind::BackupSecretKey,
- label: "Back Up Secret Key".to_owned(),
- enabled: true,
- pending: false,
- },
- HomeActionState {
- kind: HomeActionKind::RevealRawSecretKey,
- label: "Reveal Raw Secret Key".to_owned(),
- enabled: true,
- pending: false,
- },
- HomeActionState {
- kind: HomeActionKind::RemoveLocalKey,
- label: "Remove Key From This Device".to_owned(),
- enabled: true,
- pending: false,
- },
- HomeActionState {
- kind: HomeActionKind::ResetDevice,
- label: "Reset This Device".to_owned(),
- enabled: true,
- pending: false,
- },
- ],
- }
- }
-
- fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> {
- let manager = Self::accounts_manager()?;
- match action {
- HomeActionKind::BackupSecretKey => Ok(HomeActionResult::None),
- HomeActionKind::RevealRawSecretKey => {
- Self::export_selected_local_raw_secret_key(&manager)
- .map(|nsec| HomeActionResult::RevealRawSecretKey { nsec })
- }
- HomeActionKind::RemoveLocalKey => {
- Self::remove_selected_local_identity(&manager).map(HomeActionResult::IdentityState)
- }
- HomeActionKind::ResetDevice => {
- let accounts_path = storage::accounts_path()?;
- Self::reset_local_device_state(&manager, accounts_path.as_path())
- .map(HomeActionResult::IdentityState)
- }
- HomeActionKind::DisconnectSigner => {
- remote_signer::disconnect_selected_remote_signer(&manager)
- .map(HomeActionResult::IdentityState)
- }
- }
- }
-
- fn request_secret_key_backup_action(&self, password: &str) -> Result<HomeActionResult, String> {
- let manager = Self::accounts_manager()?;
- Self::export_selected_local_encrypted_secret_key(&manager, password)
- .map(|ncryptsec| HomeActionResult::RevealEncryptedSecretKey { ncryptsec })
- }
-
- fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> {
- self.remote_signer
- .take_update()
- .transpose()
- .map(|state| state.flatten())
- }
-}
-
-#[cfg(target_os = "ios")]
-fn native_options() -> eframe::NativeOptions {
- eframe::NativeOptions {
- renderer: eframe::Renderer::Wgpu,
- viewport: ViewportBuilder::default()
- .with_title(APP_NAME)
- .with_fullscreen(true),
- ..Default::default()
- }
-}
-
-#[cfg(target_os = "ios")]
-pub fn run() -> Result<(), String> {
- eframe::run_native(
- APP_NAME,
- native_options(),
- Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(IosBackend::new()))))),
- )
- .map_err(|err| err.to_string())
-}
-
-#[cfg(not(target_os = "ios"))]
-pub fn run() -> Result<(), String> {
- Err("radroots_app_ios can only launch on an ios target".to_owned())
-}
-
-pub const ENTRYPOINT_SYMBOL: &str = "radroots_ios_run";
-
-#[allow(unsafe_code)]
-#[unsafe(no_mangle)]
-pub extern "C" fn radroots_ios_run() -> i32 {
- match run() {
- Ok(()) => 0,
- Err(_) => 1,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_test_support::{
- FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD, fixture_identity_ncryptsec,
- };
- use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager;
-
- #[test]
- fn non_ios_run_is_rejected() {
- #[cfg(not(target_os = "ios"))]
- assert_eq!(
- run(),
- Err("radroots_app_ios can only launch on an ios target".to_owned())
- );
- }
-
- #[test]
- fn exported_entrypoint_symbol_is_stable() {
- assert_eq!(ENTRYPOINT_SYMBOL, "radroots_ios_run");
- }
-
- #[test]
- fn new_ios_manager_starts_in_setup_state() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
-
- assert_eq!(
- IosBackend::identity_state_from_manager(&manager),
- Ok(IdentityGateState::Missing)
- );
- }
-
- #[test]
- fn local_identity_generation_transitions_to_ready() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
-
- let state = IosBackend::generate_local_identity(&manager).expect("generate identity");
- let IdentityGateState::Ready { account_id } = state else {
- panic!("expected ready identity state");
- };
-
- assert!(!account_id.is_empty());
- }
-
- #[test]
- fn local_identity_removal_transitions_back_to_missing() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
-
- IosBackend::generate_local_identity(&manager).expect("generate identity");
- let state =
- IosBackend::remove_selected_local_identity(&manager).expect("remove selected account");
-
- assert_eq!(state, IdentityGateState::Missing);
- assert_eq!(
- manager.selected_account_id().expect("selected account"),
- None
- );
- }
-
- #[test]
- fn remove_all_local_identities_clears_every_account() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
-
- manager
- .generate_identity(Some("first".into()), true)
- .expect("generate first");
- manager
- .generate_identity(Some("second".into()), false)
- .expect("generate second");
-
- let state = IosBackend::remove_all_local_identities(&manager).expect("reset state");
-
- assert_eq!(state, IdentityGateState::Missing);
- assert_eq!(manager.list_accounts().expect("list accounts").len(), 0);
- assert_eq!(manager.selected_account_id().expect("selected"), None);
- }
-
- #[test]
- fn export_selected_local_raw_secret_key_returns_nsec() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let identity = RadrootsIdentity::generate();
-
- manager
- .upsert_identity(&identity, Some("primary".into()), true)
- .expect("store identity");
-
- let nsec =
- IosBackend::export_selected_local_raw_secret_key(&manager).expect("export secret");
-
- assert_eq!(nsec, identity.nsec());
- assert!(nsec.starts_with("nsec1"));
- }
-
- #[test]
- fn export_selected_local_encrypted_secret_key_returns_ncryptsec() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let fixture_identity =
- RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture");
-
- manager
- .upsert_identity(&fixture_identity, Some("primary".into()), true)
- .expect("store identity");
-
- let ncryptsec = IosBackend::export_selected_local_encrypted_secret_key(
- &manager,
- FIXTURE_BACKUP_PASSWORD,
- )
- .expect("export encrypted secret");
-
- let restored = RadrootsIdentity::from_encrypted_secret_key_str(
- ncryptsec.as_str(),
- FIXTURE_BACKUP_PASSWORD,
- )
- .expect("restore encrypted secret");
-
- assert_eq!(restored.secret_key_hex(), FIXTURE_ALICE.secret_key_hex);
- }
-
- #[test]
- fn import_local_identity_imports_raw_secret_key_and_selects_account() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let identity = RadrootsIdentity::generate();
-
- let state = IosBackend::import_local_identity(
- &manager,
- &RadrootsSecretImportRequest {
- mode: RadrootsSecretImportMode::RawSecretKey,
- secret_text: identity.nsec(),
- password: None,
- },
- )
- .expect("import");
-
- assert_eq!(
- state,
- IdentityGateState::Ready {
- account_id: identity.id().to_string(),
- }
- );
- assert_eq!(
- manager.selected_account_id().expect("selected"),
- Some(identity.id())
- );
- assert_eq!(manager.list_accounts().expect("list").len(), 1);
- assert_eq!(
- manager
- .export_secret_hex(&identity.id())
- .expect("export secret"),
- Some(identity.secret_key_hex())
- );
- }
-
- #[test]
- fn import_local_identity_imports_encrypted_secret_key_and_selects_account() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let encrypted_secret_key =
- fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD)
- .expect("fixture encrypted secret key");
- let fixture_identity =
- RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture");
- let fixture_account_id = fixture_identity.id();
-
- let state = IosBackend::import_local_identity(
- &manager,
- &RadrootsSecretImportRequest {
- mode: RadrootsSecretImportMode::EncryptedSecretKey,
- secret_text: encrypted_secret_key,
- password: Some(FIXTURE_BACKUP_PASSWORD.to_owned()),
- },
- )
- .expect("import");
-
- assert_eq!(
- state,
- IdentityGateState::Ready {
- account_id: fixture_account_id.to_string(),
- }
- );
- assert_eq!(
- manager.selected_account_id().expect("selected"),
- Some(fixture_account_id)
- );
- assert_eq!(manager.list_accounts().expect("list").len(), 1);
- assert_eq!(
- manager
- .export_secret_hex(&fixture_identity.id())
- .expect("export secret"),
- Some(FIXTURE_ALICE.secret_key_hex.to_owned())
- );
- }
-
- #[test]
- fn account_roster_from_manager_lists_local_managed_account() {
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let identity = RadrootsIdentity::generate();
-
- manager
- .upsert_identity(&identity, Some("primary".into()), true)
- .expect("store identity");
-
- let roster = IosBackend::account_roster_from_manager(&manager).expect("account roster");
-
- assert_eq!(roster.len(), 1);
- assert_eq!(roster[0].account_id, identity.id().to_string());
- assert_eq!(roster[0].npub, identity.npub());
- assert_eq!(roster[0].label.as_deref(), Some("primary"));
- assert_eq!(roster[0].custody, RadrootsAccountCustody::LocalManaged);
- }
-
- #[test]
- fn normalize_clipboard_secret_key_text_trims_wrapping_whitespace() {
- let clipboard_text = format!(" {} \n", FIXTURE_ALICE.nsec);
- let normalized = IosBackend::normalize_clipboard_secret_key_text(clipboard_text.as_str())
- .expect("normalize secret key");
-
- assert_eq!(normalized, FIXTURE_ALICE.nsec);
- }
-
- #[test]
- fn normalize_clipboard_secret_key_text_rejects_blank_text() {
- assert_eq!(
- IosBackend::normalize_clipboard_secret_key_text(" \n\t"),
- Err("clipboard does not contain text".to_owned())
- );
- }
-
- #[test]
- fn remove_accounts_file_if_present_deletes_existing_file() {
- let unique = format!(
- "radroots-ios-reset-{}-{}.json",
- std::process::id(),
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .expect("system time")
- .as_nanos()
- );
- let path = std::env::temp_dir().join(unique);
- std::fs::write(&path, b"{}").expect("write accounts file");
-
- IosBackend::remove_accounts_file_if_present(path.as_path()).expect("remove file");
-
- assert!(!path.exists());
- }
-}
diff --git a/crates/launchers/ios/src/offline_geocoder.rs b/crates/launchers/ios/src/offline_geocoder.rs
@@ -1,516 +0,0 @@
-#![cfg_attr(not(target_os = "ios"), allow(dead_code))]
-
-use radroots_app_core::{
- RadrootsLocationCountry, RadrootsLocationPoint, RadrootsLocationResolverError,
- RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState,
- RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation,
-};
-use radroots_geocoder::{
- Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions,
- GeocoderReverseResult,
-};
-use std::path::{Path, PathBuf};
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-const GEOCODER_ASSET_FILENAME: &str = "geonames.db";
-const GEOCODER_REVISION_FILENAME: &str = "geonames.revision";
-
-#[derive(Clone)]
-pub(crate) struct IosOfflineGeocoder {
- current: Arc<Mutex<RadrootsOfflineGeocoderState>>,
- changed: Arc<AtomicBool>,
-}
-
-impl IosOfflineGeocoder {
- pub(crate) fn from_state(state: RadrootsOfflineGeocoderState) -> Self {
- Self {
- current: Arc::new(Mutex::new(state)),
- changed: Arc::new(AtomicBool::new(false)),
- }
- }
-
- pub(crate) fn start(app_data_root: PathBuf) -> Self {
- let tracker = Self::from_state(RadrootsOfflineGeocoderState::Initializing);
- let current = Arc::clone(&tracker.current);
- let changed = Arc::clone(&tracker.changed);
-
- std::thread::spawn(move || {
- let state = initialize_offline_geocoder(app_data_root.as_path());
- if let RadrootsOfflineGeocoderState::Unavailable { debug_message, .. } = &state {
- log::warn!("ios offline geocoder unavailable: {debug_message}");
- }
- if let Ok(mut slot) = current.lock() {
- *slot = state;
- changed.store(true, Ordering::Release);
- }
- });
-
- tracker
- }
-
- pub(crate) fn current_state(&self) -> RadrootsOfflineGeocoderState {
- self.current
- .lock()
- .map(|state| state.clone())
- .unwrap_or_else(|_| {
- RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- RadrootsOfflineGeocoderPlatform::Ios,
- "ios offline geocoder state lock poisoned",
- )
- })
- }
-
- pub(crate) fn take_update(&self) -> Option<RadrootsOfflineGeocoderState> {
- if self.changed.swap(false, Ordering::AcqRel) {
- Some(self.current_state())
- } else {
- None
- }
- }
-}
-
-pub(crate) fn reverse_location(
- app_data_root: &Path,
- state: &RadrootsOfflineGeocoderState,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
-) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(app_data_root, state)?;
- let options = options.map(|options| GeocoderReverseOptions {
- limit: options.limit,
- degree_offset: options.degree_offset,
- });
- geocoder
- .reverse(
- GeocoderPoint {
- lat: point.lat,
- lng: point.lng,
- },
- options,
- )
- .map(|results| results.into_iter().map(map_reverse_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- })
-}
-
-pub(crate) fn list_countries(
- app_data_root: &Path,
- state: &RadrootsOfflineGeocoderState,
-) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(app_data_root, state)?;
- geocoder
- .country_list()
- .map(|results| results.into_iter().map(map_country_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- })
-}
-
-pub(crate) fn country_center(
- app_data_root: &Path,
- state: &RadrootsOfflineGeocoderState,
- country_id: &str,
-) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- let geocoder = geocoder_for_queries(app_data_root, state)?;
- geocoder
- .country_center(country_id)
- .map(|point| RadrootsLocationPoint {
- lat: point.lat,
- lng: point.lng,
- })
- .map_err(map_country_center_error)
-}
-
-fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState {
- let source_path = bundled_asset_path().map_err(|debug_message| {
- RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InternalError,
- RadrootsOfflineGeocoderPlatform::Ios,
- debug_message,
- )
- });
- let source_path = match source_path {
- Ok(source_path) => source_path,
- Err(state) => return state,
- };
- if !source_path.is_file() {
- return RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Ios,
- format!(
- "ios bundled geocoder asset missing at {}",
- source_path.display()
- ),
- );
- }
-
- let revision =
- match bundled_asset_revision(source_path.parent().unwrap_or_else(|| Path::new("."))) {
- Ok(revision) => revision,
- Err((kind, debug_message)) => {
- return RadrootsOfflineGeocoderState::unavailable(
- kind,
- RadrootsOfflineGeocoderPlatform::Ios,
- debug_message,
- );
- }
- };
- let staged_path = staged_db_path(app_data_root, revision.as_str());
- if let Err(debug_message) = stage_bundled_asset(source_path.as_path(), staged_path.as_path()) {
- return RadrootsOfflineGeocoderState::unavailable_with_revision(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Ios,
- revision,
- debug_message,
- );
- }
- if let Err(source) = Geocoder::open_path(staged_path.as_path()) {
- return RadrootsOfflineGeocoderState::unavailable_with_revision(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Ios,
- revision,
- format!("failed to open staged ios geocoder db: {source}"),
- );
- }
- let _ = prune_stale_revisions(staged_geocoder_root(app_data_root), revision.as_str());
- RadrootsOfflineGeocoderState::Ready
-}
-
-fn bundled_asset_path() -> Result<PathBuf, String> {
- let executable_path = std::env::current_exe()
- .map_err(|source| format!("failed to resolve ios executable path: {source}"))?;
- let Some(parent) = executable_path.parent() else {
- return Err("ios executable path did not have a parent directory".to_owned());
- };
- Ok(parent.join(GEOCODER_ASSET_FILENAME))
-}
-
-fn geocoder_for_queries(
- app_data_root: &Path,
- state: &RadrootsOfflineGeocoderState,
-) -> Result<Geocoder, RadrootsLocationResolverError> {
- match state {
- RadrootsOfflineGeocoderState::Initializing => {
- return Err(RadrootsLocationResolverError::Initializing);
- }
- RadrootsOfflineGeocoderState::Unavailable { .. } => {
- return Err(RadrootsLocationResolverError::Unavailable);
- }
- RadrootsOfflineGeocoderState::Ready => {}
- }
-
- let source_path = bundled_asset_path()
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- let revision =
- bundled_asset_revision(source_path.parent().unwrap_or_else(|| Path::new(".")))
- .map_err(|(_, message)| RadrootsLocationResolverError::QueryFailed { message })?;
- let staged_path = staged_db_path(app_data_root, revision.as_str());
- stage_bundled_asset(source_path.as_path(), staged_path.as_path())
- .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?;
- Geocoder::open_path(staged_path.as_path()).map_err(|source| {
- RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- }
- })
-}
-
-fn map_reverse_result(result: GeocoderReverseResult) -> RadrootsResolvedLocation {
- RadrootsResolvedLocation {
- id: result.id,
- name: result.name,
- admin1_id: result.admin1_id,
- admin1_name: result.admin1_name,
- country_id: result.country_id,
- country_name: result.country_name,
- point: RadrootsLocationPoint {
- lat: result.latitude,
- lng: result.longitude,
- },
- }
-}
-
-fn map_country_result(result: GeocoderCountryListResult) -> RadrootsLocationCountry {
- RadrootsLocationCountry {
- country_id: result.country_id,
- country_name: result.country,
- center: RadrootsLocationPoint {
- lat: result.lat,
- lng: result.lng,
- },
- }
-}
-
-fn map_country_center_error(source: GeocoderError) -> RadrootsLocationResolverError {
- match source {
- GeocoderError::CountryCenterNotFound { country_id } => {
- RadrootsLocationResolverError::CountryCenterNotFound { country_id }
- }
- other => RadrootsLocationResolverError::QueryFailed {
- message: other.to_string(),
- },
- }
-}
-
-fn bundled_asset_revision(
- asset_dir: &Path,
-) -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> {
- let revision_path = asset_dir.join(GEOCODER_REVISION_FILENAME);
- let revision = std::fs::read_to_string(revision_path.as_path()).map_err(|source| {
- (
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- format!(
- "ios bundled geocoder revision asset missing at {}: {source}",
- revision_path.display()
- ),
- )
- })?;
- let revision = revision.trim();
- if !is_valid_revision(revision) {
- return Err((
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- format!(
- "ios bundled geocoder revision asset invalid at {}",
- revision_path.display()
- ),
- ));
- }
- Ok(revision.to_owned())
-}
-
-fn is_valid_revision(revision: &str) -> bool {
- revision.len() == 64 && revision.bytes().all(|byte| byte.is_ascii_hexdigit())
-}
-
-fn staged_geocoder_root(app_data_root: &Path) -> PathBuf {
- app_data_root.join("geocoder")
-}
-
-fn staged_db_path(app_data_root: &Path, revision: &str) -> PathBuf {
- staged_geocoder_root(app_data_root)
- .join(revision)
- .join(GEOCODER_ASSET_FILENAME)
-}
-
-fn stage_bundled_asset(source_path: &Path, staged_path: &Path) -> Result<bool, String> {
- let Some(parent) = staged_path.parent() else {
- return Err("staged ios geocoder path did not have a parent directory".to_owned());
- };
- std::fs::create_dir_all(parent)
- .map_err(|source| format!("failed to create ios geocoder directory: {source}"))?;
- if staged_path.is_file() {
- return Ok(false);
- }
- std::fs::copy(source_path, staged_path)
- .map_err(|source| format!("failed to stage ios geocoder asset: {source}"))?;
- Ok(true)
-}
-
-fn prune_stale_revisions(staged_root: PathBuf, active_revision: &str) -> Result<(), String> {
- if !staged_root.is_dir() {
- return Ok(());
- }
-
- for entry in std::fs::read_dir(staged_root.as_path())
- .map_err(|source| format!("failed to list ios geocoder revisions: {source}"))?
- {
- let entry = entry
- .map_err(|source| format!("failed to read ios geocoder revision entry: {source}"))?;
- if entry.file_name() == std::ffi::OsStr::new(active_revision) {
- continue;
- }
-
- let path = entry.path();
- if entry
- .file_type()
- .map_err(|source| format!("failed to inspect ios geocoder revision entry: {source}"))?
- .is_dir()
- {
- std::fs::remove_dir_all(path.as_path()).map_err(|source| {
- format!(
- "failed to remove stale ios geocoder revision {}: {source}",
- path.display()
- )
- })?;
- } else {
- std::fs::remove_file(path.as_path()).map_err(|source| {
- format!(
- "failed to remove stale ios geocoder revision file {}: {source}",
- path.display()
- )
- })?;
- }
- }
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::time::{SystemTime, UNIX_EPOCH};
-
- #[test]
- fn staged_db_path_uses_ios_geocoder_directory() {
- let app_data_root = PathBuf::from(
- "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app",
- );
-
- assert_eq!(
- staged_db_path(app_data_root.as_path(), "abcd"),
- PathBuf::from(
- "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app/geocoder/abcd/geonames.db"
- )
- );
- }
-
- #[test]
- fn valid_revision_requires_sha256_hex() {
- assert!(is_valid_revision(
- "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"
- ));
- assert!(!is_valid_revision("abcd"));
- assert!(!is_valid_revision(
- "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079z"
- ));
- }
-
- #[test]
- fn missing_asset_maps_to_build_unavailable_message() {
- let state = RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Ios,
- "ios bundled geocoder asset missing at /tmp/geonames.db",
- );
-
- assert_eq!(
- state,
- RadrootsOfflineGeocoderState::Unavailable {
- kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- platform: RadrootsOfflineGeocoderPlatform::Ios,
- asset_revision: None,
- debug_message: "ios bundled geocoder asset missing at /tmp/geonames.db".to_owned(),
- }
- );
- }
-
- #[test]
- fn stage_bundled_asset_reuses_existing_staged_copy() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-ios-geocoder-test-{}",
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
- let source_path = temp_root.join("source.db");
- let staged_path = temp_root.join("staged").join("geonames.db");
-
- std::fs::create_dir_all(temp_root.as_path()).unwrap();
- std::fs::write(source_path.as_path(), b"source").unwrap();
- std::fs::create_dir_all(staged_path.parent().unwrap()).unwrap();
- std::fs::write(staged_path.as_path(), b"existing").unwrap();
-
- let copied = stage_bundled_asset(source_path.as_path(), staged_path.as_path()).unwrap();
-
- assert!(!copied);
- assert_eq!(std::fs::read(staged_path.as_path()).unwrap(), b"existing");
-
- std::fs::remove_dir_all(temp_root.as_path()).unwrap();
- }
-
- #[test]
- fn prune_stale_revisions_keeps_active_revision_only() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-ios-geocoder-prune-test-{}",
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
- let staged_root = temp_root.join("geocoder");
- let active_dir = staged_root.join("active");
- let stale_dir = staged_root.join("stale");
- let stale_file = staged_root.join("orphan.txt");
-
- std::fs::create_dir_all(active_dir.as_path()).unwrap();
- std::fs::create_dir_all(stale_dir.as_path()).unwrap();
- std::fs::write(active_dir.join("geonames.db"), b"active").unwrap();
- std::fs::write(stale_dir.join("geonames.db"), b"stale").unwrap();
- std::fs::write(stale_file.as_path(), b"orphan").unwrap();
-
- prune_stale_revisions(staged_root.clone(), "active").unwrap();
-
- assert!(active_dir.exists());
- assert!(!stale_dir.exists());
- assert!(!stale_file.exists());
-
- std::fs::remove_dir_all(temp_root.as_path()).unwrap();
- }
-
- #[test]
- fn bundled_asset_revision_reads_stamped_sidecar() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-ios-geocoder-revision-test-{}",
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
- let revision_path = temp_root.join(GEOCODER_REVISION_FILENAME);
- let revision = "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c";
-
- std::fs::create_dir_all(temp_root.as_path()).unwrap();
- std::fs::write(revision_path.as_path(), format!("{revision}\n")).unwrap();
-
- assert_eq!(
- bundled_asset_revision(temp_root.as_path()).unwrap(),
- revision.to_owned()
- );
-
- std::fs::remove_dir_all(temp_root.as_path()).unwrap();
- }
-
- #[test]
- fn reverse_result_mapping_preserves_location_fields() {
- let mapped = map_reverse_result(GeocoderReverseResult {
- id: 42,
- name: "Oslo".to_owned(),
- admin1_id: Some(12),
- admin1_name: Some("Oslo".to_owned()),
- country_id: "NO".to_owned(),
- country_name: Some("Norway".to_owned()),
- latitude: 59.9139,
- longitude: 10.7522,
- });
-
- assert_eq!(mapped.id, 42);
- assert_eq!(mapped.name, "Oslo");
- assert_eq!(mapped.admin1_id, Some(12));
- assert_eq!(mapped.admin1_name.as_deref(), Some("Oslo"));
- assert_eq!(mapped.country_id, "NO");
- assert_eq!(mapped.country_name.as_deref(), Some("Norway"));
- assert_eq!(mapped.point.lat, 59.9139);
- assert_eq!(mapped.point.lng, 10.7522);
- }
-
- #[test]
- fn unavailable_state_blocks_queries_until_ready() {
- let temp_root = std::env::temp_dir().join(format!(
- "radroots-ios-geocoder-query-state-test-{}",
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_nanos()
- ));
-
- let result = list_countries(
- temp_root.as_path(),
- &RadrootsOfflineGeocoderState::Initializing,
- );
-
- assert_eq!(result, Err(RadrootsLocationResolverError::Initializing));
- }
-}
diff --git a/crates/launchers/ios/src/remote_signer.rs b/crates/launchers/ios/src/remote_signer.rs
@@ -1,469 +0,0 @@
-use crate::storage;
-use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault};
-use radroots_app_core::{
- IdentityGateState, RadrootsAccountCustody, RadrootsPendingRemoteSignerConnection,
- RadrootsRemoteSignerPreview, RadrootsRemoteSignerSignedNote, SetupActionState,
-};
-use radroots_app_remote_signer::{
- RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerActionController,
- RadrootsAppRemoteSignerActionControllerHooks, RadrootsAppRemoteSignerActionState,
- RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks,
- RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerPendingState,
- RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState,
- RadrootsAppRemoteSignerSignedEvent, radroots_app_remote_signer_clear_pending_session,
- radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview,
- radroots_app_remote_signer_purge_all_custody_state,
- radroots_app_remote_signer_reconcile_startup,
-};
-use radroots_identity::RadrootsIdentityId;
-use radroots_nostr_accounts::prelude::{
- RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, RadrootsSecretVault,
- account_secret_slot,
-};
-use std::path::{Path, PathBuf};
-
-const REMOTE_SIGNER_LABEL: &str = "remote signer";
-
-#[derive(Clone, Copy)]
-struct IosRemoteSignerHooks;
-
-impl RadrootsAppRemoteSignerControllerHooks for IosRemoteSignerHooks {
- type ReadyState = IdentityGateState;
-
- fn reconcile_startup_state(&self) -> Result<(), String> {
- let manager = storage::accounts_manager()?;
- let store_path = sessions_path()?;
- radroots_app_remote_signer_reconcile_startup(
- &manager,
- store_path.as_path(),
- REMOTE_SIGNER_LABEL,
- load_client_secret,
- remove_client_secret,
- purge_client_secret_namespace,
- )
- }
-
- fn store_pending_session(
- &self,
- pending: &RadrootsAppRemoteSignerPendingSession,
- ) -> Result<(), String> {
- let client_account_id = pending.record.client_account_id().to_owned();
- store_client_secret(
- client_account_id.as_str(),
- pending.client_secret_key_hex.as_str(),
- )?;
- let store_path = sessions_path()?;
- let mut state = load_sessions(store_path.as_path())?;
- if let Err(error) = state.upsert_pending(pending.record.clone()) {
- let _ = remove_client_secret(client_account_id.as_str());
- return Err(error.to_string());
- }
- if let Err(error) = save_sessions(store_path.as_path(), &state) {
- let _ = remove_client_secret(client_account_id.as_str());
- return Err(error);
- }
- Ok(())
- }
-
- fn pending_session_record(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- pending_session_record()
- }
-
- fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String> {
- load_client_secret(client_account_id)
- }
-
- fn activate_pending_session(
- &self,
- client_account_id: &str,
- approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession,
- ) -> Result<Self::ReadyState, String> {
- activate_remote_session(client_account_id, approved)
- }
-
- fn clear_pending_session(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- radroots_app_remote_signer_clear_pending_session(store_path.as_path(), remove_client_secret)
- }
-}
-
-#[derive(Clone)]
-pub(crate) struct IosRemoteSigner {
- controller: RadrootsAppRemoteSignerController<IosRemoteSignerHooks>,
- action_controller: RadrootsAppRemoteSignerActionController<IosRemoteSignerHooks>,
-}
-
-impl IosRemoteSigner {
- pub(crate) fn new() -> Self {
- Self {
- controller: RadrootsAppRemoteSignerController::new(IosRemoteSignerHooks),
- action_controller: RadrootsAppRemoteSignerActionController::new(IosRemoteSignerHooks),
- }
- }
-
- pub(crate) fn take_update(&self) -> Option<Result<Option<IdentityGateState>, String>> {
- self.controller.take_update()
- }
-
- pub(crate) fn is_connecting(&self) -> bool {
- self.controller.is_connecting()
- }
-
- pub(crate) fn action_state(&self) -> Result<SetupActionState, String> {
- if self.is_connecting() {
- return Ok(SetupActionState {
- label: "Connecting Remote Signer...".to_owned(),
- enabled: false,
- pending: true,
- });
- }
-
- if self.pending_connection()?.is_some() {
- return Ok(match self.controller.pending_state() {
- RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState {
- label: "Remote Signer Approval Check Retrying".to_owned(),
- enabled: false,
- pending: false,
- },
- RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { .. } => {
- SetupActionState {
- label: "Authorize Remote Signer to Continue".to_owned(),
- enabled: false,
- pending: false,
- }
- }
- RadrootsAppRemoteSignerPendingState::Idle
- | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState {
- label: "Remote Signer Waiting for Approval".to_owned(),
- enabled: false,
- pending: false,
- },
- });
- }
-
- Ok(SetupActionState {
- label: "Connect Remote Signer".to_owned(),
- enabled: true,
- pending: false,
- })
- }
-
- pub(crate) fn begin_connect(&self, input: &str) -> Result<(), String> {
- self.controller.begin_connect(input)
- }
-
- pub(crate) fn pending_connection(
- &self,
- ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> {
- Ok(
- pending_session_record()?.map(|record| RadrootsPendingRemoteSignerConnection {
- signer_npub: record.signer_identity.public_key_npub,
- relays: record.relays,
- auth_url: match self.controller.pending_state() {
- RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { url } => Some(url),
- _ => None,
- },
- }),
- )
- }
-
- pub(crate) fn note_action_state(&self) -> Result<SetupActionState, String> {
- if selected_remote_signer_account()?.is_none() {
- return Ok(SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: false,
- pending: false,
- });
- }
-
- Ok(match self.action_controller.state() {
- RadrootsAppRemoteSignerActionState::Idle => SetupActionState {
- label: "Sign Remote Kind 1 Note".to_owned(),
- enabled: true,
- pending: false,
- },
- RadrootsAppRemoteSignerActionState::Signing => SetupActionState {
- label: "Signing Remote Kind 1 Note...".to_owned(),
- enabled: false,
- pending: true,
- },
- RadrootsAppRemoteSignerActionState::AwaitingAuthorization { .. } => SetupActionState {
- label: "Authorize Remote Signer to Continue".to_owned(),
- enabled: false,
- pending: false,
- },
- })
- }
-
- pub(crate) fn begin_sign_kind1_note_selected(&self, content: &str) -> Result<(), String> {
- self.action_controller.begin_sign_kind1_note(content)
- }
-
- pub(crate) fn take_note_update(
- &self,
- ) -> Option<Result<Option<RadrootsRemoteSignerSignedNote>, String>> {
- self.action_controller.take_update()
- }
-}
-
-pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> {
- let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?;
- let requested_permissions = preview.requested_permission_labels();
- Ok(RadrootsRemoteSignerPreview {
- source_label: preview.source_label().to_owned(),
- signer_npub: preview.signer_identity.public_key_npub,
- relays: preview.relays,
- requested_permissions,
- })
-}
-
-pub(crate) fn identity_state_from_status(
- status: RadrootsNostrSelectedAccountStatus,
-) -> Result<IdentityGateState, String> {
- match status {
- RadrootsNostrSelectedAccountStatus::NotConfigured => Ok(IdentityGateState::Missing),
- RadrootsNostrSelectedAccountStatus::Ready { account } => Ok(IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- }),
- RadrootsNostrSelectedAccountStatus::PublicOnly { account } => {
- if active_session_for_account_id(account.account_id.as_str())?.is_some() {
- Ok(IdentityGateState::Ready {
- account_id: account.account_id.to_string(),
- })
- } else {
- Ok(IdentityGateState::Missing)
- }
- }
- }
-}
-
-pub(crate) fn custody_for_account_id(account_id: &str) -> Result<RadrootsAccountCustody, String> {
- if active_session_for_account_id(account_id)?.is_some() {
- Ok(RadrootsAccountCustody::RemoteSigner)
- } else {
- Ok(RadrootsAccountCustody::LocalManaged)
- }
-}
-
-pub(crate) fn disconnect_selected_remote_signer(
- manager: &RadrootsNostrAccountsManager,
-) -> Result<IdentityGateState, String> {
- let store_path = sessions_path()?;
- let status = radroots_app_remote_signer_disconnect_selected(
- manager,
- store_path.as_path(),
- remove_client_secret,
- )?;
- identity_state_from_status(status)
-}
-
-pub(crate) fn cancel_pending_connection() -> Result<(), String> {
- let store_path = sessions_path()?;
- let _ = radroots_app_remote_signer_clear_pending_session(
- store_path.as_path(),
- remove_client_secret,
- )?;
- Ok(())
-}
-
-pub(crate) fn purge_all_custody_state() -> Result<(), String> {
- let store_path = sessions_path()?;
- radroots_app_remote_signer_purge_all_custody_state(
- store_path.as_path(),
- remove_client_secret,
- purge_client_secret_namespace,
- )
-}
-
-fn activate_remote_session(
- client_account_id: &str,
- approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession,
-) -> Result<IdentityGateState, String> {
- let manager = storage::accounts_manager()?;
- manager
- .upsert_public_identity(
- approved.user_identity.clone(),
- Some(REMOTE_SIGNER_LABEL.to_owned()),
- true,
- )
- .map_err(|source| source.to_string())?;
- let store_path = sessions_path()?;
- let activation_result = (|| -> Result<(), String> {
- let mut state = load_sessions(store_path.as_path())?;
- state
- .activate_session(
- client_account_id,
- approved.user_identity.clone(),
- approved.relays.clone(),
- approved.approved_permissions.clone(),
- )
- .ok_or_else(|| {
- "pending remote signer session disappeared before activation".to_owned()
- })?;
- save_sessions(store_path.as_path(), &state)
- })();
- if let Err(error) = activation_result {
- if let Err(rollback_error) = manager.remove_account(&approved.user_identity.id) {
- return Err(format!(
- "{error}. remote signer account rollback needs retry: {rollback_error}"
- ));
- }
- return Err(error);
- }
- Ok(IdentityGateState::Ready {
- account_id: approved.user_identity.id.to_string(),
- })
-}
-
-fn selected_remote_signer_account() -> Result<Option<String>, String> {
- let manager = storage::accounts_manager()?;
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Ok(None);
- };
- if active_session_for_account_id(account_id.as_str())?.is_some() {
- Ok(Some(account_id.to_string()))
- } else {
- Ok(None)
- }
-}
-
-fn update_active_session_relays(account_id: &str, relays: Vec<String>) -> Result<(), String> {
- let store_path = sessions_path()?;
- let mut state = load_sessions(store_path.as_path())?;
- let Some(mut session) = state.active_session_for_account_id(account_id).cloned() else {
- return Err("active remote signer session disappeared before relay update".to_owned());
- };
- if session.relays == relays {
- return Ok(());
- }
- session.relays = relays;
- state.remove_active_session_for_account_id(account_id);
- state.sessions.push(session);
- save_sessions(store_path.as_path(), &state)
-}
-
-impl RadrootsAppRemoteSignerActionControllerHooks for IosRemoteSignerHooks {
- type ReadyState = RadrootsRemoteSignerSignedNote;
-
- fn selected_active_session(
- &self,
- ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String> {
- let Some(account_id) = selected_remote_signer_account()? else {
- return Ok(None);
- };
- let Some(record) = active_session_for_account_id(account_id.as_str())? else {
- return Ok(None);
- };
- let secret = load_client_secret(record.client_account_id())?;
- Ok(Some((record, secret)))
- }
-
- fn complete_sign_event(
- &self,
- signed_event: RadrootsAppRemoteSignerSignedEvent,
- ) -> Result<Self::ReadyState, String> {
- let Some(account_id) = selected_remote_signer_account()? else {
- return Err("remote signer account is no longer selected".to_owned());
- };
- update_active_session_relays(account_id.as_str(), signed_event.relays.clone())?;
- Ok(RadrootsRemoteSignerSignedNote {
- event_id_hex: signed_event.event_id_hex,
- })
- }
-}
-
-fn pending_session_record() -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- let state = load_sessions(store_path.as_path())?;
- Ok(state.pending_session().cloned())
-}
-
-fn active_session_for_account_id(
- account_id: &str,
-) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let store_path = sessions_path()?;
- let state = load_sessions(store_path.as_path())?;
- Ok(state.active_session_for_account_id(account_id).cloned())
-}
-
-fn load_sessions(path: &Path) -> Result<RadrootsAppRemoteSignerSessionStoreState, String> {
- RadrootsAppRemoteSignerSessionStoreState::load(path).map_err(|error| error.to_string())
-}
-
-fn save_sessions(
- path: &Path,
- state: &RadrootsAppRemoteSignerSessionStoreState,
-) -> Result<(), String> {
- state.save(path).map_err(|error| error.to_string())
-}
-
-fn sessions_path() -> Result<PathBuf, String> {
- Ok(storage::app_data_root()?
- .join("nostr")
- .join("remote-signer-sessions.json"))
-}
-
-fn client_secret_vault() -> RadrootsAppleKeychainVault {
- RadrootsAppleKeychainVault::new_with_namespace_device_local(
- APPLE_NOSTR_SERVICE,
- RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE,
- )
-}
-
-fn legacy_client_secret_vault() -> RadrootsAppleKeychainVault {
- RadrootsAppleKeychainVault::new_device_local(APPLE_NOSTR_SERVICE)
-}
-
-fn client_secret_slot(client_account_id: &str) -> Result<String, String> {
- let account_id = RadrootsIdentityId::try_from(client_account_id)
- .map_err(|_| "invalid remote signer client account id".to_owned())?;
- Ok(account_secret_slot(&account_id))
-}
-
-fn store_client_secret(client_account_id: &str, secret_key_hex: &str) -> Result<(), String> {
- let slot = client_secret_slot(client_account_id)?;
- client_secret_vault()
- .store_secret(slot.as_str(), secret_key_hex)
- .map_err(|source| source.to_string())
-}
-
-fn load_client_secret(client_account_id: &str) -> Result<String, String> {
- let slot = client_secret_slot(client_account_id)?;
- if let Some(secret) = client_secret_vault()
- .load_secret(slot.as_str())
- .map_err(|source| source.to_string())?
- {
- return Ok(secret);
- }
-
- let secret = legacy_client_secret_vault()
- .load_secret(slot.as_str())
- .map_err(|source| source.to_string())?
- .ok_or_else(|| "remote signer session secret is missing".to_owned())?;
- let _ = client_secret_vault().store_secret(slot.as_str(), secret.as_str());
- let _ = legacy_client_secret_vault().remove_secret(slot.as_str());
- Ok(secret)
-}
-
-fn remove_client_secret(client_account_id: &str) -> Result<(), String> {
- let slot = client_secret_slot(client_account_id)?;
- client_secret_vault()
- .remove_secret(slot.as_str())
- .map_err(|source| source.to_string())?;
- legacy_client_secret_vault()
- .remove_secret(slot.as_str())
- .map_err(|source| source.to_string())
-}
-
-fn purge_client_secret_namespace() -> Result<(), String> {
- client_secret_vault()
- .purge_namespace()
- .map_err(|source| source.to_string())
-}
diff --git a/crates/launchers/ios/src/reverse_lookup.rs b/crates/launchers/ios/src/reverse_lookup.rs
@@ -1,122 +0,0 @@
-#![cfg_attr(not(target_os = "ios"), allow(dead_code))]
-
-#[cfg(target_os = "ios")]
-use crate::offline_geocoder;
-use radroots_app_core::{
- RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions,
- RadrootsOfflineGeocoderState, RadrootsReverseLocationLookupResult,
-};
-#[cfg(target_os = "ios")]
-use std::path::PathBuf;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-#[derive(Clone, Default)]
-pub(crate) struct IosReverseLookup {
- result: Arc<Mutex<Option<RadrootsReverseLocationLookupResult>>>,
- changed: Arc<AtomicBool>,
- pending: Arc<AtomicBool>,
-}
-
-impl IosReverseLookup {
- pub(crate) fn new() -> Self {
- Self::default()
- }
-
- #[cfg(target_os = "ios")]
- pub(crate) fn begin(
- &self,
- app_data_root: PathBuf,
- geocoder_state: RadrootsOfflineGeocoderState,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- if self.pending.swap(true, Ordering::AcqRel) {
- return Err(RadrootsLocationResolverError::QueryFailed {
- message: "offline location query is already running".to_owned(),
- });
- }
-
- if let Ok(mut slot) = self.result.lock() {
- *slot = None;
- }
-
- let result = Arc::clone(&self.result);
- let changed = Arc::clone(&self.changed);
- let pending = Arc::clone(&self.pending);
- std::thread::spawn(move || {
- let lookup_result = offline_geocoder::reverse_location(
- app_data_root.as_path(),
- &geocoder_state,
- point,
- options,
- );
- if let Ok(mut slot) = result.lock() {
- *slot = Some(lookup_result);
- changed.store(true, Ordering::Release);
- }
- pending.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- #[cfg(not(target_os = "ios"))]
- pub(crate) fn begin(
- &self,
- _app_data_root: std::path::PathBuf,
- _geocoder_state: RadrootsOfflineGeocoderState,
- _point: RadrootsLocationPoint,
- _options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- pub(crate) fn take_update(&self) -> Option<RadrootsReverseLocationLookupResult> {
- if !self.changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- match self.result.lock() {
- Ok(mut slot) => slot.take(),
- Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed {
- message: "ios reverse lookup result lock poisoned".to_owned(),
- })),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_core::RadrootsResolvedLocation;
-
- fn sample_result() -> RadrootsReverseLocationLookupResult {
- Ok(vec![RadrootsResolvedLocation {
- id: 7,
- name: "example".to_owned(),
- admin1_id: None,
- admin1_name: None,
- country_id: "US".to_owned(),
- country_name: Some("United States".to_owned()),
- point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 },
- }])
- }
-
- #[test]
- fn take_update_is_none_until_tracker_changes() {
- let tracker = IosReverseLookup::new();
-
- assert_eq!(tracker.take_update(), None);
- }
-
- #[test]
- fn take_update_returns_queued_result_once() {
- let tracker = IosReverseLookup::new();
- *tracker.result.lock().unwrap() = Some(sample_result());
- tracker.changed.store(true, Ordering::Release);
-
- assert!(matches!(tracker.take_update(), Some(Ok(results)) if results.len() == 1));
- assert_eq!(tracker.take_update(), None);
- }
-}
diff --git a/crates/launchers/ios/src/storage.rs b/crates/launchers/ios/src/storage.rs
@@ -1,124 +0,0 @@
-#[cfg(target_os = "ios")]
-use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault};
-use radroots_app_core::mobile_native_app_storage_layout;
-#[cfg(target_os = "ios")]
-use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager;
-use radroots_runtime_paths::{RadrootsPaths, RadrootsPlatform};
-use std::path::{Path, PathBuf};
-
-fn mobile_base_root_from_home(home: &Path) -> PathBuf {
- home.join("Library")
- .join("Application Support")
- .join("RadRoots")
-}
-
-fn app_paths_from_home(home: &Path) -> Result<RadrootsPaths, String> {
- let base_root = mobile_base_root_from_home(home);
- Ok(mobile_native_app_storage_layout(RadrootsPlatform::Ios, base_root.as_path())?.app_paths)
-}
-
-#[cfg(target_os = "ios")]
-pub(crate) fn accounts_path() -> Result<PathBuf, String> {
- let home = std::env::var_os("HOME")
- .map(PathBuf::from)
- .ok_or_else(|| "failed to resolve ios app container home directory".to_owned())?;
- let accounts_path = accounts_path_from_home(home.as_path())?;
- if let Some(parent) = accounts_path.parent() {
- ensure_private_directory_tree(parent)?;
- }
- Ok(accounts_path)
-}
-
-#[cfg(target_os = "ios")]
-pub(crate) fn app_data_root() -> Result<PathBuf, String> {
- let home = std::env::var_os("HOME")
- .map(PathBuf::from)
- .ok_or_else(|| "failed to resolve ios app container home directory".to_owned())?;
- let root = app_data_root_from_home(home.as_path())?;
- ensure_private_directory_tree(root.as_path())?;
- Ok(root)
-}
-
-#[cfg(target_os = "ios")]
-pub(crate) fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> {
- RadrootsNostrAccountsManager::new_file_backed_with_vault(
- accounts_path()?,
- RadrootsAppleKeychainVault::new_device_local(APPLE_NOSTR_SERVICE),
- )
- .map_err(|source| source.to_string())
-}
-
-fn accounts_path_from_home(home: &Path) -> Result<PathBuf, String> {
- Ok(app_data_root_from_home(home)?
- .join("nostr")
- .join("accounts.json"))
-}
-
-fn app_data_root_from_home(home: &Path) -> Result<PathBuf, String> {
- Ok(app_paths_from_home(home)?.data)
-}
-
-#[cfg(target_os = "ios")]
-fn ensure_private_directory_tree(leaf: &Path) -> Result<(), String> {
- use std::os::unix::fs::PermissionsExt;
-
- std::fs::create_dir_all(leaf)
- .map_err(|source| format!("failed to create ios app data directory: {source}"))?;
- std::fs::set_permissions(leaf, std::fs::Permissions::from_mode(0o700))
- .map_err(|source| format!("failed to set ios app data permissions: {source}"))?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn accounts_path_uses_ios_mobile_native_layout() {
- let home = PathBuf::from("/var/mobile/Containers/Data/Application/example");
-
- assert_eq!(
- accounts_path_from_home(home.as_path()).expect("accounts path"),
- PathBuf::from(
- "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app/nostr/accounts.json"
- )
- );
- }
-
- #[test]
- fn app_data_root_uses_ios_mobile_native_layout() {
- let home = PathBuf::from("/var/mobile/Containers/Data/Application/example");
-
- assert_eq!(
- app_data_root_from_home(home.as_path()).expect("app data root"),
- PathBuf::from(
- "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app"
- )
- );
- }
-
- #[test]
- fn mobile_paths_follow_shared_logical_root_model() {
- let home = PathBuf::from("/var/mobile/Containers/Data/Application/example");
- let paths = app_paths_from_home(home.as_path()).expect("mobile paths");
-
- assert_eq!(
- paths.config,
- PathBuf::from(
- "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/config/apps/app"
- )
- );
- assert_eq!(
- paths.data,
- PathBuf::from(
- "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app"
- )
- );
- assert_eq!(
- paths.secrets,
- PathBuf::from(
- "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/secrets/apps/app"
- )
- );
- }
-}
diff --git a/crates/launchers/web/Cargo.toml b/crates/launchers/web/Cargo.toml
@@ -1,32 +0,0 @@
-[package]
-name = "radroots_app_web"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-authors.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots web launcher"
-publish = false
-
-[lib]
-path = "src/lib.rs"
-crate-type = ["cdylib", "rlib"]
-
-[dependencies]
-eframe = { workspace = true, features = ["wgpu"] }
-js-sys = "0.3.91"
-log.workspace = true
-radroots_app_core = { path = "../../shared/core" }
-radroots_geocoder.workspace = true
-wasm-bindgen-futures.workspace = true
-web-sys = { workspace = true, features = ["Document", "Element", "HtmlCanvasElement", "Response", "Window"] }
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-nostr.workspace = true
-nostr-browser-signer.workspace = true
-wgpu = { workspace = true, features = ["std", "wgsl", "webgpu", "fragile-send-sync-non-atomic-wasm"] }
-
-[lints]
-workspace = true
diff --git a/crates/launchers/web/Trunk.toml b/crates/launchers/web/Trunk.toml
@@ -1,3 +0,0 @@
-[build]
-target = "index.html"
-dist = "dist"
diff --git a/crates/launchers/web/index.html b/crates/launchers/web/index.html
@@ -1,65 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="utf-8" />
- <meta
- name="viewport"
- content="width=device-width, initial-scale=1.0, viewport-fit=cover"
- />
- <title>Rad Roots</title>
- <base data-trunk-public-url />
- <style>
- html,
- body {
- margin: 0;
- padding: 0;
- width: 100%;
- height: 100%;
- background: #0e1116;
- color: #f4efe1;
- font-family: ui-sans-serif, system-ui, sans-serif;
- }
-
- body {
- display: grid;
- place-items: center;
- }
-
- #app-shell {
- position: relative;
- width: 100%;
- height: 100%;
- }
-
- #radroots_app_canvas {
- width: 100%;
- height: 100%;
- display: block;
- }
-
- #loading_text {
- position: absolute;
- inset: 0;
- display: grid;
- place-items: center;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- font-size: 0.75rem;
- }
- </style>
- </head>
- <body>
- <div id="app-shell">
- <canvas id="radroots_app_canvas"></canvas>
- <div id="loading_text">loading rad roots…</div>
- </div>
- <link data-trunk rel="copy-dir" href="../../assets" />
- <link
- data-trunk
- rel="rust"
- href="Cargo.toml"
- data-bin="radroots_app_web"
- data-wasm-opt="0"
- />
- </body>
-</html>
diff --git a/crates/launchers/web/src/lib.rs b/crates/launchers/web/src/lib.rs
@@ -1,760 +0,0 @@
-#![forbid(unsafe_code)]
-
-#[cfg(target_arch = "wasm32")]
-use std::cell::RefCell;
-#[cfg(target_arch = "wasm32")]
-use std::rc::Rc;
-
-#[cfg(target_arch = "wasm32")]
-use eframe::wasm_bindgen::{JsCast as _, JsValue};
-#[cfg(target_arch = "wasm32")]
-use js_sys::Uint8Array;
-#[cfg(target_arch = "wasm32")]
-use nostr::nips::nip19::ToBech32;
-#[cfg(target_arch = "wasm32")]
-use nostr::signer::NostrSigner;
-#[cfg(target_arch = "wasm32")]
-use nostr_browser_signer::{BrowserSigner, Error as BrowserSignerError};
-#[cfg(target_arch = "wasm32")]
-use radroots_app_core::{
- HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, RadrootsAccountCustody,
- RadrootsAccountSummary, RadrootsApp, RadrootsAppBackend, RadrootsLocationCountry,
- RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult,
- RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions,
- RadrootsResolvedLocation, RadrootsReverseLocationLookupResult, SetupActionState,
-};
-#[cfg(any(target_arch = "wasm32", test))]
-use radroots_app_core::{
- RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState,
- RadrootsOfflineGeocoderUnavailableKind,
-};
-#[cfg(target_arch = "wasm32")]
-use radroots_geocoder::{
- Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions,
- GeocoderReverseResult,
-};
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
-
-#[cfg(target_arch = "wasm32")]
-const GEOCODER_DB_ASSET_PATH: &str = "assets/geocoder/geonames.db";
-#[cfg(target_arch = "wasm32")]
-const GEOCODER_REVISION_ASSET_PATH: &str = "assets/geocoder/geonames.revision";
-
-#[cfg(any(target_arch = "wasm32", test))]
-fn offline_geocoder_missing_build_asset_state(
- debug_message: impl Into<String>,
-) -> RadrootsOfflineGeocoderState {
- RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Web,
- debug_message,
- )
-}
-
-#[cfg(any(target_arch = "wasm32", test))]
-fn offline_geocoder_initialization_failed_state(
- asset_revision: impl Into<String>,
- debug_message: impl Into<String>,
-) -> RadrootsOfflineGeocoderState {
- RadrootsOfflineGeocoderState::unavailable_with_revision(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Web,
- asset_revision,
- debug_message,
- )
-}
-
-#[cfg(any(target_arch = "wasm32", test))]
-fn is_valid_asset_revision(revision: &str) -> bool {
- revision.len() == 64 && revision.bytes().all(|byte| byte.is_ascii_hexdigit())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn js_error_message(value: JsValue) -> String {
- value
- .as_string()
- .unwrap_or_else(|| "javascript error".to_owned())
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn fetch_response(path: &str) -> Result<web_sys::Response, String> {
- let window = web_sys::window().ok_or_else(|| "window unavailable".to_owned())?;
- let response_value = JsFuture::from(window.fetch_with_str(path))
- .await
- .map_err(|err| format!("failed to fetch {path}: {}", js_error_message(err)))?;
- let response = response_value
- .dyn_into::<web_sys::Response>()
- .map_err(|_| format!("fetch for {path} did not return a response"))?;
- if !response.ok() {
- return Err(format!(
- "fetch for {path} failed with http {}",
- response.status()
- ));
- }
- Ok(response)
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn fetch_text_asset(path: &str) -> Result<String, String> {
- let response = fetch_response(path).await?;
- let text_promise = response.text().map_err(|err| {
- format!(
- "failed to read text body for {path}: {}",
- js_error_message(err)
- )
- })?;
- let text_value = JsFuture::from(text_promise).await.map_err(|err| {
- format!(
- "failed to load text asset {path}: {}",
- js_error_message(err)
- )
- })?;
- text_value
- .as_string()
- .ok_or_else(|| format!("text asset {path} did not decode to a string"))
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn fetch_bytes_asset(path: &str) -> Result<Vec<u8>, String> {
- let response = fetch_response(path).await?;
- let buffer_promise = response.array_buffer().map_err(|err| {
- format!(
- "failed to read binary body for {path}: {}",
- js_error_message(err)
- )
- })?;
- let buffer = JsFuture::from(buffer_promise).await.map_err(|err| {
- format!(
- "failed to load binary asset {path}: {}",
- js_error_message(err)
- )
- })?;
- Ok(Uint8Array::new(&buffer).to_vec())
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn initialize_offline_geocoder() -> Result<Geocoder, RadrootsOfflineGeocoderState> {
- let revision_text = fetch_text_asset(GEOCODER_REVISION_ASSET_PATH)
- .await
- .map_err(offline_geocoder_missing_build_asset_state)?;
- let revision = revision_text.trim().to_owned();
- if !is_valid_asset_revision(revision.as_str()) {
- return Err(offline_geocoder_missing_build_asset_state(format!(
- "web geocoder revision asset invalid at {GEOCODER_REVISION_ASSET_PATH}"
- )));
- }
-
- let bytes = fetch_bytes_asset(GEOCODER_DB_ASSET_PATH)
- .await
- .map_err(|debug_message| {
- RadrootsOfflineGeocoderState::unavailable_with_revision(
- RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset,
- RadrootsOfflineGeocoderPlatform::Web,
- revision.clone(),
- debug_message,
- )
- })?;
-
- Geocoder::open_bytes(bytes.as_slice()).map_err(|source| {
- offline_geocoder_initialization_failed_state(
- revision,
- format!("failed to open wasm geocoder from {GEOCODER_DB_ASSET_PATH}: {source}"),
- )
- })
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_reverse_result(result: GeocoderReverseResult) -> RadrootsResolvedLocation {
- RadrootsResolvedLocation {
- id: result.id,
- name: result.name,
- admin1_id: result.admin1_id,
- admin1_name: result.admin1_name,
- country_id: result.country_id,
- country_name: result.country_name,
- point: RadrootsLocationPoint {
- lat: result.latitude,
- lng: result.longitude,
- },
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_country_result(result: GeocoderCountryListResult) -> RadrootsLocationCountry {
- RadrootsLocationCountry {
- country_id: result.country_id,
- country_name: result.country,
- center: RadrootsLocationPoint {
- lat: result.lat,
- lng: result.lng,
- },
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn map_country_center_error(source: GeocoderError) -> RadrootsLocationResolverError {
- match source {
- GeocoderError::CountryCenterNotFound { country_id } => {
- RadrootsLocationResolverError::CountryCenterNotFound { country_id }
- }
- other => RadrootsLocationResolverError::QueryFailed {
- message: other.to_string(),
- },
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-#[derive(Clone)]
-struct ConnectedSigner {
- account_id: String,
- npub: String,
- signer: BrowserSigner,
-}
-
-#[cfg(target_arch = "wasm32")]
-enum WebConnectionState {
- Disconnected,
- Connecting,
- Ready(ConnectedSigner),
-}
-
-#[cfg(target_arch = "wasm32")]
-struct WebBackendState {
- connection: WebConnectionState,
- pending_result: Option<Result<ConnectedSigner, String>>,
- offline_geocoder_state: RadrootsOfflineGeocoderState,
- pending_offline_geocoder_update: Option<RadrootsOfflineGeocoderState>,
- geocoder: Option<Rc<Geocoder>>,
- pending_reverse_lookup_result: Option<RadrootsReverseLocationLookupResult>,
- pending_country_list_result: Option<RadrootsLocationCountryListResult>,
- pending_country_center_result: Option<RadrootsLocationCountryCenterLookupResult>,
-}
-
-#[cfg(target_arch = "wasm32")]
-#[derive(Clone)]
-struct WebBackend {
- state: Rc<RefCell<WebBackendState>>,
-}
-
-#[cfg(target_arch = "wasm32")]
-impl WebBackend {
- fn new() -> Self {
- let backend = Self {
- state: Rc::new(RefCell::new(WebBackendState {
- connection: WebConnectionState::Disconnected,
- pending_result: None,
- offline_geocoder_state: RadrootsOfflineGeocoderState::Initializing,
- pending_offline_geocoder_update: None,
- geocoder: None,
- pending_reverse_lookup_result: None,
- pending_country_list_result: None,
- pending_country_center_result: None,
- })),
- };
- backend.start_offline_geocoder_init();
- backend
- }
-
- fn identity_state_for_ready(connected: &ConnectedSigner) -> IdentityGateState {
- let _ = &connected.signer;
- IdentityGateState::Ready {
- account_id: connected.account_id.clone(),
- }
- }
-
- fn account_summary_for_ready(connected: &ConnectedSigner) -> RadrootsAccountSummary {
- RadrootsAccountSummary {
- account_id: connected.account_id.clone(),
- npub: connected.npub.clone(),
- label: Some("browser signer".to_owned()),
- custody: RadrootsAccountCustody::BrowserSigner,
- }
- }
-
- fn connect_error_message(err: BrowserSignerError) -> String {
- match err {
- BrowserSignerError::NoGlobalWindowObject | BrowserSignerError::NamespaceNotFound(_) => {
- "No NIP-07 browser signer detected.".to_owned()
- }
- other => format!("Browser signer connection failed: {other}"),
- }
- }
-
- fn disconnect_signer(&self) -> IdentityGateState {
- let mut state = self.state.borrow_mut();
- state.connection = WebConnectionState::Disconnected;
- state.pending_result = None;
- IdentityGateState::Missing
- }
-
- fn start_offline_geocoder_init(&self) {
- let shared_state = Rc::clone(&self.state);
- wasm_bindgen_futures::spawn_local(async move {
- let result = initialize_offline_geocoder().await;
- let mut state = shared_state.borrow_mut();
- match result {
- Ok(geocoder) => {
- state.geocoder = Some(Rc::new(geocoder));
- state.offline_geocoder_state = RadrootsOfflineGeocoderState::Ready;
- state.pending_offline_geocoder_update =
- Some(RadrootsOfflineGeocoderState::Ready);
- }
- Err(offline_geocoder_state) => {
- state.geocoder = None;
- state.offline_geocoder_state = offline_geocoder_state.clone();
- state.pending_offline_geocoder_update = Some(offline_geocoder_state);
- }
- }
- });
- }
-
- fn ready_geocoder(&self) -> Result<Rc<Geocoder>, RadrootsLocationResolverError> {
- let state = self.state.borrow();
- match &state.offline_geocoder_state {
- RadrootsOfflineGeocoderState::Initializing => {
- Err(RadrootsLocationResolverError::Initializing)
- }
- RadrootsOfflineGeocoderState::Unavailable { .. } => {
- Err(RadrootsLocationResolverError::Unavailable)
- }
- RadrootsOfflineGeocoderState::Ready => {
- state
- .geocoder
- .clone()
- .ok_or_else(|| RadrootsLocationResolverError::QueryFailed {
- message: "web geocoder was ready without an initialized engine".to_owned(),
- })
- }
- }
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-impl RadrootsAppBackend for WebBackend {
- fn load_identity_state(&self) -> Result<IdentityGateState, String> {
- let state = self.state.borrow();
- match &state.connection {
- WebConnectionState::Ready(connected) => Ok(Self::identity_state_for_ready(connected)),
- WebConnectionState::Disconnected | WebConnectionState::Connecting => {
- Ok(IdentityGateState::Missing)
- }
- }
- }
-
- fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> {
- let state = self.state.borrow();
- match &state.connection {
- WebConnectionState::Ready(connected) => {
- Ok(vec![Self::account_summary_for_ready(connected)])
- }
- WebConnectionState::Disconnected | WebConnectionState::Connecting => Ok(Vec::new()),
- }
- }
-
- fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> {
- Some(self.state.borrow().offline_geocoder_state.clone())
- }
-
- fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> {
- Ok(self
- .state
- .borrow_mut()
- .pending_offline_geocoder_update
- .take())
- }
-
- fn reverse_location(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
- let geocoder = self.ready_geocoder()?;
- let options = options.map(|options| GeocoderReverseOptions {
- limit: options.limit,
- degree_offset: options.degree_offset,
- });
- geocoder
- .reverse(
- GeocoderPoint {
- lat: point.lat,
- lng: point.lng,
- },
- options,
- )
- .map(|results| results.into_iter().map(map_reverse_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- })
- }
-
- fn request_reverse_location_lookup(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- let geocoder = self.ready_geocoder()?;
- {
- let mut state = self.state.borrow_mut();
- state.pending_reverse_lookup_result = None;
- }
- let shared_state = Rc::clone(&self.state);
- wasm_bindgen_futures::spawn_local(async move {
- let options = options.map(|options| GeocoderReverseOptions {
- limit: options.limit,
- degree_offset: options.degree_offset,
- });
- let result = geocoder
- .reverse(
- GeocoderPoint {
- lat: point.lat,
- lng: point.lng,
- },
- options,
- )
- .map(|results| results.into_iter().map(map_reverse_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- });
- shared_state.borrow_mut().pending_reverse_lookup_result = Some(result);
- });
- Ok(())
- }
-
- fn poll_reverse_location_lookup_result(
- &self,
- ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> {
- Ok(self.state.borrow_mut().pending_reverse_lookup_result.take())
- }
-
- fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> {
- let geocoder = self.ready_geocoder()?;
- {
- let mut state = self.state.borrow_mut();
- state.pending_country_list_result = None;
- }
- let shared_state = Rc::clone(&self.state);
- wasm_bindgen_futures::spawn_local(async move {
- let result = geocoder
- .country_list()
- .map(|results| results.into_iter().map(map_country_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- });
- shared_state.borrow_mut().pending_country_list_result = Some(result);
- });
- Ok(())
- }
-
- fn poll_location_country_list_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryListResult>, String> {
- Ok(self.state.borrow_mut().pending_country_list_result.take())
- }
-
- fn request_location_country_center_lookup(
- &self,
- country_id: &str,
- ) -> Result<(), RadrootsLocationResolverError> {
- let geocoder = self.ready_geocoder()?;
- {
- let mut state = self.state.borrow_mut();
- state.pending_country_center_result = None;
- }
- let shared_state = Rc::clone(&self.state);
- let country_id = country_id.to_owned();
- wasm_bindgen_futures::spawn_local(async move {
- let result = geocoder
- .country_center(country_id.as_str())
- .map(|point| RadrootsLocationPoint {
- lat: point.lat,
- lng: point.lng,
- })
- .map_err(map_country_center_error);
- shared_state.borrow_mut().pending_country_center_result = Some(result);
- });
- Ok(())
- }
-
- fn poll_location_country_center_lookup_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> {
- Ok(self.state.borrow_mut().pending_country_center_result.take())
- }
-
- fn list_location_countries(
- &self,
- ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- let geocoder = self.ready_geocoder()?;
- geocoder
- .country_list()
- .map(|results| results.into_iter().map(map_country_result).collect())
- .map_err(|source| RadrootsLocationResolverError::QueryFailed {
- message: source.to_string(),
- })
- }
-
- fn location_country_center(
- &self,
- country_id: &str,
- ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- let geocoder = self.ready_geocoder()?;
- geocoder
- .country_center(country_id)
- .map(|point| RadrootsLocationPoint {
- lat: point.lat,
- lng: point.lng,
- })
- .map_err(map_country_center_error)
- }
-
- fn setup_action_state(&self) -> SetupActionState {
- let state = self.state.borrow();
- match &state.connection {
- WebConnectionState::Connecting => SetupActionState {
- label: "Connecting Browser Signer...".to_owned(),
- enabled: false,
- pending: true,
- },
- WebConnectionState::Disconnected => SetupActionState {
- label: "Connect Browser Signer".to_owned(),
- enabled: true,
- pending: false,
- },
- WebConnectionState::Ready(_) => SetupActionState {
- label: "Browser Signer Connected".to_owned(),
- enabled: false,
- pending: false,
- },
- }
- }
-
- fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- {
- let state = self.state.borrow();
- match &state.connection {
- WebConnectionState::Connecting => return Ok(None),
- WebConnectionState::Ready(connected) => {
- return Ok(Some(Self::identity_state_for_ready(connected)));
- }
- WebConnectionState::Disconnected => {}
- }
- }
-
- let signer = BrowserSigner::new().map_err(Self::connect_error_message)?;
- {
- let mut state = self.state.borrow_mut();
- state.connection = WebConnectionState::Connecting;
- state.pending_result = None;
- }
-
- let shared_state = Rc::clone(&self.state);
- wasm_bindgen_futures::spawn_local(async move {
- let result = match signer.get_public_key().await {
- Ok(public_key) => match public_key.to_bech32() {
- Ok(npub) => Ok(ConnectedSigner {
- account_id: public_key.to_hex(),
- npub,
- signer,
- }),
- Err(source) => Err(format!("Failed to encode npub: {source}")),
- },
- Err(source) => Err(format!("Browser signer connection failed: {source}")),
- };
-
- let mut state = shared_state.borrow_mut();
- state.pending_result = Some(result);
- });
-
- Ok(None)
- }
-
- fn home_action_states(&self) -> Vec<HomeActionState> {
- let state = self.state.borrow();
- match &state.connection {
- WebConnectionState::Ready(_) => vec![HomeActionState {
- kind: HomeActionKind::DisconnectSigner,
- label: "Disconnect Browser Signer".to_owned(),
- enabled: true,
- pending: false,
- }],
- WebConnectionState::Disconnected | WebConnectionState::Connecting => Vec::new(),
- }
- }
-
- fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> {
- match action {
- HomeActionKind::DisconnectSigner => {
- Ok(HomeActionResult::IdentityState(self.disconnect_signer()))
- }
- HomeActionKind::BackupSecretKey
- | HomeActionKind::RevealRawSecretKey
- | HomeActionKind::RemoveLocalKey
- | HomeActionKind::ResetDevice => Ok(HomeActionResult::None),
- }
- }
-
- fn request_select_account(
- &self,
- account_id: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- let state = self.state.borrow();
- match &state.connection {
- WebConnectionState::Ready(connected) if connected.account_id == account_id => {
- Ok(Some(Self::identity_state_for_ready(connected)))
- }
- WebConnectionState::Ready(_) => Err("unknown browser signer account".to_owned()),
- WebConnectionState::Disconnected | WebConnectionState::Connecting => Ok(None),
- }
- }
-
- fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> {
- let mut state = self.state.borrow_mut();
- let Some(result) = state.pending_result.take() else {
- return Ok(None);
- };
-
- match result {
- Ok(connected) => {
- let identity = Self::identity_state_for_ready(&connected);
- state.connection = WebConnectionState::Ready(connected);
- Ok(Some(identity))
- }
- Err(err) => {
- state.connection = WebConnectionState::Disconnected;
- Err(err)
- }
- }
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn loading_text_element() -> Option<web_sys::Element> {
- let window = web_sys::window()?;
- let document = window.document()?;
- document.get_element_by_id("loading_text")
-}
-
-#[cfg(target_arch = "wasm32")]
-fn clear_loading_text() {
- if let Some(loading_text) = loading_text_element() {
- loading_text.remove();
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn show_loading_failure() {
- if let Some(loading_text) = loading_text_element() {
- loading_text.set_inner_html("<p>failed to start radroots app</p>");
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-async fn launch_app() -> Result<(), String> {
- let web_options = eframe::WebOptions::default();
- let window = web_sys::window().ok_or_else(|| "window unavailable".to_owned())?;
- let document = window
- .document()
- .ok_or_else(|| "document unavailable".to_owned())?;
- let canvas = document
- .get_element_by_id("radroots_app_canvas")
- .ok_or_else(|| "radroots_app_canvas missing".to_owned())?
- .dyn_into::<web_sys::HtmlCanvasElement>()
- .map_err(|_| "radroots_app_canvas is not a canvas element".to_owned())?;
-
- eframe::WebRunner::new()
- .start(
- canvas,
- web_options,
- Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(WebBackend::new()))))),
- )
- .await
- .map_err(|err| format!("failed to start radroots app: {err:?}"))
-}
-
-#[cfg(target_arch = "wasm32")]
-pub fn launch() {
- let log_level = if cfg!(debug_assertions) {
- log::LevelFilter::Info
- } else {
- log::LevelFilter::Warn
- };
- let _ = eframe::WebLogger::init(log_level);
-
- wasm_bindgen_futures::spawn_local(async {
- match launch_app().await {
- Ok(()) => clear_loading_text(),
- Err(err) => {
- log::error!("{err}");
- show_loading_failure();
- }
- }
- });
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn missing_build_asset_state_is_stable() {
- let state =
- offline_geocoder_missing_build_asset_state("web geocoder asset missing from build");
-
- assert_eq!(state.summary_label(), "Offline geocoder unavailable");
- assert_eq!(
- state.user_message(),
- Some("Offline geocoder is not available in this build.")
- );
- assert_eq!(
- state.technical_message(),
- Some("The offline geocoder data file is missing from this app build.")
- );
- assert_eq!(
- state.debug_message(),
- Some("web geocoder asset missing from build")
- );
- }
-
- #[test]
- fn wasm_revision_validation_matches_stamped_sha256_contract() {
- assert!(is_valid_asset_revision(
- "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"
- ));
- assert!(!is_valid_asset_revision("abcd"));
- assert!(!is_valid_asset_revision(
- "not-a-valid-revision-because-it-is-not-hexadecimal-or-64-bytes-long"
- ));
- }
-
- #[test]
- fn initialization_failed_state_includes_revision_context() {
- let state = offline_geocoder_initialization_failed_state(
- "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c",
- "failed to open wasm geocoder bytes",
- );
- let diagnostic = state.diagnostic().expect("diagnostic");
-
- assert_eq!(diagnostic.platform_code, "web");
- assert_eq!(
- diagnostic.asset_revision.as_deref(),
- Some("6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c")
- );
- assert_eq!(diagnostic.code, "initialization_failed");
- assert_eq!(
- state.debug_message(),
- Some("failed to open wasm geocoder bytes")
- );
- }
-
- #[test]
- fn location_resolver_unavailable_code_is_stable() {
- assert_eq!(
- radroots_app_core::RadrootsLocationResolverError::Unavailable.code(),
- "unavailable"
- );
- }
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-pub fn launch() {}
diff --git a/crates/launchers/web/src/main.rs b/crates/launchers/web/src/main.rs
@@ -1,5 +0,0 @@
-#![forbid(unsafe_code)]
-
-fn main() {
- radroots_app_web::launch();
-}
diff --git a/crates/shared/core/Cargo.toml b/crates/shared/core/Cargo.toml
@@ -1,23 +0,0 @@
-[package]
-name = "radroots_app_core"
-authors.workspace = true
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots application core"
-publish = false
-
-[lints]
-workspace = true
-
-[dependencies]
-eframe.workspace = true
-egui.workspace = true
-radroots_runtime_paths.workspace = true
-zeroize.workspace = true
-
-[dev-dependencies]
-radroots_app_test_support = { path = "../test_support" }
diff --git a/crates/shared/core/src/account_roster.rs b/crates/shared/core/src/account_roster.rs
@@ -1,37 +0,0 @@
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAccountCustody {
- LocalManaged,
- BrowserSigner,
- RemoteSigner,
-}
-
-impl RadrootsAccountCustody {
- pub fn label(self) -> &'static str {
- match self {
- Self::LocalManaged => "local managed",
- Self::BrowserSigner => "browser signer",
- Self::RemoteSigner => "remote signer",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAccountSummary {
- pub account_id: String,
- pub npub: String,
- pub label: Option<String>,
- pub custody: RadrootsAccountCustody,
-}
-
-impl RadrootsAccountSummary {
- pub fn display_label(&self) -> String {
- match self.label.as_deref() {
- Some(label) if !label.trim().is_empty() => label.to_owned(),
- _ => match self.custody {
- RadrootsAccountCustody::LocalManaged => "local account".to_owned(),
- RadrootsAccountCustody::BrowserSigner => "browser signer".to_owned(),
- RadrootsAccountCustody::RemoteSigner => "remote signer".to_owned(),
- },
- }
- }
-}
diff --git a/crates/shared/core/src/home_location_tools/country_lookup.rs b/crates/shared/core/src/home_location_tools/country_lookup.rs
@@ -1,536 +0,0 @@
-use crate::{
- RadrootsAppBackend, RadrootsLocationCountry, RadrootsLocationCountryCenterLookupResult,
- RadrootsLocationCountryListResult, RadrootsLocationPoint, RadrootsOfflineGeocoderState,
-};
-use eframe::egui;
-
-#[derive(Debug, Clone, PartialEq)]
-enum CountryListState {
- Idle,
- Pending,
- Ready(Vec<RadrootsLocationCountry>),
- Failed { message: String },
-}
-
-impl Default for CountryListState {
- fn default() -> Self {
- Self::Idle
- }
-}
-
-#[derive(Debug, Clone, PartialEq)]
-struct CountryCenterLookupResult {
- country_id: String,
- country_name: Option<String>,
- center: RadrootsLocationPoint,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-enum CountryCenterState {
- Idle,
- Pending { country_id: String },
- Ready(CountryCenterLookupResult),
- Failed { message: String },
-}
-
-impl Default for CountryCenterState {
- fn default() -> Self {
- Self::Idle
- }
-}
-
-#[derive(Debug, Default, Clone, PartialEq)]
-pub(super) struct CountryLookupTools {
- countries: CountryListState,
- selected_country_id: Option<String>,
- center: CountryCenterState,
-}
-
-impl CountryLookupTools {
- pub(super) fn clear(&mut self) {
- self.countries = CountryListState::Idle;
- self.selected_country_id = None;
- self.center = CountryCenterState::Idle;
- }
-
- pub(super) fn render(
- &mut self,
- ui: &mut egui::Ui,
- backend: &dyn RadrootsAppBackend,
- offline_geocoder_state: Option<&RadrootsOfflineGeocoderState>,
- ) {
- ui.add_space(20.0);
- ui.label("Offline country lookup");
- ui.add_space(8.0);
- ui.label("Load country data and resolve a country center using the on-device geocoder.");
- ui.add_space(8.0);
-
- let load_enabled =
- is_country_action_enabled(offline_geocoder_state) && !self.is_list_pending();
- if ui
- .add_enabled(load_enabled, egui::Button::new(self.load_button_label()))
- .clicked()
- {
- self.begin_load_countries(backend);
- }
-
- if let Some(helper_message) = availability_message(offline_geocoder_state) {
- ui.add_space(8.0);
- ui.label(helper_message);
- }
-
- if let Some(message) = self.list_status_message() {
- ui.add_space(8.0);
- ui.label(message);
- }
-
- if let Some(countries) = self.ready_countries().cloned() {
- ui.add_space(8.0);
- let selected_country_id = &mut self.selected_country_id;
- let selected_text =
- country_label_for_id(countries.as_slice(), selected_country_id.as_deref());
- egui::ComboBox::from_label("Country")
- .selected_text(selected_text)
- .show_ui(ui, |ui| {
- for country in countries.as_slice() {
- let response = ui.selectable_value(
- selected_country_id,
- Some(country.country_id.clone()),
- country_label(country),
- );
- if response.clicked() {
- self.center = CountryCenterState::Idle;
- }
- }
- });
-
- ui.add_space(8.0);
- let center_enabled =
- is_country_action_enabled(offline_geocoder_state) && !self.is_center_pending();
- if ui
- .add_enabled(
- center_enabled,
- egui::Button::new(self.center_button_label()),
- )
- .clicked()
- {
- self.begin_resolve_country_center(backend);
- }
- }
-
- if let Some(message) = self.center_status_message() {
- ui.add_space(8.0);
- ui.label(message);
- }
-
- if let Some(result) = self.center_result() {
- ui.add_space(12.0);
- ui.label(
- result
- .country_name
- .as_deref()
- .unwrap_or(result.country_id.as_str()),
- );
- ui.monospace(format!(
- "{}, {}",
- format_coordinate(result.center.lat),
- format_coordinate(result.center.lng),
- ));
- }
- }
-
- pub(super) fn apply_list_result(&mut self, result: RadrootsLocationCountryListResult) {
- match result {
- Ok(countries) if countries.is_empty() => {
- self.countries = CountryListState::Failed {
- message: "No offline countries are available.".to_owned(),
- };
- self.selected_country_id = None;
- self.center = CountryCenterState::Idle;
- }
- Ok(countries) => {
- self.selected_country_id = selected_country_id_after_refresh(
- self.selected_country_id.as_deref(),
- countries.as_slice(),
- );
- self.countries = CountryListState::Ready(countries);
- self.center = CountryCenterState::Idle;
- }
- Err(error) => {
- self.countries = CountryListState::Failed {
- message: error.user_message().to_owned(),
- };
- }
- }
- }
-
- pub(super) fn apply_list_poll_error(&mut self, message: String) {
- self.countries = CountryListState::Failed { message };
- }
-
- pub(super) fn apply_center_result(
- &mut self,
- result: RadrootsLocationCountryCenterLookupResult,
- ) {
- let country_id = match &self.center {
- CountryCenterState::Pending { country_id } => country_id.clone(),
- CountryCenterState::Idle
- | CountryCenterState::Ready(_)
- | CountryCenterState::Failed { .. } => return,
- };
-
- match result {
- Ok(center) => {
- self.center = CountryCenterState::Ready(CountryCenterLookupResult {
- country_name: self.country_name_for_id(country_id.as_str()),
- country_id,
- center,
- });
- }
- Err(error) => {
- self.center = CountryCenterState::Failed {
- message: error.user_message().to_owned(),
- };
- }
- }
- }
-
- pub(super) fn apply_center_poll_error(&mut self, message: String) {
- self.center = CountryCenterState::Failed { message };
- }
-
- pub(super) fn is_pending(&self) -> bool {
- self.is_list_pending() || self.is_center_pending()
- }
-
- fn begin_load_countries(&mut self, backend: &dyn RadrootsAppBackend) {
- self.countries = CountryListState::Idle;
- self.center = CountryCenterState::Idle;
-
- match backend.request_location_country_list() {
- Ok(()) => {
- self.countries = CountryListState::Pending;
- }
- Err(error) => {
- self.countries = CountryListState::Failed {
- message: error.user_message().to_owned(),
- };
- }
- }
- }
-
- fn begin_resolve_country_center(&mut self, backend: &dyn RadrootsAppBackend) {
- let Some(country_id) = self.selected_country_id.clone() else {
- self.center = CountryCenterState::Failed {
- message: "Select a country first.".to_owned(),
- };
- return;
- };
-
- match backend.request_location_country_center_lookup(country_id.as_str()) {
- Ok(()) => {
- self.center = CountryCenterState::Pending { country_id };
- }
- Err(error) => {
- self.center = CountryCenterState::Failed {
- message: error.user_message().to_owned(),
- };
- }
- }
- }
-
- fn is_list_pending(&self) -> bool {
- matches!(self.countries, CountryListState::Pending)
- }
-
- fn is_center_pending(&self) -> bool {
- matches!(self.center, CountryCenterState::Pending { .. })
- }
-
- fn load_button_label(&self) -> &'static str {
- if self.is_list_pending() {
- "Loading Offline Countries..."
- } else {
- "Load Offline Countries"
- }
- }
-
- fn center_button_label(&self) -> &'static str {
- if self.is_center_pending() {
- "Resolving Country Center..."
- } else {
- "Resolve Country Center"
- }
- }
-
- fn list_status_message(&self) -> Option<&str> {
- match &self.countries {
- CountryListState::Idle | CountryListState::Ready(_) => None,
- CountryListState::Pending => Some("Loading offline countries..."),
- CountryListState::Failed { message } => Some(message.as_str()),
- }
- }
-
- fn center_status_message(&self) -> Option<&str> {
- match &self.center {
- CountryCenterState::Idle | CountryCenterState::Ready(_) => None,
- CountryCenterState::Pending { .. } => Some("Resolving country center..."),
- CountryCenterState::Failed { message } => Some(message.as_str()),
- }
- }
-
- fn ready_countries(&self) -> Option<&Vec<RadrootsLocationCountry>> {
- match &self.countries {
- CountryListState::Ready(countries) => Some(countries),
- CountryListState::Idle
- | CountryListState::Pending
- | CountryListState::Failed { .. } => None,
- }
- }
-
- fn center_result(&self) -> Option<&CountryCenterLookupResult> {
- match &self.center {
- CountryCenterState::Ready(result) => Some(result),
- CountryCenterState::Idle
- | CountryCenterState::Pending { .. }
- | CountryCenterState::Failed { .. } => None,
- }
- }
-
- fn country_name_for_id(&self, country_id: &str) -> Option<String> {
- self.ready_countries()
- .and_then(|countries| {
- countries
- .iter()
- .find(|country| country.country_id == country_id)
- .map(|country| country.country_name.clone())
- })
- .flatten()
- }
-}
-
-fn is_country_action_enabled(state: Option<&RadrootsOfflineGeocoderState>) -> bool {
- matches!(state, Some(RadrootsOfflineGeocoderState::Ready))
-}
-
-fn availability_message(state: Option<&RadrootsOfflineGeocoderState>) -> Option<&str> {
- match state {
- Some(RadrootsOfflineGeocoderState::Initializing) => {
- Some("Offline country lookup is still initializing on this device.")
- }
- Some(RadrootsOfflineGeocoderState::Unavailable { .. }) => {
- state.and_then(RadrootsOfflineGeocoderState::user_message)
- }
- Some(RadrootsOfflineGeocoderState::Ready) | None => None,
- }
-}
-
-fn selected_country_id_after_refresh(
- selected_country_id: Option<&str>,
- countries: &[RadrootsLocationCountry],
-) -> Option<String> {
- if let Some(selected_country_id) = selected_country_id {
- if countries
- .iter()
- .any(|country| country.country_id == selected_country_id)
- {
- return Some(selected_country_id.to_owned());
- }
- }
-
- countries.first().map(|country| country.country_id.clone())
-}
-
-fn country_label(country: &RadrootsLocationCountry) -> String {
- country
- .country_name
- .clone()
- .unwrap_or_else(|| country.country_id.clone())
-}
-
-fn country_label_for_id(countries: &[RadrootsLocationCountry], country_id: Option<&str>) -> String {
- country_id
- .and_then(|country_id| {
- countries
- .iter()
- .find(|country| country.country_id == country_id)
- .map(country_label)
- })
- .unwrap_or_else(|| "Select a country".to_owned())
-}
-
-fn format_coordinate(value: f64) -> String {
- format!("{value:.4}")
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- IdentityGateState, RadrootsLocationResolverError, RadrootsReverseLocationLookupResult,
- SetupActionState,
- };
- use std::cell::RefCell;
- use std::collections::VecDeque;
- use std::rc::Rc;
-
- #[derive(Clone)]
- struct CountryBackend {
- list_request: Rc<RefCell<VecDeque<Result<(), RadrootsLocationResolverError>>>>,
- center_request: Rc<RefCell<VecDeque<Result<(), RadrootsLocationResolverError>>>>,
- requested_country_ids: Rc<RefCell<Vec<String>>>,
- }
-
- impl RadrootsAppBackend for CountryBackend {
- fn load_identity_state(&self) -> Result<IdentityGateState, String> {
- Ok(IdentityGateState::Missing)
- }
-
- fn setup_action_state(&self) -> SetupActionState {
- SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: true,
- pending: false,
- }
- }
-
- fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- Ok(None)
- }
-
- fn request_reverse_location_lookup(
- &self,
- _point: RadrootsLocationPoint,
- _options: Option<crate::RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- fn poll_reverse_location_lookup_result(
- &self,
- ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> {
- Ok(None)
- }
-
- fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> {
- self.list_request.borrow_mut().pop_front().unwrap_or(Ok(()))
- }
-
- fn request_location_country_center_lookup(
- &self,
- country_id: &str,
- ) -> Result<(), RadrootsLocationResolverError> {
- self.requested_country_ids
- .borrow_mut()
- .push(country_id.to_owned());
- self.center_request
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(()))
- }
- }
-
- fn country_backend(
- list_request: Vec<Result<(), RadrootsLocationResolverError>>,
- center_request: Vec<Result<(), RadrootsLocationResolverError>>,
- ) -> (CountryBackend, Rc<RefCell<Vec<String>>>) {
- let requested_country_ids = Rc::new(RefCell::new(Vec::new()));
- (
- CountryBackend {
- list_request: Rc::new(RefCell::new(list_request.into())),
- center_request: Rc::new(RefCell::new(center_request.into())),
- requested_country_ids: requested_country_ids.clone(),
- },
- requested_country_ids,
- )
- }
-
- #[test]
- fn begin_load_countries_enters_pending_state() {
- let (backend, _) = country_backend(vec![Ok(())], Vec::new());
- let mut tools = CountryLookupTools::default();
-
- tools.begin_load_countries(&backend);
-
- assert_eq!(
- tools.list_status_message(),
- Some("Loading offline countries...")
- );
- assert!(tools.is_pending());
- }
-
- #[test]
- fn apply_list_result_selects_first_country() {
- let mut tools = CountryLookupTools::default();
-
- tools.apply_list_result(Ok(vec![
- sample_country("BR", Some("Brazil"), -14.235, -51.9253),
- sample_country("KE", Some("Kenya"), 0.0236, 37.9062),
- ]));
-
- assert_eq!(tools.selected_country_id.as_deref(), Some("BR"));
- assert!(matches!(tools.ready_countries(), Some(countries) if countries.len() == 2));
- }
-
- #[test]
- fn begin_resolve_country_center_uses_selected_country_id() {
- let (backend, requested_country_ids) = country_backend(Vec::new(), vec![Ok(())]);
- let mut tools = CountryLookupTools::default();
- tools.apply_list_result(Ok(vec![
- sample_country("BR", Some("Brazil"), -14.235, -51.9253),
- sample_country("KE", Some("Kenya"), 0.0236, 37.9062),
- ]));
- tools.selected_country_id = Some("KE".to_owned());
-
- tools.begin_resolve_country_center(&backend);
-
- assert_eq!(requested_country_ids.borrow().as_slice(), ["KE"]);
- assert_eq!(
- tools.center_status_message(),
- Some("Resolving country center...")
- );
- }
-
- #[test]
- fn apply_center_result_records_country_center() {
- let mut tools = CountryLookupTools::default();
- tools.apply_list_result(Ok(vec![sample_country(
- "BR",
- Some("Brazil"),
- -14.235,
- -51.9253,
- )]));
- tools.center = CountryCenterState::Pending {
- country_id: "BR".to_owned(),
- };
-
- tools.apply_center_result(Ok(RadrootsLocationPoint {
- lat: -14.235,
- lng: -51.9253,
- }));
-
- let result = tools.center_result().expect("country center result");
- assert_eq!(result.country_id, "BR");
- assert_eq!(result.country_name.as_deref(), Some("Brazil"));
- assert_eq!(
- result.center,
- RadrootsLocationPoint {
- lat: -14.235,
- lng: -51.9253,
- }
- );
- }
-
- fn sample_country(
- country_id: &str,
- country_name: Option<&str>,
- lat: f64,
- lng: f64,
- ) -> RadrootsLocationCountry {
- RadrootsLocationCountry {
- country_id: country_id.to_owned(),
- country_name: country_name.map(str::to_owned),
- center: RadrootsLocationPoint { lat, lng },
- }
- }
-}
diff --git a/crates/shared/core/src/home_location_tools/mod.rs b/crates/shared/core/src/home_location_tools/mod.rs
@@ -1,101 +0,0 @@
-use crate::{
- RadrootsAppBackend, RadrootsLocationCountryCenterLookupResult,
- RadrootsLocationCountryListResult, RadrootsOfflineGeocoderState,
- RadrootsReverseLocationLookupResult,
-};
-use eframe::egui;
-
-mod country_lookup;
-mod reverse_lookup;
-
-use country_lookup::CountryLookupTools;
-#[cfg(test)]
-use reverse_lookup::HomeLocationLookupResult;
-use reverse_lookup::ReverseLookupTools;
-
-#[derive(Debug, Default, Clone, PartialEq)]
-pub(crate) struct HomeLocationTools {
- country_lookup: CountryLookupTools,
- reverse_lookup: ReverseLookupTools,
-}
-
-impl HomeLocationTools {
- pub(crate) fn new() -> Self {
- Self::default()
- }
-
- pub(crate) fn clear(&mut self) {
- self.country_lookup.clear();
- self.reverse_lookup.clear();
- }
-
- #[cfg(test)]
- pub(crate) fn set_query_inputs(
- &mut self,
- latitude: impl Into<String>,
- longitude: impl Into<String>,
- ) {
- self.reverse_lookup.set_query_inputs(latitude, longitude);
- }
-
- pub(crate) fn render(
- &mut self,
- ui: &mut egui::Ui,
- backend: &dyn RadrootsAppBackend,
- offline_geocoder_state: Option<&RadrootsOfflineGeocoderState>,
- ) {
- self.reverse_lookup
- .render(ui, backend, offline_geocoder_state);
- self.country_lookup
- .render(ui, backend, offline_geocoder_state);
- }
-
- pub(crate) fn apply_reverse_lookup_result(
- &mut self,
- result: RadrootsReverseLocationLookupResult,
- ) {
- self.reverse_lookup.apply_result(result);
- }
-
- pub(crate) fn apply_reverse_lookup_poll_error(&mut self, message: String) {
- self.reverse_lookup.apply_poll_error(message);
- }
-
- pub(crate) fn apply_country_list_result(&mut self, result: RadrootsLocationCountryListResult) {
- self.country_lookup.apply_list_result(result);
- }
-
- pub(crate) fn apply_country_list_poll_error(&mut self, message: String) {
- self.country_lookup.apply_list_poll_error(message);
- }
-
- pub(crate) fn apply_country_center_result(
- &mut self,
- result: RadrootsLocationCountryCenterLookupResult,
- ) {
- self.country_lookup.apply_center_result(result);
- }
-
- pub(crate) fn apply_country_center_poll_error(&mut self, message: String) {
- self.country_lookup.apply_center_poll_error(message);
- }
-
- #[cfg(test)]
- pub(crate) fn begin_resolve_with_backend(&mut self, backend: &dyn RadrootsAppBackend) {
- self.reverse_lookup.begin_resolve_with_backend(backend);
- }
-
- pub(crate) fn is_pending(&self) -> bool {
- self.reverse_lookup.is_pending() || self.country_lookup.is_pending()
- }
-
- #[cfg(test)]
- pub(crate) fn status_message(&self) -> Option<&str> {
- self.reverse_lookup.status_message()
- }
-
- #[cfg(test)]
- pub(crate) fn lookup_result(&self) -> Option<&HomeLocationLookupResult> {
- self.reverse_lookup.lookup_result()
- }
-}
diff --git a/crates/shared/core/src/home_location_tools/reverse_lookup.rs b/crates/shared/core/src/home_location_tools/reverse_lookup.rs
@@ -1,440 +0,0 @@
-use crate::{
- RadrootsAppBackend, RadrootsLocationPoint, RadrootsLocationReverseOptions,
- RadrootsOfflineGeocoderState, RadrootsResolvedLocation, RadrootsReverseLocationLookupResult,
-};
-use eframe::egui;
-
-const HOME_LOOKUP_RESULT_LIMIT: usize = 3;
-
-#[derive(Debug, Clone, PartialEq)]
-pub(crate) struct HomeLocationLookupResult {
- pub queried_point: RadrootsLocationPoint,
- pub matches: Vec<RadrootsResolvedLocation>,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-enum HomeLocationLookupState {
- Idle,
- Pending {
- queried_point: RadrootsLocationPoint,
- },
- Ready(HomeLocationLookupResult),
- Failed {
- message: String,
- },
-}
-
-impl Default for HomeLocationLookupState {
- fn default() -> Self {
- Self::Idle
- }
-}
-
-#[derive(Debug, Default, Clone, PartialEq)]
-pub(super) struct ReverseLookupTools {
- latitude_input: String,
- longitude_input: String,
- lookup_state: HomeLocationLookupState,
-}
-
-impl ReverseLookupTools {
- pub(super) fn clear(&mut self) {
- self.latitude_input.clear();
- self.longitude_input.clear();
- self.lookup_state = HomeLocationLookupState::Idle;
- }
-
- #[cfg(test)]
- pub(super) fn set_query_inputs(
- &mut self,
- latitude: impl Into<String>,
- longitude: impl Into<String>,
- ) {
- self.latitude_input = latitude.into();
- self.longitude_input = longitude.into();
- }
-
- pub(super) fn render(
- &mut self,
- ui: &mut egui::Ui,
- backend: &dyn RadrootsAppBackend,
- offline_geocoder_state: Option<&RadrootsOfflineGeocoderState>,
- ) {
- ui.add_space(20.0);
- ui.label("Offline location lookup");
- ui.add_space(8.0);
- ui.label("Resolve a latitude and longitude pair using the on-device geocoder.");
- ui.add_space(8.0);
-
- ui.horizontal(|ui| {
- ui.label("Latitude");
- ui.add(
- egui::TextEdit::singleline(&mut self.latitude_input)
- .hint_text("12.34")
- .desired_width(140.0),
- );
- ui.add_space(8.0);
- ui.label("Longitude");
- ui.add(
- egui::TextEdit::singleline(&mut self.longitude_input)
- .hint_text("-56.78")
- .desired_width(140.0),
- );
- });
- ui.add_space(8.0);
-
- let resolve_enabled = is_resolve_enabled(offline_geocoder_state) && !self.is_pending();
- if ui
- .add_enabled(
- resolve_enabled,
- egui::Button::new(self.resolve_button_label()),
- )
- .clicked()
- {
- self.begin_resolve_with_backend(backend);
- }
-
- if let Some(helper_message) = availability_message(offline_geocoder_state) {
- ui.add_space(8.0);
- ui.label(helper_message);
- }
-
- if let Some(message) = self.status_message() {
- ui.add_space(8.0);
- ui.label(message);
- }
-
- if let Some(result) = self.lookup_result() {
- ui.add_space(12.0);
- ui.label(format!(
- "Query: {}, {}",
- format_coordinate(result.queried_point.lat),
- format_coordinate(result.queried_point.lng),
- ));
- for resolved in result.matches.iter().take(HOME_LOOKUP_RESULT_LIMIT) {
- ui.add_space(8.0);
- ui.label(resolved.name.as_str());
- if let Some(admin1_name) = &resolved.admin1_name {
- ui.label(admin1_name.as_str());
- }
- if let Some(country_name) = &resolved.country_name {
- ui.label(country_name.as_str());
- } else {
- ui.label(resolved.country_id.as_str());
- }
- ui.monospace(format!(
- "{}, {}",
- format_coordinate(resolved.point.lat),
- format_coordinate(resolved.point.lng),
- ));
- }
- }
- }
-
- pub(super) fn begin_resolve_with_backend(&mut self, backend: &dyn RadrootsAppBackend) {
- self.lookup_state = HomeLocationLookupState::Idle;
-
- let query_point = match self.parse_query_point() {
- Ok(point) => point,
- Err(message) => {
- self.lookup_state = HomeLocationLookupState::Failed { message };
- return;
- }
- };
-
- let options = RadrootsLocationReverseOptions {
- limit: HOME_LOOKUP_RESULT_LIMIT,
- ..RadrootsLocationReverseOptions::default()
- };
- match backend.request_reverse_location_lookup(query_point, Some(options)) {
- Ok(()) => {
- self.lookup_state = HomeLocationLookupState::Pending {
- queried_point: query_point,
- };
- }
- Err(error) => {
- self.lookup_state = HomeLocationLookupState::Failed {
- message: error.user_message().to_owned(),
- };
- }
- }
- }
-
- pub(super) fn apply_result(&mut self, result: RadrootsReverseLocationLookupResult) {
- let queried_point = match self.lookup_state {
- HomeLocationLookupState::Pending { queried_point } => queried_point,
- HomeLocationLookupState::Idle
- | HomeLocationLookupState::Ready(_)
- | HomeLocationLookupState::Failed { .. } => return,
- };
-
- match result {
- Ok(matches) if matches.is_empty() => {
- self.lookup_state = HomeLocationLookupState::Failed {
- message: "No offline location matched that coordinate.".to_owned(),
- };
- }
- Ok(matches) => {
- self.lookup_state = HomeLocationLookupState::Ready(HomeLocationLookupResult {
- queried_point,
- matches,
- });
- }
- Err(error) => {
- self.lookup_state = HomeLocationLookupState::Failed {
- message: error.user_message().to_owned(),
- };
- }
- }
- }
-
- pub(super) fn apply_poll_error(&mut self, message: String) {
- self.lookup_state = HomeLocationLookupState::Failed { message };
- }
-
- pub(super) fn is_pending(&self) -> bool {
- matches!(self.lookup_state, HomeLocationLookupState::Pending { .. })
- }
-
- fn parse_query_point(&self) -> Result<RadrootsLocationPoint, String> {
- let lat = parse_coordinate(self.latitude_input.as_str(), "latitude", -90.0, 90.0)?;
- let lng = parse_coordinate(self.longitude_input.as_str(), "longitude", -180.0, 180.0)?;
- Ok(RadrootsLocationPoint { lat, lng })
- }
-
- fn resolve_button_label(&self) -> &'static str {
- if self.is_pending() {
- "Resolving Offline Location..."
- } else {
- "Resolve Offline Location"
- }
- }
-
- pub(super) fn status_message(&self) -> Option<&str> {
- match &self.lookup_state {
- HomeLocationLookupState::Idle | HomeLocationLookupState::Ready(_) => None,
- HomeLocationLookupState::Pending { .. } => Some("Resolving offline location..."),
- HomeLocationLookupState::Failed { message } => Some(message.as_str()),
- }
- }
-
- pub(super) fn lookup_result(&self) -> Option<&HomeLocationLookupResult> {
- match &self.lookup_state {
- HomeLocationLookupState::Ready(result) => Some(result),
- HomeLocationLookupState::Idle
- | HomeLocationLookupState::Pending { .. }
- | HomeLocationLookupState::Failed { .. } => None,
- }
- }
-}
-
-fn is_resolve_enabled(state: Option<&RadrootsOfflineGeocoderState>) -> bool {
- matches!(state, Some(RadrootsOfflineGeocoderState::Ready))
-}
-
-fn availability_message(state: Option<&RadrootsOfflineGeocoderState>) -> Option<&str> {
- match state {
- Some(RadrootsOfflineGeocoderState::Initializing) => {
- Some("Offline location resolution is still initializing on this device.")
- }
- Some(RadrootsOfflineGeocoderState::Unavailable { .. }) => {
- state.and_then(RadrootsOfflineGeocoderState::user_message)
- }
- Some(RadrootsOfflineGeocoderState::Ready) | None => None,
- }
-}
-
-fn parse_coordinate(raw: &str, label: &str, min: f64, max: f64) -> Result<f64, String> {
- let trimmed = raw.trim();
- if trimmed.is_empty() {
- return Err(format!("{label} is required"));
- }
-
- let value = trimmed
- .parse::<f64>()
- .map_err(|_| format!("{label} must be a valid number"))?;
- if !value.is_finite() {
- return Err(format!("{label} must be a finite number"));
- }
- if value < min || value > max {
- return Err(format!("{label} must be between {min} and {max}"));
- }
-
- Ok(value)
-}
-
-fn format_coordinate(value: f64) -> String {
- format!("{value:.4}")
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- IdentityGateState, RadrootsLocationCountry, RadrootsLocationResolverError, SetupActionState,
- };
- use std::cell::RefCell;
- use std::rc::Rc;
-
- #[derive(Clone)]
- struct ResolveBackend {
- start_response: Result<(), RadrootsLocationResolverError>,
- requested: Rc<
- RefCell<
- Vec<(
- RadrootsLocationPoint,
- Option<RadrootsLocationReverseOptions>,
- )>,
- >,
- >,
- }
-
- impl RadrootsAppBackend for ResolveBackend {
- fn load_identity_state(&self) -> Result<IdentityGateState, String> {
- Ok(IdentityGateState::Missing)
- }
-
- fn setup_action_state(&self) -> SetupActionState {
- SetupActionState {
- label: "Generate New Key".to_owned(),
- enabled: true,
- pending: false,
- }
- }
-
- fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- Ok(None)
- }
-
- fn request_reverse_location_lookup(
- &self,
- point: RadrootsLocationPoint,
- options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- self.requested.borrow_mut().push((point, options));
- self.start_response.clone()
- }
-
- fn list_location_countries(
- &self,
- ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-
- fn location_country_center(
- &self,
- _country_id: &str,
- ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- }
-
- fn resolve_backend(
- start_response: Result<(), RadrootsLocationResolverError>,
- ) -> (
- ResolveBackend,
- Rc<
- RefCell<
- Vec<(
- RadrootsLocationPoint,
- Option<RadrootsLocationReverseOptions>,
- )>,
- >,
- >,
- ) {
- let requested = Rc::new(RefCell::new(Vec::new()));
- (
- ResolveBackend {
- start_response,
- requested: requested.clone(),
- },
- requested,
- )
- }
-
- #[test]
- fn begin_resolve_requests_three_results() {
- let (backend, requested) = resolve_backend(Ok(()));
- let mut tools = ReverseLookupTools::default();
- tools.set_query_inputs("12.5", "-42.25");
-
- tools.begin_resolve_with_backend(&backend);
-
- let requested = requested.borrow();
- assert_eq!(requested.len(), 1);
- assert_eq!(
- requested[0].0,
- RadrootsLocationPoint {
- lat: 12.5,
- lng: -42.25,
- }
- );
- assert_eq!(
- requested[0].1,
- Some(RadrootsLocationReverseOptions {
- limit: 3,
- ..RadrootsLocationReverseOptions::default()
- })
- );
- assert!(tools.is_pending());
- }
-
- #[test]
- fn begin_resolve_rejects_out_of_range_coordinates() {
- let (backend, requested) = resolve_backend(Ok(()));
- let mut tools = ReverseLookupTools::default();
- tools.set_query_inputs("200", "10");
-
- tools.begin_resolve_with_backend(&backend);
-
- assert!(requested.borrow().is_empty());
- assert_eq!(
- tools.status_message(),
- Some("latitude must be between -90 and 90")
- );
- assert!(!tools.is_pending());
- }
-
- #[test]
- fn apply_result_keeps_up_to_three_matches_available() {
- let mut tools = ReverseLookupTools::default();
- tools.lookup_state = HomeLocationLookupState::Pending {
- queried_point: RadrootsLocationPoint {
- lat: 1.25,
- lng: -2.5,
- },
- };
-
- tools.apply_result(Ok(vec![
- sample_result(1, "one"),
- sample_result(2, "two"),
- sample_result(3, "three"),
- ]));
-
- let result = tools.lookup_result().expect("lookup result");
- assert_eq!(result.matches.len(), 3);
- assert_eq!(result.matches[0].name, "one");
- assert_eq!(result.matches[2].name, "three");
- }
-
- #[test]
- fn apply_poll_error_sets_failed_status() {
- let mut tools = ReverseLookupTools::default();
-
- tools.apply_poll_error("background worker failed".to_owned());
-
- assert_eq!(tools.status_message(), Some("background worker failed"));
- }
-
- fn sample_result(id: i64, name: &str) -> RadrootsResolvedLocation {
- RadrootsResolvedLocation {
- id,
- name: name.to_owned(),
- admin1_id: None,
- admin1_name: Some("state".to_owned()),
- country_id: "US".to_owned(),
- country_name: Some("United States".to_owned()),
- point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 },
- }
- }
-}
diff --git a/crates/shared/core/src/lib.rs b/crates/shared/core/src/lib.rs
@@ -1,2742 +0,0 @@
-#![forbid(unsafe_code)]
-
-use eframe::egui;
-use std::time::{Duration, Instant};
-use zeroize::Zeroizing;
-
-mod account_roster;
-mod home_location_tools;
-mod location_resolver;
-mod offline_geocoder;
-mod remote_signer;
-mod secret_keys;
-mod storage_paths;
-
-pub const APP_NAME: &str = "Rad Roots";
-
-pub use account_roster::{RadrootsAccountCustody, RadrootsAccountSummary};
-pub use location_resolver::{
- RadrootsLocationCountry, RadrootsLocationCountryCenterLookupResult,
- RadrootsLocationCountryListResult, RadrootsLocationPoint, RadrootsLocationResolverError,
- RadrootsLocationReverseOptions, RadrootsResolvedLocation, RadrootsReverseLocationLookupResult,
-};
-pub use offline_geocoder::{
- RadrootsOfflineGeocoderDiagnostic, RadrootsOfflineGeocoderPlatform,
- RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind,
-};
-pub use remote_signer::{
- RadrootsPendingRemoteSignerConnection, RadrootsRemoteSignerPreview,
- RadrootsRemoteSignerSignedNote,
-};
-pub use secret_keys::{RadrootsSecretImportMode, RadrootsSecretImportRequest};
-pub use storage_paths::{
- RadrootsAppStorageLayout, interactive_user_app_storage_layout_with_resolver,
- mobile_native_app_storage_layout,
-};
-
-use home_location_tools::HomeLocationTools;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct SetupActionState {
- pub label: String,
- pub enabled: bool,
- pub pending: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ImportActionState {
- pub label: String,
- pub enabled: bool,
- pub pending: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct PasteActionState {
- pub label: String,
- pub enabled: bool,
- pub pending: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct HomeActionState {
- pub kind: HomeActionKind,
- pub label: String,
- pub enabled: bool,
- pub pending: bool,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum HomeActionKind {
- BackupSecretKey,
- RevealRawSecretKey,
- RemoveLocalKey,
- ResetDevice,
- DisconnectSigner,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum HomeActionResult {
- None,
- IdentityState(IdentityGateState),
- RevealEncryptedSecretKey { ncryptsec: String },
- RevealRawSecretKey { nsec: String },
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum IdentityGateState {
- Missing,
- Ready { account_id: String },
- Unsupported { reason: String },
-}
-
-pub trait RadrootsAppBackend {
- fn load_identity_state(&self) -> Result<IdentityGateState, String>;
- fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> {
- Ok(Vec::new())
- }
- fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> {
- None
- }
- fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> {
- Ok(None)
- }
- fn setup_action_state(&self) -> SetupActionState;
- fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String>;
- fn home_setup_action_state(&self) -> Option<SetupActionState> {
- None
- }
- fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- Ok(None)
- }
- fn import_action_state(&self) -> Option<ImportActionState> {
- None
- }
- fn request_import_action(
- &self,
- _request: &RadrootsSecretImportRequest,
- ) -> Result<Option<IdentityGateState>, String> {
- Ok(None)
- }
- fn import_paste_action_state(&self) -> Option<PasteActionState> {
- None
- }
- fn request_import_paste_action(&self) -> Result<Option<String>, String> {
- Ok(None)
- }
- fn remote_signer_action_state(&self) -> Option<SetupActionState> {
- None
- }
- fn preview_remote_signer_connection(
- &self,
- _input: &str,
- ) -> Result<RadrootsRemoteSignerPreview, String> {
- Err("remote signer onboarding is not available in this build".to_owned())
- }
- fn request_remote_signer_connection(
- &self,
- _input: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- Ok(None)
- }
- fn pending_remote_signer_connection(
- &self,
- ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> {
- Ok(None)
- }
- fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> {
- Ok(())
- }
- fn remote_signer_note_action_state(&self) -> Option<SetupActionState> {
- None
- }
- fn selected_remote_signer_approved_permissions(&self) -> Option<Vec<String>> {
- None
- }
- fn request_remote_signer_note_action(&self, _content: &str) -> Result<(), String> {
- Ok(())
- }
- fn poll_remote_signer_note_action_result(
- &self,
- ) -> Result<Option<RadrootsRemoteSignerSignedNote>, String> {
- Ok(None)
- }
- fn home_action_states(&self) -> Vec<HomeActionState> {
- Vec::new()
- }
- fn request_home_action(&self, _action: HomeActionKind) -> Result<HomeActionResult, String> {
- Ok(HomeActionResult::None)
- }
- fn request_secret_key_backup_action(
- &self,
- _password: &str,
- ) -> Result<HomeActionResult, String> {
- Ok(HomeActionResult::None)
- }
- fn poll_home_action_result(&self) -> Result<Option<HomeActionResult>, String> {
- Ok(None)
- }
- fn request_select_account(
- &self,
- _account_id: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- Ok(None)
- }
- fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> {
- Ok(None)
- }
- fn reverse_location(
- &self,
- _point: RadrootsLocationPoint,
- _options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- fn request_reverse_location_lookup(
- &self,
- _point: RadrootsLocationPoint,
- _options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- fn poll_reverse_location_lookup_result(
- &self,
- ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> {
- Ok(None)
- }
- fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- fn poll_location_country_list_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryListResult>, String> {
- Ok(None)
- }
- fn request_location_country_center_lookup(
- &self,
- _country_id: &str,
- ) -> Result<(), RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- fn poll_location_country_center_lookup_result(
- &self,
- ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> {
- Ok(None)
- }
- fn list_location_countries(
- &self,
- ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
- fn location_country_center(
- &self,
- _country_id: &str,
- ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
- Err(RadrootsLocationResolverError::Unsupported)
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-enum AppScreen {
- Setup,
- Home { account_id: String },
-}
-
-const RAW_SECRET_REVEAL_TIMEOUT: Duration = Duration::from_secs(30);
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-enum RevealedSecretMaterial {
- EncryptedSecretKey(Zeroizing<String>),
- RawSecretKey {
- nsec: Zeroizing<String>,
- revealed_at: Instant,
- },
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-enum RemoteSignerEntryState {
- Closed,
- Editing,
- Review(RadrootsRemoteSignerPreview),
- WaitingApproval(RadrootsPendingRemoteSignerConnection),
-}
-
-impl RevealedSecretMaterial {
- fn label(&self) -> &'static str {
- match self {
- Self::EncryptedSecretKey(_) => "Encrypted Secret Key",
- Self::RawSecretKey { .. } => "Raw Secret Key",
- }
- }
-
- fn value(&self) -> &str {
- match self {
- Self::EncryptedSecretKey(ncryptsec) => ncryptsec.as_str(),
- Self::RawSecretKey { nsec, .. } => nsec.as_str(),
- }
- }
-
- fn dismiss_label(&self) -> &'static str {
- match self {
- Self::EncryptedSecretKey(_) => "Dismiss Encrypted Secret Key",
- Self::RawSecretKey { .. } => "Dismiss Raw Secret Key",
- }
- }
-
- fn is_raw(&self) -> bool {
- matches!(self, Self::RawSecretKey { .. })
- }
-
- fn raw_secret_expired(&self) -> bool {
- match self {
- Self::RawSecretKey { revealed_at, .. } => {
- revealed_at.elapsed() >= RAW_SECRET_REVEAL_TIMEOUT
- }
- Self::EncryptedSecretKey(_) => false,
- }
- }
-}
-
-pub struct RadrootsApp {
- backend: Box<dyn RadrootsAppBackend>,
- screen: AppScreen,
- account_roster: Vec<RadrootsAccountSummary>,
- offline_geocoder_state: Option<RadrootsOfflineGeocoderState>,
- status_message: Option<String>,
- home_location_tools: HomeLocationTools,
- pending_home_confirmation: Option<HomeActionKind>,
- pending_import_mode: Option<RadrootsSecretImportMode>,
- remote_signer_entry_state: RemoteSignerEntryState,
- remote_signer_input: Zeroizing<String>,
- secret_key_input: Zeroizing<String>,
- import_password_input: Zeroizing<String>,
- pending_secret_key_backup_entry: bool,
- secret_key_backup_password_input: Zeroizing<String>,
- secret_key_backup_password_confirm_input: Zeroizing<String>,
- remote_signer_note_input: Zeroizing<String>,
- revealed_secret_material: Option<RevealedSecretMaterial>,
-}
-
-impl RadrootsApp {
- fn clear_secret_import_entry(&mut self) {
- self.pending_import_mode = None;
- self.secret_key_input.clear();
- self.import_password_input.clear();
- }
-
- fn clear_secret_key_backup_entry(&mut self) {
- self.pending_secret_key_backup_entry = false;
- self.secret_key_backup_password_input.clear();
- self.secret_key_backup_password_confirm_input.clear();
- }
-
- fn clear_revealed_secret_material(&mut self) {
- self.revealed_secret_material = None;
- }
-
- fn clear_remote_signer_entry(&mut self) {
- self.remote_signer_entry_state = RemoteSignerEntryState::Closed;
- self.remote_signer_input.clear();
- }
-
- fn clear_secret_key_ui_state(&mut self) {
- self.clear_remote_signer_entry();
- self.clear_secret_import_entry();
- self.clear_secret_key_backup_entry();
- self.clear_revealed_secret_material();
- }
-
- fn open_import_entry(&mut self) {
- self.pending_import_mode = Some(RadrootsSecretImportMode::EncryptedSecretKey);
- self.secret_key_input.clear();
- self.import_password_input.clear();
- self.status_message = None;
- }
-
- fn import_mode(&self) -> RadrootsSecretImportMode {
- self.pending_import_mode.unwrap_or_default()
- }
-
- fn set_import_mode(&mut self, mode: RadrootsSecretImportMode) {
- self.pending_import_mode = Some(mode);
- self.secret_key_input.clear();
- self.import_password_input.clear();
- self.status_message = None;
- }
-
- fn secret_import_request(&self) -> Result<RadrootsSecretImportRequest, String> {
- let mode = self.import_mode();
- let secret_text = self.secret_key_input.trim().to_owned();
- if secret_text.is_empty() {
- return Err(match mode {
- RadrootsSecretImportMode::EncryptedSecretKey => {
- "enter an encrypted secret key to continue".to_owned()
- }
- RadrootsSecretImportMode::RawSecretKey => {
- "enter a raw secret key to continue".to_owned()
- }
- });
- }
-
- let password = if mode.requires_password() {
- if self.import_password_input.is_empty() {
- return Err("enter a password to import the encrypted secret key".to_owned());
- }
- Some(self.import_password_input.to_string())
- } else {
- None
- };
-
- Ok(RadrootsSecretImportRequest {
- mode,
- secret_text,
- password,
- })
- }
-
- fn request_secret_key_backup_action(&mut self) {
- self.status_message = None;
- self.clear_revealed_secret_material();
-
- if self.secret_key_backup_password_input.is_empty() {
- self.status_message =
- Some("enter a password to create an encrypted secret key backup".to_owned());
- return;
- }
-
- if self.secret_key_backup_password_input != self.secret_key_backup_password_confirm_input {
- self.status_message = Some("backup passwords do not match".to_owned());
- return;
- }
-
- match self
- .backend
- .request_secret_key_backup_action(self.secret_key_backup_password_input.as_str())
- {
- Ok(result) => {
- self.clear_secret_key_backup_entry();
- self.apply_home_action_result(result);
- }
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn sync_revealed_secret_material_lifetime(&mut self) {
- if self
- .revealed_secret_material
- .as_ref()
- .is_some_and(RevealedSecretMaterial::raw_secret_expired)
- {
- self.clear_revealed_secret_material();
- }
- }
-
- fn clear_raw_secret_when_app_unfocused(&mut self, ctx: &egui::Context) {
- if self
- .revealed_secret_material
- .as_ref()
- .is_some_and(RevealedSecretMaterial::is_raw)
- && ctx.input(|input| input.viewport().focused == Some(false))
- {
- self.clear_revealed_secret_material();
- }
- }
-
- pub fn new(backend: Box<dyn RadrootsAppBackend>) -> Self {
- let mut app = Self {
- backend,
- screen: AppScreen::Setup,
- account_roster: Vec::new(),
- offline_geocoder_state: None,
- status_message: None,
- home_location_tools: HomeLocationTools::new(),
- pending_home_confirmation: None,
- pending_import_mode: None,
- remote_signer_entry_state: RemoteSignerEntryState::Closed,
- remote_signer_input: Zeroizing::new(String::new()),
- secret_key_input: Zeroizing::new(String::new()),
- import_password_input: Zeroizing::new(String::new()),
- pending_secret_key_backup_entry: false,
- secret_key_backup_password_input: Zeroizing::new(String::new()),
- secret_key_backup_password_confirm_input: Zeroizing::new(String::new()),
- remote_signer_note_input: Zeroizing::new(String::new()),
- revealed_secret_material: None,
- };
- app.offline_geocoder_state = app.backend.offline_geocoder_state();
- match app.backend.load_identity_state() {
- Ok(state) => app.apply_identity_state(state),
- Err(err) => {
- app.screen = AppScreen::Setup;
- app.status_message = Some(err);
- }
- }
- app.sync_remote_signer_entry_from_backend();
- app
- }
-
- fn refresh_account_roster(&mut self) {
- match self.backend.load_account_roster() {
- Ok(account_roster) => {
- self.account_roster = account_roster;
- }
- Err(err) => {
- self.account_roster.clear();
- self.status_message = Some(err);
- }
- }
- }
-
- fn apply_identity_state(&mut self, state: IdentityGateState) {
- match state {
- IdentityGateState::Missing => {
- self.screen = AppScreen::Setup;
- self.account_roster.clear();
- self.status_message = None;
- self.home_location_tools.clear();
- self.pending_home_confirmation = None;
- self.clear_secret_key_ui_state();
- }
- IdentityGateState::Ready { account_id } => {
- self.screen = AppScreen::Home { account_id };
- self.status_message = None;
- self.refresh_account_roster();
- self.home_location_tools.clear();
- self.pending_home_confirmation = None;
- self.clear_secret_key_ui_state();
- }
- IdentityGateState::Unsupported { reason } => {
- self.screen = AppScreen::Setup;
- self.account_roster.clear();
- self.status_message = Some(reason);
- self.home_location_tools.clear();
- self.pending_home_confirmation = None;
- self.clear_secret_key_ui_state();
- }
- }
- }
-
- fn request_setup_action(&mut self) {
- self.status_message = None;
- self.clear_revealed_secret_material();
- match self.backend.request_setup_action() {
- Ok(Some(state)) => self.apply_identity_state(state),
- Ok(None) => {}
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_home_setup_action(&mut self) {
- self.status_message = None;
- self.clear_revealed_secret_material();
- self.pending_home_confirmation = None;
- self.clear_remote_signer_entry();
- self.clear_secret_import_entry();
- match self.backend.request_home_setup_action() {
- Ok(Some(state)) => self.apply_identity_state(state),
- Ok(None) => self.refresh_account_roster(),
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_import_action(&mut self) {
- self.status_message = None;
- self.clear_revealed_secret_material();
- let request = match self.secret_import_request() {
- Ok(request) => request,
- Err(err) => {
- self.status_message = Some(err);
- return;
- }
- };
- match self.backend.request_import_action(&request) {
- Ok(Some(state)) => self.apply_identity_state(state),
- Ok(None) => {}
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_import_paste_action(&mut self) {
- self.status_message = None;
- self.clear_revealed_secret_material();
- match self.backend.request_import_paste_action() {
- Ok(Some(secret_key)) => {
- self.secret_key_input = Zeroizing::new(secret_key);
- }
- Ok(None) => {}
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn open_remote_signer_entry(&mut self) {
- self.remote_signer_entry_state = RemoteSignerEntryState::Editing;
- self.remote_signer_input.clear();
- self.status_message = None;
- }
-
- fn sync_remote_signer_entry_from_backend(&mut self) {
- match self.backend.pending_remote_signer_connection() {
- Ok(Some(pending)) => {
- if !matches!(
- self.remote_signer_entry_state,
- RemoteSignerEntryState::Editing | RemoteSignerEntryState::Review(_)
- ) {
- self.remote_signer_entry_state =
- RemoteSignerEntryState::WaitingApproval(pending);
- }
- }
- Ok(None) => {
- if matches!(
- self.remote_signer_entry_state,
- RemoteSignerEntryState::WaitingApproval(_)
- ) {
- self.clear_remote_signer_entry();
- }
- }
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_remote_signer_preview(&mut self) {
- self.status_message = None;
- self.clear_revealed_secret_material();
- match self
- .backend
- .preview_remote_signer_connection(self.remote_signer_input.as_str())
- {
- Ok(preview) => {
- self.remote_signer_entry_state = RemoteSignerEntryState::Review(preview);
- }
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_remote_signer_connect(&mut self) {
- self.status_message = None;
- self.clear_revealed_secret_material();
- let pending_summary = match &self.remote_signer_entry_state {
- RemoteSignerEntryState::Review(preview) => preview.pending_summary(),
- _ => {
- self.status_message =
- Some("review the remote signer details before connecting".to_owned());
- return;
- }
- };
- match self
- .backend
- .request_remote_signer_connection(self.remote_signer_input.as_str())
- {
- Ok(Some(state)) => self.apply_identity_state(state),
- Ok(None) => {
- self.remote_signer_entry_state =
- RemoteSignerEntryState::WaitingApproval(pending_summary);
- self.sync_remote_signer_entry_from_backend();
- }
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_cancel_pending_remote_signer(&mut self) {
- self.status_message = None;
- match self
- .backend
- .request_cancel_pending_remote_signer_connection()
- {
- Ok(()) => self.clear_remote_signer_entry(),
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_remote_signer_note_action(&mut self) {
- self.status_message = None;
- match self
- .backend
- .request_remote_signer_note_action(self.remote_signer_note_input.as_str())
- {
- Ok(()) => {}
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_select_account(&mut self, account_id: &str) {
- self.status_message = None;
- self.clear_revealed_secret_material();
- self.pending_home_confirmation = None;
- self.clear_secret_key_ui_state();
- match self.backend.request_select_account(account_id) {
- Ok(Some(state)) => self.apply_identity_state(state),
- Ok(None) => self.refresh_account_roster(),
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn request_home_action(&mut self, action: HomeActionKind) {
- self.status_message = None;
- self.clear_revealed_secret_material();
- match self.backend.request_home_action(action) {
- Ok(result) => self.apply_home_action_result(result),
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- }
-
- fn apply_home_action_result(&mut self, result: HomeActionResult) {
- match result {
- HomeActionResult::IdentityState(state) => self.apply_identity_state(state),
- HomeActionResult::RevealEncryptedSecretKey { ncryptsec } => {
- self.revealed_secret_material = Some(RevealedSecretMaterial::EncryptedSecretKey(
- Zeroizing::new(ncryptsec),
- ));
- self.pending_home_confirmation = None;
- }
- HomeActionResult::RevealRawSecretKey { nsec } => {
- self.revealed_secret_material = Some(RevealedSecretMaterial::RawSecretKey {
- nsec: Zeroizing::new(nsec),
- revealed_at: Instant::now(),
- });
- self.pending_home_confirmation = None;
- }
- HomeActionResult::None => {}
- }
- }
-
- fn home_action_requires_confirmation(action: HomeActionKind) -> bool {
- !matches!(action, HomeActionKind::BackupSecretKey)
- }
-
- fn home_action_confirmation_message(action: HomeActionKind) -> &'static str {
- match action {
- HomeActionKind::BackupSecretKey => {
- "This exports the current local secret key in encrypted form for backup."
- }
- HomeActionKind::RevealRawSecretKey => {
- "This reveals the current local secret key in plaintext. Use encrypted backup instead when possible."
- }
- HomeActionKind::RemoveLocalKey => {
- "This removes the current key from this device and returns the app to setup."
- }
- HomeActionKind::ResetDevice => {
- "This removes all app-managed local identity state from this device and returns the app to setup."
- }
- HomeActionKind::DisconnectSigner => {
- "This disconnects the current external signer from the app. It does not delete the signer key."
- }
- }
- }
-
- fn sync_backend(&mut self) {
- match self.backend.poll_offline_geocoder_state() {
- Ok(Some(state)) => {
- self.offline_geocoder_state = Some(state);
- }
- Ok(None) => {}
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- match self.backend.poll_home_action_result() {
- Ok(Some(result)) => self.apply_home_action_result(result),
- Ok(None) => {}
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- match self.backend.poll_remote_signer_note_action_result() {
- Ok(Some(result)) => {
- self.remote_signer_note_input.clear();
- self.status_message = Some(format!(
- "Signed remote kind 1 note: {}",
- result.event_id_hex
- ));
- }
- Ok(None) => {}
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- match self.backend.poll_reverse_location_lookup_result() {
- Ok(Some(result)) => self.home_location_tools.apply_reverse_lookup_result(result),
- Ok(None) => {}
- Err(err) => {
- self.home_location_tools
- .apply_reverse_lookup_poll_error(err);
- }
- }
- match self.backend.poll_location_country_list_result() {
- Ok(Some(result)) => self.home_location_tools.apply_country_list_result(result),
- Ok(None) => {}
- Err(err) => {
- self.home_location_tools.apply_country_list_poll_error(err);
- }
- }
- match self.backend.poll_location_country_center_lookup_result() {
- Ok(Some(result)) => self.home_location_tools.apply_country_center_result(result),
- Ok(None) => {}
- Err(err) => {
- self.home_location_tools
- .apply_country_center_poll_error(err);
- }
- }
- match self.backend.poll_identity_state() {
- Ok(Some(state)) => self.apply_identity_state(state),
- Ok(None) => {}
- Err(err) => {
- self.status_message = Some(err);
- }
- }
- self.sync_remote_signer_entry_from_backend();
- }
-
- fn render_import_entry(
- &mut self,
- ui: &mut egui::Ui,
- import_action: &ImportActionState,
- import_paste_action: Option<&PasteActionState>,
- ) {
- let import_mode = self.import_mode();
- ui.vertical_centered(|ui| {
- ui.set_max_width(ui.available_width().min(560.0));
- ui.label(import_mode.helper_text());
- ui.add_space(8.0);
- if ui.button(import_mode.switch_label()).clicked() {
- self.set_import_mode(import_mode.toggle());
- }
- ui.add_space(8.0);
- ui.add(
- egui::TextEdit::singleline(&mut *self.secret_key_input)
- .hint_text(import_mode.hint_text())
- .desired_width(ui.available_width()),
- );
- if import_mode.requires_password() {
- ui.add_space(8.0);
- ui.add(
- egui::TextEdit::singleline(&mut *self.import_password_input)
- .password(true)
- .hint_text("Enter Backup Password")
- .desired_width(ui.available_width()),
- );
- }
- ui.add_space(8.0);
- if let Some(import_paste_action) = import_paste_action {
- let paste_clicked = ui
- .add_enabled(
- import_paste_action.enabled,
- egui::Button::new(import_paste_action.label.clone()),
- )
- .clicked();
- if paste_clicked {
- self.request_import_paste_action();
- }
- ui.add_space(8.0);
- }
- ui.horizontal_centered(|ui| {
- let confirm_clicked = ui
- .add_enabled(
- import_action.enabled,
- egui::Button::new(import_action.label.clone()),
- )
- .clicked();
- if confirm_clicked {
- self.request_import_action();
- }
-
- if ui.button("Cancel").clicked() {
- self.clear_secret_import_entry();
- self.status_message = None;
- }
- });
- });
- }
-
- fn render_secret_key_backup_entry(&mut self, ui: &mut egui::Ui, action: &HomeActionState) {
- ui.vertical_centered(|ui| {
- ui.set_max_width(ui.available_width().min(560.0));
- ui.label("Create an encrypted backup of the current local secret key.");
- ui.add_space(8.0);
- ui.add(
- egui::TextEdit::singleline(&mut *self.secret_key_backup_password_input)
- .password(true)
- .hint_text("Enter Backup Password")
- .desired_width(ui.available_width()),
- );
- ui.add_space(8.0);
- ui.add(
- egui::TextEdit::singleline(&mut *self.secret_key_backup_password_confirm_input)
- .password(true)
- .hint_text("Confirm Backup Password")
- .desired_width(ui.available_width()),
- );
- ui.add_space(8.0);
- ui.horizontal_centered(|ui| {
- let confirm_clicked = ui
- .add_enabled(action.enabled, egui::Button::new(action.label.clone()))
- .clicked();
- if confirm_clicked {
- self.request_secret_key_backup_action();
- }
-
- if ui.button("Cancel").clicked() {
- self.clear_secret_key_backup_entry();
- self.status_message = None;
- }
- });
- });
- }
-
- fn render_remote_signer_entry(&mut self, ui: &mut egui::Ui, action: &SetupActionState) {
- ui.vertical_centered(|ui| {
- ui.set_max_width(ui.available_width().min(560.0));
- match &self.remote_signer_entry_state {
- RemoteSignerEntryState::Closed => {}
- RemoteSignerEntryState::Editing => {
- ui.label(
- "Connect an approved remote signer using its bunker uri or discovery url.",
- );
- ui.add_space(8.0);
- ui.add(
- egui::TextEdit::singleline(&mut *self.remote_signer_input)
- .hint_text("bunker://... or http://localhost/connect?uri=...")
- .desired_width(ui.available_width()),
- );
- ui.add_space(8.0);
- ui.horizontal_centered(|ui| {
- if ui
- .add_enabled(action.enabled, egui::Button::new("Review Remote Signer"))
- .clicked()
- {
- self.request_remote_signer_preview();
- }
- if ui.button("Cancel").clicked() {
- self.clear_remote_signer_entry();
- self.status_message = None;
- }
- });
- }
- RemoteSignerEntryState::Review(preview) => {
- ui.label("Review the remote signer before connecting.");
- ui.add_space(8.0);
- ui.monospace(format!("source: {}", preview.source_label));
- ui.monospace(format!("signer: {}", preview.signer_npub));
- if preview.relays.is_empty() {
- ui.label("No relays were provided by this signer.");
- } else {
- ui.label("Relays");
- for relay in &preview.relays {
- ui.monospace(relay);
- }
- }
- ui.add_space(8.0);
- if preview.requested_permissions.is_empty() {
- ui.label("No additional permissions are requested in this slice.");
- } else {
- ui.label("Requested permissions");
- for permission in &preview.requested_permissions {
- ui.monospace(permission);
- }
- }
- ui.add_space(8.0);
- ui.horizontal_centered(|ui| {
- if ui
- .add_enabled(action.enabled, egui::Button::new(action.label.clone()))
- .clicked()
- {
- self.request_remote_signer_connect();
- }
- if ui.button("Cancel").clicked() {
- self.clear_remote_signer_entry();
- self.status_message = None;
- }
- });
- }
- RemoteSignerEntryState::WaitingApproval(pending) => {
- ui.label(action.label.as_str());
- if pending.auth_url.is_some() {
- ui.add_space(8.0);
- ui.label(
- "Authorize the remote signer in the browser, then keep this screen open while the app waits for the replayed response.",
- );
- } else if action.label == "Remote Signer Approval Check Retrying" {
- ui.add_space(8.0);
- ui.label(
- "The app is retrying approval checks after a relay or network failure.",
- );
- } else {
- ui.add_space(8.0);
- ui.label("Remote signer connection is waiting for signer approval.");
- }
- ui.add_space(8.0);
- ui.monospace(format!("signer: {}", pending.signer_npub));
- if pending.relays.is_empty() {
- ui.label("No relays were provided by this signer.");
- } else {
- ui.label("Relays");
- for relay in &pending.relays {
- ui.monospace(relay);
- }
- }
- if let Some(auth_url) = &pending.auth_url {
- ui.add_space(8.0);
- ui.label("Authorization url");
- ui.monospace(auth_url);
- }
- ui.add_space(8.0);
- if ui.button("Cancel Pending Remote Signer").clicked() {
- self.request_cancel_pending_remote_signer();
- }
- }
- }
- });
- }
-
- fn render_home_account_section(&mut self, ui: &mut egui::Ui) {
- let AppScreen::Home { account_id } = &self.screen else {
- return;
- };
- let selected_account_id = account_id.clone();
- let selected_summary = self
- .account_roster
- .iter()
- .find(|account| account.account_id == selected_account_id)
- .cloned();
-
- ui.label("home");
- ui.add_space(8.0);
- ui.label("A signing identity is configured.");
- ui.add_space(12.0);
-
- if let Some(summary) = selected_summary {
- ui.label(summary.display_label());
- ui.monospace(format!("account id: {}", summary.account_id));
- ui.monospace(format!("npub: {}", summary.npub));
- ui.monospace(format!("custody: {}", summary.custody.label()));
- if summary.custody == RadrootsAccountCustody::RemoteSigner {
- if let Some(note_action) = self.backend.remote_signer_note_action_state() {
- if note_action.pending {
- ui.ctx().request_repaint();
- }
- ui.add_space(16.0);
- ui.label("Remote signer note");
- if let Some(permissions) =
- self.backend.selected_remote_signer_approved_permissions()
- {
- ui.add_space(8.0);
- if permissions.is_empty() {
- ui.label("Approved permissions: none");
- } else {
- ui.label("Approved permissions");
- for permission in permissions {
- ui.monospace(permission);
- }
- }
- }
- ui.add_space(8.0);
- ui.add(
- egui::TextEdit::multiline(&mut *self.remote_signer_note_input)
- .hint_text("Write a kind 1 note to sign through the remote signer")
- .desired_rows(3)
- .desired_width(ui.available_width().min(560.0)),
- );
- ui.add_space(8.0);
- if ui
- .add_enabled(note_action.enabled, egui::Button::new(note_action.label))
- .clicked()
- {
- self.request_remote_signer_note_action();
- }
- }
- }
- } else {
- ui.label("Selected account details are unavailable.");
- ui.monospace(format!("account id: {selected_account_id}"));
- }
-
- if !self.account_roster.is_empty() {
- ui.add_space(16.0);
- ui.label("Accounts");
- let mut next_selected_account_id = None;
- for account in &self.account_roster {
- ui.add_space(8.0);
- ui.horizontal_wrapped(|ui| {
- let is_selected = account.account_id == selected_account_id;
- ui.label(account.display_label());
- ui.monospace(account.npub.as_str());
- ui.monospace(account.custody.label());
- if is_selected {
- ui.label("selected");
- } else if ui.button("Select Account").clicked() {
- next_selected_account_id = Some(account.account_id.clone());
- }
- });
- }
- if let Some(account_id) = next_selected_account_id {
- self.request_select_account(account_id.as_str());
- }
- }
-
- let home_setup_action = self.backend.home_setup_action_state();
- let import_action = self.backend.import_action_state();
- let import_paste_action = self.backend.import_paste_action_state();
- let remote_signer_action = self.backend.remote_signer_action_state();
- if home_setup_action.is_some() || import_action.is_some() || remote_signer_action.is_some()
- {
- ui.add_space(16.0);
- ui.label("Add account");
- }
-
- if let Some(home_setup_action) = home_setup_action {
- if home_setup_action.pending {
- ui.ctx().request_repaint();
- }
- ui.add_space(8.0);
- if ui
- .add_enabled(
- home_setup_action.enabled,
- egui::Button::new(home_setup_action.label),
- )
- .clicked()
- {
- self.request_home_setup_action();
- }
- }
-
- if let Some(import_action) = import_action {
- if import_action.pending {
- ui.ctx().request_repaint();
- }
- if let Some(import_paste_action) = &import_paste_action {
- if import_paste_action.pending {
- ui.ctx().request_repaint();
- }
- }
- ui.add_space(8.0);
- if self.pending_import_mode.is_some() {
- self.render_import_entry(ui, &import_action, import_paste_action.as_ref());
- } else if ui.button(import_action.label).clicked() {
- self.open_import_entry();
- }
- }
-
- if let Some(remote_signer_action) = remote_signer_action {
- if remote_signer_action.pending {
- ui.ctx().request_repaint();
- }
- ui.add_space(8.0);
- if matches!(
- self.remote_signer_entry_state,
- RemoteSignerEntryState::Closed
- ) {
- if ui
- .add_enabled(
- remote_signer_action.enabled,
- egui::Button::new(remote_signer_action.label),
- )
- .clicked()
- {
- self.open_remote_signer_entry();
- }
- } else {
- self.render_remote_signer_entry(ui, &remote_signer_action);
- }
- }
- }
-
- fn render_offline_geocoder_status(&self, ui: &mut egui::Ui) {
- let Some(state) = &self.offline_geocoder_state else {
- return;
- };
-
- ui.add_space(16.0);
- ui.label(state.summary_label());
-
- if let Some(user_message) = state.user_message() {
- ui.add_space(6.0);
- ui.label(user_message);
- ui.add_space(6.0);
- ui.collapsing("Offline geocoder details", |ui| {
- if let Some(diagnostic) = state.diagnostic() {
- ui.label(diagnostic.technical_message);
- ui.add_space(6.0);
- ui.monospace(format!("platform: {}", diagnostic.platform_code));
- ui.monospace(format!(
- "asset revision: {}",
- diagnostic.asset_revision.as_deref().unwrap_or("unknown")
- ));
- ui.monospace(format!("diagnostic code: {}", diagnostic.code));
- if ui.button("Copy Offline Geocoder Diagnostic").clicked() {
- ui.ctx().copy_text(diagnostic.export_text());
- }
- }
- if cfg!(debug_assertions) {
- if let Some(debug_message) = state.debug_message() {
- ui.add_space(6.0);
- ui.monospace(debug_message);
- }
- }
- });
- }
- }
-}
-
-impl eframe::App for RadrootsApp {
- fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
- self.sync_backend();
- self.sync_revealed_secret_material_lifetime();
- self.clear_raw_secret_when_app_unfocused(ctx);
- if matches!(
- self.offline_geocoder_state,
- Some(RadrootsOfflineGeocoderState::Initializing)
- ) {
- ctx.request_repaint_after(Duration::from_millis(100));
- }
- if self.home_location_tools.is_pending() {
- ctx.request_repaint_after(Duration::from_millis(100));
- }
- if self
- .revealed_secret_material
- .as_ref()
- .is_some_and(RevealedSecretMaterial::is_raw)
- {
- ctx.request_repaint_after(Duration::from_millis(200));
- }
-
- egui::CentralPanel::default().show(ctx, |ui| {
- ui.vertical_centered(|ui| {
- ui.add_space(48.0);
- ui.heading(APP_NAME);
- ui.add_space(12.0);
-
- match self.screen.clone() {
- AppScreen::Setup => {
- let action = self.backend.setup_action_state();
- if action.pending {
- ctx.request_repaint();
- }
- let import_action = self.backend.import_action_state();
- if let Some(import_action) = &import_action {
- if import_action.pending {
- ctx.request_repaint();
- }
- }
- let import_paste_action = self.backend.import_paste_action_state();
- if let Some(import_paste_action) = &import_paste_action {
- if import_paste_action.pending {
- ctx.request_repaint();
- }
- }
- let remote_signer_action = self.backend.remote_signer_action_state();
- if let Some(remote_signer_action) = &remote_signer_action {
- if remote_signer_action.pending {
- ctx.request_repaint();
- }
- }
-
- ui.label("setup");
- ui.add_space(8.0);
- ui.label("A signing identity is required before the app can continue.");
- ui.add_space(16.0);
- let clicked = ui
- .add_enabled(action.enabled, egui::Button::new(action.label))
- .clicked();
- if clicked {
- self.request_setup_action();
- }
-
- if let Some(import_action) = import_action {
- ui.add_space(12.0);
- if self.pending_import_mode.is_some() {
- self.render_import_entry(
- ui,
- &import_action,
- import_paste_action.as_ref(),
- );
- } else if ui.button(import_action.label).clicked() {
- self.open_import_entry();
- }
- }
-
- if let Some(remote_signer_action) = remote_signer_action {
- ui.add_space(12.0);
- if matches!(self.remote_signer_entry_state, RemoteSignerEntryState::Closed)
- {
- if ui
- .add_enabled(
- remote_signer_action.enabled,
- egui::Button::new(remote_signer_action.label),
- )
- .clicked()
- {
- self.open_remote_signer_entry();
- }
- } else {
- self.render_remote_signer_entry(ui, &remote_signer_action);
- }
- }
- }
- AppScreen::Home { .. } => {
- self.render_home_account_section(ui);
- self.home_location_tools.render(
- ui,
- self.backend.as_ref(),
- self.offline_geocoder_state.as_ref(),
- );
-
- let actions = self.backend.home_action_states();
- for (index, action) in actions.into_iter().enumerate() {
- ui.add_space(if index == 0 { 20.0 } else { 12.0 });
- if action.pending {
- ctx.request_repaint();
- }
-
- if action.kind == HomeActionKind::BackupSecretKey
- && self.pending_secret_key_backup_entry
- {
- self.render_secret_key_backup_entry(ui, &action);
- } else if action.kind == HomeActionKind::BackupSecretKey
- && ui
- .add_enabled(
- action.enabled,
- egui::Button::new(action.label.clone()),
- )
- .clicked()
- {
- self.pending_secret_key_backup_entry = true;
- self.secret_key_backup_password_input.clear();
- self.secret_key_backup_password_confirm_input.clear();
- self.status_message = None;
- } else if Self::home_action_requires_confirmation(action.kind)
- && self.pending_home_confirmation == Some(action.kind)
- {
- ui.vertical_centered(|ui| {
- ui.set_max_width(ui.available_width().min(560.0));
- ui.label(Self::home_action_confirmation_message(action.kind));
- ui.add_space(8.0);
- ui.horizontal_centered(|ui| {
- let confirm_clicked = ui
- .add_enabled(
- action.enabled,
- egui::Button::new(action.label.clone()),
- )
- .clicked();
- if confirm_clicked {
- self.request_home_action(action.kind);
- }
-
- if ui.button("Cancel").clicked() {
- self.pending_home_confirmation = None;
- self.status_message = None;
- }
- });
- });
- } else if Self::home_action_requires_confirmation(action.kind)
- && self.pending_home_confirmation.is_none()
- && ui.button(action.label.clone()).clicked()
- {
- self.pending_home_confirmation = Some(action.kind);
- } else if !Self::home_action_requires_confirmation(action.kind)
- && ui
- .add_enabled(
- action.enabled,
- egui::Button::new(action.label.clone()),
- )
- .clicked()
- {
- self.request_home_action(action.kind);
- }
- }
-
- if let Some((label, value, dismiss_label, is_raw)) =
- self.revealed_secret_material.as_ref().map(|material| {
- (
- material.label(),
- material.value().to_owned(),
- material.dismiss_label(),
- material.is_raw(),
- )
- })
- {
- ui.add_space(20.0);
- ui.label(label);
- ui.add_space(8.0);
- ui.monospace(value);
- if is_raw {
- ui.add_space(8.0);
- ui.label(
- "Raw secret reveal clears automatically after 30 seconds or when the app loses focus.",
- );
- }
- ui.add_space(8.0);
- if ui.button(dismiss_label).clicked() {
- self.clear_revealed_secret_material();
- }
- }
- }
- }
-
- if let Some(message) = &self.status_message {
- ui.add_space(16.0);
- ui.label(message);
- }
-
- self.render_offline_geocoder_status(ui);
- });
- });
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_test_support::{
- FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD, FIXTURE_BOB, fixture_identity_ncryptsec,
- };
- use std::cell::RefCell;
- use std::collections::VecDeque;
- use std::rc::Rc;
-
- #[derive(Clone)]
- struct MockBackend {
- load: Result<IdentityGateState, String>,
- account_roster: Rc<RefCell<Vec<RadrootsAccountSummary>>>,
- offline_geocoder_state: Rc<RefCell<Option<RadrootsOfflineGeocoderState>>>,
- offline_geocoder_poll:
- Rc<RefCell<VecDeque<Result<Option<RadrootsOfflineGeocoderState>, String>>>>,
- action_state: Rc<RefCell<SetupActionState>>,
- home_setup_action_state: Rc<RefCell<Option<SetupActionState>>>,
- import_action_state: Rc<RefCell<Option<ImportActionState>>>,
- import_paste_action_state: Rc<RefCell<Option<PasteActionState>>>,
- remote_signer_action_state: Rc<RefCell<Option<SetupActionState>>>,
- remote_signer_preview: Rc<RefCell<VecDeque<Result<RadrootsRemoteSignerPreview, String>>>>,
- remote_signer_request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
- pending_remote_signer: Rc<RefCell<Option<RadrootsPendingRemoteSignerConnection>>>,
- cancel_pending_remote_signer: Rc<RefCell<VecDeque<Result<(), String>>>>,
- remote_signer_note_action_state: Rc<RefCell<Option<SetupActionState>>>,
- remote_signer_note_request: Rc<RefCell<VecDeque<Result<(), String>>>>,
- remote_signer_note_poll:
- Rc<RefCell<VecDeque<Result<Option<RadrootsRemoteSignerSignedNote>, String>>>>,
- home_action_states: Rc<RefCell<Vec<HomeActionState>>>,
- request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
- home_setup_request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
- import_request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
- import_paste_request: Rc<RefCell<VecDeque<Result<Option<String>, String>>>>,
- secret_key_backup_request: Rc<RefCell<VecDeque<Result<HomeActionResult, String>>>>,
- home_request: Rc<RefCell<VecDeque<(HomeActionKind, Result<HomeActionResult, String>)>>>,
- home_poll: Rc<RefCell<VecDeque<Result<Option<HomeActionResult>, String>>>>,
- reverse_lookup_request: Rc<RefCell<VecDeque<Result<(), RadrootsLocationResolverError>>>>,
- reverse_lookup_poll:
- Rc<RefCell<VecDeque<Result<Option<RadrootsReverseLocationLookupResult>, String>>>>,
- select_account_request:
- Rc<RefCell<VecDeque<(String, Result<Option<IdentityGateState>, String>)>>>,
- poll: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>,
- }
-
- impl MockBackend {
- fn new(
- load: Result<IdentityGateState, String>,
- request: Vec<Result<Option<IdentityGateState>, String>>,
- poll: Vec<Result<Option<IdentityGateState>, String>>,
- action_state: SetupActionState,
- ) -> Self {
- Self {
- load,
- account_roster: Rc::new(RefCell::new(Vec::new())),
- offline_geocoder_state: Rc::new(RefCell::new(None)),
- offline_geocoder_poll: Rc::new(RefCell::new(VecDeque::new())),
- action_state: Rc::new(RefCell::new(action_state)),
- home_setup_action_state: Rc::new(RefCell::new(None)),
- import_action_state: Rc::new(RefCell::new(None)),
- import_paste_action_state: Rc::new(RefCell::new(None)),
- remote_signer_action_state: Rc::new(RefCell::new(None)),
- remote_signer_preview: Rc::new(RefCell::new(VecDeque::new())),
- remote_signer_request: Rc::new(RefCell::new(VecDeque::new())),
- pending_remote_signer: Rc::new(RefCell::new(None)),
- cancel_pending_remote_signer: Rc::new(RefCell::new(VecDeque::new())),
- remote_signer_note_action_state: Rc::new(RefCell::new(None)),
- remote_signer_note_request: Rc::new(RefCell::new(VecDeque::new())),
- remote_signer_note_poll: Rc::new(RefCell::new(VecDeque::new())),
- home_action_states: Rc::new(RefCell::new(Vec::new())),
- request: Rc::new(RefCell::new(request.into())),
- home_setup_request: Rc::new(RefCell::new(VecDeque::new())),
- import_request: Rc::new(RefCell::new(VecDeque::new())),
- import_paste_request: Rc::new(RefCell::new(VecDeque::new())),
- secret_key_backup_request: Rc::new(RefCell::new(VecDeque::new())),
- home_request: Rc::new(RefCell::new(VecDeque::new())),
- home_poll: Rc::new(RefCell::new(VecDeque::new())),
- reverse_lookup_request: Rc::new(RefCell::new(VecDeque::new())),
- reverse_lookup_poll: Rc::new(RefCell::new(VecDeque::new())),
- select_account_request: Rc::new(RefCell::new(VecDeque::new())),
- poll: Rc::new(RefCell::new(poll.into())),
- }
- }
-
- fn with_account_roster(self, account_roster: Vec<RadrootsAccountSummary>) -> Self {
- *self.account_roster.borrow_mut() = account_roster;
- self
- }
-
- fn with_offline_geocoder_state(
- self,
- state: RadrootsOfflineGeocoderState,
- poll: Vec<Result<Option<RadrootsOfflineGeocoderState>, String>>,
- ) -> Self {
- *self.offline_geocoder_state.borrow_mut() = Some(state);
- self.offline_geocoder_poll.borrow_mut().extend(poll);
- self
- }
-
- fn with_import_action(
- self,
- action_state: ImportActionState,
- request: Vec<Result<Option<IdentityGateState>, String>>,
- ) -> Self {
- *self.import_action_state.borrow_mut() = Some(action_state);
- self.import_request.borrow_mut().extend(request);
- self
- }
-
- fn with_home_setup_action(
- self,
- action_state: SetupActionState,
- request: Vec<Result<Option<IdentityGateState>, String>>,
- ) -> Self {
- *self.home_setup_action_state.borrow_mut() = Some(action_state);
- self.home_setup_request.borrow_mut().extend(request);
- self
- }
-
- fn with_import_paste_action(
- self,
- action_state: PasteActionState,
- request: Vec<Result<Option<String>, String>>,
- ) -> Self {
- *self.import_paste_action_state.borrow_mut() = Some(action_state);
- self.import_paste_request.borrow_mut().extend(request);
- self
- }
-
- fn with_remote_signer_action(self, action_state: SetupActionState) -> Self {
- *self.remote_signer_action_state.borrow_mut() = Some(action_state);
- self
- }
-
- fn with_remote_signer_preview(
- self,
- preview: Vec<Result<RadrootsRemoteSignerPreview, String>>,
- ) -> Self {
- self.remote_signer_preview.borrow_mut().extend(preview);
- self
- }
-
- fn with_remote_signer_request(
- self,
- request: Vec<Result<Option<IdentityGateState>, String>>,
- ) -> Self {
- self.remote_signer_request.borrow_mut().extend(request);
- self
- }
-
- fn with_pending_remote_signer(
- self,
- pending: Option<RadrootsPendingRemoteSignerConnection>,
- ) -> Self {
- *self.pending_remote_signer.borrow_mut() = pending;
- self
- }
-
- fn with_cancel_pending_remote_signer(self, request: Vec<Result<(), String>>) -> Self {
- self.cancel_pending_remote_signer
- .borrow_mut()
- .extend(request);
- self
- }
-
- fn with_home_action(
- self,
- action_state: HomeActionState,
- request: Vec<Result<HomeActionResult, String>>,
- ) -> Self {
- self.home_action_states
- .borrow_mut()
- .push(action_state.clone());
- self.home_request.borrow_mut().extend(
- request
- .into_iter()
- .map(|result| (action_state.kind, result)),
- );
- self
- }
-
- fn with_secret_key_backup_request(
- self,
- request: Vec<Result<HomeActionResult, String>>,
- ) -> Self {
- self.secret_key_backup_request.borrow_mut().extend(request);
- self
- }
-
- fn with_home_action_poll(
- self,
- poll: Vec<Result<Option<HomeActionResult>, String>>,
- ) -> Self {
- self.home_poll.borrow_mut().extend(poll);
- self
- }
-
- fn with_reverse_lookup(
- self,
- request: Vec<Result<(), RadrootsLocationResolverError>>,
- poll: Vec<Result<Option<RadrootsReverseLocationLookupResult>, String>>,
- ) -> Self {
- self.reverse_lookup_request.borrow_mut().extend(request);
- self.reverse_lookup_poll.borrow_mut().extend(poll);
- self
- }
-
- fn with_select_account(
- self,
- account_id: &str,
- request: Vec<Result<Option<IdentityGateState>, String>>,
- ) -> Self {
- self.select_account_request.borrow_mut().extend(
- request
- .into_iter()
- .map(|result| (account_id.to_owned(), result)),
- );
- self
- }
- }
-
- impl RadrootsAppBackend for MockBackend {
- fn load_identity_state(&self) -> Result<IdentityGateState, String> {
- self.load.clone()
- }
-
- fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> {
- Ok(self.account_roster.borrow().clone())
- }
-
- fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> {
- self.offline_geocoder_state.borrow().clone()
- }
-
- fn poll_offline_geocoder_state(
- &self,
- ) -> Result<Option<RadrootsOfflineGeocoderState>, String> {
- self.offline_geocoder_poll
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(None))
- }
-
- fn setup_action_state(&self) -> SetupActionState {
- self.action_state.borrow().clone()
- }
-
- fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- self.request
- .borrow_mut()
- .pop_front()
- .unwrap_or_else(|| Err("missing request response".into()))
- }
-
- fn home_setup_action_state(&self) -> Option<SetupActionState> {
- self.home_setup_action_state.borrow().clone()
- }
-
- fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> {
- self.home_setup_request
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(None))
- }
-
- fn import_action_state(&self) -> Option<ImportActionState> {
- self.import_action_state.borrow().clone()
- }
-
- fn request_import_action(
- &self,
- _request: &RadrootsSecretImportRequest,
- ) -> Result<Option<IdentityGateState>, String> {
- self.import_request
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(None))
- }
-
- fn request_secret_key_backup_action(
- &self,
- _password: &str,
- ) -> Result<HomeActionResult, String> {
- self.secret_key_backup_request
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(HomeActionResult::None))
- }
-
- fn import_paste_action_state(&self) -> Option<PasteActionState> {
- self.import_paste_action_state.borrow().clone()
- }
-
- fn request_import_paste_action(&self) -> Result<Option<String>, String> {
- self.import_paste_request
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(None))
- }
-
- fn remote_signer_action_state(&self) -> Option<SetupActionState> {
- self.remote_signer_action_state.borrow().clone()
- }
-
- fn preview_remote_signer_connection(
- &self,
- _input: &str,
- ) -> Result<RadrootsRemoteSignerPreview, String> {
- self.remote_signer_preview
- .borrow_mut()
- .pop_front()
- .unwrap_or_else(|| Err("missing remote signer preview".into()))
- }
-
- fn request_remote_signer_connection(
- &self,
- _input: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- self.remote_signer_request
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(None))
- }
-
- fn pending_remote_signer_connection(
- &self,
- ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> {
- Ok(self.pending_remote_signer.borrow().clone())
- }
-
- fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> {
- let result = self
- .cancel_pending_remote_signer
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(()));
- if result.is_ok() {
- *self.pending_remote_signer.borrow_mut() = None;
- }
- result
- }
-
- fn remote_signer_note_action_state(&self) -> Option<SetupActionState> {
- self.remote_signer_note_action_state.borrow().clone()
- }
-
- fn request_remote_signer_note_action(&self, _content: &str) -> Result<(), String> {
- self.remote_signer_note_request
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(()))
- }
-
- fn poll_remote_signer_note_action_result(
- &self,
- ) -> Result<Option<RadrootsRemoteSignerSignedNote>, String> {
- self.remote_signer_note_poll
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(None))
- }
-
- fn home_action_states(&self) -> Vec<HomeActionState> {
- self.home_action_states.borrow().clone()
- }
-
- fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> {
- let Some((expected_action, response)) = self.home_request.borrow_mut().pop_front()
- else {
- return Err("missing home action response".into());
- };
- if expected_action != action {
- return Err(format!(
- "unexpected home action request: expected {:?}, got {:?}",
- expected_action, action
- ));
- }
- response
- }
-
- fn poll_home_action_result(&self) -> Result<Option<HomeActionResult>, String> {
- self.home_poll.borrow_mut().pop_front().unwrap_or(Ok(None))
- }
-
- fn request_select_account(
- &self,
- account_id: &str,
- ) -> Result<Option<IdentityGateState>, String> {
- let Some((expected_account_id, response)) =
- self.select_account_request.borrow_mut().pop_front()
- else {
- return Err("missing select-account response".into());
- };
- if expected_account_id != account_id {
- return Err(format!(
- "unexpected account selection request: expected {expected_account_id}, got {account_id}"
- ));
- }
- response
- }
-
- fn request_reverse_location_lookup(
- &self,
- _point: RadrootsLocationPoint,
- _options: Option<RadrootsLocationReverseOptions>,
- ) -> Result<(), RadrootsLocationResolverError> {
- self.reverse_lookup_request
- .borrow_mut()
- .pop_front()
- .unwrap_or(Err(RadrootsLocationResolverError::Unsupported))
- }
-
- fn poll_reverse_location_lookup_result(
- &self,
- ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> {
- self.reverse_lookup_poll
- .borrow_mut()
- .pop_front()
- .unwrap_or(Ok(None))
- }
-
- fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> {
- self.poll.borrow_mut().pop_front().unwrap_or(Ok(None))
- }
- }
-
- fn fixture_account_summary() -> RadrootsAccountSummary {
- RadrootsAccountSummary {
- account_id: FIXTURE_ALICE.account_id.into(),
- npub: FIXTURE_ALICE.npub.into(),
- label: Some("fixture alice".into()),
- custody: RadrootsAccountCustody::LocalManaged,
- }
- }
-
- fn fixture_bob_account_summary() -> RadrootsAccountSummary {
- RadrootsAccountSummary {
- account_id: FIXTURE_BOB.account_id.into(),
- npub: FIXTURE_BOB.npub.into(),
- label: Some("fixture bob".into()),
- custody: RadrootsAccountCustody::LocalManaged,
- }
- }
-
- fn fixture_ready_state() -> IdentityGateState {
- IdentityGateState::Ready {
- account_id: FIXTURE_ALICE.account_id.into(),
- }
- }
-
- fn fixture_home_screen() -> AppScreen {
- AppScreen::Home {
- account_id: FIXTURE_ALICE.account_id.into(),
- }
- }
-
- fn fixture_remote_signer_preview() -> RadrootsRemoteSignerPreview {
- RadrootsRemoteSignerPreview {
- source_label: "discovery url".into(),
- signer_npub: FIXTURE_BOB.npub.into(),
- relays: vec!["ws://localhost:8080".into()],
- requested_permissions: vec!["sign_event:kind:1".into(), "switch_relays".into()],
- }
- }
-
- fn fixture_pending_remote_signer() -> RadrootsPendingRemoteSignerConnection {
- fixture_remote_signer_preview().pending_summary()
- }
-
- #[test]
- fn startup_missing_key_enters_setup() {
- let app = RadrootsApp::new(Box::new(MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )));
- assert_eq!(app.screen, AppScreen::Setup);
- assert_eq!(app.status_message, None);
- }
-
- #[test]
- fn startup_ready_key_enters_home() {
- let app = RadrootsApp::new(Box::new(MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )));
- assert_eq!(app.screen, fixture_home_screen());
- assert_eq!(app.status_message, None);
- }
-
- #[test]
- fn startup_ready_key_loads_account_roster() {
- let app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_account_roster(vec![fixture_account_summary()]),
- ));
-
- assert_eq!(app.account_roster, vec![fixture_account_summary()]);
- }
-
- #[test]
- fn startup_restores_pending_remote_signer_connection() {
- let app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_remote_signer_action(SetupActionState {
- label: "Connect Remote Signer".into(),
- enabled: true,
- pending: false,
- })
- .with_pending_remote_signer(Some(fixture_pending_remote_signer())),
- ));
-
- assert_eq!(
- app.remote_signer_entry_state,
- RemoteSignerEntryState::WaitingApproval(fixture_pending_remote_signer())
- );
- }
-
- #[test]
- fn startup_unsupported_shows_reason() {
- let app = RadrootsApp::new(Box::new(MockBackend::new(
- Ok(IdentityGateState::Unsupported {
- reason: "unsupported".into(),
- }),
- vec![],
- vec![],
- SetupActionState {
- label: "Connect Browser Signer".into(),
- enabled: false,
- pending: false,
- },
- )));
- assert_eq!(app.screen, AppScreen::Setup);
- assert_eq!(app.status_message.as_deref(), Some("unsupported"));
- }
-
- #[test]
- fn deferred_setup_action_transitions_to_home_after_poll() {
- let mut app = RadrootsApp::new(Box::new(MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![Ok(None)],
- vec![Ok(Some(fixture_ready_state()))],
- SetupActionState {
- label: "Connect Browser Signer".into(),
- enabled: true,
- pending: false,
- },
- )));
-
- app.request_setup_action();
- assert_eq!(app.screen, AppScreen::Setup);
-
- app.sync_backend();
-
- assert_eq!(app.screen, fixture_home_screen());
- }
-
- #[test]
- fn immediate_setup_action_transitions_to_home() {
- let mut app = RadrootsApp::new(Box::new(MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![Ok(Some(fixture_ready_state()))],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )));
-
- app.request_setup_action();
-
- assert_eq!(app.screen, fixture_home_screen());
- }
-
- #[test]
- fn home_setup_action_transitions_to_new_selected_account() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_account_roster(vec![
- fixture_account_summary(),
- fixture_bob_account_summary(),
- ])
- .with_home_setup_action(
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- vec![Ok(Some(IdentityGateState::Ready {
- account_id: FIXTURE_BOB.account_id.into(),
- }))],
- ),
- ));
-
- app.request_home_setup_action();
-
- assert_eq!(
- app.screen,
- AppScreen::Home {
- account_id: FIXTURE_BOB.account_id.into(),
- }
- );
- assert_eq!(app.account_roster.len(), 2);
- }
-
- #[test]
- fn select_account_transitions_to_requested_account() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_account_roster(vec![
- fixture_account_summary(),
- fixture_bob_account_summary(),
- ])
- .with_select_account(
- FIXTURE_BOB.account_id,
- vec![Ok(Some(IdentityGateState::Ready {
- account_id: FIXTURE_BOB.account_id.into(),
- }))],
- ),
- ));
-
- app.request_select_account(FIXTURE_BOB.account_id);
-
- assert_eq!(
- app.screen,
- AppScreen::Home {
- account_id: FIXTURE_BOB.account_id.into(),
- }
- );
- }
-
- #[test]
- fn home_remove_action_transitions_to_setup() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_home_action(
- HomeActionState {
- kind: HomeActionKind::RemoveLocalKey,
- label: "Remove Key From This Device".into(),
- enabled: true,
- pending: false,
- },
- vec![Ok(HomeActionResult::IdentityState(
- IdentityGateState::Missing,
- ))],
- ),
- ));
-
- app.pending_home_confirmation = Some(HomeActionKind::RemoveLocalKey);
- app.request_home_action(HomeActionKind::RemoveLocalKey);
-
- assert_eq!(app.screen, AppScreen::Setup);
- assert_eq!(app.status_message, None);
- assert_eq!(app.pending_home_confirmation, None);
- }
-
- #[test]
- fn failed_home_remove_action_keeps_home_screen_and_message() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_home_action(
- HomeActionState {
- kind: HomeActionKind::RemoveLocalKey,
- label: "Remove Key From This Device".into(),
- enabled: true,
- pending: false,
- },
- vec![Err("remove failed".into())],
- ),
- ));
-
- app.pending_home_confirmation = Some(HomeActionKind::RemoveLocalKey);
- app.request_home_action(HomeActionKind::RemoveLocalKey);
-
- assert!(matches!(app.screen, AppScreen::Home { .. }));
- assert_eq!(app.status_message.as_deref(), Some("remove failed"));
- assert_eq!(
- app.pending_home_confirmation,
- Some(HomeActionKind::RemoveLocalKey)
- );
- }
-
- #[test]
- fn home_reset_action_transitions_to_setup() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_home_action(
- HomeActionState {
- kind: HomeActionKind::ResetDevice,
- label: "Reset This Device".into(),
- enabled: true,
- pending: false,
- },
- vec![Ok(HomeActionResult::IdentityState(
- IdentityGateState::Missing,
- ))],
- ),
- ));
-
- app.pending_home_confirmation = Some(HomeActionKind::ResetDevice);
- app.request_home_action(HomeActionKind::ResetDevice);
-
- assert_eq!(app.screen, AppScreen::Setup);
- assert_eq!(app.status_message, None);
- assert_eq!(app.pending_home_confirmation, None);
- }
-
- #[test]
- fn failed_home_reset_action_keeps_home_screen_and_message() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_home_action(
- HomeActionState {
- kind: HomeActionKind::ResetDevice,
- label: "Reset This Device".into(),
- enabled: true,
- pending: false,
- },
- vec![Err("reset failed".into())],
- ),
- ));
-
- app.pending_home_confirmation = Some(HomeActionKind::ResetDevice);
- app.request_home_action(HomeActionKind::ResetDevice);
-
- assert!(matches!(app.screen, AppScreen::Home { .. }));
- assert_eq!(app.status_message.as_deref(), Some("reset failed"));
- assert_eq!(
- app.pending_home_confirmation,
- Some(HomeActionKind::ResetDevice)
- );
- }
-
- #[test]
- fn import_action_transitions_to_home() {
- let encrypted_secret_key =
- fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD)
- .expect("fixture encrypted secret key");
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_import_action(
- ImportActionState {
- label: "Import Secret Key".into(),
- enabled: true,
- pending: false,
- },
- vec![Ok(Some(fixture_ready_state()))],
- ),
- ));
-
- app.pending_import_mode = Some(RadrootsSecretImportMode::EncryptedSecretKey);
- app.secret_key_input = Zeroizing::new(encrypted_secret_key);
- app.import_password_input = Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into());
- app.request_import_action();
-
- assert_eq!(app.screen, fixture_home_screen());
- assert_eq!(app.pending_import_mode, None);
- assert_eq!(app.secret_key_input.as_str(), "");
- assert_eq!(app.import_password_input.as_str(), "");
- }
-
- #[test]
- fn import_paste_action_populates_secret_key_input() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_import_action(
- ImportActionState {
- label: "Import Secret Key".into(),
- enabled: true,
- pending: false,
- },
- vec![],
- )
- .with_import_paste_action(
- PasteActionState {
- label: "Paste Secret Key".into(),
- enabled: true,
- pending: false,
- },
- vec![Ok(Some(FIXTURE_ALICE.nsec.into()))],
- ),
- ));
-
- app.pending_import_mode = Some(RadrootsSecretImportMode::EncryptedSecretKey);
- app.request_import_paste_action();
-
- assert_eq!(app.secret_key_input.as_str(), FIXTURE_ALICE.nsec);
- assert_eq!(app.status_message, None);
- }
-
- #[test]
- fn remote_signer_preview_moves_entry_into_review_state() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_remote_signer_action(SetupActionState {
- label: "Connect Remote Signer".into(),
- enabled: true,
- pending: false,
- })
- .with_remote_signer_preview(vec![Ok(fixture_remote_signer_preview())]),
- ));
-
- app.open_remote_signer_entry();
- app.remote_signer_input =
- Zeroizing::new("http://localhost/connect?uri=bunker%3A%2F%2Fexample".into());
- app.request_remote_signer_preview();
-
- assert_eq!(
- app.remote_signer_entry_state,
- RemoteSignerEntryState::Review(fixture_remote_signer_preview())
- );
- }
-
- #[test]
- fn remote_signer_connect_enters_waiting_state() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_remote_signer_action(SetupActionState {
- label: "Connect Remote Signer".into(),
- enabled: true,
- pending: false,
- })
- .with_remote_signer_request(vec![Ok(None)])
- .with_pending_remote_signer(Some(fixture_pending_remote_signer())),
- ));
-
- app.remote_signer_entry_state =
- RemoteSignerEntryState::Review(fixture_remote_signer_preview());
- app.remote_signer_input =
- Zeroizing::new("http://localhost/connect?uri=bunker%3A%2F%2Fexample".into());
- app.request_remote_signer_connect();
-
- assert_eq!(
- app.remote_signer_entry_state,
- RemoteSignerEntryState::WaitingApproval(fixture_pending_remote_signer())
- );
- }
-
- #[test]
- fn cancel_pending_remote_signer_clears_waiting_state() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_remote_signer_action(SetupActionState {
- label: "Connect Remote Signer".into(),
- enabled: true,
- pending: false,
- })
- .with_pending_remote_signer(Some(fixture_pending_remote_signer()))
- .with_cancel_pending_remote_signer(vec![Ok(())]),
- ));
-
- app.request_cancel_pending_remote_signer();
-
- assert_eq!(
- app.remote_signer_entry_state,
- RemoteSignerEntryState::Closed
- );
- }
-
- #[test]
- fn encrypted_backup_home_action_reveals_secret_key_without_leaving_home() {
- let encrypted_secret_key =
- fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD)
- .expect("fixture encrypted secret key");
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_home_action(
- HomeActionState {
- kind: HomeActionKind::BackupSecretKey,
- label: "Back Up Secret Key".into(),
- enabled: true,
- pending: false,
- },
- vec![],
- )
- .with_secret_key_backup_request(vec![Ok(
- HomeActionResult::RevealEncryptedSecretKey {
- ncryptsec: encrypted_secret_key.clone(),
- },
- )]),
- ));
-
- app.pending_secret_key_backup_entry = true;
- app.secret_key_backup_password_input = Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into());
- app.secret_key_backup_password_confirm_input =
- Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into());
- app.request_secret_key_backup_action();
-
- assert!(matches!(app.screen, AppScreen::Home { .. }));
- assert_eq!(app.pending_home_confirmation, None);
- assert_eq!(app.pending_secret_key_backup_entry, false);
- let Some(RevealedSecretMaterial::EncryptedSecretKey(value)) =
- app.revealed_secret_material.as_ref()
- else {
- panic!("expected encrypted secret backup");
- };
- assert_eq!(value.as_str(), encrypted_secret_key);
- }
-
- #[test]
- fn deferred_encrypted_backup_home_action_reveals_secret_key_after_poll() {
- let encrypted_secret_key =
- fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD)
- .expect("fixture encrypted secret key");
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_home_action(
- HomeActionState {
- kind: HomeActionKind::BackupSecretKey,
- label: "Back Up Secret Key".into(),
- enabled: true,
- pending: true,
- },
- vec![],
- )
- .with_secret_key_backup_request(vec![Ok(HomeActionResult::None)])
- .with_home_action_poll(vec![Ok(Some(
- HomeActionResult::RevealEncryptedSecretKey {
- ncryptsec: encrypted_secret_key.clone(),
- },
- ))]),
- ));
-
- app.pending_secret_key_backup_entry = true;
- app.secret_key_backup_password_input = Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into());
- app.secret_key_backup_password_confirm_input =
- Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into());
- app.request_secret_key_backup_action();
- assert_eq!(app.revealed_secret_material, None);
-
- app.sync_backend();
-
- let Some(RevealedSecretMaterial::EncryptedSecretKey(value)) =
- app.revealed_secret_material.as_ref()
- else {
- panic!("expected encrypted secret backup");
- };
- assert_eq!(value.as_str(), encrypted_secret_key);
- }
-
- #[test]
- fn raw_secret_reveal_home_action_uses_advanced_path() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_home_action(
- HomeActionState {
- kind: HomeActionKind::RevealRawSecretKey,
- label: "Reveal Raw Secret Key".into(),
- enabled: true,
- pending: false,
- },
- vec![Ok(HomeActionResult::RevealRawSecretKey {
- nsec: FIXTURE_ALICE.nsec.into(),
- })],
- ),
- ));
-
- app.pending_home_confirmation = Some(HomeActionKind::RevealRawSecretKey);
- app.request_home_action(HomeActionKind::RevealRawSecretKey);
-
- let Some(RevealedSecretMaterial::RawSecretKey { nsec, .. }) =
- app.revealed_secret_material.as_ref()
- else {
- panic!("expected raw secret reveal");
- };
- assert_eq!(nsec.as_str(), FIXTURE_ALICE.nsec);
- }
-
- #[test]
- fn raw_secret_reveal_expires_after_timeout() {
- let mut app = RadrootsApp::new(Box::new(MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )));
- app.revealed_secret_material = Some(RevealedSecretMaterial::RawSecretKey {
- nsec: Zeroizing::new(FIXTURE_ALICE.nsec.into()),
- revealed_at: Instant::now() - RAW_SECRET_REVEAL_TIMEOUT - Duration::from_secs(1),
- });
-
- app.sync_revealed_secret_material_lifetime();
-
- assert_eq!(app.revealed_secret_material, None);
- }
-
- #[test]
- fn raw_secret_reveal_clears_when_app_loses_focus() {
- let mut app = RadrootsApp::new(Box::new(MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )));
- app.revealed_secret_material = Some(RevealedSecretMaterial::RawSecretKey {
- nsec: Zeroizing::new(FIXTURE_ALICE.nsec.into()),
- revealed_at: Instant::now(),
- });
-
- let ctx = egui::Context::default();
- ctx.input_mut(|input| {
- input
- .raw
- .viewports
- .entry(egui::ViewportId::ROOT)
- .or_default()
- .focused = Some(false);
- });
- app.clear_raw_secret_when_app_unfocused(&ctx);
-
- assert_eq!(app.revealed_secret_material, None);
- }
-
- #[test]
- fn deferred_home_location_lookup_updates_after_poll() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(fixture_ready_state()),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_offline_geocoder_state(RadrootsOfflineGeocoderState::Ready, vec![])
- .with_reverse_lookup(
- vec![Ok(())],
- vec![Ok(Some(Ok(vec![RadrootsResolvedLocation {
- id: 7,
- name: "Paris".into(),
- admin1_id: Some(11),
- admin1_name: Some("Ile-de-France".into()),
- country_id: "FR".into(),
- country_name: Some("France".into()),
- point: RadrootsLocationPoint {
- lat: 48.8566,
- lng: 2.3522,
- },
- }])))],
- ),
- ));
-
- app.home_location_tools
- .set_query_inputs("48.8566", "2.3522");
- app.home_location_tools
- .begin_resolve_with_backend(app.backend.as_ref());
- assert!(app.home_location_tools.is_pending());
-
- app.sync_backend();
-
- assert_eq!(app.home_location_tools.status_message(), None);
- assert_eq!(
- app.home_location_tools
- .lookup_result()
- .as_ref()
- .map(|result| result.matches[0].name.as_str()),
- Some("Paris")
- );
- }
-
- #[test]
- fn startup_uses_initial_offline_geocoder_state() {
- let app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_offline_geocoder_state(RadrootsOfflineGeocoderState::Initializing, vec![]),
- ));
-
- assert_eq!(
- app.offline_geocoder_state,
- Some(RadrootsOfflineGeocoderState::Initializing)
- );
- }
-
- #[test]
- fn offline_geocoder_state_updates_after_poll() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_offline_geocoder_state(
- RadrootsOfflineGeocoderState::Initializing,
- vec![Ok(Some(RadrootsOfflineGeocoderState::Ready))],
- ),
- ));
-
- app.sync_backend();
-
- assert_eq!(
- app.offline_geocoder_state,
- Some(RadrootsOfflineGeocoderState::Ready)
- );
- }
-
- #[test]
- fn offline_geocoder_failure_keeps_user_and_debug_messages() {
- let mut app = RadrootsApp::new(Box::new(
- MockBackend::new(
- Ok(IdentityGateState::Missing),
- vec![],
- vec![],
- SetupActionState {
- label: "Generate New Key".into(),
- enabled: true,
- pending: false,
- },
- )
- .with_offline_geocoder_state(
- RadrootsOfflineGeocoderState::Initializing,
- vec![Ok(Some(RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Desktop,
- "failed to open staged geocoder db",
- )))],
- ),
- ));
-
- app.sync_backend();
-
- assert_eq!(
- app.offline_geocoder_state,
- Some(RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Desktop,
- "failed to open staged geocoder db",
- ))
- );
- assert_eq!(
- app.offline_geocoder_state
- .as_ref()
- .and_then(RadrootsOfflineGeocoderState::user_message),
- Some("Offline geocoder could not be initialized on this device.")
- );
- assert_eq!(
- app.offline_geocoder_state
- .as_ref()
- .and_then(RadrootsOfflineGeocoderState::debug_message),
- Some("failed to open staged geocoder db")
- );
- let diagnostic = app
- .offline_geocoder_state
- .as_ref()
- .and_then(RadrootsOfflineGeocoderState::diagnostic)
- .unwrap();
- assert_eq!(diagnostic.platform_code, "desktop");
- assert_eq!(diagnostic.asset_revision, None);
- assert_eq!(diagnostic.code, "initialization_failed");
- assert!(
- !diagnostic
- .export_text()
- .contains("failed to open staged geocoder db")
- );
- }
-}
diff --git a/crates/shared/core/src/location_resolver.rs b/crates/shared/core/src/location_resolver.rs
@@ -1,123 +0,0 @@
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct RadrootsLocationPoint {
- pub lat: f64,
- pub lng: f64,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct RadrootsLocationReverseOptions {
- pub limit: usize,
- pub degree_offset: f64,
-}
-
-impl Default for RadrootsLocationReverseOptions {
- fn default() -> Self {
- Self {
- limit: 1,
- degree_offset: 0.5,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct RadrootsResolvedLocation {
- pub id: i64,
- pub name: String,
- pub admin1_id: Option<i64>,
- pub admin1_name: Option<String>,
- pub country_id: String,
- pub country_name: Option<String>,
- pub point: RadrootsLocationPoint,
-}
-
-pub type RadrootsReverseLocationLookupResult =
- Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError>;
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct RadrootsLocationCountry {
- pub country_id: String,
- pub country_name: Option<String>,
- pub center: RadrootsLocationPoint,
-}
-
-pub type RadrootsLocationCountryListResult =
- Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError>;
-
-pub type RadrootsLocationCountryCenterLookupResult =
- Result<RadrootsLocationPoint, RadrootsLocationResolverError>;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsLocationResolverError {
- Unsupported,
- Initializing,
- Unavailable,
- CountryCenterNotFound { country_id: String },
- QueryFailed { message: String },
-}
-
-impl RadrootsLocationResolverError {
- pub fn code(&self) -> &'static str {
- match self {
- Self::Unsupported => "unsupported",
- Self::Initializing => "initializing",
- Self::Unavailable => "unavailable",
- Self::CountryCenterNotFound { .. } => "country_center_not_found",
- Self::QueryFailed { .. } => "query_failed",
- }
- }
-
- pub fn user_message(&self) -> &'static str {
- match self {
- Self::Unsupported => "Offline location resolution is not available on this platform.",
- Self::Initializing => {
- "Offline location resolution is still initializing on this device."
- }
- Self::Unavailable => "Offline location resolution is not available on this device.",
- Self::CountryCenterNotFound { .. } => "The requested country center is not available.",
- Self::QueryFailed { .. } => "The offline location query could not be completed.",
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn reverse_options_default_matches_geocoder_defaults() {
- let options = RadrootsLocationReverseOptions::default();
-
- assert_eq!(options.limit, 1);
- assert_eq!(options.degree_offset, 0.5);
- }
-
- #[test]
- fn location_resolver_error_codes_are_stable() {
- assert_eq!(
- RadrootsLocationResolverError::Unsupported.code(),
- "unsupported"
- );
- assert_eq!(
- RadrootsLocationResolverError::Initializing.code(),
- "initializing"
- );
- assert_eq!(
- RadrootsLocationResolverError::Unavailable.code(),
- "unavailable"
- );
- assert_eq!(
- RadrootsLocationResolverError::CountryCenterNotFound {
- country_id: "US".to_owned(),
- }
- .code(),
- "country_center_not_found"
- );
- assert_eq!(
- RadrootsLocationResolverError::QueryFailed {
- message: "sqlite failed".to_owned(),
- }
- .code(),
- "query_failed"
- );
- }
-}
diff --git a/crates/shared/core/src/offline_geocoder.rs b/crates/shared/core/src/offline_geocoder.rs
@@ -1,237 +0,0 @@
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsOfflineGeocoderPlatform {
- Desktop,
- Ios,
- Android,
- Web,
-}
-
-impl RadrootsOfflineGeocoderPlatform {
- pub fn code(self) -> &'static str {
- match self {
- Self::Desktop => "desktop",
- Self::Ios => "ios",
- Self::Android => "android",
- Self::Web => "web",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsOfflineGeocoderUnavailableKind {
- MissingBuildAsset,
- InitializationFailed,
- InternalError,
-}
-
-impl RadrootsOfflineGeocoderUnavailableKind {
- pub fn code(self) -> &'static str {
- match self {
- Self::MissingBuildAsset => "missing_build_asset",
- Self::InitializationFailed => "initialization_failed",
- Self::InternalError => "internal_error",
- }
- }
-
- pub fn technical_message(self) -> &'static str {
- match self {
- Self::MissingBuildAsset => {
- "The offline geocoder data file is missing from this app build."
- }
- Self::InitializationFailed => {
- "The offline geocoder data file could not be prepared on this device."
- }
- Self::InternalError => {
- "The app could not complete offline geocoder setup because of an internal error."
- }
- }
- }
-
- pub fn user_message(self) -> &'static str {
- match self {
- Self::MissingBuildAsset => "Offline geocoder is not available in this build.",
- Self::InitializationFailed | Self::InternalError => {
- "Offline geocoder could not be initialized on this device."
- }
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsOfflineGeocoderDiagnostic {
- pub platform_code: &'static str,
- pub asset_revision: Option<String>,
- pub code: &'static str,
- pub summary_label: &'static str,
- pub user_message: &'static str,
- pub technical_message: &'static str,
-}
-
-impl RadrootsOfflineGeocoderDiagnostic {
- pub fn export_text(&self) -> String {
- format!(
- "offline geocoder diagnostic\nplatform: {}\nasset_revision: {}\ncode: {}\nstatus: {}\nuser: {}\ntechnical: {}",
- self.platform_code,
- self.asset_revision.as_deref().unwrap_or("unknown"),
- self.code,
- self.summary_label,
- self.user_message,
- self.technical_message
- )
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsOfflineGeocoderState {
- Initializing,
- Ready,
- Unavailable {
- kind: RadrootsOfflineGeocoderUnavailableKind,
- platform: RadrootsOfflineGeocoderPlatform,
- asset_revision: Option<String>,
- debug_message: String,
- },
-}
-
-impl RadrootsOfflineGeocoderState {
- pub fn unavailable(
- kind: RadrootsOfflineGeocoderUnavailableKind,
- platform: RadrootsOfflineGeocoderPlatform,
- debug_message: impl Into<String>,
- ) -> Self {
- Self::Unavailable {
- kind,
- platform,
- asset_revision: None,
- debug_message: debug_message.into(),
- }
- }
-
- pub fn unavailable_with_revision(
- kind: RadrootsOfflineGeocoderUnavailableKind,
- platform: RadrootsOfflineGeocoderPlatform,
- asset_revision: impl Into<String>,
- debug_message: impl Into<String>,
- ) -> Self {
- Self::Unavailable {
- kind,
- platform,
- asset_revision: Some(asset_revision.into()),
- debug_message: debug_message.into(),
- }
- }
-
- pub fn debug_message(&self) -> Option<&str> {
- match self {
- Self::Unavailable { debug_message, .. } => Some(debug_message.as_str()),
- Self::Initializing | Self::Ready => None,
- }
- }
-
- pub fn diagnostic(&self) -> Option<RadrootsOfflineGeocoderDiagnostic> {
- match self {
- Self::Unavailable {
- kind,
- platform,
- asset_revision,
- ..
- } => Some(RadrootsOfflineGeocoderDiagnostic {
- platform_code: platform.code(),
- asset_revision: asset_revision.clone(),
- code: kind.code(),
- summary_label: self.summary_label(),
- user_message: kind.user_message(),
- technical_message: kind.technical_message(),
- }),
- Self::Initializing | Self::Ready => None,
- }
- }
-
- pub fn summary_label(&self) -> &'static str {
- match self {
- Self::Initializing => "Offline geocoder: initializing",
- Self::Ready => "Offline geocoder: ready",
- Self::Unavailable { .. } => "Offline geocoder unavailable",
- }
- }
-
- pub fn platform(&self) -> Option<RadrootsOfflineGeocoderPlatform> {
- match self {
- Self::Unavailable { platform, .. } => Some(*platform),
- Self::Initializing | Self::Ready => None,
- }
- }
-
- pub fn asset_revision(&self) -> Option<&str> {
- match self {
- Self::Unavailable { asset_revision, .. } => asset_revision.as_deref(),
- Self::Initializing | Self::Ready => None,
- }
- }
-
- pub fn technical_message(&self) -> Option<&'static str> {
- match self {
- Self::Unavailable { kind, .. } => Some(kind.technical_message()),
- Self::Initializing | Self::Ready => None,
- }
- }
-
- pub fn user_message(&self) -> Option<&'static str> {
- match self {
- Self::Unavailable { kind, .. } => Some(kind.user_message()),
- Self::Initializing | Self::Ready => None,
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn unavailable_state_exposes_release_safe_diagnostic() {
- let state = RadrootsOfflineGeocoderState::unavailable(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Desktop,
- "failed to open staged geocoder db: /tmp/geonames.db",
- );
- let diagnostic = state.diagnostic().unwrap();
-
- assert_eq!(diagnostic.platform_code, "desktop");
- assert_eq!(diagnostic.asset_revision, None);
- assert_eq!(diagnostic.code, "initialization_failed");
- assert_eq!(diagnostic.summary_label, "Offline geocoder unavailable");
- assert_eq!(
- diagnostic.user_message,
- "Offline geocoder could not be initialized on this device."
- );
- assert_eq!(
- diagnostic.technical_message,
- "The offline geocoder data file could not be prepared on this device."
- );
- assert!(!diagnostic.export_text().contains("/tmp/geonames.db"));
- }
-
- #[test]
- fn unavailable_state_with_revision_exports_release_safe_platform_context() {
- let state = RadrootsOfflineGeocoderState::unavailable_with_revision(
- RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
- RadrootsOfflineGeocoderPlatform::Android,
- "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c",
- "failed to open staged android geocoder db: /data/user/0/org.radroots.app.android/files/geocoder.db",
- );
- let diagnostic = state.diagnostic().unwrap();
- let export_text = diagnostic.export_text();
-
- assert_eq!(diagnostic.platform_code, "android");
- assert_eq!(
- diagnostic.asset_revision.as_deref(),
- Some("6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c")
- );
- assert!(export_text.contains("platform: android"));
- assert!(export_text.contains(
- "asset_revision: 6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"
- ));
- assert!(!export_text.contains("/data/user/0/org.radroots.app.android/files/geocoder.db"));
- }
-}
diff --git a/crates/shared/core/src/remote_signer.rs b/crates/shared/core/src/remote_signer.rs
@@ -1,29 +0,0 @@
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsRemoteSignerPreview {
- pub source_label: String,
- pub signer_npub: String,
- pub relays: Vec<String>,
- pub requested_permissions: Vec<String>,
-}
-
-impl RadrootsRemoteSignerPreview {
- pub fn pending_summary(&self) -> RadrootsPendingRemoteSignerConnection {
- RadrootsPendingRemoteSignerConnection {
- signer_npub: self.signer_npub.clone(),
- relays: self.relays.clone(),
- auth_url: None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsPendingRemoteSignerConnection {
- pub signer_npub: String,
- pub relays: Vec<String>,
- pub auth_url: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsRemoteSignerSignedNote {
- pub event_id_hex: String,
-}
diff --git a/crates/shared/core/src/secret_keys.rs b/crates/shared/core/src/secret_keys.rs
@@ -1,58 +0,0 @@
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum RadrootsSecretImportMode {
- #[default]
- EncryptedSecretKey,
- RawSecretKey,
-}
-
-impl RadrootsSecretImportMode {
- pub fn helper_text(self) -> &'static str {
- match self {
- Self::EncryptedSecretKey => {
- "Import an existing local identity by entering its encrypted secret key and password."
- }
- Self::RawSecretKey => {
- "Advanced: import an existing local identity by entering its raw nsec secret key."
- }
- }
- }
-
- pub fn hint_text(self) -> &'static str {
- match self {
- Self::EncryptedSecretKey => "ncryptsec1...",
- Self::RawSecretKey => "nsec1...",
- }
- }
-
- pub fn mode_label(self) -> &'static str {
- match self {
- Self::EncryptedSecretKey => "Encrypted Secret Key",
- Self::RawSecretKey => "Raw Secret Key",
- }
- }
-
- pub fn switch_label(self) -> &'static str {
- match self {
- Self::EncryptedSecretKey => "Use Raw Secret Key Instead",
- Self::RawSecretKey => "Use Encrypted Secret Key Instead",
- }
- }
-
- pub fn requires_password(self) -> bool {
- matches!(self, Self::EncryptedSecretKey)
- }
-
- pub fn toggle(self) -> Self {
- match self {
- Self::EncryptedSecretKey => Self::RawSecretKey,
- Self::RawSecretKey => Self::EncryptedSecretKey,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsSecretImportRequest {
- pub mode: RadrootsSecretImportMode,
- pub secret_text: String,
- pub password: Option<String>,
-}
diff --git a/crates/shared/core/src/storage_paths.rs b/crates/shared/core/src/storage_paths.rs
@@ -1,122 +0,0 @@
-use std::path::{Path, PathBuf};
-
-use radroots_runtime_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace,
-};
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppStorageLayout {
- pub runtime_root: PathBuf,
- pub app_paths: RadrootsPaths,
-}
-
-fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> {
- RadrootsRuntimeNamespace::app("app")
- .map_err(|source| format!("failed to resolve app runtime namespace: {source}"))
-}
-
-fn runtime_root_from_paths(roots: &RadrootsPaths) -> Result<PathBuf, String> {
- roots
- .config
- .parent()
- .map(Path::to_path_buf)
- .ok_or_else(|| "resolved app config root had no parent".to_owned())
-}
-
-pub fn interactive_user_app_storage_layout_with_resolver(
- resolver: &RadrootsPathResolver,
-) -> Result<RadrootsAppStorageLayout, String> {
- let roots = resolver
- .resolve(
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- )
- .map_err(|source| format!("failed to resolve app interactive-user roots: {source}"))?;
- let namespace = app_namespace()?;
- Ok(RadrootsAppStorageLayout {
- runtime_root: runtime_root_from_paths(&roots)?,
- app_paths: roots.namespaced(&namespace),
- })
-}
-
-pub fn mobile_native_app_storage_layout(
- platform: RadrootsPlatform,
- base_root: &Path,
-) -> Result<RadrootsAppStorageLayout, String> {
- let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default());
- let roots = resolver
- .resolve(
- RadrootsPathProfile::MobileNative,
- &RadrootsPathOverrides::mobile(RadrootsPaths::from_base_root(base_root)),
- )
- .map_err(|source| format!("failed to resolve app mobile-native roots: {source}"))?;
- let namespace = app_namespace()?;
- Ok(RadrootsAppStorageLayout {
- runtime_root: runtime_root_from_paths(&roots)?,
- app_paths: roots.namespaced(&namespace),
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- interactive_user_app_storage_layout_with_resolver, mobile_native_app_storage_layout,
- };
- use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform};
- use std::path::PathBuf;
-
- #[test]
- fn interactive_user_layout_keeps_runtime_root_and_namespaced_paths() {
- let resolver = RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(PathBuf::from("/home/treesap")),
- ..RadrootsHostEnvironment::default()
- },
- );
-
- let layout =
- interactive_user_app_storage_layout_with_resolver(&resolver).expect("app layout");
-
- assert_eq!(
- layout.runtime_root,
- PathBuf::from("/home/treesap/.radroots")
- );
- assert_eq!(
- layout.app_paths.data,
- PathBuf::from("/home/treesap/.radroots/data/apps/app")
- );
- assert_eq!(
- layout.app_paths.logs,
- PathBuf::from("/home/treesap/.radroots/logs/apps/app")
- );
- }
-
- #[test]
- fn mobile_native_layout_keeps_explicit_runtime_root_and_namespaced_paths() {
- let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots");
-
- let layout =
- mobile_native_app_storage_layout(RadrootsPlatform::Android, base_root.as_path())
- .expect("mobile layout");
-
- assert_eq!(layout.runtime_root, base_root);
- assert_eq!(
- layout.app_paths.config,
- PathBuf::from(
- "/data/user/0/org.radroots.app.android/no_backup/RadRoots/config/apps/app"
- )
- );
- assert_eq!(
- layout.app_paths.data,
- PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app")
- );
- assert_eq!(
- layout.app_paths.secrets,
- PathBuf::from(
- "/data/user/0/org.radroots.app.android/no_backup/RadRoots/secrets/apps/app"
- )
- );
- }
-}
diff --git a/crates/shared/remote_signer/Cargo.toml b/crates/shared/remote_signer/Cargo.toml
@@ -1,29 +0,0 @@
-[package]
-name = "radroots_app_remote_signer"
-authors.workspace = true
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots app local remote signer session support"
-publish = false
-
-[lints]
-workspace = true
-
-[dependencies]
-nostr = { workspace = true, features = ["nip44"] }
-radroots_identity.workspace = true
-radroots_nostr_accounts = { workspace = true, features = ["memory-vault"] }
-radroots_nostr.workspace = true
-radroots_nostr_connect.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-tokio.workspace = true
-url.workspace = true
-
-[dev-dependencies]
-radroots_app_test_support = { path = "../test_support" }
-tempfile = "3.23.0"
diff --git a/crates/shared/remote_signer/src/action.rs b/crates/shared/remote_signer/src/action.rs
@@ -1,327 +0,0 @@
-use crate::protocol::{
- RadrootsAppRemoteSignerProgressUpdate, RadrootsAppRemoteSignerSignedEvent,
- radroots_app_remote_signer_sign_kind1_note_with_progress,
-};
-use crate::session::RadrootsAppRemoteSignerSessionRecord;
-use std::marker::PhantomData;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, Mutex};
-
-type RadrootsAppRemoteSignerSignNoteFn = Arc<
- dyn Fn(
- &RadrootsAppRemoteSignerSessionRecord,
- &str,
- &str,
- Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync>,
- ) -> Result<RadrootsAppRemoteSignerSignedEvent, String>
- + Send
- + Sync,
->;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsAppRemoteSignerActionState {
- Idle,
- Signing,
- AwaitingAuthorization { url: String },
-}
-
-pub trait RadrootsAppRemoteSignerActionControllerHooks: Clone + Send + Sync + 'static {
- type ReadyState: Send + Sync + 'static;
-
- fn selected_active_session(
- &self,
- ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String>;
-
- fn complete_sign_event(
- &self,
- signed_event: RadrootsAppRemoteSignerSignedEvent,
- ) -> Result<Self::ReadyState, String>;
-}
-
-pub struct RadrootsAppRemoteSignerActionController<H>
-where
- H: RadrootsAppRemoteSignerActionControllerHooks,
-{
- hooks: H,
- sign_kind1_note: RadrootsAppRemoteSignerSignNoteFn,
- update: Arc<Mutex<Option<Result<Option<H::ReadyState>, String>>>>,
- changed: Arc<AtomicBool>,
- signing: Arc<AtomicBool>,
- state: Arc<Mutex<RadrootsAppRemoteSignerActionState>>,
- _ready_state: PhantomData<H::ReadyState>,
-}
-
-impl<H> Clone for RadrootsAppRemoteSignerActionController<H>
-where
- H: RadrootsAppRemoteSignerActionControllerHooks,
-{
- fn clone(&self) -> Self {
- Self {
- hooks: self.hooks.clone(),
- sign_kind1_note: Arc::clone(&self.sign_kind1_note),
- update: Arc::clone(&self.update),
- changed: Arc::clone(&self.changed),
- signing: Arc::clone(&self.signing),
- state: Arc::clone(&self.state),
- _ready_state: PhantomData,
- }
- }
-}
-
-impl<H> RadrootsAppRemoteSignerActionController<H>
-where
- H: RadrootsAppRemoteSignerActionControllerHooks,
-{
- pub fn new(hooks: H) -> Self {
- Self {
- hooks,
- sign_kind1_note: Arc::new(default_sign_kind1_note),
- update: Arc::new(Mutex::new(None)),
- changed: Arc::new(AtomicBool::new(false)),
- signing: Arc::new(AtomicBool::new(false)),
- state: Arc::new(Mutex::new(RadrootsAppRemoteSignerActionState::Idle)),
- _ready_state: PhantomData,
- }
- }
-
- #[cfg(test)]
- fn new_with_ops(hooks: H, sign_kind1_note: RadrootsAppRemoteSignerSignNoteFn) -> Self {
- Self {
- hooks,
- sign_kind1_note,
- update: Arc::new(Mutex::new(None)),
- changed: Arc::new(AtomicBool::new(false)),
- signing: Arc::new(AtomicBool::new(false)),
- state: Arc::new(Mutex::new(RadrootsAppRemoteSignerActionState::Idle)),
- _ready_state: PhantomData,
- }
- }
-
- pub fn take_update(&self) -> Option<Result<Option<H::ReadyState>, String>> {
- if !self.changed.swap(false, Ordering::AcqRel) {
- return None;
- }
- self.update.lock().ok().and_then(|mut slot| slot.take())
- }
-
- pub fn is_signing(&self) -> bool {
- self.signing.load(Ordering::Acquire)
- }
-
- pub fn state(&self) -> RadrootsAppRemoteSignerActionState {
- self.state
- .lock()
- .map(|state| state.clone())
- .unwrap_or(RadrootsAppRemoteSignerActionState::Idle)
- }
-
- pub fn begin_sign_kind1_note(&self, content: &str) -> Result<(), String> {
- if self.signing.swap(true, Ordering::AcqRel) {
- return Err("remote signer note signing is already running".to_owned());
- }
-
- let Some((record, client_secret_key_hex)) = self.hooks.selected_active_session()? else {
- self.signing.store(false, Ordering::Release);
- return Err("select a remote signer account before signing a note".to_owned());
- };
- let note_content = content.trim().to_owned();
- if note_content.is_empty() {
- self.signing.store(false, Ordering::Release);
- return Err("enter a note before requesting a remote signature".to_owned());
- }
-
- self.set_state(RadrootsAppRemoteSignerActionState::Signing);
- if let Ok(mut slot) = self.update.lock() {
- *slot = None;
- }
-
- let tracker = self.clone();
- std::thread::spawn(move || {
- let progress_tracker = tracker.clone();
- let progress: Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync> =
- Arc::new(move |update| progress_tracker.apply_progress(update));
- let outcome = (tracker.sign_kind1_note)(
- &record,
- client_secret_key_hex.as_str(),
- note_content.as_str(),
- progress,
- )
- .and_then(|signed_event| tracker.hooks.complete_sign_event(signed_event));
-
- tracker.set_state(RadrootsAppRemoteSignerActionState::Idle);
- tracker.signing.store(false, Ordering::Release);
- match outcome {
- Ok(result) => tracker.push_update(Ok(Some(result))),
- Err(error) => tracker.push_update(Err(error)),
- }
- });
-
- Ok(())
- }
-
- fn apply_progress(&self, update: RadrootsAppRemoteSignerProgressUpdate) {
- match update {
- RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url } => {
- let next =
- RadrootsAppRemoteSignerActionState::AwaitingAuthorization { url: url.clone() };
- if self.set_state(next) {
- self.push_update(Err(format!(
- "authorize the remote signer to continue: {url}"
- )));
- }
- }
- }
- }
-
- fn push_update(&self, result: Result<Option<H::ReadyState>, String>) {
- if let Ok(mut slot) = self.update.lock() {
- *slot = Some(result);
- self.changed.store(true, Ordering::Release);
- }
- }
-
- fn set_state(&self, next: RadrootsAppRemoteSignerActionState) -> bool {
- if let Ok(mut state) = self.state.lock() {
- if *state == next {
- return false;
- }
- *state = next;
- return true;
- }
- false
- }
-}
-
-fn default_sign_kind1_note(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
- content: &str,
- progress: Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync>,
-) -> Result<RadrootsAppRemoteSignerSignedEvent, String> {
- radroots_app_remote_signer_sign_kind1_note_with_progress(
- record,
- client_secret_key_hex,
- content,
- move |update| progress(update),
- )
- .map_err(|error| error.to_string())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::session::RadrootsAppRemoteSignerSessionRecord;
- use radroots_app_test_support::{FIXTURE_ALICE, FIXTURE_BOB, fixture_identity};
- use std::sync::mpsc;
- use std::sync::{Condvar, Mutex};
- use std::time::Duration;
-
- #[derive(Clone)]
- struct TestHooks {
- session: Option<(RadrootsAppRemoteSignerSessionRecord, String)>,
- }
-
- impl RadrootsAppRemoteSignerActionControllerHooks for TestHooks {
- type ReadyState = String;
-
- fn selected_active_session(
- &self,
- ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String> {
- Ok(self.session.clone())
- }
-
- fn complete_sign_event(
- &self,
- signed_event: RadrootsAppRemoteSignerSignedEvent,
- ) -> Result<Self::ReadyState, String> {
- Ok(signed_event.event_id_hex)
- }
- }
-
- fn fixture_session() -> RadrootsAppRemoteSignerSessionRecord {
- let client = fixture_identity(&FIXTURE_ALICE)
- .expect("client")
- .to_public();
- let signer = fixture_identity(&FIXTURE_BOB).expect("signer").to_public();
- let mut record = RadrootsAppRemoteSignerSessionRecord::pending(
- client,
- signer.clone(),
- vec!["ws://localhost:8080".to_owned()],
- );
- record.user_identity = Some(signer);
- record.status = crate::session::RadrootsAppRemoteSignerSessionStatus::Active;
- record
- }
-
- fn wait_for_update(
- controller: &RadrootsAppRemoteSignerActionController<TestHooks>,
- ) -> Result<Option<String>, String> {
- let deadline = std::time::Instant::now() + Duration::from_secs(2);
- loop {
- if let Some(update) = controller.take_update() {
- return update;
- }
- if std::time::Instant::now() >= deadline {
- panic!("timed out waiting for action update");
- }
- std::thread::sleep(Duration::from_millis(10));
- }
- }
-
- #[test]
- fn sign_controller_reports_auth_challenge_then_success() {
- let hooks = TestHooks {
- session: Some((fixture_session(), "client-secret".to_owned())),
- };
- let (challenge_seen_tx, challenge_seen_rx) = mpsc::channel();
- let release_gate = Arc::new((Mutex::new(false), Condvar::new()));
- let controller = RadrootsAppRemoteSignerActionController::new_with_ops(
- hooks,
- Arc::new({
- let release_gate = Arc::clone(&release_gate);
- move |_, _, _, progress| {
- progress(RadrootsAppRemoteSignerProgressUpdate::AuthChallenge {
- url: "http://localhost/auth".to_owned(),
- });
- challenge_seen_tx.send(()).expect("challenge seen");
- let (released, condvar) = &*release_gate;
- let mut released = released.lock().expect("release gate lock");
- while !*released {
- released = condvar.wait(released).expect("release gate wait");
- }
- Ok(RadrootsAppRemoteSignerSignedEvent {
- event_id_hex: "deadbeef".to_owned(),
- event_json: "{\"id\":\"deadbeef\"}".to_owned(),
- relays: vec!["ws://localhost:8080".to_owned()],
- })
- }
- }),
- );
-
- controller
- .begin_sign_kind1_note("hello from remote signer")
- .expect("begin signing");
-
- challenge_seen_rx
- .recv_timeout(Duration::from_secs(1))
- .expect("challenge notification");
- let first = wait_for_update(&controller).expect_err("auth challenge status");
- assert_eq!(
- first,
- "authorize the remote signer to continue: http://localhost/auth"
- );
- assert_eq!(
- controller.state(),
- RadrootsAppRemoteSignerActionState::AwaitingAuthorization {
- url: "http://localhost/auth".to_owned()
- }
- );
-
- let (released, condvar) = &*release_gate;
- *released.lock().expect("release gate lock") = true;
- condvar.notify_one();
- let second = wait_for_update(&controller).expect("signed");
- assert_eq!(second, Some("deadbeef".to_owned()));
- assert_eq!(controller.state(), RadrootsAppRemoteSignerActionState::Idle);
- }
-}
diff --git a/crates/shared/remote_signer/src/controller.rs b/crates/shared/remote_signer/src/controller.rs
@@ -1,852 +0,0 @@
-use crate::protocol::{
- RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome,
- RadrootsAppRemoteSignerPendingPoller, RadrootsAppRemoteSignerPendingSession,
- RadrootsAppRemoteSignerProgressUpdate, radroots_app_remote_signer_connect_pending,
- radroots_app_remote_signer_open_pending_poller,
- radroots_app_remote_signer_poll_pending_poller_with_progress,
-};
-use crate::session::RadrootsAppRemoteSignerSessionRecord;
-use std::marker::PhantomData;
-use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
-use std::sync::{Arc, Mutex};
-use std::time::Duration;
-
-type RadrootsAppRemoteSignerConnectPendingFn =
- Arc<dyn Fn(&str) -> Result<RadrootsAppRemoteSignerPendingSession, String> + Send + Sync>;
-type RadrootsAppRemoteSignerPollPendingFn = Arc<
- dyn Fn(
- &RadrootsAppRemoteSignerSessionRecord,
- &str,
- Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync>,
- ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, String>
- + Send
- + Sync,
->;
-type RadrootsAppRemoteSignerSleepFn = Arc<dyn Fn(Duration) + Send + Sync>;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsAppRemoteSignerPendingState {
- Idle,
- WaitingApproval,
- AwaitingAuthorization { url: String },
- TransportFailure { message: String },
-}
-
-pub trait RadrootsAppRemoteSignerControllerHooks: Clone + Send + Sync + 'static {
- type ReadyState: Send + Sync + 'static;
-
- fn reconcile_startup_state(&self) -> Result<(), String> {
- Ok(())
- }
-
- fn store_pending_session(
- &self,
- pending: &RadrootsAppRemoteSignerPendingSession,
- ) -> Result<(), String>;
-
- fn pending_session_record(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String>;
-
- fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String>;
-
- fn activate_pending_session(
- &self,
- client_account_id: &str,
- approved: RadrootsAppRemoteSignerApprovedSession,
- ) -> Result<Self::ReadyState, String>;
-
- fn clear_pending_session(&self)
- -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String>;
-}
-
-pub struct RadrootsAppRemoteSignerController<H>
-where
- H: RadrootsAppRemoteSignerControllerHooks,
-{
- hooks: H,
- connect_pending: RadrootsAppRemoteSignerConnectPendingFn,
- poll_pending: RadrootsAppRemoteSignerPollPendingFn,
- sleep: RadrootsAppRemoteSignerSleepFn,
- update: Arc<Mutex<Option<Result<Option<H::ReadyState>, String>>>>,
- changed: Arc<AtomicBool>,
- connecting: Arc<AtomicBool>,
- polling: Arc<AtomicBool>,
- poll_generation: Arc<AtomicU64>,
- pending_state: Arc<Mutex<RadrootsAppRemoteSignerPendingState>>,
- _ready_state: PhantomData<H::ReadyState>,
-}
-
-impl<H> Clone for RadrootsAppRemoteSignerController<H>
-where
- H: RadrootsAppRemoteSignerControllerHooks,
-{
- fn clone(&self) -> Self {
- Self {
- hooks: self.hooks.clone(),
- connect_pending: Arc::clone(&self.connect_pending),
- poll_pending: Arc::clone(&self.poll_pending),
- sleep: Arc::clone(&self.sleep),
- update: Arc::clone(&self.update),
- changed: Arc::clone(&self.changed),
- connecting: Arc::clone(&self.connecting),
- polling: Arc::clone(&self.polling),
- poll_generation: Arc::clone(&self.poll_generation),
- pending_state: Arc::clone(&self.pending_state),
- _ready_state: PhantomData,
- }
- }
-}
-
-impl<H> RadrootsAppRemoteSignerController<H>
-where
- H: RadrootsAppRemoteSignerControllerHooks,
-{
- pub fn new(hooks: H) -> Self {
- Self::new_with_ops(
- hooks,
- Arc::new(default_connect_pending),
- default_poll_pending(),
- Arc::new(std::thread::sleep),
- )
- }
-
- fn new_with_ops(
- hooks: H,
- connect_pending: RadrootsAppRemoteSignerConnectPendingFn,
- poll_pending: RadrootsAppRemoteSignerPollPendingFn,
- sleep: RadrootsAppRemoteSignerSleepFn,
- ) -> Self {
- let controller = Self {
- hooks,
- connect_pending,
- poll_pending,
- sleep,
- update: Arc::new(Mutex::new(None)),
- changed: Arc::new(AtomicBool::new(false)),
- connecting: Arc::new(AtomicBool::new(false)),
- polling: Arc::new(AtomicBool::new(false)),
- poll_generation: Arc::new(AtomicU64::new(0)),
- pending_state: Arc::new(Mutex::new(RadrootsAppRemoteSignerPendingState::Idle)),
- _ready_state: PhantomData,
- };
- if let Err(error) = controller.reconcile_startup_state() {
- controller.push_update(Err(error));
- } else if let Err(error) = controller.resume_pending() {
- controller.push_update(Err(error));
- }
- controller
- }
-
- pub fn take_update(&self) -> Option<Result<Option<H::ReadyState>, String>> {
- if !self.changed.swap(false, Ordering::AcqRel) {
- return None;
- }
-
- self.update.lock().ok().and_then(|mut slot| slot.take())
- }
-
- pub fn is_connecting(&self) -> bool {
- self.connecting.load(Ordering::Acquire)
- }
-
- pub fn pending_state(&self) -> RadrootsAppRemoteSignerPendingState {
- self.pending_state
- .lock()
- .map(|state| state.clone())
- .unwrap_or(RadrootsAppRemoteSignerPendingState::Idle)
- }
-
- pub fn begin_connect(&self, input: &str) -> Result<(), String> {
- if self.connecting.swap(true, Ordering::AcqRel) {
- return Err("remote signer connection is already starting".to_owned());
- }
-
- if self.pending_session_record()?.is_some() {
- self.connecting.store(false, Ordering::Release);
- return Err("a remote signer connection is already pending approval".to_owned());
- }
-
- if let Ok(mut slot) = self.update.lock() {
- *slot = None;
- }
- self.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle);
-
- let tracker = self.clone();
- let input = input.to_owned();
- std::thread::spawn(move || {
- let outcome = (|| -> Result<(), String> {
- let pending = (tracker.connect_pending)(input.as_str())?;
- tracker.hooks.store_pending_session(&pending)?;
- tracker.start_polling();
- Ok(())
- })();
-
- if let Err(error) = outcome {
- tracker.push_update(Err(error));
- }
- tracker.connecting.store(false, Ordering::Release);
- });
-
- Ok(())
- }
-
- pub fn pending_session_record(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- self.hooks.pending_session_record()
- }
-
- fn reconcile_startup_state(&self) -> Result<(), String> {
- self.hooks.reconcile_startup_state()
- }
-
- fn resume_pending(&self) -> Result<(), String> {
- let Some(record) = self.pending_session_record()? else {
- return Ok(());
- };
- self.hooks
- .load_pending_client_secret(record.client_account_id())?;
- self.start_polling();
- Ok(())
- }
-
- fn start_polling(&self) {
- let request_generation = self.poll_generation.fetch_add(1, Ordering::AcqRel) + 1;
- if self.polling.swap(true, Ordering::AcqRel) {
- return;
- }
-
- let tracker = self.clone();
- std::thread::spawn(move || {
- loop {
- let pending_record = match tracker.hooks.pending_session_record() {
- Ok(Some(record)) => record,
- Ok(None) => {
- tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle);
- tracker.finish_polling(request_generation);
- return;
- }
- Err(error) => {
- tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle);
- tracker.push_update(Err(error));
- tracker.finish_polling(request_generation);
- return;
- }
- };
- let client_secret_key_hex = match tracker
- .hooks
- .load_pending_client_secret(pending_record.client_account_id())
- {
- Ok(secret) => secret,
- Err(error) => {
- tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle);
- tracker.push_update(Err(error));
- tracker.finish_polling(request_generation);
- return;
- }
- };
-
- let progress_tracker = tracker.clone();
- let progress: Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync> =
- Arc::new(move |update| progress_tracker.apply_progress(update));
-
- match (tracker.poll_pending)(
- &pending_record,
- client_secret_key_hex.as_str(),
- progress,
- ) {
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval) => {
- tracker.set_pending_state(
- RadrootsAppRemoteSignerPendingState::WaitingApproval,
- );
- (tracker.sleep)(Duration::from_secs(1));
- }
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message }) => {
- let changed = tracker.set_pending_state(
- RadrootsAppRemoteSignerPendingState::TransportFailure {
- message: message.clone(),
- },
- );
- if changed {
- tracker.push_update(Err(format!(
- "remote signer approval check failed: {message}"
- )));
- }
- (tracker.sleep)(Duration::from_secs(1));
- }
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::Approved(approved)) => {
- tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle);
- let ready_state = match tracker
- .hooks
- .activate_pending_session(pending_record.client_account_id(), approved)
- {
- Ok(state) => state,
- Err(error) => {
- tracker
- .set_pending_state(RadrootsAppRemoteSignerPendingState::Idle);
- tracker.push_update(Err(error));
- tracker.finish_polling(request_generation);
- return;
- }
- };
- tracker.push_update(Ok(Some(ready_state)));
- tracker.finish_polling(request_generation);
- return;
- }
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message })
- | Ok(RadrootsAppRemoteSignerPendingPollOutcome::FatalError { message }) => {
- tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle);
- let outcome = tracker
- .hooks
- .clear_pending_session()
- .map(|_| None)
- .unwrap_or_else(|cleanup_error| Some(cleanup_error));
- let error = match outcome {
- Some(cleanup_error) => format!(
- "{message}. remote signer cleanup needs retry: {cleanup_error}"
- ),
- None => message,
- };
- tracker.push_update(Err(error));
- tracker.finish_polling(request_generation);
- return;
- }
- Err(error) => {
- tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle);
- tracker.push_update(Err(error));
- tracker.finish_polling(request_generation);
- return;
- }
- }
- }
- });
- }
-
- fn push_update(&self, result: Result<Option<H::ReadyState>, String>) {
- if let Ok(mut slot) = self.update.lock() {
- *slot = Some(result);
- self.changed.store(true, Ordering::Release);
- }
- }
-
- fn apply_progress(&self, update: RadrootsAppRemoteSignerProgressUpdate) {
- match update {
- RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url } => {
- let next =
- RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { url: url.clone() };
- if self.set_pending_state(next) {
- self.push_update(Err(format!(
- "authorize the remote signer to continue: {url}"
- )));
- }
- }
- }
- }
-
- fn set_pending_state(&self, next: RadrootsAppRemoteSignerPendingState) -> bool {
- if let Ok(mut state) = self.pending_state.lock() {
- if *state == next {
- return false;
- }
- *state = next;
- return true;
- }
- false
- }
-
- fn finish_polling(&self, worker_generation: u64) {
- self.polling.store(false, Ordering::Release);
- if self.poll_generation.load(Ordering::Acquire) != worker_generation {
- self.start_polling();
- }
- }
-}
-
-fn default_connect_pending(input: &str) -> Result<RadrootsAppRemoteSignerPendingSession, String> {
- radroots_app_remote_signer_connect_pending(input).map_err(|error| error.to_string())
-}
-
-fn default_poll_pending() -> RadrootsAppRemoteSignerPollPendingFn {
- let cache: Arc<Mutex<Option<(String, RadrootsAppRemoteSignerPendingPoller)>>> =
- Arc::new(Mutex::new(None));
- Arc::new(
- move |record, client_secret_key_hex, progress| -> Result<_, String> {
- let client_account_id = record.client_account_id().to_owned();
- let mut cache = cache
- .lock()
- .map_err(|_| "pending poller cache lock poisoned".to_owned())?;
- let poller = match cache.as_mut() {
- Some((cached_account_id, poller)) if *cached_account_id == client_account_id => {
- poller
- }
- _ => {
- let poller = radroots_app_remote_signer_open_pending_poller(
- record,
- client_secret_key_hex,
- )
- .map_err(|error| error.to_string())?;
- *cache = Some((client_account_id.clone(), poller));
- &mut cache.as_mut().expect("cache initialized").1
- }
- };
- let outcome = radroots_app_remote_signer_poll_pending_poller_with_progress(
- poller,
- &mut |update| progress(update),
- )
- .map_err(|error| error.to_string())?;
- if !matches!(
- outcome,
- RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval
- | RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { .. }
- ) {
- *cache = None;
- }
- Ok(outcome)
- },
- )
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_test_support::{FIXTURE_ALICE, FIXTURE_BOB, FIXTURE_CAROL, fixture_identity};
- use radroots_identity::RadrootsIdentityPublic;
- use std::collections::{HashMap, VecDeque};
- use std::sync::Condvar;
- use std::sync::mpsc::{self, Receiver, Sender};
- use std::time::Instant;
-
- #[derive(Clone, Debug)]
- enum TestPendingBehavior {
- PendingApproval,
- TransportFailure(&'static str),
- Rejected(&'static str),
- }
-
- #[derive(Default)]
- struct TestHooksState {
- pending: Option<RadrootsAppRemoteSignerSessionRecord>,
- active: HashMap<String, String>,
- secrets: HashMap<String, String>,
- pending_record_gate: Option<PendingRecordGate>,
- clear_pending_gate: Option<ClearPendingGate>,
- }
-
- #[derive(Clone)]
- struct PendingRecordGate {
- entered: Sender<()>,
- release: Arc<(Mutex<bool>, Condvar)>,
- }
-
- #[derive(Clone)]
- struct ClearPendingGate {
- entered: Sender<()>,
- release: Arc<(Mutex<bool>, Condvar)>,
- }
-
- #[derive(Clone, Default)]
- struct TestHooks {
- state: Arc<Mutex<TestHooksState>>,
- }
-
- impl TestHooks {
- fn set_pending(&self, record: Option<RadrootsAppRemoteSignerSessionRecord>) {
- self.state.lock().expect("hooks lock").pending = record;
- }
-
- fn set_secret(&self, client_account_id: &str, secret: &str) {
- self.state
- .lock()
- .expect("hooks lock")
- .secrets
- .insert(client_account_id.to_owned(), secret.to_owned());
- }
-
- fn install_pending_record_gate(
- &self,
- entered: Sender<()>,
- release: Arc<(Mutex<bool>, Condvar)>,
- ) {
- self.state.lock().expect("hooks lock").pending_record_gate =
- Some(PendingRecordGate { entered, release });
- }
-
- fn install_clear_pending_gate(
- &self,
- entered: Sender<()>,
- release: Arc<(Mutex<bool>, Condvar)>,
- ) {
- self.state.lock().expect("hooks lock").clear_pending_gate =
- Some(ClearPendingGate { entered, release });
- }
- }
-
- impl RadrootsAppRemoteSignerControllerHooks for TestHooks {
- type ReadyState = String;
-
- fn store_pending_session(
- &self,
- pending: &RadrootsAppRemoteSignerPendingSession,
- ) -> Result<(), String> {
- let mut state = self
- .state
- .lock()
- .map_err(|_| "hooks lock poisoned".to_owned())?;
- state.pending = Some(pending.record.clone());
- state.secrets.insert(
- pending.record.client_account_id().to_owned(),
- pending.client_secret_key_hex.clone(),
- );
- Ok(())
- }
-
- fn pending_session_record(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let gate = {
- self.state
- .lock()
- .map_err(|_| "hooks lock poisoned".to_owned())?
- .pending_record_gate
- .take()
- };
- if let Some(gate) = gate {
- let _ = gate.entered.send(());
- wait_for_gate(&gate.release);
- }
- self.state
- .lock()
- .map_err(|_| "hooks lock poisoned".to_owned())
- .map(|state| state.pending.clone())
- }
-
- fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String> {
- self.state
- .lock()
- .map_err(|_| "hooks lock poisoned".to_owned())?
- .secrets
- .get(client_account_id)
- .cloned()
- .ok_or_else(|| "missing pending client secret".to_owned())
- }
-
- fn activate_pending_session(
- &self,
- client_account_id: &str,
- approved: RadrootsAppRemoteSignerApprovedSession,
- ) -> Result<Self::ReadyState, String> {
- let mut state = self
- .state
- .lock()
- .map_err(|_| "hooks lock poisoned".to_owned())?;
- state.pending = None;
- state.active.insert(
- client_account_id.to_owned(),
- approved.user_identity.id.to_string(),
- );
- Ok(approved.user_identity.id.to_string())
- }
-
- fn clear_pending_session(
- &self,
- ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let (removed, gate) = {
- let mut state = self
- .state
- .lock()
- .map_err(|_| "hooks lock poisoned".to_owned())?;
- (state.pending.take(), state.clear_pending_gate.take())
- };
- if let Some(gate) = gate {
- let _ = gate.entered.send(());
- wait_for_gate(&gate.release);
- }
- Ok(removed)
- }
- }
-
- fn wait_for_gate(gate: &Arc<(Mutex<bool>, Condvar)>) {
- let (ready_lock, ready_cvar) = &**gate;
- let mut ready = ready_lock.lock().expect("gate lock");
- while !*ready {
- ready = ready_cvar.wait(ready).expect("gate wait");
- }
- }
-
- fn open_gate(gate: &Arc<(Mutex<bool>, Condvar)>) {
- let (ready_lock, ready_cvar) = &**gate;
- let mut ready = ready_lock.lock().expect("gate lock");
- *ready = true;
- ready_cvar.notify_all();
- }
-
- fn fixture_public(
- fixture: &radroots_app_test_support::RadrootsAppApprovedFixtureIdentity,
- ) -> RadrootsIdentityPublic {
- fixture_identity(fixture).expect("identity").to_public()
- }
-
- fn pending_record(client: &str, signer: &str) -> RadrootsAppRemoteSignerSessionRecord {
- RadrootsAppRemoteSignerSessionRecord::pending(
- fixture_public(match client {
- "alice-client" => &FIXTURE_ALICE,
- "bob-client" => &FIXTURE_BOB,
- _ => &FIXTURE_CAROL,
- }),
- fixture_public(match signer {
- "alice-signer" => &FIXTURE_ALICE,
- "bob-signer" => &FIXTURE_BOB,
- _ => &FIXTURE_CAROL,
- }),
- vec!["wss://relay.example".to_owned()],
- )
- }
-
- fn pending_session_for_input(
- input: &str,
- ) -> Result<RadrootsAppRemoteSignerPendingSession, String> {
- let record = match input {
- "next" => pending_record("bob-client", "bob-signer"),
- "reject-next" => pending_record("carol-client", "carol-signer"),
- other => return Err(format!("unexpected connect input: {other}")),
- };
- Ok(RadrootsAppRemoteSignerPendingSession {
- client_secret_key_hex: format!("secret-for-{}", record.client_account_id()),
- record,
- })
- }
-
- fn no_sleep(_: Duration) {}
-
- fn wait_for_message(receiver: &Receiver<String>) -> String {
- receiver
- .recv_timeout(Duration::from_secs(2))
- .expect("timed out waiting for poll message")
- }
-
- fn wait_for_update(
- controller: &RadrootsAppRemoteSignerController<TestHooks>,
- ) -> Result<Option<String>, String> {
- let deadline = Instant::now() + Duration::from_secs(2);
- loop {
- if let Some(update) = controller.take_update() {
- return update;
- }
- if Instant::now() >= deadline {
- panic!("timed out waiting for controller update");
- }
- std::thread::sleep(Duration::from_millis(10));
- }
- }
-
- #[test]
- fn restart_request_during_empty_exit_window_respawns_poller() {
- let hooks = TestHooks::default();
- let (poll_tx, poll_rx) = mpsc::channel();
-
- let controller = RadrootsAppRemoteSignerController::new_with_ops(
- hooks.clone(),
- Arc::new(pending_session_for_input),
- Arc::new(move |record, _, _progress| {
- poll_tx
- .send(record.client_account_id().to_owned())
- .expect("send poll id");
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected {
- message: "rejected".to_owned(),
- })
- }),
- Arc::new(no_sleep),
- );
-
- let initial = pending_record("alice-client", "alice-signer");
- hooks.set_secret(initial.client_account_id(), "secret-for-initial");
- hooks.set_pending(Some(initial.clone()));
- let (entered_tx, entered_rx) = mpsc::channel();
- let release = Arc::new((Mutex::new(false), Condvar::new()));
- hooks.install_pending_record_gate(entered_tx, Arc::clone(&release));
- controller.start_polling();
-
- hooks.set_pending(None);
- entered_rx
- .recv_timeout(Duration::from_secs(2))
- .expect("pending record gate was not entered");
-
- let next = pending_session_for_input("next").expect("next pending");
- hooks.set_secret(next.record.client_account_id(), "secret-for-next");
- hooks.set_pending(Some(next.record.clone()));
- controller.start_polling();
- open_gate(&release);
-
- assert_eq!(wait_for_message(&poll_rx), next.record.client_account_id());
- }
-
- #[test]
- fn begin_connect_after_pending_clear_restarts_polling() {
- let hooks = TestHooks::default();
- let (poll_tx, poll_rx) = mpsc::channel();
-
- let controller = RadrootsAppRemoteSignerController::new_with_ops(
- hooks.clone(),
- Arc::new(pending_session_for_input),
- Arc::new(move |record, _, _progress| {
- poll_tx
- .send(record.client_account_id().to_owned())
- .expect("send poll id");
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected {
- message: "rejected".to_owned(),
- })
- }),
- Arc::new(no_sleep),
- );
-
- let initial = pending_record("alice-client", "alice-signer");
- hooks.set_secret(initial.client_account_id(), "secret-for-initial");
- hooks.set_pending(Some(initial));
- let (entered_tx, entered_rx) = mpsc::channel();
- let release = Arc::new((Mutex::new(false), Condvar::new()));
- hooks.install_pending_record_gate(entered_tx, Arc::clone(&release));
- controller.start_polling();
-
- hooks.set_pending(None);
- entered_rx
- .recv_timeout(Duration::from_secs(2))
- .expect("pending record gate was not entered");
-
- controller.begin_connect("next").expect("begin connect");
- open_gate(&release);
-
- let expected = pending_session_for_input("next")
- .expect("next pending")
- .record
- .client_account_id()
- .to_owned();
- assert_eq!(wait_for_message(&poll_rx), expected);
- }
-
- #[test]
- fn begin_connect_after_rejection_cleanup_restarts_polling() {
- let hooks = TestHooks::default();
- let (poll_tx, poll_rx) = mpsc::channel();
-
- let controller = RadrootsAppRemoteSignerController::new_with_ops(
- hooks.clone(),
- Arc::new(pending_session_for_input),
- Arc::new(move |record, _, _progress| {
- poll_tx
- .send(record.client_account_id().to_owned())
- .expect("send poll id");
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected {
- message: "rejected".to_owned(),
- })
- }),
- Arc::new(no_sleep),
- );
-
- let initial = pending_record("alice-client", "alice-signer");
- hooks.set_secret(initial.client_account_id(), "secret-for-initial");
- hooks.set_pending(Some(initial));
- let (clear_tx, clear_rx) = mpsc::channel();
- let release = Arc::new((Mutex::new(false), Condvar::new()));
- hooks.install_clear_pending_gate(clear_tx, Arc::clone(&release));
- controller.start_polling();
-
- assert_eq!(
- wait_for_message(&poll_rx),
- fixture_public(&FIXTURE_ALICE).id.to_string()
- );
- clear_rx
- .recv_timeout(Duration::from_secs(2))
- .expect("clear pending gate was not entered");
-
- controller
- .begin_connect("reject-next")
- .expect("begin connect after rejection");
- open_gate(&release);
-
- let expected = pending_session_for_input("reject-next")
- .expect("reject-next pending")
- .record
- .client_account_id()
- .to_owned();
- assert_eq!(wait_for_message(&poll_rx), expected);
- }
-
- #[test]
- fn transport_failure_recovers_back_to_waiting_approval() {
- let hooks = TestHooks::default();
- let pending = pending_record("alice-client", "alice-signer");
- hooks.set_secret(pending.client_account_id(), "secret-for-initial");
- hooks.set_pending(Some(pending.clone()));
- let outcomes = Arc::new(Mutex::new(VecDeque::from([
- TestPendingBehavior::TransportFailure("relay down"),
- TestPendingBehavior::PendingApproval,
- TestPendingBehavior::Rejected("done"),
- ])));
- let (sleep_enter_tx, sleep_enter_rx) = mpsc::channel();
- let first_sleep_release = Arc::new((Mutex::new(false), Condvar::new()));
- let second_sleep_release = Arc::new((Mutex::new(false), Condvar::new()));
- let first_sleep_release_for_closure = Arc::clone(&first_sleep_release);
- let second_sleep_release_for_closure = Arc::clone(&second_sleep_release);
- let sleep_tick = Arc::new(AtomicU64::new(0));
- let sleep_tick_for_closure = Arc::clone(&sleep_tick);
-
- let controller = RadrootsAppRemoteSignerController::new_with_ops(
- hooks.clone(),
- Arc::new(pending_session_for_input),
- Arc::new(move |_, _, _progress| {
- let next = outcomes
- .lock()
- .expect("outcomes lock")
- .pop_front()
- .expect("missing test outcome");
- match next {
- TestPendingBehavior::PendingApproval => {
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval)
- }
- TestPendingBehavior::TransportFailure(message) => Ok(
- RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure {
- message: message.to_owned(),
- },
- ),
- TestPendingBehavior::Rejected(message) => {
- Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected {
- message: message.to_owned(),
- })
- }
- }
- }),
- Arc::new(move |_| {
- let tick = sleep_tick_for_closure.fetch_add(1, Ordering::AcqRel) + 1;
- let _ = sleep_enter_tx.send(tick);
- match tick {
- 1 => wait_for_gate(&first_sleep_release_for_closure),
- 2 => wait_for_gate(&second_sleep_release_for_closure),
- _ => {}
- }
- }),
- );
-
- let update = wait_for_update(&controller).expect_err("transport failure update");
- assert_eq!(update, "remote signer approval check failed: relay down");
- assert_eq!(
- sleep_enter_rx
- .recv_timeout(Duration::from_secs(2))
- .expect("transport retry sleep"),
- 1
- );
- open_gate(&first_sleep_release);
-
- assert_eq!(
- sleep_enter_rx
- .recv_timeout(Duration::from_secs(2))
- .expect("pending approval sleep"),
- 2
- );
- assert_eq!(
- controller.pending_state(),
- RadrootsAppRemoteSignerPendingState::WaitingApproval
- );
- open_gate(&second_sleep_release);
- }
-}
diff --git a/crates/shared/remote_signer/src/custody.rs b/crates/shared/remote_signer/src/custody.rs
@@ -1,624 +0,0 @@
-use crate::session::{
- RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStatus,
- RadrootsAppRemoteSignerSessionStoreLoadResult, RadrootsAppRemoteSignerSessionStoreState,
-};
-use radroots_nostr_accounts::prelude::{
- RadrootsNostrAccountRecord, RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus,
-};
-use std::collections::HashSet;
-use std::path::Path;
-
-pub fn radroots_app_remote_signer_clear_pending_session(
- path: &Path,
- remove_client_secret: impl Fn(&str) -> Result<(), String>,
-) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> {
- let state = load_sessions(path)?;
- let Some(record) = state.pending_session().cloned() else {
- return Ok(None);
- };
- let mut next_state = state.clone();
- let removed = next_state.remove_pending_session();
- if removed.is_none() {
- return Err("remote signer pending session record cleanup could not complete".to_owned());
- }
- save_sessions(path, &next_state)?;
-
- if let Err(error) = remove_client_secret(record.client_account_id()) {
- return Err(format!(
- "remote signer pending session record was removed but session secret cleanup needs retry: {error}"
- ));
- }
-
- Ok(removed)
-}
-
-pub fn radroots_app_remote_signer_disconnect_selected(
- manager: &RadrootsNostrAccountsManager,
- path: &Path,
- remove_client_secret: impl Fn(&str) -> Result<(), String>,
-) -> Result<RadrootsNostrSelectedAccountStatus, String> {
- let Some(account_id) = manager
- .selected_account_id()
- .map_err(|source| source.to_string())?
- else {
- return Ok(RadrootsNostrSelectedAccountStatus::NotConfigured);
- };
-
- let state = load_sessions(path)?;
- let Some(session) = state
- .active_session_for_account_id(account_id.as_str())
- .cloned()
- else {
- return Ok(RadrootsNostrSelectedAccountStatus::NotConfigured);
- };
-
- let mut next_state = state.clone();
- let removed = next_state.remove_active_session_for_account_id(account_id.as_str());
- if removed.is_none() {
- return Err("remote signer session record cleanup could not complete".to_owned());
- }
- save_sessions(path, &next_state)?;
-
- if let Err(error) = manager.remove_account(&account_id) {
- if let Err(rollback_error) = save_sessions(path, &state) {
- return Err(format!(
- "failed to remove remote signer account: {error}. session rollback also failed: {rollback_error}"
- ));
- }
- return Err(error.to_string());
- }
-
- if let Err(error) = remove_client_secret(session.client_account_id()) {
- return Err(format!(
- "remote signer account and session were removed but session secret cleanup needs retry: {error}"
- ));
- }
-
- manager
- .selected_account_status()
- .map_err(|source| source.to_string())
-}
-
-pub fn radroots_app_remote_signer_reconcile_startup(
- manager: &RadrootsNostrAccountsManager,
- path: &Path,
- remote_signer_label: &str,
- load_client_secret: impl Fn(&str) -> Result<String, String>,
- remove_client_secret: impl Fn(&str) -> Result<(), String>,
- purge_client_secret_namespace: impl Fn() -> Result<(), String>,
-) -> Result<(), String> {
- let load = load_sessions_with_recovery(path)?;
- let mut state = load.state;
- let mut dirty = false;
- let accounts = manager
- .list_accounts()
- .map_err(|source| source.to_string())?;
- let account_ids = accounts
- .iter()
- .map(|record| record.account_id.to_string())
- .collect::<HashSet<_>>();
- let active_session_account_ids = state
- .sessions
- .iter()
- .filter(|record| record.status == RadrootsAppRemoteSignerSessionStatus::Active)
- .filter_map(|record| record.account_id().map(ToOwned::to_owned))
- .collect::<HashSet<_>>();
-
- let should_purge_namespace = load.recovered_from_corruption || state.sessions.is_empty();
-
- if should_purge_namespace {
- purge_client_secret_namespace()?;
- }
-
- for account in remote_signer_public_only_accounts(manager, &accounts, remote_signer_label)?
- .into_iter()
- .filter(|account| !active_session_account_ids.contains(account.account_id.as_str()))
- {
- manager
- .remove_account(&account.account_id)
- .map_err(|source| source.to_string())?;
- }
-
- if let Some(record) = state.pending_session().cloned() {
- if load_client_secret(record.client_account_id()).is_err() {
- state.remove_pending_session();
- dirty = true;
- }
- }
-
- let stale_active_sessions = state
- .sessions
- .iter()
- .filter(|record| record.status == RadrootsAppRemoteSignerSessionStatus::Active)
- .filter_map(|record| {
- let account_id = record.account_id()?;
- (!account_ids.contains(account_id)).then_some(record.clone())
- })
- .collect::<Vec<_>>();
-
- for session in stale_active_sessions {
- remove_client_secret(session.client_account_id())?;
- let Some(account_id) = session.account_id() else {
- continue;
- };
- state.remove_active_session_for_account_id(account_id);
- dirty = true;
- }
-
- if dirty || load.recovered_from_corruption {
- save_sessions(path, &state)?;
- }
-
- Ok(())
-}
-
-pub fn radroots_app_remote_signer_purge_all_custody_state(
- path: &Path,
- remove_client_secret: impl Fn(&str) -> Result<(), String>,
- purge_client_secret_namespace: impl Fn() -> Result<(), String>,
-) -> Result<(), String> {
- let load = load_sessions_with_recovery(path)?;
- for record in &load.state.sessions {
- remove_client_secret(record.client_account_id())?;
- }
- purge_client_secret_namespace()?;
- remove_sessions_file_if_present(path)?;
- Ok(())
-}
-
-fn remote_signer_public_only_accounts(
- manager: &RadrootsNostrAccountsManager,
- accounts: &[RadrootsNostrAccountRecord],
- remote_signer_label: &str,
-) -> Result<Vec<RadrootsNostrAccountRecord>, String> {
- let mut stale = Vec::new();
- for account in accounts {
- if account.label.as_deref() != Some(remote_signer_label) {
- continue;
- }
- if manager
- .get_signing_identity(&account.account_id)
- .map_err(|source| source.to_string())?
- .is_none()
- {
- stale.push(account.clone());
- }
- }
- Ok(stale)
-}
-
-fn load_sessions(path: &Path) -> Result<RadrootsAppRemoteSignerSessionStoreState, String> {
- RadrootsAppRemoteSignerSessionStoreState::load(path).map_err(|error| error.to_string())
-}
-
-fn load_sessions_with_recovery(
- path: &Path,
-) -> Result<RadrootsAppRemoteSignerSessionStoreLoadResult, String> {
- RadrootsAppRemoteSignerSessionStoreState::load_with_recovery(path)
- .map_err(|error| error.to_string())
-}
-
-fn save_sessions(
- path: &Path,
- state: &RadrootsAppRemoteSignerSessionStoreState,
-) -> Result<(), String> {
- state.save(path).map_err(|error| error.to_string())
-}
-
-fn remove_sessions_file_if_present(path: &Path) -> Result<(), String> {
- match std::fs::remove_file(path) {
- Ok(()) => Ok(()),
- Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
- Err(error) => Err(format!(
- "failed to remove remote signer session store: {error}"
- )),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_test_support::{FIXTURE_ALICE, FIXTURE_BOB, FIXTURE_CAROL, fixture_identity};
- use radroots_identity::RadrootsIdentityId;
- use radroots_nostr_accounts::prelude::{
- RadrootsNostrAccountsManager, RadrootsNostrSecretVaultMemory,
- RadrootsNostrSelectedAccountStatus, RadrootsSecretVault, account_secret_slot,
- };
-
- const REMOTE_SIGNER_LABEL: &str = "remote signer";
-
- fn fixture_public(
- label: &radroots_app_test_support::RadrootsAppApprovedFixtureIdentity,
- ) -> radroots_identity::RadrootsIdentityPublic {
- fixture_identity(label).expect("identity").to_public()
- }
-
- fn fixture_account_id(value: &str) -> RadrootsIdentityId {
- RadrootsIdentityId::try_from(value).expect("account id")
- }
-
- fn secret_store_secret(
- vault: &RadrootsNostrSecretVaultMemory,
- client_account_id: &str,
- secret: &str,
- ) {
- let slot = account_secret_slot(&fixture_account_id(client_account_id));
- vault
- .store_secret(slot.as_str(), secret)
- .expect("store secret");
- }
-
- fn secret_loader(
- vault: RadrootsNostrSecretVaultMemory,
- ) -> impl Fn(&str) -> Result<String, String> {
- move |client_account_id| {
- let slot = account_secret_slot(&fixture_account_id(client_account_id));
- vault
- .load_secret(slot.as_str())
- .map_err(|source| source.to_string())?
- .ok_or_else(|| "missing secret".to_owned())
- }
- }
-
- fn secret_remover(
- vault: RadrootsNostrSecretVaultMemory,
- ) -> impl Fn(&str) -> Result<(), String> {
- move |client_account_id| {
- let slot = account_secret_slot(&fixture_account_id(client_account_id));
- vault
- .remove_secret(slot.as_str())
- .map_err(|source| source.to_string())
- }
- }
-
- fn secret_namespace_purger(
- vault: RadrootsNostrSecretVaultMemory,
- client_account_ids: Vec<String>,
- ) -> impl Fn() -> Result<(), String> {
- move || {
- for client_account_id in &client_account_ids {
- let slot = account_secret_slot(&fixture_account_id(client_account_id));
- vault
- .remove_secret(slot.as_str())
- .map_err(|source| source.to_string())?;
- }
- Ok(())
- }
- }
-
- fn write_pending_state(path: &Path) -> RadrootsAppRemoteSignerSessionRecord {
- let record = RadrootsAppRemoteSignerSessionRecord::pending(
- fixture_public(&FIXTURE_ALICE),
- fixture_public(&FIXTURE_BOB),
- vec!["wss://relay.example.com".to_owned()],
- );
- let mut state = RadrootsAppRemoteSignerSessionStoreState::default();
- state.upsert_pending(record.clone()).expect("pending");
- state.save(path).expect("save");
- record
- }
-
- fn write_active_state(path: &Path) -> RadrootsAppRemoteSignerSessionRecord {
- let user_identity = fixture_public(&FIXTURE_CAROL);
- let mut record = RadrootsAppRemoteSignerSessionRecord::pending(
- fixture_public(&FIXTURE_ALICE),
- fixture_public(&FIXTURE_BOB),
- vec!["wss://relay.example.com".to_owned()],
- );
- record.user_identity = Some(user_identity);
- record.status = RadrootsAppRemoteSignerSessionStatus::Active;
- let mut state = RadrootsAppRemoteSignerSessionStoreState::default();
- state.sessions.push(record.clone());
- state.save(path).expect("save");
- record
- }
-
- #[test]
- fn clear_pending_session_removes_secret_and_session_record() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- let record = write_pending_state(path.as_path());
- let vault = RadrootsNostrSecretVaultMemory::new();
- secret_store_secret(&vault, record.client_account_id(), "deadbeef");
-
- let removed = radroots_app_remote_signer_clear_pending_session(
- path.as_path(),
- secret_remover(vault.clone()),
- )
- .expect("clear pending");
-
- assert_eq!(
- removed.expect("removed").client_account_id(),
- record.client_account_id()
- );
- assert!(
- vault
- .load_secret(
- account_secret_slot(&fixture_account_id(record.client_account_id())).as_str()
- )
- .expect("load")
- .is_none()
- );
- assert!(
- RadrootsAppRemoteSignerSessionStoreState::load(path.as_path())
- .expect("load")
- .sessions
- .is_empty()
- );
- }
-
- #[test]
- fn clear_pending_session_leaves_secret_for_retry_when_secret_cleanup_fails() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- let record = write_pending_state(path.as_path());
- let vault = RadrootsNostrSecretVaultMemory::new();
- secret_store_secret(&vault, record.client_account_id(), "deadbeef");
-
- let error = radroots_app_remote_signer_clear_pending_session(
- path.as_path(),
- |_client_account_id| Err("vault unavailable".to_owned()),
- )
- .expect_err("cleanup retry");
-
- assert!(error.contains("session secret cleanup needs retry"));
- assert!(
- RadrootsAppRemoteSignerSessionStoreState::load(path.as_path())
- .expect("load")
- .sessions
- .is_empty()
- );
- assert_eq!(
- vault
- .load_secret(
- account_secret_slot(&fixture_account_id(record.client_account_id())).as_str()
- )
- .expect("load retained secret")
- .as_deref(),
- Some("deadbeef")
- );
- }
-
- #[test]
- fn disconnect_selected_remote_signer_leaves_session_for_retry_when_secret_cleanup_fails() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- let record = write_active_state(path.as_path());
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- manager
- .upsert_public_identity(
- record.user_identity.clone().expect("user"),
- Some(REMOTE_SIGNER_LABEL.to_owned()),
- true,
- )
- .expect("upsert");
-
- let error = radroots_app_remote_signer_disconnect_selected(
- &manager,
- path.as_path(),
- |_client_account_id| Err("vault unavailable".to_owned()),
- )
- .expect_err("cleanup failure");
-
- assert!(error.contains("session secret cleanup needs retry"));
- assert!(matches!(
- manager.selected_account_status().expect("status"),
- RadrootsNostrSelectedAccountStatus::NotConfigured
- ));
- assert!(
- RadrootsAppRemoteSignerSessionStoreState::load(path.as_path())
- .expect("load")
- .active_session_for_account_id(
- record
- .account_id()
- .expect("account id after disconnect failure")
- )
- .is_none()
- );
- }
-
- #[test]
- fn reconcile_startup_removes_remote_signer_public_only_accounts_after_store_quarantine() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- std::fs::write(path.as_path(), "{invalid").expect("write invalid");
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let public = fixture_public(&FIXTURE_CAROL);
- let account_id = public.id.clone();
- manager
- .upsert_public_identity(public, Some(REMOTE_SIGNER_LABEL.to_owned()), true)
- .expect("upsert");
-
- radroots_app_remote_signer_reconcile_startup(
- &manager,
- path.as_path(),
- REMOTE_SIGNER_LABEL,
- secret_loader(RadrootsNostrSecretVaultMemory::new()),
- secret_remover(RadrootsNostrSecretVaultMemory::new()),
- secret_namespace_purger(RadrootsNostrSecretVaultMemory::new(), Vec::new()),
- )
- .expect("reconcile");
-
- assert!(
- manager
- .list_accounts()
- .expect("accounts")
- .iter()
- .all(|record| record.account_id != account_id)
- );
- assert!(
- RadrootsAppRemoteSignerSessionStoreState::load(path.as_path())
- .expect("load")
- .sessions
- .is_empty()
- );
- }
-
- #[test]
- fn reconcile_startup_removes_orphan_remote_signer_public_only_accounts_without_corruption() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- RadrootsAppRemoteSignerSessionStoreState::default()
- .save(path.as_path())
- .expect("save empty");
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let public = fixture_public(&FIXTURE_CAROL);
- let account_id = public.id.clone();
- manager
- .upsert_public_identity(public, Some(REMOTE_SIGNER_LABEL.to_owned()), true)
- .expect("upsert");
-
- radroots_app_remote_signer_reconcile_startup(
- &manager,
- path.as_path(),
- REMOTE_SIGNER_LABEL,
- secret_loader(RadrootsNostrSecretVaultMemory::new()),
- secret_remover(RadrootsNostrSecretVaultMemory::new()),
- secret_namespace_purger(RadrootsNostrSecretVaultMemory::new(), Vec::new()),
- )
- .expect("reconcile orphan account");
-
- assert!(
- manager
- .list_accounts()
- .expect("accounts")
- .iter()
- .all(|record| record.account_id != account_id)
- );
- }
-
- #[test]
- fn purge_all_custody_state_removes_all_tracked_client_secrets_and_session_file() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- let pending = write_pending_state(path.as_path());
- let mut active = write_active_state(path.as_path());
- active.client_identity = fixture_public(&FIXTURE_BOB);
- let mut state =
- RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load");
- state.sessions.push(active.clone());
- state.save(path.as_path()).expect("save");
-
- let vault = RadrootsNostrSecretVaultMemory::new();
- secret_store_secret(&vault, pending.client_account_id(), "pending");
- secret_store_secret(&vault, active.client_account_id(), "active");
-
- radroots_app_remote_signer_purge_all_custody_state(
- path.as_path(),
- secret_remover(vault.clone()),
- secret_namespace_purger(
- vault.clone(),
- vec![
- pending.client_account_id().to_owned(),
- active.client_account_id().to_owned(),
- ],
- ),
- )
- .expect("purge");
-
- assert!(!path.exists());
- assert!(
- vault
- .load_secret(
- account_secret_slot(&fixture_account_id(pending.client_account_id())).as_str()
- )
- .expect("pending removed")
- .is_none()
- );
- assert!(
- vault
- .load_secret(
- account_secret_slot(&fixture_account_id(active.client_account_id())).as_str()
- )
- .expect("active removed")
- .is_none()
- );
- }
-
- #[test]
- fn reconcile_startup_purges_namespace_after_store_quarantine() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- std::fs::write(path.as_path(), "{invalid").expect("write invalid");
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let alice_client_account_id = fixture_public(&FIXTURE_ALICE).id;
- let bob_client_account_id = fixture_public(&FIXTURE_BOB).id;
- let public = fixture_public(&FIXTURE_CAROL);
- manager
- .upsert_public_identity(public, Some(REMOTE_SIGNER_LABEL.to_owned()), true)
- .expect("upsert");
-
- let vault = RadrootsNostrSecretVaultMemory::new();
- secret_store_secret(&vault, alice_client_account_id.as_str(), "pending");
- secret_store_secret(&vault, bob_client_account_id.as_str(), "active");
-
- radroots_app_remote_signer_reconcile_startup(
- &manager,
- path.as_path(),
- REMOTE_SIGNER_LABEL,
- secret_loader(vault.clone()),
- secret_remover(vault.clone()),
- secret_namespace_purger(
- vault.clone(),
- vec![
- alice_client_account_id.to_string(),
- bob_client_account_id.to_string(),
- ],
- ),
- )
- .expect("reconcile after quarantine");
-
- assert!(
- vault
- .load_secret(
- account_secret_slot(&fixture_account_id(alice_client_account_id.as_str()))
- .as_str()
- )
- .expect("pending removed by namespace purge")
- .is_none()
- );
- assert!(
- vault
- .load_secret(
- account_secret_slot(&fixture_account_id(bob_client_account_id.as_str()))
- .as_str()
- )
- .expect("active removed by namespace purge")
- .is_none()
- );
- }
-
- #[test]
- fn reconcile_startup_purges_namespace_when_session_store_is_empty() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- RadrootsAppRemoteSignerSessionStoreState::default()
- .save(path.as_path())
- .expect("save empty");
- let manager = RadrootsNostrAccountsManager::new_in_memory();
- let alice_client_account_id = fixture_public(&FIXTURE_ALICE).id;
-
- let vault = RadrootsNostrSecretVaultMemory::new();
- secret_store_secret(&vault, alice_client_account_id.as_str(), "pending");
-
- radroots_app_remote_signer_reconcile_startup(
- &manager,
- path.as_path(),
- REMOTE_SIGNER_LABEL,
- secret_loader(vault.clone()),
- secret_remover(vault.clone()),
- secret_namespace_purger(vault.clone(), vec![alice_client_account_id.to_string()]),
- )
- .expect("reconcile empty store");
-
- assert!(
- vault
- .load_secret(
- account_secret_slot(&fixture_account_id(alice_client_account_id.as_str()))
- .as_str()
- )
- .expect("pending removed by empty-store namespace purge")
- .is_none()
- );
- }
-}
diff --git a/crates/shared/remote_signer/src/error.rs b/crates/shared/remote_signer/src/error.rs
@@ -1,64 +0,0 @@
-use radroots_nostr_connect::prelude::{RadrootsNostrConnectError, RadrootsNostrConnectMethod};
-use std::fmt;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsAppRemoteSignerError {
- EmptyInput,
- UnsupportedClientUri,
- MissingDiscoveryUri,
- InvalidDiscoveryUrl(String),
- InvalidBunkerUri(String),
- InvalidSessionStore(String),
- SessionStoreIo(String),
- PendingSessionExists,
- MissingClientSecret,
- ConnectFailed(String),
- RequestTimedOut {
- method: RadrootsNostrConnectMethod,
- },
- UnexpectedResponse {
- method: RadrootsNostrConnectMethod,
- response: String,
- },
-}
-
-impl fmt::Display for RadrootsAppRemoteSignerError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::EmptyInput => f.write_str("enter a bunker or discovery url to continue"),
- Self::UnsupportedClientUri => f.write_str(
- "enter a bunker or discovery url from the signer; raw nostrconnect client uris are signer-side only",
- ),
- Self::MissingDiscoveryUri => {
- f.write_str("discovery url does not contain a remote signer uri")
- }
- Self::InvalidDiscoveryUrl(reason) => {
- write!(f, "invalid discovery url: {reason}")
- }
- Self::InvalidBunkerUri(reason) => {
- write!(f, "invalid remote signer uri: {reason}")
- }
- Self::InvalidSessionStore(reason) => write!(f, "invalid remote signer store: {reason}"),
- Self::SessionStoreIo(reason) => write!(f, "remote signer storage failed: {reason}"),
- Self::PendingSessionExists => {
- f.write_str("a remote signer connection is already pending approval")
- }
- Self::MissingClientSecret => f.write_str("remote signer session secret is missing"),
- Self::ConnectFailed(reason) => write!(f, "remote signer connection failed: {reason}"),
- Self::RequestTimedOut { method } => {
- write!(f, "remote signer request `{method}` timed out")
- }
- Self::UnexpectedResponse { method, response } => {
- write!(f, "remote signer returned an unexpected `{method}` response: {response}")
- }
- }
- }
-}
-
-impl std::error::Error for RadrootsAppRemoteSignerError {}
-
-impl From<RadrootsNostrConnectError> for RadrootsAppRemoteSignerError {
- fn from(value: RadrootsNostrConnectError) -> Self {
- Self::InvalidBunkerUri(value.to_string())
- }
-}
diff --git a/crates/shared/remote_signer/src/input.rs b/crates/shared/remote_signer/src/input.rs
@@ -1,168 +0,0 @@
-use crate::error::RadrootsAppRemoteSignerError;
-use radroots_identity::RadrootsIdentityPublic;
-use radroots_nostr_connect::prelude::{
- RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions,
- RadrootsNostrConnectUri,
-};
-use radroots_nostr_connect::uri::RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME;
-use url::Url;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppRemoteSignerSource {
- BunkerUri,
- DiscoveryUrl,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppRemoteSignerTarget {
- pub source: RadrootsAppRemoteSignerSource,
- pub signer_identity: RadrootsIdentityPublic,
- pub relays: Vec<String>,
- pub connect_secret: Option<String>,
- pub requested_permissions: RadrootsNostrConnectPermissions,
-}
-
-impl RadrootsAppRemoteSignerTarget {
- pub fn source_label(&self) -> &'static str {
- match self.source {
- RadrootsAppRemoteSignerSource::BunkerUri => "bunker uri",
- RadrootsAppRemoteSignerSource::DiscoveryUrl => "discovery url",
- }
- }
-
- pub fn requested_permission_labels(&self) -> Vec<String> {
- self.requested_permissions
- .as_slice()
- .iter()
- .map(ToString::to_string)
- .collect()
- }
-}
-
-pub fn radroots_app_remote_signer_requested_permissions() -> RadrootsNostrConnectPermissions {
- vec![
- RadrootsNostrConnectPermission::with_parameter(
- RadrootsNostrConnectMethod::SignEvent,
- "kind:1",
- ),
- RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays),
- ]
- .into()
-}
-
-pub fn radroots_app_remote_signer_preview(
- input: &str,
-) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> {
- let trimmed = input.trim();
- if trimmed.is_empty() {
- return Err(RadrootsAppRemoteSignerError::EmptyInput);
- }
-
- if trimmed.starts_with(&format!("{RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME}://")) {
- return parse_bunker_uri(trimmed, RadrootsAppRemoteSignerSource::BunkerUri);
- }
-
- if trimmed.starts_with("nostrconnect://") {
- return Err(RadrootsAppRemoteSignerError::UnsupportedClientUri);
- }
-
- parse_discovery_url(trimmed)
-}
-
-fn parse_discovery_url(
- value: &str,
-) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> {
- let url = Url::parse(value)
- .map_err(|error| RadrootsAppRemoteSignerError::InvalidDiscoveryUrl(error.to_string()))?;
- let Some((_, bunker_uri)) = url.query_pairs().find(|(key, _)| key == "uri") else {
- return Err(RadrootsAppRemoteSignerError::MissingDiscoveryUri);
- };
- parse_bunker_uri(
- bunker_uri.as_ref(),
- RadrootsAppRemoteSignerSource::DiscoveryUrl,
- )
-}
-
-fn parse_bunker_uri(
- value: &str,
- source: RadrootsAppRemoteSignerSource,
-) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> {
- let uri = RadrootsNostrConnectUri::parse(value)?;
- let RadrootsNostrConnectUri::Bunker(bunker_uri) = uri else {
- return Err(RadrootsAppRemoteSignerError::UnsupportedClientUri);
- };
- Ok(RadrootsAppRemoteSignerTarget {
- source,
- signer_identity: RadrootsIdentityPublic::new(bunker_uri.remote_signer_public_key),
- relays: bunker_uri
- .relays
- .into_iter()
- .map(|relay| relay.to_string())
- .collect(),
- connect_secret: bunker_uri.secret,
- requested_permissions: radroots_app_remote_signer_requested_permissions(),
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_test_support::{FIXTURE_ALICE, RELAY_PRIMARY_WSS};
-
- fn bunker_uri() -> String {
- format!(
- "bunker://{}?relay={}",
- FIXTURE_ALICE.npub,
- urlencoding(RELAY_PRIMARY_WSS)
- )
- }
-
- fn discovery_url() -> String {
- format!(
- "http://localhost/connect?uri={}",
- urlencoding(bunker_uri().as_str())
- )
- }
-
- fn urlencoding(value: &str) -> String {
- url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
- }
-
- #[test]
- fn parses_direct_bunker_uri() {
- let preview = radroots_app_remote_signer_preview(bunker_uri().as_str()).expect("preview");
-
- assert_eq!(preview.source, RadrootsAppRemoteSignerSource::BunkerUri);
- assert_eq!(preview.signer_identity.public_key_npub, FIXTURE_ALICE.npub);
- assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]);
- assert_eq!(preview.connect_secret, None);
- assert_eq!(
- preview.requested_permission_labels(),
- vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()]
- );
- }
-
- #[test]
- fn parses_discovery_url_with_bunker_uri() {
- let preview =
- radroots_app_remote_signer_preview(discovery_url().as_str()).expect("preview");
-
- assert_eq!(preview.source, RadrootsAppRemoteSignerSource::DiscoveryUrl);
- assert_eq!(preview.signer_identity.public_key_npub, FIXTURE_ALICE.npub);
- assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]);
- assert_eq!(
- preview.requested_permission_labels(),
- vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()]
- );
- }
-
- #[test]
- fn rejects_client_side_nostrconnect_uri_input() {
- let err = radroots_app_remote_signer_preview(
- "nostrconnect://npub1test?relay=wss%3A%2F%2Frelay.example.com&secret=test",
- )
- .expect_err("client uri rejected");
-
- assert_eq!(err, RadrootsAppRemoteSignerError::UnsupportedClientUri);
- }
-}
diff --git a/crates/shared/remote_signer/src/lib.rs b/crates/shared/remote_signer/src/lib.rs
@@ -1,47 +0,0 @@
-#![forbid(unsafe_code)]
-
-mod action;
-mod controller;
-mod custody;
-mod error;
-mod input;
-mod protocol;
-mod session;
-
-pub const RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE: &str = "remote-signer";
-
-pub use action::{
- RadrootsAppRemoteSignerActionController, RadrootsAppRemoteSignerActionControllerHooks,
- RadrootsAppRemoteSignerActionState,
-};
-pub use controller::{
- RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks,
- RadrootsAppRemoteSignerPendingState,
-};
-pub use custody::{
- radroots_app_remote_signer_clear_pending_session,
- radroots_app_remote_signer_disconnect_selected,
- radroots_app_remote_signer_purge_all_custody_state,
- radroots_app_remote_signer_reconcile_startup,
-};
-pub use error::RadrootsAppRemoteSignerError;
-pub use input::{
- RadrootsAppRemoteSignerSource, RadrootsAppRemoteSignerTarget,
- radroots_app_remote_signer_preview, radroots_app_remote_signer_requested_permissions,
-};
-pub use protocol::{
- RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome,
- RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerProgressUpdate,
- RadrootsAppRemoteSignerSignedEvent, radroots_app_remote_signer_connect_pending,
- radroots_app_remote_signer_poll_pending_session,
- radroots_app_remote_signer_poll_pending_session_with_progress,
- radroots_app_remote_signer_sign_kind1_note,
- radroots_app_remote_signer_sign_kind1_note_with_progress,
- radroots_app_remote_signer_sign_unsigned_event,
- radroots_app_remote_signer_sign_unsigned_event_with_progress,
-};
-pub use session::{
- RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION, RadrootsAppRemoteSignerSessionRecord,
- RadrootsAppRemoteSignerSessionStatus, RadrootsAppRemoteSignerSessionStoreLoadResult,
- RadrootsAppRemoteSignerSessionStoreState,
-};
diff --git a/crates/shared/remote_signer/src/protocol.rs b/crates/shared/remote_signer/src/protocol.rs
@@ -1,789 +0,0 @@
-use crate::error::RadrootsAppRemoteSignerError;
-use crate::input::{RadrootsAppRemoteSignerTarget, radroots_app_remote_signer_preview};
-use crate::session::RadrootsAppRemoteSignerSessionRecord;
-use nostr::JsonUtil;
-use nostr::nips::nip44;
-use nostr::nips::nip44::Version;
-use nostr::{EventBuilder, UnsignedEvent};
-use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
-use radroots_nostr::prelude::{
- RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter,
- RadrootsNostrKind, RadrootsNostrRelayPoolNotification, RadrootsNostrTag,
- RadrootsNostrTimestamp, radroots_nostr_filter_tag, radroots_nostr_kind,
-};
-use radroots_nostr_connect::message::RADROOTS_NOSTR_CONNECT_RPC_KIND;
-use radroots_nostr_connect::prelude::{
- RadrootsNostrConnectMethod, RadrootsNostrConnectPendingConnectionPollOutcome,
- RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
- RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
- RadrootsNostrConnectResponseEnvelope,
-};
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::time::Duration;
-use tokio::runtime::Builder;
-use tokio::sync::broadcast;
-use tokio::time::timeout;
-
-const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
-const GET_SESSION_CAPABILITY_TIMEOUT: Duration = Duration::from_secs(60);
-const SWITCH_RELAYS_TIMEOUT: Duration = Duration::from_secs(30);
-const SIGN_EVENT_TIMEOUT: Duration = Duration::from_secs(60);
-static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(1);
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppRemoteSignerPendingSession {
- pub record: RadrootsAppRemoteSignerSessionRecord,
- pub client_secret_key_hex: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppRemoteSignerApprovedSession {
- pub user_identity: RadrootsIdentityPublic,
- pub relays: Vec<String>,
- pub approved_permissions: RadrootsNostrConnectPermissions,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsAppRemoteSignerSignedEvent {
- pub event_id_hex: String,
- pub event_json: String,
- pub relays: Vec<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsAppRemoteSignerProgressUpdate {
- AuthChallenge { url: String },
-}
-
-#[derive(Debug, Clone)]
-pub enum RadrootsAppRemoteSignerPendingPollOutcome {
- PendingApproval,
- Approved(RadrootsAppRemoteSignerApprovedSession),
- TransportFailure { message: String },
- Rejected { message: String },
- FatalError { message: String },
-}
-
-pub(crate) struct RadrootsAppRemoteSignerPendingPoller {
- client: ConnectedRemoteSignerSessionClient,
-}
-
-struct ConnectedRemoteSignerSessionClient {
- runtime: tokio::runtime::Runtime,
- client_identity: RadrootsIdentity,
- target: RadrootsAppRemoteSignerTarget,
- client: RadrootsNostrClient,
- notifications: broadcast::Receiver<RadrootsNostrRelayPoolNotification>,
-}
-
-pub fn radroots_app_remote_signer_connect_pending(
- input: &str,
-) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> {
- let target = radroots_app_remote_signer_preview(input)?;
- connect_pending_session(target)
-}
-
-pub fn radroots_app_remote_signer_poll_pending_session(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
-) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> {
- radroots_app_remote_signer_poll_pending_session_with_progress(
- record,
- client_secret_key_hex,
- |_| {},
- )
-}
-
-pub fn radroots_app_remote_signer_poll_pending_session_with_progress<F>(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
- mut progress: F,
-) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError>
-where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
-{
- let mut poller = radroots_app_remote_signer_open_pending_poller(record, client_secret_key_hex)?;
- radroots_app_remote_signer_poll_pending_poller_with_progress(&mut poller, &mut progress)
-}
-
-pub(crate) fn radroots_app_remote_signer_open_pending_poller(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
-) -> Result<RadrootsAppRemoteSignerPendingPoller, RadrootsAppRemoteSignerError> {
- let client_identity = load_client_identity(client_secret_key_hex)?;
- let target = target_for_record(record);
- Ok(RadrootsAppRemoteSignerPendingPoller {
- client: ConnectedRemoteSignerSessionClient::connect(client_identity, target)?,
- })
-}
-
-pub(crate) fn radroots_app_remote_signer_poll_pending_poller_with_progress<F>(
- poller: &mut RadrootsAppRemoteSignerPendingPoller,
- progress: &mut F,
-) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError>
-where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
-{
- poller.poll_with_progress(progress)
-}
-
-pub fn radroots_app_remote_signer_sign_kind1_note(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
- content: &str,
-) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> {
- radroots_app_remote_signer_sign_kind1_note_with_progress(
- record,
- client_secret_key_hex,
- content,
- |_| {},
- )
-}
-
-pub fn radroots_app_remote_signer_sign_kind1_note_with_progress<F>(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
- content: &str,
- mut progress: F,
-) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError>
-where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
-{
- sign_kind1_note(record, client_secret_key_hex, content, &mut progress)
-}
-
-pub fn radroots_app_remote_signer_sign_unsigned_event(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
- unsigned_event: UnsignedEvent,
-) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> {
- radroots_app_remote_signer_sign_unsigned_event_with_progress(
- record,
- client_secret_key_hex,
- unsigned_event,
- |_| {},
- )
-}
-
-pub fn radroots_app_remote_signer_sign_unsigned_event_with_progress<F>(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
- unsigned_event: UnsignedEvent,
- mut progress: F,
-) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError>
-where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
-{
- sign_unsigned_event(record, client_secret_key_hex, unsigned_event, &mut progress)
-}
-
-fn connect_pending_session(
- target: RadrootsAppRemoteSignerTarget,
-) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> {
- let client_identity = RadrootsIdentity::generate();
- let connect_request = connect_request_for_target(&target)?;
- let response = execute_request(
- &client_identity,
- &target,
- RadrootsNostrConnectMethod::Connect,
- connect_request,
- CONNECT_TIMEOUT,
- )?;
-
- match response {
- RadrootsNostrConnectResponse::ConnectAcknowledged
- | RadrootsNostrConnectResponse::ConnectSecretEcho(_) => {
- Ok(RadrootsAppRemoteSignerPendingSession {
- record: RadrootsAppRemoteSignerSessionRecord::pending(
- client_identity.to_public(),
- target.signer_identity,
- target.relays,
- ),
- client_secret_key_hex: client_identity.secret_key_hex(),
- })
- }
- other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse {
- method: RadrootsNostrConnectMethod::Connect,
- response: format!("{other:?}"),
- }),
- }
-}
-
-fn connect_request_for_target(
- target: &RadrootsAppRemoteSignerTarget,
-) -> Result<RadrootsNostrConnectRequest, RadrootsAppRemoteSignerError> {
- Ok(RadrootsNostrConnectRequest::Connect {
- remote_signer_public_key: parse_public_key_hex(
- target.signer_identity.public_key_hex.as_str(),
- )?,
- secret: target.connect_secret.clone(),
- requested_permissions: target.requested_permissions.clone(),
- })
-}
-
-fn sign_kind1_note<F>(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
- content: &str,
- progress: &mut F,
-) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError>
-where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
-{
- if !record.allows_sign_event_kind1() {
- return Err(RadrootsAppRemoteSignerError::ConnectFailed(
- "remote signer has not approved sign_event:kind:1".to_owned(),
- ));
- }
- let user_identity = record.user_identity.as_ref().ok_or_else(|| {
- RadrootsAppRemoteSignerError::ConnectFailed(
- "remote signer session is missing the approved user identity".to_owned(),
- )
- })?;
- let unsigned_event = EventBuilder::text_note(content.trim())
- .build(parse_public_key_hex(user_identity.public_key_hex.as_str())?);
- sign_unsigned_event(record, client_secret_key_hex, unsigned_event, progress)
-}
-
-fn sign_unsigned_event<F>(
- record: &RadrootsAppRemoteSignerSessionRecord,
- client_secret_key_hex: &str,
- unsigned_event: UnsignedEvent,
- progress: &mut F,
-) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError>
-where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
-{
- let client_identity = load_client_identity(client_secret_key_hex)?;
- let target = target_for_record(record);
- let mut client = ConnectedRemoteSignerSessionClient::connect(client_identity, target)?;
- let relays = client.sync_relays_if_allowed(record, progress)?;
- let response = client.execute_request_with_progress(
- RadrootsNostrConnectMethod::SignEvent,
- RadrootsNostrConnectRequest::SignEvent(unsigned_event),
- SIGN_EVENT_TIMEOUT,
- progress,
- )?;
-
- match response {
- RadrootsNostrConnectResponse::SignedEvent(event) => {
- Ok(RadrootsAppRemoteSignerSignedEvent {
- event_id_hex: event.id.to_hex(),
- event_json: event.as_json(),
- relays,
- })
- }
- RadrootsNostrConnectResponse::Error { error, .. } => {
- Err(RadrootsAppRemoteSignerError::ConnectFailed(error))
- }
- other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse {
- method: RadrootsNostrConnectMethod::SignEvent,
- response: format!("{other:?}"),
- }),
- }
-}
-
-fn execute_request(
- client_identity: &RadrootsIdentity,
- target: &RadrootsAppRemoteSignerTarget,
- method: RadrootsNostrConnectMethod,
- request: RadrootsNostrConnectRequest,
- request_timeout: Duration,
-) -> Result<RadrootsNostrConnectResponse, RadrootsAppRemoteSignerError> {
- let mut client =
- ConnectedRemoteSignerSessionClient::connect(client_identity.clone(), target.clone())?;
- client.execute_request_with_progress(method, request, request_timeout, &mut |_| {})
-}
-
-impl RadrootsAppRemoteSignerPendingPoller {
- fn poll_with_progress<F>(
- &mut self,
- progress: &mut F,
- ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError>
- where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
- {
- match self.client.execute_request_with_progress(
- RadrootsNostrConnectMethod::GetSessionCapability,
- RadrootsNostrConnectRequest::GetSessionCapability,
- GET_SESSION_CAPABILITY_TIMEOUT,
- progress,
- ) {
- Ok(response) => Ok(classify_pending_poll_response(response)),
- Err(error) => Ok(classify_pending_poll_error(error)),
- }
- }
-}
-
-impl ConnectedRemoteSignerSessionClient {
- fn connect(
- client_identity: RadrootsIdentity,
- target: RadrootsAppRemoteSignerTarget,
- ) -> Result<Self, RadrootsAppRemoteSignerError> {
- let runtime = Builder::new_current_thread()
- .enable_all()
- .build()
- .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?;
- let client = RadrootsNostrClient::from_identity(&client_identity);
- let notifications = runtime.block_on(async {
- for relay in &target.relays {
- client.add_relay(relay).await.map_err(|error| {
- RadrootsAppRemoteSignerError::ConnectFailed(error.to_string())
- })?;
- }
- client.connect().await;
- let filter = radroots_nostr_filter_tag(
- RadrootsNostrFilter::new()
- .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND))
- .since(RadrootsNostrTimestamp::now()),
- "p",
- vec![client_identity.public_key_hex()],
- )
- .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?;
- let notifications = client.notifications();
- client
- .subscribe(filter, None)
- .await
- .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?;
- Ok::<_, RadrootsAppRemoteSignerError>(notifications)
- })?;
-
- Ok(Self {
- runtime,
- client_identity,
- target,
- client,
- notifications,
- })
- }
-
- fn sync_relays_if_allowed<F>(
- &mut self,
- record: &RadrootsAppRemoteSignerSessionRecord,
- progress: &mut F,
- ) -> Result<Vec<String>, RadrootsAppRemoteSignerError>
- where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
- {
- if !record.allows_switch_relays() {
- return Ok(self.target.relays.clone());
- }
-
- match self.execute_request_with_progress(
- RadrootsNostrConnectMethod::SwitchRelays,
- RadrootsNostrConnectRequest::SwitchRelays,
- SWITCH_RELAYS_TIMEOUT,
- progress,
- )? {
- RadrootsNostrConnectResponse::RelayList(relays) => {
- let relays: Vec<String> =
- relays.into_iter().map(|relay| relay.to_string()).collect();
- self.target.relays = relays.clone();
- Ok(relays)
- }
- RadrootsNostrConnectResponse::RelayListUnchanged => Ok(self.target.relays.clone()),
- RadrootsNostrConnectResponse::Error { error, .. } => {
- Err(RadrootsAppRemoteSignerError::ConnectFailed(format!(
- "remote signer rejected relay update: {error}"
- )))
- }
- other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse {
- method: RadrootsNostrConnectMethod::SwitchRelays,
- response: format!("{other:?}"),
- }),
- }
- }
-
- fn execute_request_with_progress<F>(
- &mut self,
- method: RadrootsNostrConnectMethod,
- request: RadrootsNostrConnectRequest,
- request_timeout: Duration,
- progress: &mut F,
- ) -> Result<RadrootsNostrConnectResponse, RadrootsAppRemoteSignerError>
- where
- F: FnMut(RadrootsAppRemoteSignerProgressUpdate),
- {
- let request_id = next_request_id(method.to_string().as_str());
- let response_method = method.clone();
- self.runtime.block_on(async {
- let event_builder = build_request_event(
- &self.client_identity,
- &self.target.signer_identity,
- request_id.as_str(),
- request,
- )?;
- self.client
- .send_event_builder(event_builder)
- .await
- .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?;
-
- timeout(request_timeout, async {
- loop {
- let notification = match self.notifications.recv().await {
- Ok(notification) => notification,
- Err(broadcast::error::RecvError::Lagged(_)) => continue,
- Err(broadcast::error::RecvError::Closed) => {
- return Err(RadrootsAppRemoteSignerError::ConnectFailed(
- "remote signer notification stream closed".to_owned(),
- ));
- }
- };
- let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification
- else {
- continue;
- };
- let event = *event;
- if event.kind != RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) {
- continue;
- }
- if event.pubkey.to_hex() != self.target.signer_identity.public_key_hex {
- continue;
- }
- match parse_response_event(
- &self.client_identity,
- &event,
- &response_method,
- request_id.as_str(),
- )? {
- Some(RadrootsNostrConnectResponse::AuthUrl(url)) => {
- progress(RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url });
- }
- Some(response) => return Ok(response),
- None => continue,
- }
- }
- })
- .await
- .map_err(|_| RadrootsAppRemoteSignerError::RequestTimedOut {
- method: response_method.clone(),
- })?
- })
- }
-}
-
-fn build_request_event(
- client_identity: &RadrootsIdentity,
- signer_identity: &RadrootsIdentityPublic,
- request_id: &str,
- request: RadrootsNostrConnectRequest,
-) -> Result<RadrootsNostrEventBuilder, RadrootsAppRemoteSignerError> {
- let payload = serde_json::to_string(&RadrootsNostrConnectRequestMessage::new(
- request_id.to_owned(),
- request,
- ))
- .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?;
- let signer_public_key = parse_public_key_hex(signer_identity.public_key_hex.as_str())?;
- let ciphertext = nip44::encrypt(
- client_identity.keys().secret_key(),
- &signer_public_key,
- payload,
- Version::V2,
- )
- .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?;
- Ok(RadrootsNostrEventBuilder::new(
- radroots_nostr_kind(RADROOTS_NOSTR_CONNECT_RPC_KIND),
- ciphertext,
- )
- .tags(vec![RadrootsNostrTag::public_key(signer_public_key)]))
-}
-
-fn parse_response_event(
- client_identity: &RadrootsIdentity,
- event: &RadrootsNostrEvent,
- method: &RadrootsNostrConnectMethod,
- request_id: &str,
-) -> Result<Option<RadrootsNostrConnectResponse>, RadrootsAppRemoteSignerError> {
- let decrypted = nip44::decrypt(
- client_identity.keys().secret_key(),
- &event.pubkey,
- &event.content,
- )
- .map_err(|error| RadrootsAppRemoteSignerError::UnexpectedResponse {
- method: method.clone(),
- response: format!("failed to decrypt signer response: {error}"),
- })?;
- let envelope: RadrootsNostrConnectResponseEnvelope =
- serde_json::from_str(&decrypted).map_err(|error| {
- RadrootsAppRemoteSignerError::UnexpectedResponse {
- method: method.clone(),
- response: format!("failed to decode signer response envelope: {error}"),
- }
- })?;
- if envelope.id != request_id {
- return Ok(None);
- }
- let response =
- RadrootsNostrConnectResponse::from_envelope(method, envelope).map_err(|error| {
- RadrootsAppRemoteSignerError::UnexpectedResponse {
- method: method.clone(),
- response: format!("failed to decode signer response payload: {error}"),
- }
- })?;
- Ok(Some(response))
-}
-
-fn classify_pending_poll_response(
- response: RadrootsNostrConnectResponse,
-) -> RadrootsAppRemoteSignerPendingPollOutcome {
- match response.into_pending_connection_poll_outcome() {
- RadrootsNostrConnectPendingConnectionPollOutcome::Approved(public_key) => {
- RadrootsAppRemoteSignerPendingPollOutcome::Approved(
- RadrootsAppRemoteSignerApprovedSession {
- user_identity: RadrootsIdentityPublic::new(public_key),
- relays: Vec::new(),
- approved_permissions: RadrootsNostrConnectPermissions::default(),
- },
- )
- }
- RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability) => {
- RadrootsAppRemoteSignerPendingPollOutcome::Approved(
- RadrootsAppRemoteSignerApprovedSession {
- user_identity: RadrootsIdentityPublic::new(capability.user_public_key),
- relays: capability
- .relays
- .into_iter()
- .map(|relay| relay.to_string())
- .collect(),
- approved_permissions: capability.permissions,
- },
- )
- }
- RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval => {
- RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval
- }
- RadrootsNostrConnectPendingConnectionPollOutcome::Rejected { message } => {
- RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message }
- }
- RadrootsNostrConnectPendingConnectionPollOutcome::AuthChallenge { url } => {
- RadrootsAppRemoteSignerPendingPollOutcome::FatalError {
- message: format!("unexpected remote signer authorization challenge: {url}"),
- }
- }
- RadrootsNostrConnectPendingConnectionPollOutcome::UnexpectedResponse { response } => {
- RadrootsAppRemoteSignerPendingPollOutcome::FatalError {
- message: format!("unexpected remote signer response: {response}"),
- }
- }
- }
-}
-
-fn classify_pending_poll_error(
- error: RadrootsAppRemoteSignerError,
-) -> RadrootsAppRemoteSignerPendingPollOutcome {
- match error {
- RadrootsAppRemoteSignerError::RequestTimedOut { .. } => {
- RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure {
- message: "remote signer did not respond yet".to_owned(),
- }
- }
- RadrootsAppRemoteSignerError::ConnectFailed(message) => {
- RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message }
- }
- RadrootsAppRemoteSignerError::UnexpectedResponse { .. } => {
- RadrootsAppRemoteSignerPendingPollOutcome::FatalError {
- message: error.to_string(),
- }
- }
- other => RadrootsAppRemoteSignerPendingPollOutcome::FatalError {
- message: other.to_string(),
- },
- }
-}
-
-fn next_request_id(prefix: &str) -> String {
- let tick = REQUEST_COUNTER.fetch_add(1, Ordering::AcqRel);
- format!("{prefix}-{tick}")
-}
-
-fn parse_public_key_hex(value: &str) -> Result<nostr::PublicKey, RadrootsAppRemoteSignerError> {
- nostr::PublicKey::parse(value)
- .or_else(|_| nostr::PublicKey::from_hex(value))
- .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))
-}
-
-fn load_client_identity(
- client_secret_key_hex: &str,
-) -> Result<RadrootsIdentity, RadrootsAppRemoteSignerError> {
- RadrootsIdentity::from_secret_key_str(client_secret_key_hex)
- .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))
-}
-
-fn target_for_record(
- record: &RadrootsAppRemoteSignerSessionRecord,
-) -> RadrootsAppRemoteSignerTarget {
- RadrootsAppRemoteSignerTarget {
- source: crate::RadrootsAppRemoteSignerSource::BunkerUri,
- signer_identity: record.signer_identity.clone(),
- relays: record.relays.clone(),
- connect_secret: None,
- requested_permissions: if record.approved_permissions.is_empty() {
- crate::radroots_app_remote_signer_requested_permissions()
- } else {
- record.approved_permissions.clone()
- },
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::radroots_app_remote_signer_preview;
- use nostr::PublicKey;
- use radroots_app_test_support::{FIXTURE_ALICE, RELAY_PRIMARY_WSS, fixture_identity};
- use radroots_nostr_connect::prelude::{
- RadrootsNostrConnectPermission, RadrootsNostrConnectRemoteSessionCapability,
- };
-
- fn fixture_public_key() -> PublicKey {
- fixture_identity(&FIXTURE_ALICE)
- .expect("identity")
- .public_key()
- }
-
- fn fixture_discovery_url() -> String {
- format!(
- "http://localhost/connect?uri={}",
- url::form_urlencoded::byte_serialize(
- format!("bunker://{}?relay={RELAY_PRIMARY_WSS}", FIXTURE_ALICE.npub).as_bytes()
- )
- .collect::<String>()
- )
- }
-
- #[test]
- fn pending_connection_response_is_classified_as_pending_approval() {
- let outcome =
- classify_pending_poll_response(RadrootsNostrConnectResponse::PendingConnection);
-
- assert!(matches!(
- outcome,
- RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval
- ));
- }
-
- #[test]
- fn signer_error_response_is_classified_as_rejected() {
- let outcome = classify_pending_poll_response(RadrootsNostrConnectResponse::Error {
- result: None,
- error: "unauthorized".to_owned(),
- });
-
- assert!(matches!(
- outcome,
- RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message }
- if message == "unauthorized"
- ));
- }
-
- #[test]
- fn session_capability_success_is_classified_as_approved() {
- let outcome =
- classify_pending_poll_response(RadrootsNostrConnectResponse::RemoteSessionCapability(
- RadrootsNostrConnectRemoteSessionCapability {
- user_public_key: fixture_public_key(),
- relays: vec![nostr::RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay")],
- permissions: vec![
- RadrootsNostrConnectPermission::with_parameter(
- RadrootsNostrConnectMethod::SignEvent,
- "kind:1",
- ),
- RadrootsNostrConnectPermission::new(
- RadrootsNostrConnectMethod::SwitchRelays,
- ),
- ]
- .into(),
- },
- ));
-
- assert!(matches!(
- outcome,
- RadrootsAppRemoteSignerPendingPollOutcome::Approved(
- RadrootsAppRemoteSignerApprovedSession { user_identity, approved_permissions, .. }
- ) if user_identity.public_key_hex == fixture_public_key().to_hex()
- && approved_permissions.to_string() == "sign_event:kind:1,switch_relays"
- ));
- }
-
- #[test]
- fn timeout_error_is_classified_as_transport_failure() {
- let outcome = classify_pending_poll_error(RadrootsAppRemoteSignerError::RequestTimedOut {
- method: RadrootsNostrConnectMethod::GetSessionCapability,
- });
-
- assert!(matches!(
- outcome,
- RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message }
- if message == "remote signer did not respond yet"
- ));
- }
-
- #[test]
- fn unexpected_response_error_is_fatal() {
- let outcome =
- classify_pending_poll_error(RadrootsAppRemoteSignerError::UnexpectedResponse {
- method: RadrootsNostrConnectMethod::GetSessionCapability,
- response: "failed to decode signer response envelope: bad".to_owned(),
- });
-
- assert!(matches!(
- outcome,
- RadrootsAppRemoteSignerPendingPollOutcome::FatalError { message }
- if message.contains("unexpected `get_session_capability` response")
- ));
- }
-
- #[test]
- fn connect_request_uses_explicit_requested_permissions() {
- let target =
- radroots_app_remote_signer_preview(fixture_discovery_url().as_str()).expect("preview");
-
- let request = connect_request_for_target(&target).expect("request");
-
- match request {
- RadrootsNostrConnectRequest::Connect {
- requested_permissions,
- ..
- } => assert_eq!(
- requested_permissions.to_string(),
- "sign_event:kind:1,switch_relays"
- ),
- other => panic!("unexpected request: {other:?}"),
- }
- }
-
- #[test]
- fn sign_kind1_note_output_carries_signed_relay_state() {
- let signed_event = RadrootsAppRemoteSignerSignedEvent {
- event_id_hex: "deadbeef".to_owned(),
- event_json: "{\"id\":\"deadbeef\"}".to_owned(),
- relays: vec!["ws://localhost:8080".to_owned()],
- };
-
- assert_eq!(signed_event.event_id_hex, "deadbeef");
- assert_eq!(signed_event.relays, vec!["ws://localhost:8080".to_owned()]);
- }
-
- #[test]
- fn target_for_record_uses_approved_permissions_when_available() {
- let client_identity = fixture_identity(&FIXTURE_ALICE)
- .expect("client")
- .to_public();
- let signer_identity = fixture_identity(&FIXTURE_ALICE)
- .expect("signer")
- .to_public();
- let mut record = RadrootsAppRemoteSignerSessionRecord::pending(
- client_identity,
- signer_identity,
- vec![RELAY_PRIMARY_WSS.to_owned()],
- );
- record.approved_permissions = vec![RadrootsNostrConnectPermission::new(
- RadrootsNostrConnectMethod::SwitchRelays,
- )]
- .into();
-
- let target = target_for_record(&record);
-
- assert_eq!(target.requested_permissions.to_string(), "switch_relays");
- }
-}
diff --git a/crates/shared/remote_signer/src/session.rs b/crates/shared/remote_signer/src/session.rs
@@ -1,501 +0,0 @@
-use crate::error::RadrootsAppRemoteSignerError;
-use radroots_identity::RadrootsIdentityPublic;
-use radroots_nostr_connect::prelude::{
- RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions,
-};
-use serde::{Deserialize, Serialize};
-use std::io::Write;
-use std::path::Path;
-use std::time::{SystemTime, UNIX_EPOCH};
-
-pub const RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION: u32 = 1;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-pub enum RadrootsAppRemoteSignerSessionStatus {
- PendingApproval,
- Active,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RadrootsAppRemoteSignerSessionRecord {
- pub client_identity: RadrootsIdentityPublic,
- pub signer_identity: RadrootsIdentityPublic,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub user_identity: Option<RadrootsIdentityPublic>,
- pub relays: Vec<String>,
- #[serde(default)]
- pub approved_permissions: RadrootsNostrConnectPermissions,
- pub status: RadrootsAppRemoteSignerSessionStatus,
- pub created_at_unix: u64,
- pub updated_at_unix: u64,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RadrootsAppRemoteSignerSessionStoreState {
- pub version: u32,
- pub sessions: Vec<RadrootsAppRemoteSignerSessionRecord>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RadrootsAppRemoteSignerSessionStoreLoadResult {
- pub state: RadrootsAppRemoteSignerSessionStoreState,
- pub recovered_from_corruption: bool,
-}
-
-impl Default for RadrootsAppRemoteSignerSessionStoreState {
- fn default() -> Self {
- Self {
- version: RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION,
- sessions: Vec::new(),
- }
- }
-}
-
-impl RadrootsAppRemoteSignerSessionRecord {
- pub fn pending(
- client_identity: RadrootsIdentityPublic,
- signer_identity: RadrootsIdentityPublic,
- relays: Vec<String>,
- ) -> Self {
- let now = now_unix_secs();
- Self {
- client_identity,
- signer_identity,
- user_identity: None,
- relays,
- approved_permissions: RadrootsNostrConnectPermissions::default(),
- status: RadrootsAppRemoteSignerSessionStatus::PendingApproval,
- created_at_unix: now,
- updated_at_unix: now,
- }
- }
-
- pub fn account_id(&self) -> Option<&str> {
- self.user_identity
- .as_ref()
- .map(|identity| identity.id.as_str())
- }
-
- pub fn client_account_id(&self) -> &str {
- self.client_identity.id.as_str()
- }
-
- pub fn approved_permission_labels(&self) -> Vec<String> {
- self.approved_permissions
- .as_slice()
- .iter()
- .map(ToString::to_string)
- .collect()
- }
-
- pub fn allows_sign_event_kind1(&self) -> bool {
- self.approved_permissions
- .as_slice()
- .iter()
- .any(|permission| {
- permission_matches(
- permission,
- &RadrootsNostrConnectPermission::with_parameter(
- RadrootsNostrConnectMethod::SignEvent,
- "kind:1",
- ),
- )
- })
- }
-
- pub fn allows_switch_relays(&self) -> bool {
- self.approved_permissions
- .as_slice()
- .iter()
- .any(|permission| {
- permission_matches(
- permission,
- &RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays),
- )
- })
- }
-}
-
-impl RadrootsAppRemoteSignerSessionStoreState {
- pub fn load(path: &Path) -> Result<Self, RadrootsAppRemoteSignerError> {
- Ok(Self::load_with_recovery(path)?.state)
- }
-
- pub fn load_with_recovery(
- path: &Path,
- ) -> Result<RadrootsAppRemoteSignerSessionStoreLoadResult, RadrootsAppRemoteSignerError> {
- match std::fs::read(path) {
- Ok(contents) => Self::load_bytes(path, contents),
- Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
- Ok(RadrootsAppRemoteSignerSessionStoreLoadResult {
- state: Self::default(),
- recovered_from_corruption: false,
- })
- }
- Err(error) => Err(RadrootsAppRemoteSignerError::SessionStoreIo(
- error.to_string(),
- )),
- }
- }
-
- pub fn save(&self, path: &Path) -> Result<(), RadrootsAppRemoteSignerError> {
- if let Some(parent) = path.parent() {
- std::fs::create_dir_all(parent)
- .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))?;
- }
- let json = serde_json::to_string_pretty(self)
- .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))?;
- let temp_path = temporary_store_path(path);
- let mut file = std::fs::OpenOptions::new()
- .write(true)
- .create_new(true)
- .open(temp_path.as_path())
- .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))?;
- if let Err(error) = (|| -> Result<(), std::io::Error> {
- file.write_all(json.as_bytes())?;
- file.flush()?;
- file.sync_all()
- })() {
- let _ = std::fs::remove_file(temp_path.as_path());
- return Err(RadrootsAppRemoteSignerError::SessionStoreIo(
- error.to_string(),
- ));
- }
-
- #[cfg(windows)]
- if path.exists() {
- std::fs::remove_file(path)
- .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))?;
- }
-
- std::fs::rename(temp_path.as_path(), path)
- .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))
- }
-
- pub fn pending_session(&self) -> Option<&RadrootsAppRemoteSignerSessionRecord> {
- self.sessions
- .iter()
- .find(|record| record.status == RadrootsAppRemoteSignerSessionStatus::PendingApproval)
- }
-
- pub fn active_session_for_account_id(
- &self,
- account_id: &str,
- ) -> Option<&RadrootsAppRemoteSignerSessionRecord> {
- self.sessions.iter().find(|record| {
- record.status == RadrootsAppRemoteSignerSessionStatus::Active
- && record.account_id() == Some(account_id)
- })
- }
-
- pub fn upsert_pending(
- &mut self,
- pending: RadrootsAppRemoteSignerSessionRecord,
- ) -> Result<(), RadrootsAppRemoteSignerError> {
- if self.pending_session().is_some() {
- return Err(RadrootsAppRemoteSignerError::PendingSessionExists);
- }
- self.sessions
- .retain(|record| record.client_account_id() != pending.client_account_id());
- self.sessions.push(pending);
- Ok(())
- }
-
- pub fn activate_session(
- &mut self,
- client_account_id: &str,
- user_identity: RadrootsIdentityPublic,
- relays: Vec<String>,
- approved_permissions: RadrootsNostrConnectPermissions,
- ) -> Option<RadrootsAppRemoteSignerSessionRecord> {
- let now = now_unix_secs();
- self.sessions.retain(|record| {
- !(record.status == RadrootsAppRemoteSignerSessionStatus::Active
- && record.account_id() == Some(user_identity.id.as_str()))
- });
- let record = self
- .sessions
- .iter_mut()
- .find(|record| record.client_account_id() == client_account_id)?;
- record.user_identity = Some(user_identity);
- record.relays = relays;
- record.approved_permissions = approved_permissions;
- record.status = RadrootsAppRemoteSignerSessionStatus::Active;
- record.updated_at_unix = now;
- Some(record.clone())
- }
-
- pub fn remove_pending_session(&mut self) -> Option<RadrootsAppRemoteSignerSessionRecord> {
- let index = self.sessions.iter().position(|record| {
- record.status == RadrootsAppRemoteSignerSessionStatus::PendingApproval
- })?;
- Some(self.sessions.remove(index))
- }
-
- pub fn remove_active_session_for_account_id(
- &mut self,
- account_id: &str,
- ) -> Option<RadrootsAppRemoteSignerSessionRecord> {
- let index = self.sessions.iter().position(|record| {
- record.status == RadrootsAppRemoteSignerSessionStatus::Active
- && record.account_id() == Some(account_id)
- })?;
- Some(self.sessions.remove(index))
- }
-
- fn load_bytes(
- path: &Path,
- contents: Vec<u8>,
- ) -> Result<RadrootsAppRemoteSignerSessionStoreLoadResult, RadrootsAppRemoteSignerError> {
- let contents = String::from_utf8(contents).map_err(|error| {
- RadrootsAppRemoteSignerError::InvalidSessionStore(format!(
- "session store was not valid utf-8: {error}"
- ))
- });
-
- let contents = match contents {
- Ok(contents) => contents,
- Err(error) => {
- quarantine_invalid_store(path)?;
- let _ = error;
- return Ok(RadrootsAppRemoteSignerSessionStoreLoadResult {
- state: Self::default(),
- recovered_from_corruption: true,
- });
- }
- };
-
- let state = match serde_json::from_str::<Self>(&contents) {
- Ok(state) => state,
- Err(error) => {
- quarantine_invalid_store(path)?;
- let _ = error;
- return Ok(RadrootsAppRemoteSignerSessionStoreLoadResult {
- state: Self::default(),
- recovered_from_corruption: true,
- });
- }
- };
-
- if state.version != RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION {
- quarantine_invalid_store(path)?;
- return Ok(RadrootsAppRemoteSignerSessionStoreLoadResult {
- state: Self::default(),
- recovered_from_corruption: true,
- });
- }
-
- Ok(RadrootsAppRemoteSignerSessionStoreLoadResult {
- state,
- recovered_from_corruption: false,
- })
- }
-}
-
-fn permission_matches(
- granted_permission: &RadrootsNostrConnectPermission,
- required_permission: &RadrootsNostrConnectPermission,
-) -> bool {
- if granted_permission.method != required_permission.method {
- return false;
- }
-
- match (
- &granted_permission.method,
- granted_permission.parameter.as_deref(),
- required_permission.parameter.as_deref(),
- ) {
- (RadrootsNostrConnectMethod::SignEvent, None, _) => true,
- (RadrootsNostrConnectMethod::SignEvent, Some(parameter), Some(required)) => {
- parameter == required || parameter == sign_event_kind_suffix(required)
- }
- (_, None, _) => true,
- (_, Some(parameter), Some(required)) => parameter == required,
- (_, Some(_), None) => false,
- }
-}
-
-fn sign_event_kind_suffix(value: &str) -> &str {
- value.strip_prefix("kind:").unwrap_or(value)
-}
-
-fn now_unix_secs() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_secs())
- .unwrap_or(0)
-}
-
-fn temporary_store_path(path: &Path) -> std::path::PathBuf {
- let process_id = std::process::id();
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_nanos())
- .unwrap_or(0);
- path.with_extension(format!("json.tmp-{process_id}-{timestamp}"))
-}
-
-fn quarantine_invalid_store(path: &Path) -> Result<(), RadrootsAppRemoteSignerError> {
- let process_id = std::process::id();
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_secs())
- .unwrap_or(0);
- let file_name = path
- .file_name()
- .and_then(|name| name.to_str())
- .unwrap_or("remote-signer-sessions.json");
- let quarantine_path =
- path.with_file_name(format!("{file_name}.corrupt-{timestamp}-{process_id}"));
- std::fs::rename(path, quarantine_path.as_path())
- .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use radroots_app_test_support::{FIXTURE_ALICE, FIXTURE_BOB, fixture_identity};
- use radroots_nostr_connect::prelude::{
- RadrootsNostrConnectMethod, RadrootsNostrConnectPermission,
- };
-
- fn fixture_public(
- label: &radroots_app_test_support::RadrootsAppApprovedFixtureIdentity,
- ) -> RadrootsIdentityPublic {
- fixture_identity(label).expect("identity").to_public()
- }
-
- fn pending_record() -> RadrootsAppRemoteSignerSessionRecord {
- RadrootsAppRemoteSignerSessionRecord::pending(
- fixture_public(&FIXTURE_ALICE),
- fixture_public(&FIXTURE_BOB),
- vec!["wss://relay.example.com".to_owned()],
- )
- }
-
- #[test]
- fn pending_store_round_trips() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- let mut state = RadrootsAppRemoteSignerSessionStoreState::default();
- state.upsert_pending(pending_record()).expect("pending");
- state.save(path.as_path()).expect("save");
-
- let loaded = RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load");
-
- assert_eq!(loaded.sessions.len(), 1);
- assert_eq!(
- loaded.sessions[0].status,
- RadrootsAppRemoteSignerSessionStatus::PendingApproval
- );
- }
-
- #[test]
- fn activate_session_replaces_pending_with_active_user_identity() {
- let mut state = RadrootsAppRemoteSignerSessionStoreState::default();
- let pending = pending_record();
- let client_account_id = pending.client_account_id().to_owned();
- state.upsert_pending(pending).expect("pending");
-
- let alice_public = fixture_public(&FIXTURE_ALICE);
- let active = state
- .activate_session(
- client_account_id.as_str(),
- alice_public.clone(),
- vec!["wss://relay.updated.example".to_owned()],
- vec![
- RadrootsNostrConnectPermission::with_parameter(
- RadrootsNostrConnectMethod::SignEvent,
- "kind:1",
- ),
- RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays),
- ]
- .into(),
- )
- .expect("active");
-
- assert_eq!(active.status, RadrootsAppRemoteSignerSessionStatus::Active);
- assert_eq!(active.account_id(), Some(alice_public.id.as_str()));
- assert_eq!(
- active.relays,
- vec!["wss://relay.updated.example".to_owned()]
- );
- assert_eq!(
- active.approved_permission_labels(),
- vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()]
- );
- assert!(active.allows_sign_event_kind1());
- assert!(active.allows_switch_relays());
- assert!(state.pending_session().is_none());
- }
-
- #[test]
- fn remove_active_session_matches_user_account_id() {
- let mut state = RadrootsAppRemoteSignerSessionStoreState::default();
- let pending = pending_record();
- let client_account_id = pending.client_account_id().to_owned();
- state.upsert_pending(pending).expect("pending");
- let alice_public = fixture_public(&FIXTURE_ALICE);
- state.activate_session(
- client_account_id.as_str(),
- alice_public.clone(),
- vec!["wss://relay.updated.example".to_owned()],
- RadrootsNostrConnectPermissions::default(),
- );
-
- let removed = state
- .remove_active_session_for_account_id(alice_public.id.as_str())
- .expect("removed");
-
- assert_eq!(removed.account_id(), Some(alice_public.id.as_str()));
- assert!(state.sessions.is_empty());
- }
-
- #[test]
- fn load_recovers_from_invalid_json_by_quarantining_store() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- std::fs::write(path.as_path(), "{invalid").expect("write invalid");
-
- let loaded = RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load");
-
- assert!(loaded.sessions.is_empty());
- assert!(!path.exists());
- let quarantined = std::fs::read_dir(temp.path())
- .expect("read dir")
- .filter_map(|entry| entry.ok())
- .any(|entry| entry.file_name().to_string_lossy().contains("corrupt"));
- assert!(quarantined);
- }
-
- #[test]
- fn load_recovers_from_unsupported_schema_version() {
- let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("sessions.json");
- std::fs::write(path.as_path(), r#"{"version":999,"sessions":[]}"#).expect("write invalid");
-
- let loaded = RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load");
-
- assert_eq!(
- loaded.version,
- RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION
- );
- assert!(loaded.sessions.is_empty());
- assert!(!path.exists());
- }
-
- #[test]
- fn active_session_permission_helpers_respect_sign_event_and_switch_relays() {
- let mut record = pending_record();
- record.user_identity = Some(fixture_public(&FIXTURE_ALICE));
- record.status = RadrootsAppRemoteSignerSessionStatus::Active;
- record.approved_permissions = vec![RadrootsNostrConnectPermission::with_parameter(
- RadrootsNostrConnectMethod::SignEvent,
- "1",
- )]
- .into();
-
- assert!(record.allows_sign_event_kind1());
- assert!(!record.allows_switch_relays());
- }
-}
diff --git a/crates/shared/test_support/Cargo.toml b/crates/shared/test_support/Cargo.toml
@@ -1,17 +0,0 @@
-[package]
-name = "radroots_app_test_support"
-authors.workspace = true
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-repository.workspace = true
-homepage.workspace = true
-description = "Rad Roots app test support"
-publish = false
-
-[lints]
-workspace = true
-
-[dependencies]
-radroots_identity.workspace = true
diff --git a/crates/shared/test_support/src/lib.rs b/crates/shared/test_support/src/lib.rs
@@ -1,100 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_identity::{
- RadrootsIdentity, RadrootsIdentityEncryptedSecretKeyOptions,
- RadrootsIdentityEncryptedSecretKeySecurity,
-};
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub struct RadrootsAppApprovedFixtureIdentity {
- pub label: &'static str,
- pub account_id: &'static str,
- pub username: &'static str,
- pub email: &'static str,
- pub secret_key_hex: &'static str,
- pub nsec: &'static str,
- pub npub: &'static str,
-}
-
-pub const FIXTURE_ALICE: RadrootsAppApprovedFixtureIdentity = RadrootsAppApprovedFixtureIdentity {
- label: "fixture_alice",
- account_id: "fixture-account-alice",
- username: "fixture_alice",
- email: "fixture_alice@fixtures.test",
- secret_key_hex: "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5",
- nsec: "nsec1zrznqntvnt36rgt00ps0rny0tca8vgj6ye3m82vf5rthtyvm0h6syu7drz",
- npub: "npub1tp2ez55a5zatxxemrv0eses3ea05xhw2snuh3jy7azjqejn3q00s3vy5a9",
-};
-
-pub const FIXTURE_BOB: RadrootsAppApprovedFixtureIdentity = RadrootsAppApprovedFixtureIdentity {
- label: "fixture_bob",
- account_id: "fixture-account-bob",
- username: "fixture_bob",
- email: "fixture_bob@fixtures.test",
- secret_key_hex: "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8",
- nsec: "nsec1tyujayrg7ejrrvf0wqscldsjs89kksea0unu2htp7xnrlcdfdluqrjya9h",
- npub: "npub1uqnxu08mp55gd7guw06ls68nhxp8xuf7tlxe0sypvcl42x9ykwhsd55k2g",
-};
-
-pub const FIXTURE_CAROL: RadrootsAppApprovedFixtureIdentity = RadrootsAppApprovedFixtureIdentity {
- label: "fixture_carol",
- account_id: "fixture-account-carol",
- username: "fixture_carol",
- email: "fixture_carol@fixtures.test",
- secret_key_hex: "4d6c20fdd86857de77ff5cfa5c545751ba2efd126e0b6642dae9764d782d6509",
- nsec: "nsec1f4kzplwcdptaualltna9c4zh2xazalgjdc9kvsk6a9my67pdv5ys2pqkaj",
- npub: "npub1r9ft33558zvtemluludhdxwy5a66f5fmf2d6qztt5fh0q3yjhvwqgzmkl6",
-};
-
-pub const FIXTURE_DIEGO: RadrootsAppApprovedFixtureIdentity = RadrootsAppApprovedFixtureIdentity {
- label: "fixture_diego",
- account_id: "fixture-account-diego",
- username: "fixture_diego",
- email: "fixture_diego@fixtures.test",
- secret_key_hex: "9de56c1fdfce9ab00af85b3d7003c1d15cffb84cdf303c3a83c1a3fb1a2d0db0",
- nsec: "nsec1nhjkc87le6dtqzhctv7hqq7p69w0lwzvmucrcw5rcx3lkx3dpkcqkrmgp5",
- npub: "npub1t5l2kmncadlyv757r94xx3tvn7hmj0ac3dc99wpj9xrs3zvj82jqwwcglm",
-};
-
-pub const RELAY_PRIMARY_WSS: &str = "wss://relay.example.com";
-pub const RELAY_SECONDARY_WSS: &str = "wss://relay-2.example.com";
-pub const RELAY_TERTIARY_WSS: &str = "wss://relay-3.example.com";
-
-pub const APP_PRIMARY_URL: &str = "https://app.example.com";
-pub const API_PRIMARY_URL: &str = "https://api.example.com";
-pub const CDN_PRIMARY_URL: &str = "https://cdn.example.com";
-pub const FIXTURE_BACKUP_PASSWORD: &str = "fixture-backup-password";
-
-pub fn fixture_identity(
- fixture: &RadrootsAppApprovedFixtureIdentity,
-) -> Result<RadrootsIdentity, radroots_identity::IdentityError> {
- RadrootsIdentity::from_secret_key_str(fixture.secret_key_hex)
-}
-
-pub fn fixture_identity_ncryptsec(
- fixture: &RadrootsAppApprovedFixtureIdentity,
- password: &str,
-) -> Result<String, radroots_identity::IdentityError> {
- fixture_identity(fixture)?.encrypt_secret_key_ncryptsec_with_options(
- password,
- RadrootsIdentityEncryptedSecretKeyOptions {
- log_n: 10,
- key_security: RadrootsIdentityEncryptedSecretKeySecurity::Weak,
- },
- )
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn approved_fixture_identities_match_exported_strings() {
- for fixture in [FIXTURE_ALICE, FIXTURE_BOB, FIXTURE_CAROL, FIXTURE_DIEGO] {
- let identity = fixture_identity(&fixture).expect("fixture identity");
- assert_eq!(identity.secret_key_hex(), fixture.secret_key_hex);
- assert_eq!(identity.nsec(), fixture.nsec);
- assert_eq!(identity.npub(), fixture.npub);
- }
- }
-}
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/build.gradle.kts b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/build.gradle.kts
@@ -1,32 +0,0 @@
-plugins {
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
-}
-
-android {
- namespace = "org.radroots.app.android.security"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
- consumerProguardFiles("consumer-rules.pro")
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-
- kotlinOptions {
- jvmTarget = "17"
- }
-
- testOptions {
- unitTests.isIncludeAndroidResources = false
- }
-}
-
-dependencies {
- implementation("androidx.biometric:biometric:1.1.0")
- testImplementation("junit:junit:4.13.2")
-}
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/consumer-rules.pro b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/consumer-rules.pro
@@ -1 +0,0 @@
-
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/settings.gradle.kts b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/settings.gradle.kts
@@ -1,17 +0,0 @@
-pluginManagement {
- repositories {
- gradlePluginPortal()
- google()
- mavenCentral()
- }
-}
-
-dependencyResolutionManagement {
- repositoriesMode.set(org.gradle.api.initialization.resolve.RepositoriesMode.FAIL_ON_PROJECT_REPOS)
- repositories {
- google()
- mavenCentral()
- }
-}
-
-rootProject.name = "RadRootsAndroidSecurity"
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/AndroidManifest.xml b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/AndroidManifest.xml
@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeySecurityLevel.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeySecurityLevel.kt
@@ -1,73 +0,0 @@
-package org.radroots.app.android.security
-
-import android.os.Build
-import android.security.keystore.KeyInfo
-import android.security.keystore.KeyProperties
-
-internal enum class RadRootsAndroidKeySecurityLevel {
- STRONGBOX,
- TRUSTED_ENVIRONMENT,
- SOFTWARE_OR_UNKNOWN,
-}
-
-internal object RadRootsAndroidKeySecurityLevels {
- fun fromKeyInfo(keyInfo: KeyInfo): RadRootsAndroidKeySecurityLevel {
- return fromPlatformValues(
- sdkInt = Build.VERSION.SDK_INT,
- securityLevel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- keyInfo.securityLevel
- } else {
- null
- },
- isInsideSecureHardware = isInsideSecureHardwareFallback(keyInfo),
- )
- }
-
- fun fromPlatformValues(
- sdkInt: Int,
- securityLevel: Int?,
- isInsideSecureHardware: Boolean,
- ): RadRootsAndroidKeySecurityLevel {
- if (sdkInt >= Build.VERSION_CODES.S && securityLevel != null) {
- return when (securityLevel) {
- KeyProperties.SECURITY_LEVEL_STRONGBOX -> RadRootsAndroidKeySecurityLevel.STRONGBOX
- KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT,
- KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE,
- -> RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT
- else -> RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN
- }
- }
-
- return if (isInsideSecureHardware) {
- RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT
- } else {
- RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN
- }
- }
-
- @Suppress("DEPRECATION")
- private fun isInsideSecureHardwareFallback(keyInfo: KeyInfo): Boolean {
- return keyInfo.isInsideSecureHardware
- }
-}
-
-internal fun shouldRequestStrongBox(
- policy: RadRootsAndroidSecretAccessPolicy,
- sdkInt: Int,
- hasStrongBoxFeature: Boolean,
-): Boolean {
- return policy.preferStrongBox &&
- sdkInt >= Build.VERSION_CODES.P &&
- hasStrongBoxFeature
-}
-
-internal fun acceptsStrongBoxVerificationResult(
- sdkInt: Int,
- securityLevel: RadRootsAndroidKeySecurityLevel,
-): Boolean {
- return when (securityLevel) {
- RadRootsAndroidKeySecurityLevel.STRONGBOX -> true
- RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT -> sdkInt < Build.VERSION_CODES.S
- RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN -> false
- }
-}
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeystoreSecretStore.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeystoreSecretStore.kt
@@ -1,371 +0,0 @@
-package org.radroots.app.android.security
-
-import android.content.Context
-import android.content.pm.PackageManager
-import android.os.Build
-import android.security.keystore.KeyGenParameterSpec
-import android.security.keystore.KeyInfo
-import android.security.keystore.KeyProperties
-import android.security.keystore.StrongBoxUnavailableException
-import java.io.File
-import java.nio.ByteBuffer
-import java.nio.file.AtomicMoveNotSupportedException
-import java.nio.file.Files
-import java.nio.file.StandardCopyOption
-import java.security.KeyStore
-import javax.crypto.Cipher
-import javax.crypto.KeyGenerator
-import javax.crypto.SecretKey
-import javax.crypto.SecretKeyFactory
-import javax.crypto.spec.GCMParameterSpec
-
-class RadRootsAndroidKeystoreSecretStore(
- private val context: Context,
-) {
- fun putSecret(
- servicePrefix: String,
- namespace: String,
- name: String,
- value: ByteArray,
- policy: RadRootsAndroidSecretAccessPolicy,
- ) {
- validateIdentifiers(servicePrefix, namespace, name)
- requireSupportedPolicy(policy)
- val key = getOrCreateKey(masterKeyAlias(servicePrefix, namespace), policy)
- val cipher = Cipher.getInstance(cipherTransformation)
- cipher.init(Cipher.ENCRYPT_MODE, key)
- val iv = cipher.iv
- val ciphertext = cipher.doFinal(value)
- val target = RadRootsAndroidStoragePaths.secretFile(context, servicePrefix, namespace, name)
- writeSecretFile(target, encodeSecretBlob(iv, ciphertext))
- }
-
- fun getSecret(
- servicePrefix: String,
- namespace: String,
- name: String,
- ): ByteArray? {
- validateIdentifiers(servicePrefix, namespace, name)
- val target = RadRootsAndroidStoragePaths.secretFile(context, servicePrefix, namespace, name)
- val legacyTarget = RadRootsAndroidStoragePaths.legacySecretFile(
- context.noBackupFilesDir,
- servicePrefix,
- namespace,
- name,
- )
- val source = when {
- target.exists() -> target
- legacyTarget.exists() -> legacyTarget
- else -> null
- }
- if (source == null) {
- return null
- }
- val secretBlob = readSecretFile(source)
- val (iv, ciphertext) = decodeSecretBlob(secretBlob)
- val cipher = Cipher.getInstance(cipherTransformation)
- cipher.init(
- Cipher.DECRYPT_MODE,
- getOrCreateKey(
- masterKeyAlias(servicePrefix, namespace),
- RadRootsAndroidSecretAccessPolicy.SECURE_LOCAL_SECRET,
- ),
- GCMParameterSpec(gcmTagBits, iv),
- )
- return try {
- cipher.doFinal(ciphertext)
- } catch (cause: Throwable) {
- throw RadRootsAndroidSecurityError.KeystoreFailure(
- "failed to decrypt secret",
- cause,
- )
- }
- }
-
- fun deleteSecret(
- servicePrefix: String,
- namespace: String,
- name: String,
- ) {
- validateIdentifiers(servicePrefix, namespace, name)
- val current = RadRootsAndroidStoragePaths.secretFile(context, servicePrefix, namespace, name)
- val legacy = RadRootsAndroidStoragePaths.legacySecretFile(
- context.noBackupFilesDir,
- servicePrefix,
- namespace,
- name,
- )
- deleteSecretFileIfPresent(current)
- deleteSecretFileIfPresent(legacy)
- }
-
- fun deleteNamespace(
- servicePrefix: String,
- namespace: String,
- ) {
- validateNamespace(servicePrefix, namespace)
- val secretsDir = RadRootsAndroidStoragePaths.secretsDir(context)
- val prefix = RadRootsAndroidStoragePaths.namespaceFilePrefix(servicePrefix, namespace)
- val children = secretsDir.listFiles().orEmpty()
- for (child in children) {
- if (!child.isFile || !child.name.startsWith(prefix) || !child.name.endsWith(".bin")) {
- continue
- }
- if (!child.delete()) {
- throw RadRootsAndroidSecurityError.StorageFailure(
- "failed to delete encrypted secret namespace file",
- )
- }
- }
- deleteKey(masterKeyAlias(servicePrefix, namespace))
- }
-
- fun resolveRadrootsBaseRoot(): File = RadRootsAndroidStoragePaths.baseRoot(context)
-
- private fun validateIdentifiers(servicePrefix: String, namespace: String, name: String) {
- if (servicePrefix.isBlank()) {
- throw RadRootsAndroidSecurityError.InvalidInput("service prefix must not be blank")
- }
- if (namespace.isBlank()) {
- throw RadRootsAndroidSecurityError.InvalidInput("namespace must not be blank")
- }
- if (name.isBlank()) {
- throw RadRootsAndroidSecurityError.InvalidInput("name must not be blank")
- }
- }
-
- private fun validateNamespace(servicePrefix: String, namespace: String) {
- if (servicePrefix.isBlank()) {
- throw RadRootsAndroidSecurityError.InvalidInput("service prefix must not be blank")
- }
- if (namespace.isBlank()) {
- throw RadRootsAndroidSecurityError.InvalidInput("namespace must not be blank")
- }
- }
-
- private fun requireSupportedPolicy(policy: RadRootsAndroidSecretAccessPolicy) {
- if (!policy.deviceLocalOnly) {
- throw RadRootsAndroidSecurityError.InvalidInput(
- "android security store supports only device-local secrets",
- )
- }
- }
-
- private fun masterKeyAlias(servicePrefix: String, namespace: String): String =
- "org.radroots.app.android.security.v1.${RadRootsAndroidStoragePaths.secretFileId(servicePrefix, namespace, "master")}"
-
- private fun getOrCreateKey(
- alias: String,
- policy: RadRootsAndroidSecretAccessPolicy,
- ): SecretKey {
- val keyStore = KeyStore.getInstance(androidKeystoreProvider).apply { load(null) }
- val existing = keyStore.getKey(alias, null)
- if (existing is SecretKey) {
- return existing
- }
- return createKey(alias, policy)
- }
-
- private fun createKey(
- alias: String,
- policy: RadRootsAndroidSecretAccessPolicy,
- ): SecretKey {
- val requestStrongBox = shouldRequestStrongBox(
- policy = policy,
- sdkInt = Build.VERSION.SDK_INT,
- hasStrongBoxFeature = canRequestStrongBox(),
- )
-
- return try {
- val generated = generateKey(alias, policy, requestStrongBox = requestStrongBox)
- if (requestStrongBox && !isAcceptableStrongBoxResult(generated.securityLevel)) {
- deleteKey(alias)
- return generateKey(alias, policy, requestStrongBox = false).key
- }
- generated.key
- } catch (cause: StrongBoxUnavailableException) {
- if (!requestStrongBox) {
- throw keystoreFailure(cause)
- }
- deleteKey(alias)
- generateKey(alias, policy, requestStrongBox = false).key
- } catch (cause: Throwable) {
- throw keystoreFailure(cause)
- }
- }
-
- private fun generateKey(
- alias: String,
- policy: RadRootsAndroidSecretAccessPolicy,
- requestStrongBox: Boolean,
- ): AndroidKeyCreationResult {
- val builder = KeyGenParameterSpec.Builder(
- alias,
- KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
- )
- .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
- .setKeySize(256)
- .setRandomizedEncryptionRequired(true)
-
- if (policy.userPresenceRequired) {
- builder.setUserAuthenticationRequired(true)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- builder.setUserAuthenticationParameters(
- 0,
- KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL,
- )
- }
- }
-
- if (requestStrongBox && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- builder.setIsStrongBoxBacked(true)
- }
-
- val keyGenerator = KeyGenerator.getInstance(
- KeyProperties.KEY_ALGORITHM_AES,
- androidKeystoreProvider,
- )
- keyGenerator.init(builder.build())
- val key = keyGenerator.generateKey()
- return AndroidKeyCreationResult(
- key = key,
- securityLevel = resolveKeySecurityLevel(key),
- )
- }
-
- private fun writeSecretFile(target: File, encoded: ByteArray) {
- val parent = target.parentFile
- ?: throw RadRootsAndroidSecurityError.StorageFailure("secret file has no parent directory")
- if (!parent.exists() && !parent.mkdirs()) {
- throw RadRootsAndroidSecurityError.StorageFailure("failed to create secret directory")
- }
- val temp = File(parent, "${target.name}.tmp")
- try {
- temp.writeBytes(encoded)
- try {
- Files.move(
- temp.toPath(),
- target.toPath(),
- StandardCopyOption.ATOMIC_MOVE,
- StandardCopyOption.REPLACE_EXISTING,
- )
- } catch (_: AtomicMoveNotSupportedException) {
- Files.move(
- temp.toPath(),
- target.toPath(),
- StandardCopyOption.REPLACE_EXISTING,
- )
- }
- } catch (cause: Throwable) {
- temp.delete()
- throw RadRootsAndroidSecurityError.StorageFailure(
- "failed to write encrypted secret file",
- cause,
- )
- }
- }
-
- private fun readSecretFile(target: File): ByteArray {
- return try {
- target.readBytes()
- } catch (cause: Throwable) {
- throw RadRootsAndroidSecurityError.StorageFailure(
- "failed to read encrypted secret file",
- cause,
- )
- }
- }
-
- private fun encodeSecretBlob(iv: ByteArray, ciphertext: ByteArray): ByteArray {
- val buffer = ByteBuffer.allocate(1 + Int.SIZE_BYTES + iv.size + ciphertext.size)
- buffer.put(secretBlobVersion)
- buffer.putInt(iv.size)
- buffer.put(iv)
- buffer.put(ciphertext)
- return buffer.array()
- }
-
- private fun decodeSecretBlob(blob: ByteArray): Pair<ByteArray, ByteArray> {
- try {
- val buffer = ByteBuffer.wrap(blob)
- val version = buffer.get()
- if (version != secretBlobVersion) {
- throw RadRootsAndroidSecurityError.StorageFailure("unsupported encrypted secret version")
- }
- val ivLength = buffer.int
- if (ivLength <= 0 || ivLength > buffer.remaining()) {
- throw RadRootsAndroidSecurityError.StorageFailure("invalid encrypted secret iv length")
- }
- val iv = ByteArray(ivLength)
- buffer.get(iv)
- val ciphertext = ByteArray(buffer.remaining())
- buffer.get(ciphertext)
- return iv to ciphertext
- } catch (error: RadRootsAndroidSecurityError.StorageFailure) {
- throw error
- } catch (cause: Throwable) {
- throw RadRootsAndroidSecurityError.StorageFailure(
- "failed to decode encrypted secret file",
- cause,
- )
- }
- }
-
- private fun canRequestStrongBox(): Boolean {
- return context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
- }
-
- private fun isAcceptableStrongBoxResult(
- securityLevel: RadRootsAndroidKeySecurityLevel,
- ): Boolean {
- return acceptsStrongBoxVerificationResult(
- sdkInt = Build.VERSION.SDK_INT,
- securityLevel = securityLevel,
- )
- }
-
- private fun resolveKeySecurityLevel(key: SecretKey): RadRootsAndroidKeySecurityLevel {
- val keyFactory = SecretKeyFactory.getInstance(key.algorithm, androidKeystoreProvider)
- val keyInfo = keyFactory.getKeySpec(key, KeyInfo::class.java) as KeyInfo
- return RadRootsAndroidKeySecurityLevels.fromKeyInfo(keyInfo)
- }
-
- private fun deleteKey(alias: String) {
- val keyStore = KeyStore.getInstance(androidKeystoreProvider).apply { load(null) }
- if (keyStore.containsAlias(alias)) {
- keyStore.deleteEntry(alias)
- }
- }
-
- private fun deleteSecretFileIfPresent(target: File) {
- if (!target.exists()) {
- return
- }
- if (!target.delete()) {
- throw RadRootsAndroidSecurityError.StorageFailure("failed to delete encrypted secret file")
- }
- }
-
- private fun keystoreFailure(cause: Throwable): RadRootsAndroidSecurityError.KeystoreFailure {
- return when (cause) {
- is RadRootsAndroidSecurityError.KeystoreFailure -> cause
- else -> RadRootsAndroidSecurityError.KeystoreFailure(
- "failed to create keystore secret key",
- cause,
- )
- }
- }
-
- private companion object {
- const val androidKeystoreProvider = "AndroidKeyStore"
- const val cipherTransformation = "AES/GCM/NoPadding"
- const val gcmTagBits = 128
- const val secretBlobVersion: Byte = 1
- }
-}
-
-private data class AndroidKeyCreationResult(
- val key: SecretKey,
- val securityLevel: RadRootsAndroidKeySecurityLevel,
-)
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecretAccessPolicy.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecretAccessPolicy.kt
@@ -1,15 +0,0 @@
-package org.radroots.app.android.security
-
-data class RadRootsAndroidSecretAccessPolicy(
- val deviceLocalOnly: Boolean,
- val userPresenceRequired: Boolean,
- val preferStrongBox: Boolean,
-) {
- companion object {
- val SECURE_LOCAL_SECRET = RadRootsAndroidSecretAccessPolicy(
- deviceLocalOnly = true,
- userPresenceRequired = false,
- preferStrongBox = true,
- )
- }
-}
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityBridge.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityBridge.kt
@@ -1,197 +0,0 @@
-package org.radroots.app.android.security
-
-import android.content.Context
-import androidx.fragment.app.FragmentActivity
-
-object RadRootsAndroidSecurityBridge {
- const val STATUS_SUCCESS = 0
- const val STATUS_NOT_FOUND = 1
- const val STATUS_INVALID_INPUT = 2
- const val STATUS_ERROR = 3
-
- const val USER_PRESENCE_RESULT_NONE = 0
- const val USER_PRESENCE_RESULT_SUCCESS = 1
- const val USER_PRESENCE_RESULT_ERROR = 2
-
- @Volatile
- private var applicationContext: Context? = null
-
- @Volatile
- private var currentActivity: FragmentActivity? = null
-
- @Volatile
- private var lastErrorMessage: String? = null
-
- @Volatile
- private var userPresenceVerificationPending: Boolean = false
-
- @Volatile
- private var userPresenceVerificationResult: Int = USER_PRESENCE_RESULT_NONE
-
- @JvmStatic
- fun initialize(context: Context) {
- applicationContext = context.applicationContext
- currentActivity = context as? FragmentActivity
- clearError()
- }
-
- @JvmStatic
- fun putSecret(
- servicePrefix: String,
- namespace: String,
- name: String,
- value: ByteArray,
- deviceLocalOnly: Boolean,
- userPresenceRequired: Boolean,
- preferStrongBox: Boolean,
- ): Int {
- return try {
- secretStore().putSecret(
- servicePrefix = servicePrefix,
- namespace = namespace,
- name = name,
- value = value,
- policy = RadRootsAndroidSecretAccessPolicy(
- deviceLocalOnly = deviceLocalOnly,
- userPresenceRequired = userPresenceRequired,
- preferStrongBox = preferStrongBox,
- ),
- )
- clearError()
- STATUS_SUCCESS
- } catch (cause: Throwable) {
- captureError(cause)
- }
- }
-
- @JvmStatic
- fun getSecret(
- servicePrefix: String,
- namespace: String,
- name: String,
- ): ByteArray? {
- return try {
- val secret = secretStore().getSecret(servicePrefix, namespace, name)
- clearError()
- secret
- } catch (cause: Throwable) {
- captureError(cause)
- null
- }
- }
-
- @JvmStatic
- fun deleteSecret(
- servicePrefix: String,
- namespace: String,
- name: String,
- ): Int {
- return try {
- secretStore().deleteSecret(servicePrefix, namespace, name)
- clearError()
- STATUS_SUCCESS
- } catch (cause: Throwable) {
- captureError(cause)
- }
- }
-
- @JvmStatic
- fun deleteSecretNamespace(
- servicePrefix: String,
- namespace: String,
- ): Int {
- return try {
- secretStore().deleteNamespace(servicePrefix, namespace)
- clearError()
- STATUS_SUCCESS
- } catch (cause: Throwable) {
- captureError(cause)
- }
- }
-
- @JvmStatic
- fun resolveRadrootsBaseRoot(): String? {
- return try {
- val path = secretStore().resolveRadrootsBaseRoot().absolutePath
- clearError()
- path
- } catch (cause: Throwable) {
- captureError(cause)
- null
- }
- }
-
- @JvmStatic
- fun beginUserPresenceVerification(reason: String): Int {
- return try {
- if (reason.isBlank()) {
- throw RadRootsAndroidSecurityError.InvalidInput("verification reason must not be blank")
- }
- if (userPresenceVerificationPending) {
- throw RadRootsAndroidSecurityError.InvalidInput("device authentication is already in progress")
- }
- val activity = currentActivity
- ?: throw RadRootsAndroidSecurityError.InvalidInput("android security bridge has no active activity")
-
- clearError()
- userPresenceVerificationPending = true
- userPresenceVerificationResult = USER_PRESENCE_RESULT_NONE
-
- RadRootsAndroidUserPresenceVerifier(activity).beginVerification(
- reason = reason,
- onSuccess = {
- clearError()
- userPresenceVerificationPending = false
- userPresenceVerificationResult = USER_PRESENCE_RESULT_SUCCESS
- },
- onFailure = { cause ->
- lastErrorMessage = cause.message ?: cause.toString()
- userPresenceVerificationPending = false
- userPresenceVerificationResult = USER_PRESENCE_RESULT_ERROR
- },
- )
-
- STATUS_SUCCESS
- } catch (cause: Throwable) {
- userPresenceVerificationPending = false
- userPresenceVerificationResult = USER_PRESENCE_RESULT_NONE
- captureError(cause)
- }
- }
-
- @JvmStatic
- fun isUserPresenceVerificationPending(): Boolean = userPresenceVerificationPending
-
- @JvmStatic
- fun takeUserPresenceVerificationResult(): Int {
- val result = userPresenceVerificationResult
- userPresenceVerificationResult = USER_PRESENCE_RESULT_NONE
- return result
- }
-
- @JvmStatic
- fun takeLastErrorMessage(): String? {
- val message = lastErrorMessage
- lastErrorMessage = null
- return message
- }
-
- private fun secretStore(): RadRootsAndroidKeystoreSecretStore {
- val context = applicationContext
- ?: throw RadRootsAndroidSecurityError.InvalidInput("android security bridge is not initialized")
- return RadRootsAndroidKeystoreSecretStore(context)
- }
-
- private fun captureError(cause: Throwable): Int {
- lastErrorMessage = cause.message ?: cause.toString()
- return when (cause) {
- is RadRootsAndroidSecurityError.NotFound -> STATUS_NOT_FOUND
- is RadRootsAndroidSecurityError.InvalidInput -> STATUS_INVALID_INPUT
- else -> STATUS_ERROR
- }
- }
-
- private fun clearError() {
- lastErrorMessage = null
- }
-}
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityError.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityError.kt
@@ -1,23 +0,0 @@
-package org.radroots.app.android.security
-
-sealed class RadRootsAndroidSecurityError(
- message: String,
- cause: Throwable? = null,
-) : Exception(message, cause) {
- class InvalidInput(message: String) : RadRootsAndroidSecurityError(message)
-
- class NotFound(message: String) : RadRootsAndroidSecurityError(message)
-
- class KeystoreFailure(message: String, cause: Throwable? = null) :
- RadRootsAndroidSecurityError(message, cause)
-
- class StorageFailure(message: String, cause: Throwable? = null) :
- RadRootsAndroidSecurityError(message, cause)
-
- class UserCancelled(message: String) : RadRootsAndroidSecurityError(message)
-
- class UserPresenceUnavailable(message: String) : RadRootsAndroidSecurityError(message)
-
- class UserPresenceFailure(message: String, cause: Throwable? = null) :
- RadRootsAndroidSecurityError(message, cause)
-}
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidStoragePaths.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidStoragePaths.kt
@@ -1,99 +0,0 @@
-package org.radroots.app.android.security
-
-import android.content.Context
-import java.io.File
-import java.security.MessageDigest
-
-object RadRootsAndroidStoragePaths {
- private const val rootDirName = "RadRoots"
- private const val configDirName = "config"
- private const val dataDirName = "data"
- private const val secretsRootDirName = "secrets"
- private const val appsDirName = "apps"
- private const val appRuntimeDirName = "app"
- private const val nostrDirName = "nostr"
- private const val accountsFileName = "accounts.json"
-
- fun baseRoot(context: Context): File = baseRoot(context.noBackupFilesDir)
-
- fun baseRoot(baseDir: File): File = File(baseDir, rootDirName)
-
- fun appDataRoot(context: Context): File = appDataRoot(context.noBackupFilesDir)
-
- fun appDataRoot(baseDir: File): File =
- File(
- File(
- File(baseRoot(baseDir), dataDirName),
- appsDirName,
- ),
- appRuntimeDirName,
- )
-
- fun nostrRoot(context: Context): File = nostrRoot(context.noBackupFilesDir)
-
- fun nostrRoot(baseDir: File): File = File(appDataRoot(baseDir), nostrDirName)
-
- fun secretsDir(context: Context): File = secretsDir(context.noBackupFilesDir)
-
- fun secretsDir(baseDir: File): File =
- File(
- File(
- File(baseRoot(baseDir), secretsRootDirName),
- appsDirName,
- ),
- appRuntimeDirName,
- )
-
- fun accountsFile(context: Context): File = accountsFile(context.noBackupFilesDir)
-
- fun accountsFile(baseDir: File): File = File(nostrRoot(baseDir), accountsFileName)
-
- fun secretFile(
- context: Context,
- servicePrefix: String,
- namespace: String,
- name: String,
- ): File = secretFile(context.noBackupFilesDir, servicePrefix, namespace, name)
-
- fun secretFile(
- baseDir: File,
- servicePrefix: String,
- namespace: String,
- name: String,
- ): File = File(
- secretsDir(baseDir),
- "${secretNamespaceId(servicePrefix, namespace)}.${secretFileId(servicePrefix, namespace, name)}.bin",
- )
-
- fun legacySecretFile(
- baseDir: File,
- servicePrefix: String,
- namespace: String,
- name: String,
- ): File = File(secretsDir(baseDir), "${secretFileId(servicePrefix, namespace, name)}.bin")
-
- fun secretNamespaceId(servicePrefix: String, namespace: String): String {
- val digest = MessageDigest.getInstance("SHA-256")
- val encoded = buildString {
- append(servicePrefix)
- append('\u0000')
- append(namespace)
- }.toByteArray(Charsets.UTF_8)
- return digest.digest(encoded).joinToString("") { "%02x".format(it) }
- }
-
- fun namespaceFilePrefix(servicePrefix: String, namespace: String): String =
- "${secretNamespaceId(servicePrefix, namespace)}."
-
- fun secretFileId(servicePrefix: String, namespace: String, name: String): String {
- val digest = MessageDigest.getInstance("SHA-256")
- val encoded = buildString {
- append(servicePrefix)
- append('\u0000')
- append(namespace)
- append('\u0000')
- append(name)
- }.toByteArray(Charsets.UTF_8)
- return digest.digest(encoded).joinToString("") { "%02x".format(it) }
- }
-}
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidUserPresenceVerifier.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidUserPresenceVerifier.kt
@@ -1,149 +0,0 @@
-package org.radroots.app.android.security
-
-import android.app.KeyguardManager
-import android.os.Build
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
-import androidx.fragment.app.FragmentActivity
-import androidx.core.content.ContextCompat
-
-class RadRootsAndroidUserPresenceVerifier(
- private val activity: FragmentActivity,
-) {
- fun beginVerification(
- reason: String,
- onSuccess: () -> Unit,
- onFailure: (RadRootsAndroidSecurityError) -> Unit,
- ) {
- if (reason.isBlank()) {
- onFailure(RadRootsAndroidSecurityError.InvalidInput("verification reason must not be blank"))
- return
- }
-
- val promptInfo = try {
- buildPromptInfo(reason)
- } catch (error: RadRootsAndroidSecurityError) {
- onFailure(error)
- return
- }
-
- val executor = ContextCompat.getMainExecutor(activity)
- val prompt = BiometricPrompt(
- activity,
- executor,
- object : BiometricPrompt.AuthenticationCallback() {
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
- onSuccess()
- }
-
- override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
- onFailure(mapAuthenticationError(errorCode, errString))
- }
-
- override fun onAuthenticationFailed() {
- onFailure(
- RadRootsAndroidSecurityError.UserPresenceFailure(
- "device authentication failed",
- ),
- )
- }
- },
- )
-
- activity.runOnUiThread {
- prompt.authenticate(promptInfo)
- }
- }
-
- private fun buildPromptInfo(reason: String): BiometricPrompt.PromptInfo {
- ensureAuthenticationAvailable()
-
- val builder = BiometricPrompt.PromptInfo.Builder()
- .setTitle("Rad Roots")
- .setSubtitle("Authenticate to $reason")
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- builder.setAllowedAuthenticators(
- BiometricManager.Authenticators.BIOMETRIC_STRONG or
- BiometricManager.Authenticators.DEVICE_CREDENTIAL,
- )
- } else if (deviceCredentialAvailable()) {
- builder.setDeviceCredentialAllowed(true)
- } else {
- builder.setNegativeButtonText("Cancel")
- }
-
- return builder.build()
- }
-
- private fun ensureAuthenticationAvailable() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- when (
- BiometricManager.from(activity).canAuthenticate(
- BiometricManager.Authenticators.BIOMETRIC_STRONG or
- BiometricManager.Authenticators.DEVICE_CREDENTIAL,
- )
- ) {
- BiometricManager.BIOMETRIC_SUCCESS -> return
- BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
- throw RadRootsAndroidSecurityError.UserPresenceUnavailable(
- "no device authentication method is enrolled",
- )
- BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
- BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
- throw RadRootsAndroidSecurityError.UserPresenceUnavailable(
- "device authentication is unavailable",
- )
- else ->
- throw RadRootsAndroidSecurityError.UserPresenceFailure(
- "failed to prepare device authentication",
- )
- }
- }
-
- val biometricStatus = BiometricManager.from(activity).canAuthenticate()
- if (biometricStatus == BiometricManager.BIOMETRIC_SUCCESS || deviceCredentialAvailable()) {
- return
- }
-
- throw when (biometricStatus) {
- BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
- RadRootsAndroidSecurityError.UserPresenceUnavailable(
- "no biometric or device credential is available",
- )
- BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
- BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
- RadRootsAndroidSecurityError.UserPresenceUnavailable(
- "device authentication is unavailable",
- )
- else ->
- RadRootsAndroidSecurityError.UserPresenceFailure(
- "failed to prepare device authentication",
- )
- }
- }
-
- private fun deviceCredentialAvailable(): Boolean {
- val keyguardManager = activity.getSystemService(KeyguardManager::class.java)
- return keyguardManager?.isDeviceSecure == true
- }
-
- private fun mapAuthenticationError(
- errorCode: Int,
- errString: CharSequence,
- ): RadRootsAndroidSecurityError {
- val message = errString.toString()
- return when (errorCode) {
- BiometricPrompt.ERROR_NEGATIVE_BUTTON,
- BiometricPrompt.ERROR_USER_CANCELED,
- BiometricPrompt.ERROR_CANCELED ->
- RadRootsAndroidSecurityError.UserCancelled(message)
- BiometricPrompt.ERROR_HW_NOT_PRESENT,
- BiometricPrompt.ERROR_HW_UNAVAILABLE,
- BiometricPrompt.ERROR_NO_BIOMETRICS,
- BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL ->
- RadRootsAndroidSecurityError.UserPresenceUnavailable(message)
- else -> RadRootsAndroidSecurityError.UserPresenceFailure(message)
- }
- }
-}
diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/test/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityTests.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/test/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityTests.kt
@@ -1,175 +0,0 @@
-package org.radroots.app.android.security
-
-import android.os.Build
-import android.security.keystore.KeyProperties
-import java.io.File
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-class RadRootsAndroidSecurityTests {
- @Test
- fun secureLocalSecretPolicyDefaultsAreStable() {
- val policy = RadRootsAndroidSecretAccessPolicy.SECURE_LOCAL_SECRET
-
- assertTrue(policy.deviceLocalOnly)
- assertFalse(policy.userPresenceRequired)
- assertTrue(policy.preferStrongBox)
- }
-
- @Test
- fun mobileNativeRootsUseNoBackupLayout() {
- val baseDir = File("/data/user/0/org.radroots.app.android/no_backup")
-
- assertEquals(
- File("/data/user/0/org.radroots.app.android/no_backup/RadRoots"),
- RadRootsAndroidStoragePaths.baseRoot(baseDir),
- )
- assertEquals(
- File("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app"),
- RadRootsAndroidStoragePaths.appDataRoot(baseDir),
- )
- assertEquals(
- File("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app/nostr"),
- RadRootsAndroidStoragePaths.nostrRoot(baseDir),
- )
- assertEquals(
- File("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app/nostr/accounts.json"),
- RadRootsAndroidStoragePaths.accountsFile(baseDir),
- )
- }
-
- @Test
- fun secretFileIdIsDeterministic() {
- val first = RadRootsAndroidStoragePaths.secretFileId(
- servicePrefix = "org.radroots.app.nostr",
- namespace = "nostr",
- name = "account-1",
- )
- val second = RadRootsAndroidStoragePaths.secretFileId(
- servicePrefix = "org.radroots.app.nostr",
- namespace = "nostr",
- name = "account-1",
- )
-
- assertEquals(first, second)
- assertEquals(64, first.length)
- }
-
- @Test
- fun secretFileNamesCarryNamespacePrefix() {
- val baseDir = File("/data/user/0/org.radroots.app.android/no_backup")
- val path = RadRootsAndroidStoragePaths.secretFile(
- baseDir = baseDir,
- servicePrefix = "org.radroots.app.nostr",
- namespace = "remote-signer",
- name = "client-1",
- )
-
- assertTrue(path.name.endsWith(".bin"))
- assertTrue(
- path.name.startsWith(
- "${RadRootsAndroidStoragePaths.secretNamespaceId("org.radroots.app.nostr", "remote-signer")}.",
- ),
- )
- }
-
- @Test
- fun strongBoxIsRequestedOnlyWhenSupported() {
- val policy = RadRootsAndroidSecretAccessPolicy.SECURE_LOCAL_SECRET
-
- assertTrue(
- shouldRequestStrongBox(
- policy = policy,
- sdkInt = Build.VERSION_CODES.P,
- hasStrongBoxFeature = true,
- ),
- )
- assertFalse(
- shouldRequestStrongBox(
- policy = policy,
- sdkInt = Build.VERSION_CODES.O_MR1,
- hasStrongBoxFeature = true,
- ),
- )
- assertFalse(
- shouldRequestStrongBox(
- policy = policy.copy(preferStrongBox = false),
- sdkInt = Build.VERSION_CODES.P,
- hasStrongBoxFeature = true,
- ),
- )
- assertFalse(
- shouldRequestStrongBox(
- policy = policy,
- sdkInt = Build.VERSION_CODES.P,
- hasStrongBoxFeature = false,
- ),
- )
- }
-
- @Test
- fun securityLevelMappingPrefersVerifiedPlatformTier() {
- assertEquals(
- RadRootsAndroidKeySecurityLevel.STRONGBOX,
- RadRootsAndroidKeySecurityLevels.fromPlatformValues(
- sdkInt = Build.VERSION_CODES.S,
- securityLevel = KeyProperties.SECURITY_LEVEL_STRONGBOX,
- isInsideSecureHardware = true,
- ),
- )
- assertEquals(
- RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT,
- RadRootsAndroidKeySecurityLevels.fromPlatformValues(
- sdkInt = Build.VERSION_CODES.S,
- securityLevel = KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT,
- isInsideSecureHardware = true,
- ),
- )
- assertEquals(
- RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN,
- RadRootsAndroidKeySecurityLevels.fromPlatformValues(
- sdkInt = Build.VERSION_CODES.S,
- securityLevel = KeyProperties.SECURITY_LEVEL_SOFTWARE,
- isInsideSecureHardware = false,
- ),
- )
- assertEquals(
- RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT,
- RadRootsAndroidKeySecurityLevels.fromPlatformValues(
- sdkInt = Build.VERSION_CODES.R,
- securityLevel = null,
- isInsideSecureHardware = true,
- ),
- )
- }
-
- @Test
- fun strongBoxVerificationAcceptsOnlyBestAvailableTier() {
- assertTrue(
- acceptsStrongBoxVerificationResult(
- sdkInt = Build.VERSION_CODES.S,
- securityLevel = RadRootsAndroidKeySecurityLevel.STRONGBOX,
- ),
- )
- assertFalse(
- acceptsStrongBoxVerificationResult(
- sdkInt = Build.VERSION_CODES.S,
- securityLevel = RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT,
- ),
- )
- assertTrue(
- acceptsStrongBoxVerificationResult(
- sdkInt = Build.VERSION_CODES.R,
- securityLevel = RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT,
- ),
- )
- assertFalse(
- acceptsStrongBoxVerificationResult(
- sdkInt = Build.VERSION_CODES.R,
- securityLevel = RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN,
- ),
- )
- }
-}
diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Package.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Package.swift
@@ -1,42 +0,0 @@
-// swift-tools-version: 6.0
-import PackageDescription
-
-let package = Package(
- name: "RadRootsAppleSecurity",
- platforms: [
- .iOS(.v17),
- .macOS(.v14)
- ],
- products: [
- .library(
- name: "RadRootsAppleSecurity",
- targets: ["RadRootsAppleSecurity"]
- ),
- .library(
- name: "RadRootsAppleSecurityFFI",
- type: .static,
- targets: ["RadRootsAppleSecurityFFI"]
- ),
- .library(
- name: "RadRootsAppleSecurityFFIDynamic",
- type: .dynamic,
- targets: ["RadRootsAppleSecurityFFI"]
- )
- ],
- targets: [
- .target(
- name: "RadRootsAppleSecurity",
- path: "Sources/RadRootsAppleSecurity"
- ),
- .target(
- name: "RadRootsAppleSecurityFFI",
- dependencies: ["RadRootsAppleSecurity"],
- path: "Sources/RadRootsAppleSecurityFFI"
- ),
- .testTarget(
- name: "RadRootsAppleSecurityTests",
- dependencies: ["RadRootsAppleSecurity", "RadRootsAppleSecurityFFI"],
- path: "Tests/RadRootsAppleSecurityTests"
- )
- ]
-)
diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleKeychainSecretStore.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleKeychainSecretStore.swift
@@ -1,123 +0,0 @@
-import Foundation
-import Security
-
-public final class RadRootsAppleKeychainSecretStore: @unchecked Sendable {
- public let servicePrefix: String
-
- public init(servicePrefix: String = "org.radroots.app.apple-security") {
- self.servicePrefix = servicePrefix
- }
-
- public func put(
- _ value: Data,
- for key: RadRootsAppleSecretKey,
- policy: RadRootsAppleSecretAccessPolicy = .secureLocalSecret
- ) throws {
- try delete(key)
-
- let query = try writeQuery(for: key, value: value, policy: policy)
- let status = SecItemAdd(query as CFDictionary, nil)
- guard status == errSecSuccess else {
- throw Self.mapSecurityStatus(status, defaultMessage: "keychain write failed")
- }
- }
-
- public func get(_ key: RadRootsAppleSecretKey) throws -> Data? {
- var query = baseQuery(for: key)
- query[kSecReturnData as String] = true
- query[kSecMatchLimit as String] = kSecMatchLimitOne
-
- var result: CFTypeRef?
- let status = SecItemCopyMatching(query as CFDictionary, &result)
- if status == errSecItemNotFound {
- return nil
- }
- guard status == errSecSuccess else {
- throw Self.mapSecurityStatus(status, defaultMessage: "keychain read failed")
- }
- guard let data = result as? Data else {
- throw RadRootsAppleSecurityError.permanentFailure(
- "keychain read returned an invalid value type"
- )
- }
- return data
- }
-
- public func contains(_ key: RadRootsAppleSecretKey) throws -> Bool {
- try get(key) != nil
- }
-
- public func delete(_ key: RadRootsAppleSecretKey) throws {
- let status = SecItemDelete(baseQuery(for: key) as CFDictionary)
- guard status == errSecSuccess || status == errSecItemNotFound else {
- throw Self.mapSecurityStatus(status, defaultMessage: "keychain delete failed")
- }
- }
-
- public func deleteNamespace(_ namespace: String) throws {
- guard !namespace.isEmpty else {
- throw RadRootsAppleSecurityError.invalidRequest("secret namespace cannot be empty")
- }
- let status = SecItemDelete(namespaceQuery(namespace) as CFDictionary)
- guard status == errSecSuccess || status == errSecItemNotFound else {
- throw Self.mapSecurityStatus(status, defaultMessage: "keychain namespace delete failed")
- }
- }
-
- func baseQuery(for key: RadRootsAppleSecretKey) -> [String: Any] {
- [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: key.serviceName(servicePrefix: servicePrefix),
- kSecAttrAccount as String: key.name
- ]
- }
-
- func namespaceQuery(_ namespace: String) -> [String: Any] {
- [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: "\(servicePrefix).\(namespace)"
- ]
- }
-
- func writeQuery(
- for key: RadRootsAppleSecretKey,
- value: Data,
- policy: RadRootsAppleSecretAccessPolicy
- ) throws -> [String: Any] {
- var query = baseQuery(for: key)
- query[kSecValueData as String] = value
- query[kSecAttrAccessible as String] = accessibilityConstant(for: policy)
- return query
- }
-
- func accessibilityConstant(for policy: RadRootsAppleSecretAccessPolicy) -> CFString {
- switch (policy.accessibility, policy.deviceLocalOnly) {
- case (.whenUnlocked, true):
- return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
- case (.whenUnlocked, false):
- return kSecAttrAccessibleWhenUnlocked
- case (.afterFirstUnlock, true):
- return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
- case (.afterFirstUnlock, false):
- return kSecAttrAccessibleAfterFirstUnlock
- }
- }
-
- static func mapSecurityStatus(
- _ status: OSStatus,
- defaultMessage: String
- ) -> RadRootsAppleSecurityError {
- switch status {
- case errSecAuthFailed:
- return .permissionDenied(defaultMessage)
- case errSecInteractionNotAllowed:
- return .transientFailure(defaultMessage)
- case errSecUserCanceled:
- return .userCancelled(defaultMessage)
- case errSecNotAvailable:
- return .unavailable(defaultMessage)
- default:
- return .keychainStatus(status, defaultMessage)
- }
- }
-}
diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretAccessPolicy.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretAccessPolicy.swift
@@ -1,28 +0,0 @@
-import Foundation
-
-public enum RadRootsAppleSecretAccessibility: Int32, Sendable {
- case whenUnlocked = 0
- case afterFirstUnlock = 1
-}
-
-public struct RadRootsAppleSecretAccessPolicy: Sendable, Equatable {
- public let accessibility: RadRootsAppleSecretAccessibility
- public let deviceLocalOnly: Bool
- public let userPresenceRequired: Bool
-
- public init(
- accessibility: RadRootsAppleSecretAccessibility,
- deviceLocalOnly: Bool,
- userPresenceRequired: Bool
- ) {
- self.accessibility = accessibility
- self.deviceLocalOnly = deviceLocalOnly
- self.userPresenceRequired = userPresenceRequired
- }
-
- public static let secureLocalSecret = Self(
- accessibility: .whenUnlocked,
- deviceLocalOnly: true,
- userPresenceRequired: false
- )
-}
diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretKey.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretKey.swift
@@ -1,21 +0,0 @@
-import Foundation
-
-public struct RadRootsAppleSecretKey: Hashable, Sendable {
- public let namespace: String
- public let name: String
-
- public init(namespace: String, name: String) throws {
- guard !namespace.isEmpty else {
- throw RadRootsAppleSecurityError.invalidRequest("secret namespace cannot be empty")
- }
- guard !name.isEmpty else {
- throw RadRootsAppleSecurityError.invalidRequest("secret name cannot be empty")
- }
- self.namespace = namespace
- self.name = name
- }
-
- func serviceName(servicePrefix: String) -> String {
- "\(servicePrefix).\(namespace)"
- }
-}
diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecurityError.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecurityError.swift
@@ -1,28 +0,0 @@
-import Foundation
-import Security
-
-public enum RadRootsAppleSecurityError: Error, Sendable {
- case invalidRequest(String)
- case permissionDenied(String)
- case userCancelled(String)
- case unavailable(String)
- case transientFailure(String)
- case permanentFailure(String)
- case keychainStatus(OSStatus, String)
-}
-
-extension RadRootsAppleSecurityError: LocalizedError {
- public var errorDescription: String? {
- switch self {
- case let .invalidRequest(message),
- let .permissionDenied(message),
- let .userCancelled(message),
- let .unavailable(message),
- let .transientFailure(message),
- let .permanentFailure(message):
- return message
- case let .keychainStatus(status, message):
- return "\(message) (status \(status))"
- }
- }
-}
diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleUserPresence.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleUserPresence.swift
@@ -1,197 +0,0 @@
-import Foundation
-
-#if canImport(LocalAuthentication)
-import LocalAuthentication
-#endif
-
-public enum RadRootsAppleUserPresencePolicy: Sendable {
- case deviceOwnerAuthentication
- case deviceOwnerAuthenticationWithBiometrics
-}
-
-public enum RadRootsAppleUserPresenceSupport: Sendable {
- case none
- case deviceCredential
- case biometricsOrDeviceCredential
-}
-
-public enum RadRootsAppleBiometryKind: Sendable {
- case none
- case touchID
- case faceID
- case opticID
- case unknown
-}
-
-public struct RadRootsAppleUserPresenceStatus: Sendable {
- public let support: RadRootsAppleUserPresenceSupport
- public let biometryKind: RadRootsAppleBiometryKind
- public let canEvaluateDeviceCredential: Bool
- public let canEvaluateBiometrics: Bool
-
- public init(
- support: RadRootsAppleUserPresenceSupport,
- biometryKind: RadRootsAppleBiometryKind,
- canEvaluateDeviceCredential: Bool,
- canEvaluateBiometrics: Bool
- ) {
- self.support = support
- self.biometryKind = biometryKind
- self.canEvaluateDeviceCredential = canEvaluateDeviceCredential
- self.canEvaluateBiometrics = canEvaluateBiometrics
- }
-}
-
-public actor RadRootsAppleUserPresence {
- public init() {}
-
- public static func verifySync(
- reason: String,
- policy: RadRootsAppleUserPresencePolicy = .deviceOwnerAuthentication
- ) throws -> Bool {
- #if canImport(LocalAuthentication)
- let context = LAContext()
- let lock = NSLock()
- let semaphore = DispatchSemaphore(value: 0)
- var result: Result<Bool, Error>?
-
- context.evaluatePolicy(
- Self.makePolicy(policy),
- localizedReason: reason
- ) { success, error in
- lock.lock()
- if let error {
- result = .failure(Self.adapt(error: error))
- } else {
- result = .success(success)
- }
- lock.unlock()
- semaphore.signal()
- }
-
- semaphore.wait()
-
- lock.lock()
- defer { lock.unlock() }
- return try result?.get() ?? {
- throw RadRootsAppleSecurityError.transientFailure(
- "local authentication did not return a result"
- )
- }()
- #else
- throw RadRootsAppleSecurityError.unavailable("local authentication is unavailable")
- #endif
- }
-
- public func currentStatus() -> RadRootsAppleUserPresenceStatus {
- #if canImport(LocalAuthentication)
- let context = LAContext()
- return Self.makeStatus(context: context)
- #else
- return RadRootsAppleUserPresenceStatus(
- support: .none,
- biometryKind: .none,
- canEvaluateDeviceCredential: false,
- canEvaluateBiometrics: false
- )
- #endif
- }
-
- public func verify(
- reason: String,
- policy: RadRootsAppleUserPresencePolicy = .deviceOwnerAuthentication
- ) async throws -> Bool {
- #if canImport(LocalAuthentication)
- let context = LAContext()
- return try await withCheckedThrowingContinuation { continuation in
- context.evaluatePolicy(
- Self.makePolicy(policy),
- localizedReason: reason
- ) { success, error in
- if let error {
- continuation.resume(throwing: Self.adapt(error: error))
- } else {
- continuation.resume(returning: success)
- }
- }
- }
- #else
- throw RadRootsAppleSecurityError.unavailable("local authentication is unavailable")
- #endif
- }
-
- #if canImport(LocalAuthentication)
- private static func makePolicy(_ policy: RadRootsAppleUserPresencePolicy) -> LAPolicy {
- switch policy {
- case .deviceOwnerAuthentication:
- return .deviceOwnerAuthentication
- case .deviceOwnerAuthenticationWithBiometrics:
- return .deviceOwnerAuthenticationWithBiometrics
- }
- }
-
- private static func makeStatus(context: LAContext) -> RadRootsAppleUserPresenceStatus {
- var biometricsError: NSError?
- let canEvaluateBiometrics = context.canEvaluatePolicy(
- .deviceOwnerAuthenticationWithBiometrics,
- error: &biometricsError
- )
-
- var deviceCredentialError: NSError?
- let canEvaluateDeviceCredential = context.canEvaluatePolicy(
- .deviceOwnerAuthentication,
- error: &deviceCredentialError
- )
-
- let support: RadRootsAppleUserPresenceSupport
- if canEvaluateBiometrics {
- support = .biometricsOrDeviceCredential
- } else if canEvaluateDeviceCredential {
- support = .deviceCredential
- } else {
- support = .none
- }
-
- return RadRootsAppleUserPresenceStatus(
- support: support,
- biometryKind: makeBiometryKind(context.biometryType),
- canEvaluateDeviceCredential: canEvaluateDeviceCredential,
- canEvaluateBiometrics: canEvaluateBiometrics
- )
- }
-
- private static func makeBiometryKind(_ biometryType: LABiometryType) -> RadRootsAppleBiometryKind {
- switch biometryType {
- case .none:
- return .none
- case .touchID:
- return .touchID
- case .faceID:
- return .faceID
- case .opticID:
- return .opticID
- @unknown default:
- return .unknown
- }
- }
-
- private static func adapt(error: Error) -> RadRootsAppleSecurityError {
- if let laError = error as? LAError {
- switch laError.code {
- case .userCancel, .userFallback:
- return .userCancelled(laError.localizedDescription)
- case .appCancel, .systemCancel, .notInteractive:
- return .transientFailure(laError.localizedDescription)
- case .biometryNotAvailable, .biometryNotEnrolled, .passcodeNotSet:
- return .unavailable(laError.localizedDescription)
- case .authenticationFailed:
- return .permissionDenied(laError.localizedDescription)
- default:
- return .permanentFailure(laError.localizedDescription)
- }
- }
-
- return .permanentFailure(error.localizedDescription)
- }
- #endif
-}
diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurityFFI/RadRootsAppleSecurityFFI.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurityFFI/RadRootsAppleSecurityFFI.swift
@@ -1,250 +0,0 @@
-import Foundation
-import RadRootsAppleSecurity
-
-private let defaultServicePrefix = "org.radroots.app.apple-security"
-
-private enum RadRootsAppleFFIStatus: Int32 {
- case success = 0
- case notFound = 1
- case invalidInput = 2
- case error = 3
-}
-
-@_cdecl("radroots_apple_secret_store_put")
-public func radroots_apple_secret_store_put(
- _ servicePrefix: UnsafePointer<CChar>?,
- _ namespace: UnsafePointer<CChar>?,
- _ name: UnsafePointer<CChar>?,
- _ valuePtr: UnsafePointer<UInt8>?,
- _ valueLen: Int,
- _ accessibilityRaw: Int32,
- _ deviceLocalOnlyRaw: Int32,
- _ userPresenceRequiredRaw: Int32,
- _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
-) -> Int32 {
- do {
- let store = try makeStore(servicePrefix: servicePrefix)
- let key = try makeKey(namespace: namespace, name: name)
- let policy = try makePolicy(
- accessibilityRaw: accessibilityRaw,
- deviceLocalOnlyRaw: deviceLocalOnlyRaw,
- userPresenceRequiredRaw: userPresenceRequiredRaw
- )
- guard let valuePtr else {
- throw RadRootsAppleSecurityError.invalidRequest("secret value pointer cannot be null")
- }
- let value = Data(bytes: valuePtr, count: valueLen)
- try store.put(value, for: key, policy: policy)
- return RadRootsAppleFFIStatus.success.rawValue
- } catch {
- setError(error, into: errorOut)
- return statusForError(error)
- }
-}
-
-@_cdecl("radroots_apple_secret_store_get")
-public func radroots_apple_secret_store_get(
- _ servicePrefix: UnsafePointer<CChar>?,
- _ namespace: UnsafePointer<CChar>?,
- _ name: UnsafePointer<CChar>?,
- _ valueOut: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>?,
- _ valueLenOut: UnsafeMutablePointer<Int>?,
- _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
-) -> Int32 {
- do {
- guard let valueOut, let valueLenOut else {
- throw RadRootsAppleSecurityError.invalidRequest("output buffers cannot be null")
- }
- let store = try makeStore(servicePrefix: servicePrefix)
- let key = try makeKey(namespace: namespace, name: name)
- guard let value = try store.get(key) else {
- valueOut.pointee = nil
- valueLenOut.pointee = 0
- return RadRootsAppleFFIStatus.notFound.rawValue
- }
-
- let output = UnsafeMutablePointer<UInt8>.allocate(capacity: value.count)
- value.copyBytes(to: output, count: value.count)
- valueOut.pointee = output
- valueLenOut.pointee = value.count
- return RadRootsAppleFFIStatus.success.rawValue
- } catch {
- setError(error, into: errorOut)
- return statusForError(error)
- }
-}
-
-@_cdecl("radroots_apple_secret_store_contains")
-public func radroots_apple_secret_store_contains(
- _ servicePrefix: UnsafePointer<CChar>?,
- _ namespace: UnsafePointer<CChar>?,
- _ name: UnsafePointer<CChar>?,
- _ containsOut: UnsafeMutablePointer<Int32>?,
- _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
-) -> Int32 {
- do {
- guard let containsOut else {
- throw RadRootsAppleSecurityError.invalidRequest("contains output cannot be null")
- }
- let store = try makeStore(servicePrefix: servicePrefix)
- let key = try makeKey(namespace: namespace, name: name)
- containsOut.pointee = try store.contains(key) ? 1 : 0
- return RadRootsAppleFFIStatus.success.rawValue
- } catch {
- setError(error, into: errorOut)
- return statusForError(error)
- }
-}
-
-@_cdecl("radroots_apple_secret_store_delete")
-public func radroots_apple_secret_store_delete(
- _ servicePrefix: UnsafePointer<CChar>?,
- _ namespace: UnsafePointer<CChar>?,
- _ name: UnsafePointer<CChar>?,
- _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
-) -> Int32 {
- do {
- let store = try makeStore(servicePrefix: servicePrefix)
- let key = try makeKey(namespace: namespace, name: name)
- try store.delete(key)
- return RadRootsAppleFFIStatus.success.rawValue
- } catch {
- setError(error, into: errorOut)
- return statusForError(error)
- }
-}
-
-@_cdecl("radroots_apple_secret_store_delete_namespace")
-public func radroots_apple_secret_store_delete_namespace(
- _ servicePrefix: UnsafePointer<CChar>?,
- _ namespace: UnsafePointer<CChar>?,
- _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
-) -> Int32 {
- do {
- let store = try makeStore(servicePrefix: servicePrefix)
- let namespace = try makeNamespace(namespace)
- try store.deleteNamespace(namespace)
- return RadRootsAppleFFIStatus.success.rawValue
- } catch {
- setError(error, into: errorOut)
- return statusForError(error)
- }
-}
-
-@_cdecl("radroots_apple_user_presence_verify")
-public func radroots_apple_user_presence_verify(
- _ reason: UnsafePointer<CChar>?,
- _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
-) -> Int32 {
- do {
- guard let reasonPointer = reason else {
- throw RadRootsAppleSecurityError.invalidRequest("verification reason is required")
- }
- let reason = String(cString: reasonPointer)
- guard !reason.isEmpty else {
- throw RadRootsAppleSecurityError.invalidRequest("verification reason cannot be empty")
- }
- guard try RadRootsAppleUserPresence.verifySync(reason: reason) else {
- throw RadRootsAppleSecurityError.permissionDenied(
- "local authentication did not authorize access"
- )
- }
- return RadRootsAppleFFIStatus.success.rawValue
- } catch {
- setError(error, into: errorOut)
- return statusForError(error)
- }
-}
-
-@_cdecl("radroots_apple_buffer_free")
-public func radroots_apple_buffer_free(
- _ buffer: UnsafeMutablePointer<UInt8>?,
- _ length: Int
-) {
- guard let buffer else {
- return
- }
- buffer.deallocate()
- _ = length
-}
-
-@_cdecl("radroots_apple_c_string_free")
-public func radroots_apple_c_string_free(_ string: UnsafeMutablePointer<CChar>?) {
- string?.deallocate()
-}
-
-private func makeStore(
- servicePrefix: UnsafePointer<CChar>?
-) throws -> RadRootsAppleKeychainSecretStore {
- let service = servicePrefix.map(String.init(cString:)) ?? defaultServicePrefix
- guard !service.isEmpty else {
- throw RadRootsAppleSecurityError.invalidRequest("service prefix cannot be empty")
- }
- return RadRootsAppleKeychainSecretStore(servicePrefix: service)
-}
-
-private func makeKey(
- namespace: UnsafePointer<CChar>?,
- name: UnsafePointer<CChar>?
-) throws -> RadRootsAppleSecretKey {
- let namespaceValue = try makeNamespace(namespace)
- guard let name else {
- throw RadRootsAppleSecurityError.invalidRequest("secret namespace and name are required")
- }
- return try RadRootsAppleSecretKey(
- namespace: namespaceValue,
- name: String(cString: name)
- )
-}
-
-private func makeNamespace(
- _ namespace: UnsafePointer<CChar>?
-) throws -> String {
- guard let namespace else {
- throw RadRootsAppleSecurityError.invalidRequest("secret namespace is required")
- }
- let value = String(cString: namespace)
- guard !value.isEmpty else {
- throw RadRootsAppleSecurityError.invalidRequest("secret namespace cannot be empty")
- }
- return value
-}
-
-private func makePolicy(
- accessibilityRaw: Int32,
- deviceLocalOnlyRaw: Int32,
- userPresenceRequiredRaw: Int32
-) throws -> RadRootsAppleSecretAccessPolicy {
- guard let accessibility = RadRootsAppleSecretAccessibility(rawValue: accessibilityRaw) else {
- throw RadRootsAppleSecurityError.invalidRequest("invalid accessibility value")
- }
- return RadRootsAppleSecretAccessPolicy(
- accessibility: accessibility,
- deviceLocalOnly: deviceLocalOnlyRaw != 0,
- userPresenceRequired: userPresenceRequiredRaw != 0
- )
-}
-
-private func setError(
- _ error: Error,
- into errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
-) {
- guard let errorOut else {
- return
- }
- errorOut.pointee = duplicateCString(error.localizedDescription)
-}
-
-private func statusForError(_ error: Error) -> Int32 {
- if case RadRootsAppleSecurityError.invalidRequest = error {
- return RadRootsAppleFFIStatus.invalidInput.rawValue
- }
- return RadRootsAppleFFIStatus.error.rawValue
-}
-
-private func duplicateCString(_ value: String) -> UnsafeMutablePointer<CChar>? {
- let bytes = Array(value.utf8CString)
- let pointer = UnsafeMutablePointer<CChar>.allocate(capacity: bytes.count)
- pointer.initialize(from: bytes, count: bytes.count)
- return pointer
-}
diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Tests/RadRootsAppleSecurityTests/RadRootsAppleSecurityTests.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Tests/RadRootsAppleSecurityTests/RadRootsAppleSecurityTests.swift
@@ -1,60 +0,0 @@
-import Foundation
-import Security
-@testable import RadRootsAppleSecurity
-@testable import RadRootsAppleSecurityFFI
-import Testing
-
-struct RadRootsAppleSecurityTests {
- @Test
- func secretKeyRejectsEmptyNamespace() throws {
- #expect(throws: RadRootsAppleSecurityError.self) {
- _ = try RadRootsAppleSecretKey(namespace: "", name: "secret")
- }
- }
-
- @Test
- func secretKeyRejectsEmptyName() throws {
- #expect(throws: RadRootsAppleSecurityError.self) {
- _ = try RadRootsAppleSecretKey(namespace: "nostr", name: "")
- }
- }
-
- @Test
- func baseQueryUsesStableServicePrefixAndAccountName() throws {
- let store = RadRootsAppleKeychainSecretStore(servicePrefix: "org.radroots.app.nostr")
- let key = try RadRootsAppleSecretKey(namespace: "accounts", name: "account-1")
-
- let query = store.baseQuery(for: key)
-
- #expect(query[kSecAttrService as String] as? String == "org.radroots.app.nostr.accounts")
- #expect(query[kSecAttrAccount as String] as? String == "account-1")
- #expect(query[kSecClass as String] != nil)
- }
-
- @Test
- func secureLocalSecretDefaultsToDeviceLocalWhenUnlocked() {
- let policy = RadRootsAppleSecretAccessPolicy.secureLocalSecret
-
- #expect(policy.accessibility == .whenUnlocked)
- #expect(policy.deviceLocalOnly)
- #expect(!policy.userPresenceRequired)
- }
-
- @Test
- func accessibilityConstantMatchesPolicy() {
- let store = RadRootsAppleKeychainSecretStore()
- let localPolicy = RadRootsAppleSecretAccessPolicy(
- accessibility: .whenUnlocked,
- deviceLocalOnly: true,
- userPresenceRequired: false
- )
- let syncedPolicy = RadRootsAppleSecretAccessPolicy(
- accessibility: .afterFirstUnlock,
- deviceLocalOnly: false,
- userPresenceRequired: false
- )
-
- #expect(store.accessibilityConstant(for: localPolicy) == kSecAttrAccessibleWhenUnlockedThisDeviceOnly)
- #expect(store.accessibilityConstant(for: syncedPolicy) == kSecAttrAccessibleAfterFirstUnlock)
- }
-}
diff --git a/platforms/android/Scripts/android_toolchain_config.sh b/platforms/android/Scripts/android_toolchain_config.sh
@@ -1,83 +0,0 @@
-#!/usr/bin/env bash
-
-android_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-android_dir="$(cd "${android_script_dir}/.." && pwd -P)"
-app_root="$(cd "${android_dir}/../.." && pwd -P)"
-
-android_tooling_dir="${android_dir}/.tooling"
-android_download_dir="${android_tooling_dir}/downloads"
-android_sdk_dir="${android_tooling_dir}/android-sdk"
-android_gradle_user_home="${android_tooling_dir}/gradle-user-home"
-android_user_home="${android_tooling_dir}/android-user-home"
-android_emulator_home="${android_tooling_dir}/emulator-home"
-android_avd_home="${android_tooling_dir}/avd"
-android_cargo_install_root="${android_tooling_dir}/cargo"
-android_cargo_bin_dir="${android_cargo_install_root}/bin"
-android_local_properties_path="${android_dir}/local.properties"
-
-android_sdk_api_level="34"
-android_build_tools_version="34.0.0"
-android_ndk_version="26.1.10909125"
-android_gradle_version="8.7"
-android_gradle_distribution_sha256="544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d"
-android_cmdline_tools_version="14742923"
-android_cargo_ndk_version="4.1.2"
-
-android_gradle_home="${android_tooling_dir}/gradle/gradle-${android_gradle_version}"
-android_gradle_bin="${android_gradle_home}/bin/gradle"
-android_sdkmanager_bin="${android_sdk_dir}/cmdline-tools/latest/bin/sdkmanager"
-android_avdmanager_bin="${android_sdk_dir}/cmdline-tools/latest/bin/avdmanager"
-android_ndk_dir="${android_sdk_dir}/ndk/${android_ndk_version}"
-android_emulator_bin="${android_sdk_dir}/emulator/emulator"
-android_adb_bin="${android_sdk_dir}/platform-tools/adb"
-android_cargo_ndk_bin="${android_cargo_bin_dir}/cargo-ndk"
-
-android_rust_target="aarch64-linux-android"
-android_abi="arm64-v8a"
-
-android_sdk_packages=(
- "platform-tools"
- "platforms;android-${android_sdk_api_level}"
- "build-tools;${android_build_tools_version}"
- "ndk;${android_ndk_version}"
-)
-
-android_gradle_distribution_url() {
- echo "https://services.gradle.org/distributions/gradle-${android_gradle_version}-bin.zip"
-}
-
-android_cmdline_tools_platform() {
- case "$(uname -s)" in
- Darwin)
- echo "mac"
- ;;
- Linux)
- echo "linux"
- ;;
- *)
- echo "unsupported"
- ;;
- esac
-}
-
-android_cmdline_tools_zip_name() {
- local platform_name
- platform_name="$(android_cmdline_tools_platform)"
- echo "commandlinetools-${platform_name}-${android_cmdline_tools_version}_latest.zip"
-}
-
-android_cmdline_tools_url() {
- echo "https://dl.google.com/android/repository/$(android_cmdline_tools_zip_name)"
-}
-
-android_emulator_system_image_package() {
- echo "system-images;android-${android_sdk_api_level};google_apis;${android_abi}"
-}
-
-android_emulator_packages() {
- printf '%s\n' "emulator" "$(android_emulator_system_image_package)"
-}
-
-android_avd_name() {
- echo "RadRoots_API_${android_sdk_api_level}"
-}
diff --git a/platforms/android/Scripts/bootstrap_android_toolchain.sh b/platforms/android/Scripts/bootstrap_android_toolchain.sh
@@ -1,226 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-source "${script_dir}/android_toolchain_config.sh"
-
-require_command() {
- if command -v "$1" >/dev/null 2>&1; then
- return
- fi
- echo "missing required command: $1" >&2
- exit 1
-}
-
-require_java_17() {
- require_command java
-
- local version_output
- version_output="$(java -version 2>&1 | head -n 1)"
- local java_major
- java_major="$(echo "${version_output}" | sed -E 's/.*version "([0-9]+).*/\1/')"
- if [[ -z "${java_major}" || "${java_major}" -lt 17 ]]; then
- echo "android bootstrap requires java 17 or newer" >&2
- exit 1
- fi
-}
-
-checksum_file() {
- if command -v sha256sum >/dev/null 2>&1; then
- sha256sum "$1" | awk '{print $1}'
- return
- fi
-
- if command -v shasum >/dev/null 2>&1; then
- shasum -a 256 "$1" | awk '{print $1}'
- return
- fi
-
- echo "missing required command: sha256sum or shasum" >&2
- exit 1
-}
-
-download_if_missing() {
- local url="$1"
- local destination="$2"
-
- if [[ -f "${destination}" ]]; then
- return
- fi
-
- mkdir -p "$(dirname "${destination}")"
- curl -fsSL "${url}" -o "${destination}"
-}
-
-validate_zip_archive() {
- unzip -tqq "$1" >/dev/null 2>&1
-}
-
-ensure_valid_zip_download() {
- local url="$1"
- local destination="$2"
-
- download_if_missing "${url}" "${destination}"
- if validate_zip_archive "${destination}"; then
- return
- fi
-
- rm -f "${destination}"
- download_if_missing "${url}" "${destination}"
- if ! validate_zip_archive "${destination}"; then
- echo "invalid zip archive: ${destination}" >&2
- exit 1
- fi
-}
-
-ensure_gradle_distribution() {
- if [[ -x "${android_gradle_bin}" ]]; then
- return
- fi
-
- local gradle_zip="${android_download_dir}/gradle-${android_gradle_version}-bin.zip"
- ensure_valid_zip_download "$(android_gradle_distribution_url)" "${gradle_zip}"
-
- local actual_checksum
- actual_checksum="$(checksum_file "${gradle_zip}")"
- if [[ "${actual_checksum}" != "${android_gradle_distribution_sha256}" ]]; then
- rm -f "${gradle_zip}"
- ensure_valid_zip_download "$(android_gradle_distribution_url)" "${gradle_zip}"
- actual_checksum="$(checksum_file "${gradle_zip}")"
- if [[ "${actual_checksum}" != "${android_gradle_distribution_sha256}" ]]; then
- echo "gradle distribution checksum mismatch" >&2
- exit 1
- fi
- fi
-
- rm -rf "$(dirname "${android_gradle_home}")"
- mkdir -p "$(dirname "${android_gradle_home}")"
- unzip -q "${gradle_zip}" -d "$(dirname "${android_gradle_home}")"
-}
-
-ensure_android_cmdline_tools() {
- if [[ -x "${android_sdkmanager_bin}" ]]; then
- return
- fi
-
- local platform_name
- platform_name="$(android_cmdline_tools_platform)"
- if [[ "${platform_name}" == "unsupported" ]]; then
- echo "android bootstrap supports only darwin and linux hosts" >&2
- exit 1
- fi
-
- local cmdline_zip="${android_download_dir}/$(android_cmdline_tools_zip_name)"
- local tmp_dir="${android_tooling_dir}/tmp/cmdline-tools"
-
- ensure_valid_zip_download "$(android_cmdline_tools_url)" "${cmdline_zip}"
-
- rm -rf "${tmp_dir}" "${android_sdk_dir}/cmdline-tools/latest"
- mkdir -p "${tmp_dir}" "${android_sdk_dir}/cmdline-tools/latest"
- unzip -q "${cmdline_zip}" -d "${tmp_dir}"
- mv "${tmp_dir}/cmdline-tools/"* "${android_sdk_dir}/cmdline-tools/latest/"
- rm -rf "${tmp_dir}"
-}
-
-accept_android_licenses() {
- set +o pipefail
- yes | "${android_sdkmanager_bin}" --sdk_root="${android_sdk_dir}" --licenses >/dev/null
- set -o pipefail
-}
-
-ensure_android_sdk_packages() {
- accept_android_licenses
- "${android_sdkmanager_bin}" --sdk_root="${android_sdk_dir}" "${android_sdk_packages[@]}"
-}
-
-ensure_android_emulator_packages() {
- accept_android_licenses
- local packages=()
- while IFS= read -r package; do
- packages+=("${package}")
- done < <(android_emulator_packages)
- "${android_sdkmanager_bin}" --sdk_root="${android_sdk_dir}" "${packages[@]}"
-}
-
-ensure_cargo_ndk() {
- if [[ -x "${android_cargo_ndk_bin}" ]]; then
- local installed_version
- installed_version="$(PATH="${android_cargo_bin_dir}:${PATH}" cargo ndk --version | awk '{print $2}')"
- if [[ "${installed_version}" == "${android_cargo_ndk_version}" ]]; then
- return
- fi
- fi
-
- cargo install \
- --locked \
- --force \
- --root "${android_cargo_install_root}" \
- --version "${android_cargo_ndk_version}" \
- cargo-ndk
-}
-
-ensure_rust_target() {
- if rustup target list --installed | grep -Fx "${android_rust_target}" >/dev/null 2>&1; then
- return
- fi
- rustup target add "${android_rust_target}"
-}
-
-write_local_properties() {
- cat <<EOF > "${android_local_properties_path}"
-sdk.dir=${android_sdk_dir}
-EOF
-}
-
-main() {
- local with_emulator="false"
- local print_gradle_bin="false"
-
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --with-emulator)
- with_emulator="true"
- shift
- ;;
- --print-gradle-bin)
- print_gradle_bin="true"
- shift
- ;;
- *)
- echo "unknown bootstrap option: $1" >&2
- exit 1
- ;;
- esac
- done
-
- require_command curl
- require_command unzip
- require_command cargo
- require_command rustup
- require_java_17
-
- mkdir -p \
- "${android_tooling_dir}" \
- "${android_download_dir}" \
- "${android_gradle_user_home}" \
- "${android_user_home}" \
- "${android_emulator_home}" \
- "${android_avd_home}" \
- "${android_cargo_install_root}"
-
- ensure_gradle_distribution
- ensure_android_cmdline_tools
- ensure_android_sdk_packages
- if [[ "${with_emulator}" == "true" ]]; then
- ensure_android_emulator_packages
- fi
- ensure_cargo_ndk
- ensure_rust_target
- write_local_properties
-
- if [[ "${print_gradle_bin}" == "true" ]]; then
- printf '%s\n' "${android_gradle_bin}"
- fi
-}
-
-main "$@"
diff --git a/platforms/android/Scripts/build_rust_android.sh b/platforms/android/Scripts/build_rust_android.sh
@@ -1,65 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-source "${script_dir}/android_toolchain_config.sh"
-
-require_command() {
- if command -v "$1" >/dev/null 2>&1; then
- return
- fi
- echo "missing required command: $1" >&2
- exit 1
-}
-
-profile_for_build_type() {
- case "${1}" in
- Release)
- echo "release"
- ;;
- *)
- echo "debug"
- ;;
- esac
-}
-
-missing_bootstrap() {
- echo "android build requires bootstrapped local toolchain files under platforms/android/.tooling" >&2
- exit 1
-}
-
-require_command cargo
-require_command rustup
-
-if [[ ! -d "${android_sdk_dir}" || ! -d "${android_ndk_dir}" || ! -x "${android_cargo_ndk_bin}" ]]; then
- missing_bootstrap
-fi
-
-if ! rustup target list --installed | grep -Fx "${android_rust_target}" >/dev/null 2>&1; then
- missing_bootstrap
-fi
-
-build_type="${1:-Debug}"
-profile="$(profile_for_build_type "${build_type}")"
-
-export PATH="${android_cargo_bin_dir}:${PATH}"
-export ANDROID_HOME="${android_sdk_dir}"
-export ANDROID_SDK_ROOT="${android_sdk_dir}"
-export ANDROID_NDK_HOME="${android_ndk_dir}"
-export ANDROID_NDK_ROOT="${android_ndk_dir}"
-export ANDROID_USER_HOME="${android_user_home}"
-
-cargo_args=(
- ndk
- -t "${android_abi}"
- -o "${app_root}/target/android/jniLibs"
- build
- --manifest-path "${app_root}/Cargo.toml"
- -p radroots_app_android
-)
-
-if [[ "${profile}" == "release" ]]; then
- cargo_args+=(--release)
-fi
-
-cargo "${cargo_args[@]}"
diff --git a/platforms/android/app/build.gradle.kts b/platforms/android/app/build.gradle.kts
@@ -1,87 +0,0 @@
-plugins {
- id("com.android.application")
- id("org.jetbrains.kotlin.android")
-}
-
-val rustBuildScript = file("../Scripts/build_rust_android.sh")
-val rustJniLibsDir = file("../../../target/android/jniLibs")
-val rustInputs = files(
- "../../../Cargo.toml",
- "../../../Cargo.lock",
- rustBuildScript,
- fileTree("../../../crates/shared/core"),
- fileTree("../../../crates/shared/remote_signer"),
- fileTree("../../../crates/bridges/android/security"),
- fileTree("../../../crates/launchers/android"),
-)
-
-android {
- namespace = "org.radroots.app.android"
- compileSdk = 34
- ndkVersion = "26.1.10909125"
-
- defaultConfig {
- applicationId = "org.radroots.app.android"
- minSdk = 26
- targetSdk = 34
- versionCode = 1
- versionName = "0.1.0"
-
- ndk {
- abiFilters += "arm64-v8a"
- }
- }
-
- buildTypes {
- debug {}
- release {
- isMinifyEnabled = false
- }
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-
- kotlinOptions {
- jvmTarget = "17"
- }
-
- sourceSets {
- getByName("main") {
- jniLibs.srcDir(rustJniLibsDir)
- assets.srcDir("../../../assets")
- }
- }
-}
-
-val buildRustDebug = tasks.register("buildRustDebug", org.gradle.api.tasks.Exec::class) {
- workingDir = rootDir
- commandLine("bash", rustBuildScript.absolutePath, "Debug")
- inputs.files(rustInputs)
- outputs.dir(rustJniLibsDir)
-}
-
-val buildRustRelease = tasks.register("buildRustRelease", org.gradle.api.tasks.Exec::class) {
- workingDir = rootDir
- commandLine("bash", rustBuildScript.absolutePath, "Release")
- inputs.files(rustInputs)
- outputs.dir(rustJniLibsDir)
-}
-
-afterEvaluate {
- tasks.named("preDebugBuild").configure {
- dependsOn(buildRustDebug)
- }
- tasks.named("preReleaseBuild").configure {
- dependsOn(buildRustRelease)
- }
-}
-
-dependencies {
- implementation("androidx.games:games-activity:2.0.2")
- implementation("androidx.appcompat:appcompat:1.7.0")
- implementation("androidx.core:core-ktx:1.13.1")
- implementation(project(":radrootsAndroidSecurity"))
-}
diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <application
- android:allowBackup="false"
- android:icon="@drawable/radroots_logo"
- android:label="@string/app_name"
- android:roundIcon="@drawable/radroots_logo"
- android:supportsRtl="true"
- android:theme="@style/Theme.RadRoots">
- <activity
- android:name=".MainActivity"
- android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|smallestScreenSize|uiMode|fontScale"
- android:exported="true"
- android:launchMode="singleTask"
- android:windowSoftInputMode="adjustResize">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
-
- <meta-data
- android:name="android.app.lib_name"
- android:value="radroots_app_android" />
- </activity>
- </application>
- <uses-permission android:name="android.permission.INTERNET" />
-</manifest>
diff --git a/platforms/android/app/src/main/kotlin/org/radroots/app/android/MainActivity.kt b/platforms/android/app/src/main/kotlin/org/radroots/app/android/MainActivity.kt
@@ -1,13 +0,0 @@
-package org.radroots.app.android
-
-import android.os.Bundle
-import com.google.androidgamesdk.GameActivity
-import org.radroots.app.android.security.RadRootsAndroidSecurityBridge
-
-class MainActivity : GameActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- RadRootsAndroidAppBridge.initialize(this)
- RadRootsAndroidSecurityBridge.initialize(this)
- super.onCreate(savedInstanceState)
- }
-}
diff --git a/platforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt b/platforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt
@@ -1,129 +0,0 @@
-package org.radroots.app.android
-
-import android.content.Context
-import java.io.File
-import java.io.FileNotFoundException
-
-object RadRootsAndroidAppBridge {
- private const val GEOCODER_ASSET_PATH = "geocoder/geonames.db"
- private const val GEOCODER_REVISION_ASSET_PATH = "geocoder/geonames.revision"
- private const val GEOCODER_FILE_NAME = "geonames.db"
- private const val GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET = 1
- private const val GEOCODER_ERROR_KIND_INITIALIZATION_FAILED = 2
- private const val GEOCODER_ERROR_KIND_INTERNAL_ERROR = 3
-
- @Volatile
- private var appContext: Context? = null
-
- @Volatile
- private var lastErrorMessage: String? = null
-
- @Volatile
- private var lastErrorKind: Int = 0
-
- @JvmStatic
- fun initialize(context: Context) {
- appContext = context.applicationContext
- }
-
- @JvmStatic
- @Synchronized
- fun stageOfflineGeocoderAsset(): String? {
- val context = appContext
- ?: return fail(
- GEOCODER_ERROR_KIND_INTERNAL_ERROR,
- "android app bridge is not initialized",
- )
- val revision = loadGeocoderRevision(context) ?: return null
- val targetDir = stagedGeocoderDirectory(context, revision)
- if (!targetDir.exists() && !targetDir.mkdirs()) {
- return fail(
- GEOCODER_ERROR_KIND_INITIALIZATION_FAILED,
- "failed to create android geocoder directory: ${targetDir.absolutePath}",
- )
- }
-
- val targetFile = File(targetDir, GEOCODER_FILE_NAME)
- if (targetFile.isFile) {
- lastErrorMessage = null
- lastErrorKind = 0
- return targetFile.absolutePath
- }
- return try {
- context.assets.open(GEOCODER_ASSET_PATH).use { input ->
- targetFile.outputStream().use { output ->
- input.copyTo(output)
- }
- }
- lastErrorMessage = null
- lastErrorKind = 0
- targetFile.absolutePath
- } catch (_: FileNotFoundException) {
- fail(
- GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET,
- "android bundled geocoder asset missing at assets/$GEOCODER_ASSET_PATH",
- )
- } catch (source: Exception) {
- fail(
- GEOCODER_ERROR_KIND_INITIALIZATION_FAILED,
- "failed to stage android geocoder asset: ${source.message ?: source.javaClass.simpleName}",
- )
- }
- }
-
- private fun loadGeocoderRevision(context: Context): String? {
- val revision = try {
- context.assets.open(GEOCODER_REVISION_ASSET_PATH).bufferedReader().use { it.readText() }
- } catch (_: FileNotFoundException) {
- return fail(
- GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET,
- "android bundled geocoder revision asset missing at assets/$GEOCODER_REVISION_ASSET_PATH",
- )
- } catch (source: Exception) {
- return fail(
- GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET,
- "failed to read android geocoder revision asset at assets/$GEOCODER_REVISION_ASSET_PATH: ${source.message ?: source.javaClass.simpleName}",
- )
- }.trim()
-
- if (!isValidRevision(revision)) {
- return fail(
- GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET,
- "android bundled geocoder revision asset invalid at assets/$GEOCODER_REVISION_ASSET_PATH",
- )
- }
-
- return revision
- }
-
- private fun isValidRevision(revision: String): Boolean {
- return revision.length == 64 && revision.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
- }
-
- private fun stagedGeocoderDirectory(context: Context, revision: String): File {
- return File(context.noBackupFilesDir, "RadRoots/app/android/geocoder/$revision")
- }
-
- @JvmStatic
- @Synchronized
- fun takeLastErrorKind(): Int {
- val value = lastErrorKind
- lastErrorKind = 0
- return value
- }
-
- @JvmStatic
- @Synchronized
- fun takeLastErrorMessage(): String? {
- val value = lastErrorMessage
- lastErrorMessage = null
- return value
- }
-
- @Synchronized
- private fun fail(kind: Int, message: String): String? {
- lastErrorKind = kind
- lastErrorMessage = message
- return null
- }
-}
diff --git a/platforms/android/app/src/main/res/drawable-nodpi/radroots_logo.png b/platforms/android/app/src/main/res/drawable-nodpi/radroots_logo.png
Binary files differ.
diff --git a/platforms/android/app/src/main/res/values/strings.xml b/platforms/android/app/src/main/res/values/strings.xml
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="app_name">Rad Roots</string>
-</resources>
diff --git a/platforms/android/app/src/main/res/values/themes.xml b/platforms/android/app/src/main/res/values/themes.xml
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <style name="Theme.RadRoots" parent="Theme.AppCompat.NoActionBar">
- <item name="windowActionBar">false</item>
- <item name="windowNoTitle">true</item>
- </style>
-</resources>
diff --git a/platforms/android/build.gradle.kts b/platforms/android/build.gradle.kts
@@ -1,8 +0,0 @@
-plugins {
- id("com.android.application") version "8.5.2" apply false
- id("org.jetbrains.kotlin.android") version "1.9.24" apply false
-}
-
-tasks.register("clean", Delete::class) {
- delete(rootProject.layout.buildDirectory)
-}
diff --git a/platforms/android/gradle.properties b/platforms/android/gradle.properties
@@ -1,3 +0,0 @@
-android.useAndroidX=true
-org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
-kotlin.code.style=official
diff --git a/platforms/android/settings.gradle.kts b/platforms/android/settings.gradle.kts
@@ -1,22 +0,0 @@
-pluginManagement {
- repositories {
- gradlePluginPortal()
- google()
- mavenCentral()
- }
-}
-
-dependencyResolutionManagement {
- repositoriesMode.set(org.gradle.api.initialization.resolve.RepositoriesMode.FAIL_ON_PROJECT_REPOS)
- repositories {
- google()
- mavenCentral()
- }
-}
-
-rootProject.name = "RadRootsAndroid"
-
-include(":app")
-include(":radrootsAndroidSecurity")
-
-project(":radrootsAndroidSecurity").projectDir = file("../../native/bridges/android/security/kotlin/RadRootsAndroidSecurity")
diff --git a/platforms/ios/App/Bridge/RadRootsIOS-Bridging-Header.h b/platforms/ios/App/Bridge/RadRootsIOS-Bridging-Header.h
@@ -1 +0,0 @@
-#include "RadRootsIOSBridge.h"
diff --git a/platforms/ios/App/Bridge/RadRootsIOSBridge.h b/platforms/ios/App/Bridge/RadRootsIOSBridge.h
@@ -1,5 +0,0 @@
-#include <stdint.h>
-
-int32_t radroots_ios_run(void);
-char *radroots_ios_clipboard_text_copy(void);
-void radroots_ios_string_free(char *value);
diff --git a/platforms/ios/App/RadRootsIOS.entitlements b/platforms/ios/App/RadRootsIOS.entitlements
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>keychain-access-groups</key>
- <array>
- <string>$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string>
- </array>
-</dict>
-</plist>
diff --git a/platforms/ios/App/Resources/Info.plist b/platforms/ios/App/Resources/Info.plist
@@ -1,74 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>CFBundleDevelopmentRegion</key>
- <string>en</string>
- <key>CFBundleDisplayName</key>
- <string>Rad Roots</string>
- <key>CFBundleExecutable</key>
- <string>$(EXECUTABLE_NAME)</string>
- <key>CFBundleIdentifier</key>
- <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
- <key>CFBundleIcons</key>
- <dict>
- <key>CFBundlePrimaryIcon</key>
- <dict>
- <key>CFBundleIconFiles</key>
- <array>
- <string>RadRootsIcon-20@2x</string>
- <string>RadRootsIcon-20@3x</string>
- <string>RadRootsIcon-29@2x</string>
- <string>RadRootsIcon-29@3x</string>
- <string>RadRootsIcon-40@2x</string>
- <string>RadRootsIcon-40@3x</string>
- <string>RadRootsIcon-60@2x</string>
- <string>RadRootsIcon-60@3x</string>
- </array>
- </dict>
- </dict>
- <key>CFBundleIcons~ipad</key>
- <dict>
- <key>CFBundlePrimaryIcon</key>
- <dict>
- <key>CFBundleIconFiles</key>
- <array>
- <string>RadRootsIcon-20@2x~ipad</string>
- <string>RadRootsIcon-29@2x~ipad</string>
- <string>RadRootsIcon-40@2x~ipad</string>
- <string>RadRootsIcon-76@2x~ipad</string>
- <string>RadRootsIcon-83.5@2x~ipad</string>
- </array>
- </dict>
- </dict>
- <key>CFBundleInfoDictionaryVersion</key>
- <string>6.0</string>
- <key>CFBundleName</key>
- <string>Rad Roots</string>
- <key>CFBundlePackageType</key>
- <string>APPL</string>
- <key>CFBundleShortVersionString</key>
- <string>$(MARKETING_VERSION)</string>
- <key>CFBundleVersion</key>
- <string>$(CURRENT_PROJECT_VERSION)</string>
- <key>LSRequiresIPhoneOS</key>
- <true/>
- <key>UIApplicationSupportsIndirectInputEvents</key>
- <true/>
- <key>UILaunchStoryboardName</key>
- <string>LaunchScreen</string>
- <key>UISupportedInterfaceOrientations</key>
- <array>
- <string>UIInterfaceOrientationPortrait</string>
- <string>UIInterfaceOrientationLandscapeLeft</string>
- <string>UIInterfaceOrientationLandscapeRight</string>
- </array>
- <key>UISupportedInterfaceOrientations~ipad</key>
- <array>
- <string>UIInterfaceOrientationPortrait</string>
- <string>UIInterfaceOrientationPortraitUpsideDown</string>
- <string>UIInterfaceOrientationLandscapeLeft</string>
- <string>UIInterfaceOrientationLandscapeRight</string>
- </array>
-</dict>
-</plist>
diff --git a/platforms/ios/App/Resources/LaunchScreen.storyboard b/platforms/ios/App/Resources/LaunchScreen.storyboard
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
- <device id="retina6_12" orientation="portrait" appearance="light"/>
- <dependencies>
- <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
- <capability name="Safe area layout guides" minToolsVersion="9.0"/>
- <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
- </dependencies>
- <scenes>
- <scene sceneID="EHf-IW-A2E">
- <objects>
- <viewController id="01J-lp-oVM" sceneMemberID="viewController">
- <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
- <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
- <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
- <subviews>
- <label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Rad Roots" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8c2-Jc-d8M">
- <rect key="frame" x="125.5" y="403" width="142" height="46"/>
- <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="28"/>
- <color key="textColor" red="0.96078431369999995" green="0.96862745100000002" blue="0.98039215690000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
- <nil key="highlightedColor"/>
- </label>
- </subviews>
- <viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
- <color key="backgroundColor" red="0.062745098039215685" green="0.074509803921568626" blue="0.10196078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
- <constraints>
- <constraint firstItem="8c2-Jc-d8M" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="1Vb-H9-Hoy"/>
- <constraint firstItem="8c2-Jc-d8M" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="rOw-uE-8Jh"/>
- </constraints>
- </view>
- </viewController>
- <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
- </objects>
- <point key="canvasLocation" x="80" y="84"/>
- </scene>
- </scenes>
-</document>
diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@2x.png b/platforms/ios/App/Resources/RadRootsIcon-20@2x.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-20@2x~ipad.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@3x.png b/platforms/ios/App/Resources/RadRootsIcon-20@3x.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@2x.png b/platforms/ios/App/Resources/RadRootsIcon-29@2x.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-29@2x~ipad.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@3x.png b/platforms/ios/App/Resources/RadRootsIcon-29@3x.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@2x.png b/platforms/ios/App/Resources/RadRootsIcon-40@2x.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-40@2x~ipad.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@3x.png b/platforms/ios/App/Resources/RadRootsIcon-40@3x.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-60@2x.png b/platforms/ios/App/Resources/RadRootsIcon-60@2x.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-60@3x.png b/platforms/ios/App/Resources/RadRootsIcon-60@3x.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-76@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-76@2x~ipad.png
Binary files differ.
diff --git a/platforms/ios/App/Resources/RadRootsIcon-83.5@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-83.5@2x~ipad.png
Binary files differ.
diff --git a/platforms/ios/App/main.swift b/platforms/ios/App/main.swift
@@ -1,23 +0,0 @@
-import Foundation
-import UIKit
-
-@_cdecl("radroots_ios_clipboard_text_copy")
-func radroots_ios_clipboard_text_copy() -> UnsafeMutablePointer<CChar>? {
- guard let clipboardText = UIPasteboard.general.string?
- .trimmingCharacters(in: .whitespacesAndNewlines),
- !clipboardText.isEmpty
- else {
- return nil
- }
-
- return clipboardText.withCString { value in
- strdup(value)
- }
-}
-
-@_cdecl("radroots_ios_string_free")
-func radroots_ios_string_free(_ value: UnsafeMutablePointer<CChar>?) {
- free(value)
-}
-
-_ = radroots_ios_run()
diff --git a/platforms/ios/Config/Base.xcconfig b/platforms/ios/Config/Base.xcconfig
@@ -1,17 +0,0 @@
-PRODUCT_NAME = $(TARGET_NAME)
-PRODUCT_MODULE_NAME = RadRootsIOS
-GENERATE_INFOPLIST_FILE = NO
-IPHONEOS_DEPLOYMENT_TARGET = 17.0
-CODE_SIGN_ENTITLEMENTS = $(SRCROOT)/App/RadRootsIOS.entitlements
-SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/App/Bridge/RadRootsIOS-Bridging-Header.h
-LIBRARY_SEARCH_PATHS = $(inherited)
-LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks
-OTHER_LDFLAGS = $(inherited) "$(RUST_LIBRARY_PATH)" -framework Foundation -framework UIKit -framework CoreFoundation -framework Metal -lobjc -liconv
-RUST_TARGET_TRIPLE = aarch64-apple-ios-sim
-RUST_TARGET_TRIPLE[sdk=iphoneos*] = aarch64-apple-ios
-RUST_TARGET_TRIPLE[sdk=iphonesimulator*] = aarch64-apple-ios-sim
-RUST_CARGO_PROFILE = debug
-RUST_LIBRARY_PATH = $(SRCROOT)/../../target/aarch64-apple-ios-sim/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a
-RUST_LIBRARY_PATH[sdk=iphoneos*] = $(SRCROOT)/../../target/aarch64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a
-RUST_LIBRARY_PATH[sdk=iphonesimulator*][arch=x86_64] = $(SRCROOT)/../../target/x86_64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a
-SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO
diff --git a/platforms/ios/Config/Debug.xcconfig b/platforms/ios/Config/Debug.xcconfig
@@ -1,3 +0,0 @@
-#include "Base.xcconfig"
-
-RUST_CARGO_PROFILE = debug
diff --git a/platforms/ios/Config/Release.xcconfig b/platforms/ios/Config/Release.xcconfig
@@ -1,3 +0,0 @@
-#include "Base.xcconfig"
-
-RUST_CARGO_PROFILE = release
diff --git a/platforms/ios/Scripts/build_rust_ios.sh b/platforms/ios/Scripts/build_rust_ios.sh
@@ -1,89 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-ios_root="$(cd "${script_dir}/.." && pwd -P)"
-app_root="$(cd "${ios_root}/../.." && pwd -P)"
-ios_target_dir="${app_root}/target"
-
-require_command() {
- if command -v "$1" >/dev/null 2>&1; then
- return
- fi
- echo "missing required command: $1" >&2
- exit 1
-}
-
-require_rust_target() {
- local target="$1"
- if rustup target list --installed | grep -Fx "${target}" >/dev/null 2>&1; then
- return
- fi
- echo "missing required rust target: ${target}" >&2
- exit 1
-}
-
-profile_for_configuration() {
- case "${1}" in
- Release)
- echo "release"
- ;;
- *)
- echo "debug"
- ;;
- esac
-}
-
-build_target() {
- local target="$1"
- local profile="$2"
- local cargo_args=(
- build
- --manifest-path "${app_root}/Cargo.toml"
- -p radroots_app_ios
- --target "${target}"
- )
- if [[ "${profile}" == "release" ]]; then
- cargo_args+=(--release)
- fi
- CARGO_TARGET_DIR="${ios_target_dir}" cargo "${cargo_args[@]}"
-}
-
-build_targets() {
- local profile="$1"
- shift
- for target in "$@"; do
- require_rust_target "${target}"
- build_target "${target}" "${profile}"
- done
-}
-
-require_command cargo
-require_command rustup
-
-configuration="${CONFIGURATION:-Debug}"
-profile="$(profile_for_configuration "${configuration}")"
-sdk_name="${SDK_NAME:-}"
-archs="${ARCHS:-}"
-
-if [[ -n "${sdk_name}" ]]; then
- case "${sdk_name}" in
- iphoneos*)
- build_targets "${profile}" aarch64-apple-ios
- ;;
- iphonesimulator*)
- if [[ " ${archs} " == *" x86_64 "* ]]; then
- build_targets "${profile}" aarch64-apple-ios-sim x86_64-apple-ios
- else
- build_targets "${profile}" aarch64-apple-ios-sim
- fi
- ;;
- *)
- echo "unsupported iOS SDK_NAME: ${sdk_name}" >&2
- exit 1
- ;;
- esac
- exit 0
-fi
-
-build_targets "${profile}" aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
diff --git a/platforms/ios/Scripts/sync_geocoder_resource.sh b/platforms/ios/Scripts/sync_geocoder_resource.sh
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-app_root="$(cd "$script_dir/../../.." && pwd -P)"
-
-source_db="$app_root/assets/geocoder/geonames.db"
-source_revision="$app_root/assets/geocoder/geonames.revision"
-target_dir="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
-target_db="$target_dir/geonames.db"
-target_revision="$target_dir/geonames.revision"
-
-mkdir -p "$target_dir"
-
-if [[ -f "$source_db" ]]; then
- if [[ ! -f "$source_revision" ]]; then
- printf 'stamped ios geocoder revision asset missing at build time: %s\n' "$source_revision" >&2
- exit 1
- fi
- cp "$source_db" "$target_db"
- cp "$source_revision" "$target_revision"
- printf 'synced ios geocoder asset: %s\n' "$target_db"
- printf 'synced ios geocoder revision: %s\n' "$target_revision"
- exit 0
-fi
-
-if [[ -f "$target_db" ]]; then
- rm -f "$target_db"
-fi
-if [[ -f "$target_revision" ]]; then
- rm -f "$target_revision"
-fi
-
-printf 'ios geocoder asset not present at build time: %s\n' "$source_db"
diff --git a/platforms/ios/project.yml b/platforms/ios/project.yml
@@ -1,84 +0,0 @@
-name: RadRootsIOS
-
-options:
- createIntermediateGroups: true
- deploymentTarget:
- iOS: "17.0"
-
-settings:
- base:
- PRODUCT_BUNDLE_IDENTIFIER: org.radroots.app.ios
- MARKETING_VERSION: 0.1.0
- CURRENT_PROJECT_VERSION: 1
- SWIFT_VERSION: 6.0
- CODE_SIGN_STYLE: Automatic
-
-configs:
- Debug: debug
- Release: release
-
-packages:
- RadRootsAppleSecurity:
- path: ../../native/bridges/apple/security/swift/RadRootsAppleSecurity
- group: Native/Apple/Swift
-
-targetTemplates:
- app_base:
- type: application
- platform: iOS
- deploymentTarget: "17.0"
- supportedDestinations:
- - iOS
- - iOS Simulator
- configFiles:
- Debug: Config/Debug.xcconfig
- Release: Config/Release.xcconfig
- sources:
- - path: App/main.swift
- - path: App/Bridge
- buildPhase: headers
- - path: App/Resources
- excludes:
- - Info.plist
- settings:
- base:
- PRODUCT_NAME: $(TARGET_NAME)
- INFOPLIST_FILE: App/Resources/Info.plist
- TARGETED_DEVICE_FAMILY: 1,2
- preBuildScripts:
- - name: Build Rust iOS Library
- script: |
- "$SRCROOT/Scripts/build_rust_ios.sh"
- basedOnDependencyAnalysis: false
- inputFiles:
- - $(SRCROOT)/../../Cargo.toml
- - $(SRCROOT)/../../Cargo.lock
- - $(SRCROOT)/../../crates/shared/core/Cargo.toml
- - $(SRCROOT)/../../crates/shared/core/src/lib.rs
- - $(SRCROOT)/../../crates/shared/remote_signer/Cargo.toml
- - $(SRCROOT)/../../crates/shared/remote_signer/src/lib.rs
- - $(SRCROOT)/../../crates/bridges/apple/security/Cargo.toml
- - $(SRCROOT)/../../crates/bridges/apple/security/src/lib.rs
- - $(SRCROOT)/../../crates/launchers/ios/Cargo.toml
- - $(SRCROOT)/../../crates/launchers/ios/src/lib.rs
- - $(SRCROOT)/Scripts/build_rust_ios.sh
- outputFiles:
- - $(SRCROOT)/../../target/$(RUST_TARGET_TRIPLE)/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a
- - $(SRCROOT)/../../target/x86_64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a
- postBuildScripts:
- - name: Sync iOS Geocoder Asset
- script: |
- "$SRCROOT/Scripts/sync_geocoder_resource.sh"
- basedOnDependencyAnalysis: false
- dependencies:
- - sdk: UIKit.framework
- - sdk: Foundation.framework
- - sdk: CoreFoundation.framework
- - sdk: Metal.framework
- - package: RadRootsAppleSecurity
- product: RadRootsAppleSecurityFFI
-
-targets:
- RadRootsIOS:
- templates:
- - app_base
diff --git a/scripts/build-android-host.sh b/scripts/build-android-host.sh
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-app_root="$(cd "${script_dir}/.." && pwd -P)"
-android_root="${app_root}/platforms/android"
-configuration="${CONFIGURATION:-Debug}"
-
-source "${android_root}/Scripts/android_toolchain_config.sh"
-
-"${android_root}/Scripts/bootstrap_android_toolchain.sh"
-
-gradle_task=":app:assembleDebug"
-expected_apk="${android_root}/app/build/outputs/apk/debug/app-debug.apk"
-if [[ "${configuration}" == "Release" ]]; then
- gradle_task=":app:assembleRelease"
- expected_apk="${android_root}/app/build/outputs/apk/release/app-release-unsigned.apk"
-fi
-
-(
- cd "${android_root}"
- GRADLE_USER_HOME="${android_gradle_user_home}" \
- ANDROID_USER_HOME="${android_user_home}" \
- ANDROID_HOME="${android_sdk_dir}" \
- ANDROID_SDK_ROOT="${android_sdk_dir}" \
- "${android_gradle_bin}" --no-daemon "${gradle_task}"
-)
-
-if [[ ! -f "${expected_apk}" ]]; then
- echo "missing expected android apk: ${expected_apk}" >&2
- exit 1
-fi
-
-printf '%s\n' "${expected_apk}"
diff --git a/scripts/build-ios-host.sh b/scripts/build-ios-host.sh
@@ -1,63 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-app_root="$(cd "${script_dir}/.." && pwd -P)"
-ios_root="${app_root}/platforms/ios"
-project_name="RadRootsIOS"
-configuration="${CONFIGURATION:-Debug}"
-derived_data_dir="${ios_root}/.derived-data"
-expected_app="${derived_data_dir}/Build/Products/${configuration}-iphonesimulator/${project_name}.app"
-
-require_command() {
- if command -v "$1" >/dev/null 2>&1; then
- return
- fi
- echo "missing required command: $1" >&2
- exit 1
-}
-
-ios_sim_host_arch() {
- case "$(uname -m)" in
- arm64|aarch64)
- echo "arm64"
- ;;
- x86_64)
- echo "x86_64"
- ;;
- *)
- echo "unsupported host architecture for ios simulator: $(uname -m)" >&2
- exit 1
- ;;
- esac
-}
-
-require_command xcodegen
-require_command xcodebuild
-
-"${script_dir}/check-ios-target.sh"
-
-host_arch="$(ios_sim_host_arch)"
-
-(
- cd "${ios_root}"
- xcodegen generate
- xcodebuild \
- -project "${project_name}.xcodeproj" \
- -scheme "${project_name}" \
- -configuration "${configuration}" \
- -sdk iphonesimulator \
- -destination "generic/platform=iOS Simulator" \
- -derivedDataPath "${derived_data_dir}" \
- ARCHS="${host_arch}" \
- CODE_SIGNING_ALLOWED=YES \
- ONLY_ACTIVE_ARCH=YES \
- build
-)
-
-if [[ ! -d "${expected_app}" ]]; then
- echo "missing expected ios app bundle: ${expected_app}" >&2
- exit 1
-fi
-
-printf '%s\n' "${expected_app}"
diff --git a/scripts/check-android-target.sh b/scripts/check-android-target.sh
@@ -1,20 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-app_root="$(cd "${script_dir}/.." && pwd -P)"
-android_root="${app_root}/platforms/android"
-
-source "${android_root}/Scripts/android_toolchain_config.sh"
-
-"${android_root}/Scripts/bootstrap_android_toolchain.sh"
-
-export PATH="${android_cargo_bin_dir}:${PATH}"
-export ANDROID_HOME="${android_sdk_dir}"
-export ANDROID_SDK_ROOT="${android_sdk_dir}"
-export ANDROID_NDK_HOME="${android_ndk_dir}"
-export ANDROID_NDK_ROOT="${android_ndk_dir}"
-export ANDROID_USER_HOME="${android_user_home}"
-
-CARGO_TARGET_DIR="${app_root}/target" \
- cargo ndk -t "${android_abi}" check --manifest-path "${app_root}/Cargo.toml" -p radroots_app_android
diff --git a/scripts/check-ios-target.sh b/scripts/check-ios-target.sh
@@ -1,58 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-app_root="$(cd "${script_dir}/.." && pwd -P)"
-ios_target_dir="${app_root}/target"
-
-require_command() {
- if command -v "$1" >/dev/null 2>&1; then
- return
- fi
- echo "missing required command: $1" >&2
- exit 1
-}
-
-ios_sim_rust_targets_for_host() {
- case "$(uname -m)" in
- arm64|aarch64)
- printf '%s\n' "aarch64-apple-ios-sim"
- ;;
- x86_64)
- printf '%s\n' "aarch64-apple-ios-sim" "x86_64-apple-ios"
- ;;
- *)
- echo "unsupported host architecture for ios simulator: $(uname -m)" >&2
- exit 1
- ;;
- esac
-}
-
-require_rust_target() {
- local target="$1"
- if rustup target list --installed | grep -Fx "${target}" >/dev/null 2>&1; then
- return
- fi
- echo "missing required rust target: ${target}" >&2
- exit 1
-}
-
-require_command cargo
-require_command rustup
-
-cd "${app_root}"
-
-declare -a targets=()
-if [[ -n "${IOS_SIM_RUST_TARGET:-}" ]]; then
- targets=("${IOS_SIM_RUST_TARGET}")
-else
- while IFS= read -r target; do
- targets+=("${target}")
- done < <(ios_sim_rust_targets_for_host)
-fi
-
-for target in "${targets[@]}"; do
- require_rust_target "${target}"
- CARGO_TARGET_DIR="${ios_target_dir}" \
- cargo check --manifest-path "${app_root}/Cargo.toml" -p radroots_app_ios --target "${target}"
-done
diff --git a/scripts/check.sh b/scripts/check.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel)"
+
+cd "${repo_root}"
+cargo check -p radroots_app
diff --git a/scripts/run-android-emulator.sh b/scripts/run-android-emulator.sh
@@ -1,131 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-app_root="$(cd "${script_dir}/.." && pwd -P)"
-android_root="${app_root}/platforms/android"
-bundle_id="org.radroots.app.android"
-activity_name="${bundle_id}/.MainActivity"
-
-source "${android_root}/Scripts/android_toolchain_config.sh"
-
-require_command() {
- if command -v "$1" >/dev/null 2>&1; then
- return
- fi
- echo "missing required command: $1" >&2
- exit 1
-}
-
-host_os() {
- uname -s
-}
-
-host_arch() {
- uname -m
-}
-
-android_emulator_gpu_mode() {
- if [[ -n "${RADROOTS_ANDROID_EMULATOR_GPU_MODE:-}" ]]; then
- printf '%s\n' "${RADROOTS_ANDROID_EMULATOR_GPU_MODE}"
- return
- fi
-
- if [[ "$(host_os)" == "Darwin" && "$(host_arch)" == "arm64" ]]; then
- printf '%s\n' "swiftshader"
- return
- fi
-
- printf '%s\n' "auto"
-}
-
-running_emulator_serial() {
- local target_avd="$1"
- while read -r serial state _; do
- [[ "${serial}" == emulator-* ]] || continue
- [[ "${state}" == "device" || "${state}" == "offline" ]] || continue
- if [[ "$("${android_adb_bin}" -s "${serial}" emu avd name 2>/dev/null | sed -n '1p' | tr -d '\r')" == "${target_avd}" ]]; then
- printf '%s\n' "${serial}"
- return
- fi
- done < <("${android_adb_bin}" devices | tail -n +2)
-}
-
-ensure_avd() {
- local avd_name="$1"
- if [[ -d "${android_avd_home}/${avd_name}.avd" ]]; then
- return
- fi
-
- mkdir -p "${android_avd_home}" "${android_emulator_home}"
- printf 'no\n' | \
- ANDROID_AVD_HOME="${android_avd_home}" \
- ANDROID_EMULATOR_HOME="${android_emulator_home}" \
- "${android_avdmanager_bin}" create avd --force --name "${avd_name}" --package "$(android_emulator_system_image_package)"
-}
-
-wait_for_boot_complete() {
- local serial="$1"
- "${android_adb_bin}" -s "${serial}" wait-for-device >/dev/null
- until [[ "$("${android_adb_bin}" -s "${serial}" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]]; do
- sleep 2
- done
-}
-
-launch_emulator_if_needed() {
- local avd_name="$1"
- local serial
- local gpu_mode
- serial="$(running_emulator_serial "${avd_name}" || true)"
- if [[ -n "${serial}" ]]; then
- printf '%s\n' "${serial}"
- return
- fi
-
- gpu_mode="$(android_emulator_gpu_mode)"
- ANDROID_AVD_HOME="${android_avd_home}" \
- ANDROID_EMULATOR_HOME="${android_emulator_home}" \
- nohup "${android_emulator_bin}" \
- -avd "${avd_name}" \
- -gpu "${gpu_mode}" \
- -no-snapshot-load \
- -no-snapshot-save >/tmp/radroots-android-emulator.log 2>&1 &
-
- for _ in $(seq 1 60); do
- serial="$(running_emulator_serial "${avd_name}" || true)"
- if [[ -n "${serial}" ]]; then
- printf '%s\n' "${serial}"
- return
- fi
- sleep 2
- done
-
- echo "android emulator failed to start" >&2
- exit 1
-}
-
-require_command mktemp
-
-avd_name="${1:-$(android_avd_name)}"
-
-"${android_root}/Scripts/bootstrap_android_toolchain.sh" --with-emulator
-
-build_log="$(mktemp)"
-trap 'rm -f "${build_log}"' EXIT
-
-if ! "${script_dir}/build-android-host.sh" | tee "${build_log}"; then
- exit 1
-fi
-
-apk_path="$(tail -n 1 "${build_log}")"
-if [[ ! -f "${apk_path}" ]]; then
- echo "missing built android apk: ${apk_path}" >&2
- exit 1
-fi
-
-ensure_avd "${avd_name}"
-serial="$(launch_emulator_if_needed "${avd_name}")"
-wait_for_boot_complete "${serial}"
-
-"${android_adb_bin}" -s "${serial}" install -r "${apk_path}"
-"${android_adb_bin}" -s "${serial}" shell am start -n "${activity_name}"
diff --git a/scripts/run-ios-simulator.sh b/scripts/run-ios-simulator.sh
@@ -1,61 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
-app_root="$(cd "${script_dir}/.." && pwd -P)"
-bundle_id="org.radroots.app.ios"
-device_selector="${1:-${IOS_SIMULATOR_DEVICE:-iPhone 16}}"
-
-require_command() {
- if command -v "$1" >/dev/null 2>&1; then
- return
- fi
- echo "missing required command: $1" >&2
- exit 1
-}
-
-resolve_simulator_udid() {
- local selector="$1"
- if [[ "${selector}" =~ ^[0-9A-F-]{36}$ ]]; then
- printf '%s\n' "${selector}"
- return
- fi
-
- local line
- line="$(
- xcrun simctl list devices available |
- awk -v name="${selector}" '$0 ~ ("^[[:space:]]+" name " \\(") { print; exit }'
- )"
-
- if [[ -z "${line}" ]]; then
- echo "unable to find available iOS simulator: ${selector}" >&2
- exit 1
- fi
-
- printf '%s\n' "${line}" | awk -F '[()]' '{ print $2 }'
-}
-
-require_command open
-require_command xcrun
-require_command mktemp
-
-build_log="$(mktemp)"
-trap 'rm -f "${build_log}"' EXIT
-
-if ! "${script_dir}/build-ios-host.sh" | tee "${build_log}"; then
- exit 1
-fi
-
-app_path="$(tail -n 1 "${build_log}")"
-if [[ ! -d "${app_path}" ]]; then
- echo "missing built iOS app bundle: ${app_path}" >&2
- exit 1
-fi
-
-device_udid="$(resolve_simulator_udid "${device_selector}")"
-
-xcrun simctl boot "${device_udid}" >/dev/null 2>&1 || true
-xcrun simctl bootstatus "${device_udid}" -b
-open -a Simulator --args -CurrentDeviceUDID "${device_udid}" >/dev/null 2>&1 || open -a Simulator >/dev/null 2>&1
-xcrun simctl install "${device_udid}" "${app_path}"
-xcrun simctl launch "${device_udid}" "${bundle_id}"
diff --git a/scripts/run.sh b/scripts/run.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel)"
+
+cd "${repo_root}"
+cargo run -p radroots_app -- "$@"
diff --git a/scripts/verify-approved-test-fixtures.sh b/scripts/verify-approved-test-fixtures.sh
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
-cd "$repo_root"
-
-if rg -n \
- --glob '*.rs' \
- --glob '*.kt' \
- --glob '*.swift' \
- --glob '*.sh' \
- --glob '!scripts/verify-approved-test-fixtures.sh' \
- 'npub1abc|nsec1example' \
- crates native platforms scripts; then
- echo "found banned placeholder fixture literals" >&2
- exit 1
-fi
diff --git a/scripts/with-wasm-toolchain.sh b/scripts/with-wasm-toolchain.sh
@@ -1,38 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-if [ "$#" -eq 0 ]; then
- echo "usage: $0 <command> [args...]" >&2
- exit 64
-fi
-
-unset NO_COLOR
-
-probe_wasm_clang() {
- local clang_bin="$1"
- local probe_file
-
- if [ ! -x "$clang_bin" ]; then
- return 1
- fi
-
- probe_file="$(mktemp)"
- trap 'rm -f "$probe_file"' RETURN
- printf 'int main(void){return 0;}\n' \
- | "$clang_bin" --target=wasm32-unknown-unknown -x c -c - -o "$probe_file" >/dev/null 2>&1
-}
-
-if [ -z "${CC_wasm32_unknown_unknown:-}" ]; then
- if probe_wasm_clang /opt/homebrew/opt/llvm/bin/clang; then
- export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
- export CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang
- elif command -v clang >/dev/null 2>&1 && probe_wasm_clang "$(command -v clang)"; then
- export CC_wasm32_unknown_unknown
- CC_wasm32_unknown_unknown="$(command -v clang)"
- else
- echo "no wasm-capable clang found; install llvm or set CC_wasm32_unknown_unknown" >&2
- exit 1
- fi
-fi
-
-exec "$@"
diff --git a/src/app.rs b/src/app.rs
@@ -0,0 +1,40 @@
+use gpui::{AppContext, Application, WindowOptions, px, size};
+
+fn titlebar_options() -> gpui::TitlebarOptions {
+ gpui::TitlebarOptions {
+ title: None,
+ appears_transparent: true,
+ ..Default::default()
+ }
+}
+
+pub fn launch() {
+ let app = Application::new();
+
+ app.run(|cx| {
+ cx.on_window_closed(|cx| {
+ if cx.windows().is_empty() {
+ cx.quit();
+ }
+ })
+ .detach();
+
+ cx.spawn(async move |cx| {
+ cx.open_window(
+ WindowOptions {
+ app_id: Some("org.radroots.app".to_owned()),
+ window_min_size: Some(size(px(640.0), px(480.0))),
+ titlebar: Some(titlebar_options()),
+ ..Default::default()
+ },
+ |_, cx| cx.new(|_| crate::window::PlaceholderView),
+ )
+ .expect("main radroots app window should open");
+
+ cx.update(|cx| cx.activate(true))
+ .expect("radroots app activation should succeed");
+ })
+ .detach();
+ });
+
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -0,0 +1,8 @@
+#![forbid(unsafe_code)]
+
+mod app;
+mod window;
+
+pub fn run() {
+ app::launch();
+}
diff --git a/src/main.rs b/src/main.rs
@@ -0,0 +1,5 @@
+#![forbid(unsafe_code)]
+
+fn main() {
+ radroots_app::run();
+}
diff --git a/src/window.rs b/src/window.rs
@@ -0,0 +1,21 @@
+use gpui::{Context, FontWeight, IntoElement, ParentElement, Render, Styled, Window, div, px, rgb};
+
+pub struct PlaceholderView;
+
+impl Render for PlaceholderView {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .size_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .bg(rgb(0xF5F1E8))
+ .child(
+ div()
+ .text_size(px(20.0))
+ .font_weight(FontWeight::SEMIBOLD)
+ .text_color(rgb(0x1F2C23))
+ .child("radroots"),
+ )
+ }
+}