diff --git a/.env.example b/.env.example
index 5fe6ab2..e6ee106 100644
--- a/.env.example
+++ b/.env.example
@@ -4,7 +4,9 @@
DATABASE_URL=postgres://opencade:opencade@db:5432/opencade
# Generate with: openssl rand -hex 32
SESSION_SECRET=replace-with-at-least-32-random-bytes
-ALLOWED_ORIGINS=http://localhost:1420,tauri://localhost
+# Generate independently with: openssl rand -hex 32
+RELAY_AUTH_SECRET=replace-with-a-different-32-byte-random-secret
+ALLOWED_ORIGINS=http://localhost:1420,tauri://localhost,http://tauri.localhost,https://tauri.localhost
OPENCADE_ENV=production
# Optional overrides:
@@ -15,4 +17,4 @@ OPENCADE_ENV=production
# POSTGRES_DB=opencade
# RFC 8489 server as a numeric IP:port. Both variables should identify the same service.
# OPENCADE_STUN_SERVER=203.0.113.10:3478
-# VITE_STUN_SERVER=203.0.113.10:3478
+# RELAY_URL=wss://relay.example.com/relay
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 70bd52f..2d3f90f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -118,6 +118,7 @@ jobs:
run: docker compose config
env:
SESSION_SECRET: ci-compose-secret-at-least-32-bytes
+ RELAY_AUTH_SECRET: ci-relay-secret-at-least-32-bytes-long
desktop:
name: Desktop Rust (Windows / MSRV)
@@ -149,15 +150,34 @@ jobs:
- name: Build Windows LAN alpha tools
run: cargo build -p opencade-networking --bins --release --locked
+ - name: Build Windows desktop alpha
+ run: cargo build --manifest-path apps/client/src-tauri/Cargo.toml --release --locked
+
+ - name: Package and verify Windows alpha kit
+ shell: pwsh
+ run: .\scripts\alpha\OpenCade-Alpha.ps1 -Mode Package -KitRoot alpha-kit
+
+ - name: Alpha kit verifier rejects tampering
+ shell: pwsh
+ run: |
+ Copy-Item alpha-kit alpha-kit-tampered -Recurse
+ Add-Content -LiteralPath alpha-kit-tampered\opencade-client.exe -Value "tampered"
+ $failedClosed = $false
+ try {
+ .\scripts\alpha\OpenCade-Alpha.ps1 -Mode Verify -KitRoot alpha-kit-tampered
+ } catch {
+ $failedClosed = $true
+ }
+ if (-not $failedClosed) {
+ throw "Alpha kit verifier accepted a tampered executable"
+ }
+
- name: Upload Windows LAN alpha tools
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
- name: opencade-lan-tools-windows
- path: |
- target/release/opencade-match-probe.exe
- target/release/opencade-match-verify.exe
- target/release/opencade-alpha-summary.exe
+ name: opencade-windows-alpha
+ path: alpha-kit
if-no-files-found: error
retention-days: 14
diff --git a/.gitignore b/.gitignore
index d10f9c4..69eedf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,8 @@ apps/client/src-tauri/target/
apps/client/src-tauri/gen/schemas/
apps/client/dist/
apps/client/bundle/
+/alpha-kit/
+/alpha-kit-tampered/
# ── Environment / Secrets ────────────────────────────────────
.env
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fcb26b1..0bb1bdf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -88,7 +88,7 @@ opencade/
│ ├── fbneo/ # FBNeo adapter (first adapter)
│ └── mame/ # (future)
├── services/
-│ └── relay/ # opencade-relay (TURN-like fallback)
+│ └── relay/ # authenticated readiness-probe WebSocket fallback
├── research/ # NOT SHIPPED — observations only (see GUARDRAILS.md)
├── docs/
├── docker/
diff --git a/Cargo.lock b/Cargo.lock
index 8575d25..48a0a21 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -233,6 +233,16 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -397,7 +407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -407,7 +417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
dependencies = [
"cfg-if",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1002,7 +1012,7 @@ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
"libc",
"wasi",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1011,7 +1021,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1038,6 +1048,15 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "opencade-adapter-retroarch"
+version = "0.1.0"
+dependencies = [
+ "opencade-emulator-sdk",
+ "serde",
+ "sha2 0.11.0",
+]
+
[[package]]
name = "opencade-emulator-sdk"
version = "0.1.0"
@@ -1063,12 +1082,16 @@ name = "opencade-networking"
version = "0.1.0"
dependencies = [
"chrono",
+ "futures-util",
"opencade-emulator-sdk",
"opencade-protocol",
+ "opencade-shared",
"serde",
"serde_json",
"thiserror",
"tokio",
+ "tokio-tungstenite 0.30.0",
+ "url",
"uuid",
]
@@ -1091,9 +1114,11 @@ dependencies = [
"dashmap",
"futures",
"opencade-protocol",
+ "opencade-shared",
"serde",
"serde_json",
"tokio",
+ "tokio-tungstenite 0.30.0",
"tracing",
"tracing-subscriber",
"uuid",
@@ -1110,6 +1135,7 @@ dependencies = [
"futures-util",
"hex",
"opencade-protocol",
+ "opencade-shared",
"rand_core 0.6.4",
"regex",
"serde",
@@ -1130,10 +1156,19 @@ dependencies = [
name = "opencade-shared"
version = "0.1.0"
dependencies = [
+ "hex",
+ "hmac",
"serde",
+ "sha2 0.11.0",
"thiserror",
]
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
[[package]]
name = "parking"
version = "2.2.1"
@@ -1333,6 +1368,20 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "rustix"
version = "1.1.4"
@@ -1343,7 +1392,52 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
- "windows-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
]
[[package]]
@@ -1358,12 +1452,44 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "serde"
version = "1.0.229"
@@ -1530,7 +1656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1793,7 +1919,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1873,7 +1999,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1887,6 +2013,16 @@ dependencies = [
"syn 3.0.4",
]
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
[[package]]
name = "tokio-stream"
version = "0.1.19"
@@ -1918,7 +2054,11 @@ checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71"
dependencies = [
"futures-util",
"log",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pki-types",
"tokio",
+ "tokio-rustls",
"tungstenite 0.30.0",
]
@@ -2133,6 +2273,8 @@ dependencies = [
"httparse",
"log",
"rand 0.10.2",
+ "rustls",
+ "rustls-pki-types",
"sha1 0.11.0",
"thiserror",
]
@@ -2170,6 +2312,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.8"
@@ -2290,7 +2438,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2352,6 +2500,15 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -2361,6 +2518,70 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
[[package]]
name = "winnow"
version = "1.0.4"
@@ -2443,6 +2664,12 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
[[package]]
name = "zerotrie"
version = "0.2.5"
diff --git a/Cargo.toml b/Cargo.toml
index 7b53925..57503f6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
"packages/shared",
"packages/game-definitions",
"adapters/fbneo",
+ "adapters/retroarch",
"services/relay",
]
resolver = "2"
diff --git a/README.md b/README.md
index 78e3fa5..33e54cf 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,11 @@
> **Open-source arcade netplay — a clean-room, community-owned alternative for low-latency rollback matchmaking and emulation.**
-OpenCade is a monorepo for a modern arcade netplay platform: Rust server (Axum + PostgreSQL), Tauri + React + TypeScript desktop client, and a pluggable emulator adapter SDK. The repository now contains an executable **Proof of Match** control plane, deterministic mock-adapter data plane, safe local FBNeo launch boundary, direct UDP transport, and an evidence-gated STUN/hole-punch path. FBNeo netplay, physical cross-NAT results, behavior classification, and relay fallback remain explicitly unproven and are not advertised as implemented.
+OpenCade is a monorepo for a modern arcade netplay platform: Rust server (Axum + PostgreSQL), Tauri + React + TypeScript desktop client, and a pluggable emulator adapter SDK. The repository contains an executable **Proof of Match** control plane, deterministic mock-adapter data plane, safe local FBNeo launch, direct UDP/STUN/hole-punching, authenticated readiness-probe relay fallback, and an experimental native-process RetroArch adapter. Standalone FBNeo netplay, physical cross-NAT results, behavior classification, and real two-machine RetroArch play remain explicitly unproven.
## Support — keep it community-owned
-OpenCade exists to replace proprietary Fightcade with a community-owned, self-hostable, and fully auditable alternative. Proof-of-Match is done — lobby, signaling, deterministic transport, STUN discovery, and bounded hole punching run today. What remains is physical network evidence, relay fallback, and the emulator seam — expensive, unglamorous systems work that determines whether matches actually connect.
+OpenCade exists to replace proprietary Fightcade with a community-owned, self-hostable, and fully auditable alternative. Proof-of-Match is done — lobby, signaling, deterministic transport, STUN discovery, bounded hole punching, authenticated relay fallback, and an experimental user-supplied RetroArch process boundary run today. What remains is physical two-Windows Proof-of-Play evidence and production release hardening.
**[☕ Buy Me a Coffee — https://buymeacoffee.com/zendevve](https://buymeacoffee.com/zendevve)** — one coffee keeps the relay and adapter work open. No paywall, no premium.
@@ -40,7 +40,7 @@ curl http://localhost:8080/health
docker compose logs -f opencade-server
```
-This starts `opencade-server` and PostgreSQL. The server applies committed SQLx migrations before it begins serving. A relay service is intentionally deferred.
+This starts `opencade-server`, PostgreSQL, and the authenticated readiness-probe relay. The server applies committed SQLx migrations before it begins serving.
```bash
# stop
@@ -90,8 +90,10 @@ OpenCade/
│ ├── networking/ # Bounded input frames, deterministic in-memory + direct UDP transports
│ └── shared/ # Cross-cutting utils, logging, config
├── adapters/
-│ └── fbneo/ # FBNeo reference adapter (first implementation)
-├── services/ # Reserved for a future relay service
+│ ├── fbneo/ # Standalone FBNeo local-play adapter
+│ └── retroarch/ # Experimental native-process netplay adapter
+├── services/ # Authenticated bounded readiness-probe relay
+├── scripts/alpha/ # Windows alpha-kit packager, verifier, doctor, and launcher
├── research/ # OBSERVATIONS ONLY — never shipped (see Clean-Room Notice)
│ ├── observations/ # Dated, factual notes from black-box behavior
│ ├── protocol/ # Captured message field notes (no replay)
@@ -141,11 +143,12 @@ High-level: `Client (Tauri)` ↔ `Server (Axum REST + authenticated WebSocket)`
- **Server:** auth (Argon2id), hashed sessions, games, server hints, lobbies, durable challenges,
rooms/matches, and authenticated WebSocket signaling (`offer`/`answer`/`candidate`).
-- **Networking:** deterministic in-memory and connected UDP transports, RFC 8489 reflexive-address
- discovery, and bounded nonce-bound hole punching are implemented. Cone/symmetric behavior
- classification and relay fallback remain deferred pending physical campaign evidence.
+- **Networking:** deterministic in-memory, direct UDP, RFC 8489 discovery, nonce-bound hole punching,
+ and a short-lived room-member-ticket WebSocket relay are implemented. Physical cross-NAT evidence
+ and cone/symmetric behavior classification remain pending.
- **Client:** login/register, games, lobby challenges, match state, local availability scan, diagnostics, and redacted report export; Rust core owns process spawn, filesystem validation, and diagnostics.
-- **Emulator SDK:** trait-based adapters with safe process launch (no shell injection, argument escaping), ROM validation, and game-definition TOML.
+- **Emulator SDK:** explicit OpenCade/native/blocked netplay modes, safe process launch, ROM validation,
+ game-definition TOML, and an experimental user-supplied RetroArch + FBNeo-core adapter.
Full reference: **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**.
@@ -161,7 +164,7 @@ Full reference: **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**.
| **M3** | Client Shell | Tauri + React shell, routing (Games/Lobbies/Friends/Servers/Settings), Rust fs/process/logging | Client launches, talks to server, diagnostics panel |
| **M4** | Emulator SDK | Adapter trait (`detect`/`validate`/`getVersion`/`launch`/`stop`/`configure`/`getSupportedGames`), FBNeo adapter, TOML game definitions | Local ROM scan + safe launch for one title |
| **M5** | Matchmaking | Lobbies, game versions, server browser, matchmaking & room lifecycle | Create/join/spectate room e2e with two peers |
-| **M6** | NAT & Relay | STUN, hole-punching, `opencade-relay` TURN fallback, RTT/loss/jitter, Network Test | Direct + relayed matches measured; relay Docker image |
+| **M6** | NAT & Relay | STUN, hole-punching, authenticated `opencade-relay` WebSocket fallback, RTT/loss/jitter, Network Test | Direct + relayed matches measured; relay Docker image |
| **M7** | MVP Release | Hardening, bans/reports, replay hooks, packaging, docs | Tagged MVP, signed artifacts, no proprietary content |
---
diff --git a/adapters/fbneo/src/lib.rs b/adapters/fbneo/src/lib.rs
index 0b85150..0f1969f 100644
--- a/adapters/fbneo/src/lib.rs
+++ b/adapters/fbneo/src/lib.rs
@@ -1,5 +1,5 @@
use opencade_emulator_sdk::{
- AdapterCapabilities, AdapterError, DetectedEmulator, EmulatorAdapter, NetplayReadiness,
+ AdapterCapabilities, AdapterError, DetectedEmulator, EmulatorAdapter, NetplayMode,
StdProcessLauncher, ValidationReport, spawn_validated,
};
use std::path::{Path, PathBuf};
@@ -45,7 +45,7 @@ impl EmulatorAdapter for FbneoAdapter {
fn capabilities(&self) -> AdapterCapabilities {
AdapterCapabilities {
local_play: true,
- netplay: NetplayReadiness::BlockedNoPublicInterface,
+ netplay: NetplayMode::BlockedNoPublicInterface,
}
}
@@ -158,7 +158,7 @@ mod tests {
);
assert_eq!(
adapter.capabilities().netplay,
- NetplayReadiness::BlockedNoPublicInterface
+ NetplayMode::BlockedNoPublicInterface
);
assert!(!adapter.capabilities().supports_netplay());
std::fs::remove_dir_all(root).expect("remove fixture");
diff --git a/adapters/retroarch/Cargo.toml b/adapters/retroarch/Cargo.toml
new file mode 100644
index 0000000..3814e13
--- /dev/null
+++ b/adapters/retroarch/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "opencade-adapter-retroarch"
+version = "0.1.0"
+edition = "2024"
+description = "OpenCade process-boundary adapter for user-supplied RetroArch and FBNeo core"
+license = "Apache-2.0"
+repository = "https://github.com/opencade/opencade"
+rust-version = "1.98"
+
+[dependencies]
+opencade-emulator-sdk = { path = "../../packages/emulator-sdk" }
+serde = { version = "1.0.229", features = ["derive"] }
+sha2 = "0.11.0"
diff --git a/adapters/retroarch/src/lib.rs b/adapters/retroarch/src/lib.rs
new file mode 100644
index 0000000..63b2804
--- /dev/null
+++ b/adapters/retroarch/src/lib.rs
@@ -0,0 +1,352 @@
+use opencade_emulator_sdk::{
+ AdapterCapabilities, AdapterError, DetectedEmulator, EmulatorAdapter, LaunchSpec,
+ MatchDescriptor, NetplayMode, PeerRole, ProcessLauncher, StdProcessLauncher, ValidationReport,
+ canonicalize_below,
+};
+use serde::Serialize;
+use sha2::{Digest, Sha256};
+use std::ffi::OsString;
+use std::fs::File;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+use std::process::Child;
+
+const MAX_HASHED_FILE_BYTES: u64 = 512 * 1024 * 1024;
+
+#[cfg(target_os = "windows")]
+const EXECUTABLE: &str = "retroarch.exe";
+#[cfg(target_os = "macos")]
+const EXECUTABLE: &str = "retroarch";
+#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
+const EXECUTABLE: &str = "retroarch";
+
+#[cfg(target_os = "windows")]
+const FBNEO_CORE: &str = "fbneo_libretro.dll";
+#[cfg(target_os = "macos")]
+const FBNEO_CORE: &str = "fbneo_libretro.dylib";
+#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
+const FBNEO_CORE: &str = "fbneo_libretro.so";
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct CompatibilityFingerprint {
+ pub retroarch_version: Option,
+ pub executable_sha256: String,
+ pub core_sha256: String,
+ pub content_sha256: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct RetroarchAdapter {
+ install_root: PathBuf,
+}
+
+impl RetroarchAdapter {
+ pub fn new(install_root: impl Into) -> Self {
+ Self {
+ install_root: install_root.into(),
+ }
+ }
+
+ pub fn rom_root(&self) -> PathBuf {
+ self.install_root.join("ROMs")
+ }
+
+ fn executable(&self) -> PathBuf {
+ self.install_root.join(EXECUTABLE)
+ }
+
+ fn core(&self) -> PathBuf {
+ self.install_root.join("cores").join(FBNEO_CORE)
+ }
+
+ fn installed_version(&self) -> Option {
+ std::fs::read_to_string(self.install_root.join("VERSION.txt"))
+ .ok()
+ .map(|value| value.trim().to_owned())
+ .filter(|value| !value.is_empty())
+ }
+
+ pub fn fingerprint(&self, rom_path: &Path) -> Result {
+ let executable = canonicalize_below(&self.executable(), &self.install_root)?;
+ let core = canonicalize_below(&self.core(), &self.install_root)?;
+ let rom = canonicalize_below(rom_path, &self.rom_root())?;
+ Ok(CompatibilityFingerprint {
+ retroarch_version: self.installed_version(),
+ executable_sha256: sha256_file(&executable)?,
+ core_sha256: sha256_file(&core)?,
+ content_sha256: sha256_file(&rom)?,
+ })
+ }
+
+ pub fn match_launch_spec(
+ &self,
+ rom_path: &Path,
+ descriptor: &MatchDescriptor,
+ ) -> Result {
+ self.prepare_match(descriptor)?;
+ let transport = descriptor.transport_lease()?;
+ self.validate(rom_path)?;
+ let executable = canonicalize_below(&self.executable(), &self.install_root)?;
+ let core = canonicalize_below(&self.core(), &self.install_root)?;
+ let rom = canonicalize_below(rom_path, &self.rom_root())?;
+ let current_dir = executable
+ .parent()
+ .ok_or_else(|| AdapterError::Validation("RetroArch has no parent directory".into()))?
+ .to_path_buf();
+
+ let mut args = vec![OsString::from("-L"), core.into_os_string()];
+ match descriptor.role {
+ PeerRole::Host => args.push(OsString::from("--host")),
+ PeerRole::Guest => {
+ args.push(OsString::from("--connect"));
+ args.push(OsString::from(transport.peer_endpoint.ip().to_string()));
+ }
+ }
+ let netplay_port = match descriptor.role {
+ PeerRole::Host => transport.local_endpoint.port(),
+ PeerRole::Guest => transport.peer_endpoint.port(),
+ };
+ args.extend([
+ OsString::from("--port"),
+ OsString::from(netplay_port.to_string()),
+ OsString::from("--frames"),
+ OsString::from(descriptor.input_delay_frames.to_string()),
+ OsString::from("--verbose"),
+ rom.into_os_string(),
+ ]);
+ Ok(LaunchSpec {
+ executable,
+ current_dir,
+ args,
+ })
+ }
+
+ pub fn launch_match_with(
+ &self,
+ launcher: &L,
+ rom_path: &Path,
+ descriptor: &MatchDescriptor,
+ ) -> Result {
+ launcher.spawn(&self.match_launch_spec(rom_path, descriptor)?)
+ }
+}
+
+impl EmulatorAdapter for RetroarchAdapter {
+ fn id(&self) -> &str {
+ "retroarch_fbneo"
+ }
+
+ fn capabilities(&self) -> AdapterCapabilities {
+ AdapterCapabilities {
+ local_play: true,
+ netplay: NetplayMode::NativeProcess,
+ }
+ }
+
+ fn detect(&self, install_dir: &Path) -> Result {
+ let executable = install_dir.join(EXECUTABLE);
+ let core = install_dir.join("cores").join(FBNEO_CORE);
+ if !executable.is_file() || !core.is_file() {
+ return Err(AdapterError::NotDetected(format!(
+ "expected {EXECUTABLE} and cores/{FBNEO_CORE} below the RetroArch root"
+ )));
+ }
+ Ok(DetectedEmulator {
+ id: self.id().into(),
+ executable: executable.canonicalize().map_err(AdapterError::Io)?,
+ install_root: install_dir.canonicalize().map_err(AdapterError::Io)?,
+ version: self.installed_version(),
+ })
+ }
+
+ fn validate(&self, rom_path: &Path) -> Result {
+ self.detect(&self.install_root)?;
+ canonicalize_below(rom_path, &self.rom_root())?;
+ if rom_path.extension().and_then(|value| value.to_str()) != Some("zip") {
+ return Err(AdapterError::Validation(
+ "RetroArch FBNeo content must be a .zip file".into(),
+ ));
+ }
+ let mut report = ValidationReport::valid();
+ if self.installed_version().is_none() {
+ report.warnings.push(
+ "VERSION.txt is missing; exact RetroArch version will not appear in evidence"
+ .into(),
+ );
+ }
+ Ok(report)
+ }
+
+ fn get_version(&self) -> Result {
+ self.installed_version()
+ .ok_or_else(|| AdapterError::NotDetected("VERSION.txt not found".into()))
+ }
+
+ fn launch(&self, rom_path: &Path) -> Result {
+ self.validate(rom_path)?;
+ let executable = canonicalize_below(&self.executable(), &self.install_root)?;
+ let core = canonicalize_below(&self.core(), &self.install_root)?;
+ let rom = canonicalize_below(rom_path, &self.rom_root())?;
+ let current_dir = executable
+ .parent()
+ .ok_or_else(|| AdapterError::Validation("RetroArch has no parent directory".into()))?
+ .to_path_buf();
+ let spec = LaunchSpec {
+ executable,
+ current_dir,
+ args: vec![
+ OsString::from("-L"),
+ core.into_os_string(),
+ OsString::from("--verbose"),
+ rom.into_os_string(),
+ ],
+ };
+ StdProcessLauncher.spawn(&spec)
+ }
+
+ fn launch_match(
+ &self,
+ rom_path: &Path,
+ descriptor: &MatchDescriptor,
+ ) -> Result {
+ self.launch_match_with(&StdProcessLauncher, rom_path, descriptor)
+ }
+
+ fn stop(&self, child: &mut Child) -> Result<(), AdapterError> {
+ child.kill().map_err(AdapterError::Io)?;
+ child.wait().map_err(AdapterError::Io)?;
+ Ok(())
+ }
+}
+
+fn sha256_file(path: &Path) -> Result {
+ let mut file = File::open(path).map_err(AdapterError::Io)?;
+ let length = file.metadata().map_err(AdapterError::Io)?.len();
+ if length > MAX_HASHED_FILE_BYTES {
+ return Err(AdapterError::Validation(
+ "compatibility input exceeds 512 MiB hashing limit".into(),
+ ));
+ }
+ let mut hasher = Sha256::new();
+ let mut buffer = [0_u8; 64 * 1024];
+ loop {
+ let read = file.read(&mut buffer).map_err(AdapterError::Io)?;
+ if read == 0 {
+ break;
+ }
+ hasher.update(&buffer[..read]);
+ }
+ let digest = hasher.finalize();
+ let mut encoded = String::with_capacity(digest.len() * 2);
+ for byte in digest {
+ use std::fmt::Write as _;
+ write!(&mut encoded, "{byte:02x}")
+ .map_err(|_| AdapterError::Validation("failed to encode compatibility hash".into()))?;
+ }
+ Ok(encoded)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use opencade_emulator_sdk::{TransportKind, ValidationReport};
+ use std::sync::atomic::{AtomicU64, Ordering};
+
+ static FIXTURE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
+
+ fn fixture() -> PathBuf {
+ let root = std::env::temp_dir().join(format!(
+ "opencade-retroarch-{}-{}",
+ std::process::id(),
+ FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed)
+ ));
+ std::fs::create_dir_all(root.join("cores")).expect("core directory");
+ std::fs::create_dir_all(root.join("ROMs")).expect("ROM directory");
+ std::fs::write(root.join(EXECUTABLE), b"retroarch").expect("executable");
+ std::fs::write(root.join("cores").join(FBNEO_CORE), b"fbneo-core").expect("core");
+ std::fs::write(root.join("ROMs/sfiii3.zip"), b"content").expect("content");
+ std::fs::write(root.join("VERSION.txt"), "1.22.0\n").expect("version");
+ root
+ }
+
+ fn descriptor(role: PeerRole) -> MatchDescriptor {
+ MatchDescriptor {
+ room_id: "room-1".into(),
+ game_id: "sfiii3".into(),
+ local_user_id: "user-a".into(),
+ peer_user_id: "user-b".into(),
+ role,
+ transport: TransportKind::DirectUdp,
+ local_endpoint: "192.168.1.10:42000".parse().expect("endpoint"),
+ peer_endpoint: "192.168.1.11:42001".parse().expect("endpoint"),
+ input_delay_frames: 2,
+ }
+ }
+
+ #[test]
+ fn detects_native_netplay_capability_and_fingerprints_inputs() {
+ let root = fixture();
+ let adapter = RetroarchAdapter::new(&root);
+ assert_eq!(adapter.capabilities().netplay, NetplayMode::NativeProcess);
+ assert_eq!(
+ adapter
+ .validate(&root.join("ROMs/sfiii3.zip"))
+ .expect("valid"),
+ ValidationReport::valid()
+ );
+ let fingerprint = adapter
+ .fingerprint(&root.join("ROMs/sfiii3.zip"))
+ .expect("fingerprint");
+ assert_eq!(fingerprint.retroarch_version.as_deref(), Some("1.22.0"));
+ assert_eq!(fingerprint.executable_sha256.len(), 64);
+ assert_eq!(fingerprint.core_sha256.len(), 64);
+ assert_eq!(fingerprint.content_sha256.len(), 64);
+ std::fs::remove_dir_all(root).expect("remove fixture");
+ }
+
+ #[test]
+ fn builds_host_and_guest_arguments_without_a_shell() {
+ let root = fixture();
+ let adapter = RetroarchAdapter::new(&root);
+ let host = adapter
+ .match_launch_spec(&root.join("ROMs/sfiii3.zip"), &descriptor(PeerRole::Host))
+ .expect("host spec");
+ assert!(host.args.iter().any(|arg| arg == "--host"));
+ assert!(host.args.iter().any(|arg| arg == "42000"));
+
+ let guest = adapter
+ .match_launch_spec(&root.join("ROMs/sfiii3.zip"), &descriptor(PeerRole::Guest))
+ .expect("guest spec");
+ let args = guest
+ .args
+ .iter()
+ .map(|arg| arg.to_string_lossy())
+ .collect::>();
+ assert!(
+ args.windows(2)
+ .any(|pair| pair == ["--connect", "192.168.1.11"])
+ );
+ assert_eq!(
+ guest.args.last(),
+ Some(
+ &root
+ .join("ROMs/sfiii3.zip")
+ .canonicalize()
+ .expect("rom")
+ .into_os_string()
+ )
+ );
+ std::fs::remove_dir_all(root).expect("remove fixture");
+ }
+
+ #[test]
+ fn rejects_content_outside_the_rom_root() {
+ let root = fixture();
+ let outside = root.parent().expect("parent").join("outside-retroarch.zip");
+ std::fs::write(&outside, b"outside").expect("outside content");
+ let adapter = RetroarchAdapter::new(&root);
+ assert!(adapter.fingerprint(&outside).is_err());
+ std::fs::remove_file(outside).expect("remove outside content");
+ std::fs::remove_dir_all(root).expect("remove fixture");
+ }
+}
diff --git a/apps/client/src-tauri/Cargo.lock b/apps/client/src-tauri/Cargo.lock
index 381b5f5..7ab1985 100644
--- a/apps/client/src-tauri/Cargo.lock
+++ b/apps/client/src-tauri/Cargo.lock
@@ -8,6 +8,17 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures 0.2.17",
+]
+
[[package]]
name = "aho-corasick"
version = "1.1.5"
@@ -47,6 +58,148 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+[[package]]
+name = "apple-native-keyring-store"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b350bfd03649e07aa05c0a81b3e15934374e585c98204a57e20b9d49f49bb9a"
+dependencies = [
+ "keyring-core",
+ "log",
+ "security-framework",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
[[package]]
name = "atk"
version = "0.18.2"
@@ -82,6 +235,29 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+[[package]]
+name = "aws-lc-rs"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
[[package]]
name = "base64"
version = "0.21.7"
@@ -133,6 +309,24 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "block2"
version = "0.6.2"
@@ -142,6 +336,19 @@ dependencies = [
"objc2",
]
+[[package]]
+name = "blocking"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
[[package]]
name = "brotli"
version = "8.0.4"
@@ -266,6 +473,15 @@ dependencies = [
"toml 0.9.12+spec-1.1.0",
]
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
[[package]]
name = "cc"
version = "1.4.4"
@@ -273,6 +489,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
dependencies = [
"find-msvc-tools",
+ "jobserver",
+ "libc",
"shlex",
]
@@ -309,6 +527,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core",
+]
+
[[package]]
name = "chrono"
version = "0.4.45"
@@ -323,6 +558,31 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common 0.1.7",
+ "inout",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cmov"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
+
[[package]]
name = "combine"
version = "4.6.7"
@@ -333,6 +593,21 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
[[package]]
name = "cookie"
version = "0.18.2"
@@ -392,6 +667,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "crc32fast"
version = "1.5.1"
@@ -426,6 +710,15 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
[[package]]
name = "cssparser"
version = "0.36.0"
@@ -465,6 +758,15 @@ version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
+[[package]]
+name = "ctutils"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+dependencies = [
+ "cmov",
+]
+
[[package]]
name = "darling"
version = "0.23.0"
@@ -499,6 +801,12 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "data-encoding"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+
[[package]]
name = "dbus"
version = "0.9.12"
@@ -577,8 +885,21 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
- "block-buffer",
- "crypto-common",
+ "block-buffer 0.10.4",
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.1",
+ "const-oid",
+ "crypto-common 0.2.2",
+ "ctutils",
]
[[package]]
@@ -734,6 +1055,33 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -761,6 +1109,26 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
[[package]]
name = "fastrand"
version = "2.5.0"
@@ -850,6 +1218,12 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "futures-channel"
version = "0.3.34"
@@ -857,6 +1231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
+ "futures-sink",
]
[[package]]
@@ -882,6 +1257,19 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
[[package]]
name = "futures-macro"
version = "0.3.34"
@@ -1037,8 +1425,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"wasi",
+ "wasm-bindgen",
]
[[package]]
@@ -1060,8 +1450,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi 6.0.0",
+ "rand_core",
+ "wasm-bindgen",
]
[[package]]
@@ -1236,6 +1629,12 @@ 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"
@@ -1243,22 +1642,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
-name = "html5ever"
-version = "0.38.0"
+name = "hkdf"
+version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
- "log",
- "markup5ever",
+ "hmac 0.12.1",
]
[[package]]
-name = "http"
-version = "1.5.0"
+name = "hmac"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
- "bytes",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "hmac"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+dependencies = [
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+dependencies = [
+ "log",
+ "markup5ever",
+]
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
"itoa",
]
@@ -1291,6 +1717,15 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+[[package]]
+name = "hybrid-array"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
+dependencies = [
+ "typenum",
+]
+
[[package]]
name = "hyper"
version = "1.11.0"
@@ -1311,6 +1746,21 @@ dependencies = [
"want",
]
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -1510,6 +1960,16 @@ dependencies = [
"cfb",
]
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
[[package]]
name = "ipnet"
version = "2.12.1"
@@ -1614,6 +2074,36 @@ dependencies = [
"windows-sys 0.45.0",
]
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.20",
+ "walkdir",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -1642,6 +2132,16 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
[[package]]
name = "js-sys"
version = "0.3.104"
@@ -1686,6 +2186,27 @@ dependencies = [
"unicode-segmentation",
]
+[[package]]
+name = "keyring"
+version = "4.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72585bb6cc9bc370d1d545b7e23fcce71dfd4461c5e15275e3cf51bdfd9a980a"
+dependencies = [
+ "apple-native-keyring-store",
+ "keyring-core",
+ "windows-native-keyring-store",
+ "zbus-secret-service-keyring-store",
+]
+
+[[package]]
+name = "keyring-core"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d"
+dependencies = [
+ "log",
+]
+
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1750,6 +2271,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
[[package]]
name = "litemap"
version = "0.8.3"
@@ -1771,6 +2298,12 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
[[package]]
name = "markup5ever"
version = "0.38.0"
@@ -1893,12 +2426,75 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+[[package]]
+name = "num-integer"
+version = "0.1.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -2140,14 +2736,27 @@ dependencies = [
"thiserror 2.0.20",
]
+[[package]]
+name = "opencade-adapter-retroarch"
+version = "0.1.0"
+dependencies = [
+ "opencade-emulator-sdk",
+ "serde",
+ "sha2 0.11.0",
+]
+
[[package]]
name = "opencade-client"
version = "0.1.0"
dependencies = [
+ "keyring",
"opencade-adapter-fbneo",
+ "opencade-adapter-retroarch",
"opencade-emulator-sdk",
"opencade-networking",
"opencade-protocol",
+ "opencade-shared",
+ "reqwest",
"serde",
"serde_json",
"tauri",
@@ -2171,12 +2780,16 @@ name = "opencade-networking"
version = "0.1.0"
dependencies = [
"chrono",
+ "futures-util",
"opencade-emulator-sdk",
"opencade-protocol",
+ "opencade-shared",
"serde",
"serde_json",
"thiserror 2.0.20",
"tokio",
+ "tokio-tungstenite",
+ "url",
"uuid",
]
@@ -2191,12 +2804,39 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "opencade-shared"
+version = "0.1.0"
+dependencies = [
+ "hex",
+ "hmac 0.13.0",
+ "serde",
+ "sha2 0.11.0",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
[[package]]
name = "pango"
version = "0.18.3"
@@ -2222,6 +2862,12 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -2310,6 +2956,17 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.34"
@@ -2355,6 +3012,20 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "portable-atomic"
version = "1.15.0"
@@ -2462,6 +3133,63 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.20",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.20",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "quote"
version = "1.0.47"
@@ -2483,6 +3211,32 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core",
+]
+
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -2545,66 +3299,175 @@ dependencies = [
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "aws-lc-rs",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
]
[[package]]
-name = "regex-syntax"
-version = "0.8.11"
+name = "rustls-pki-types"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
[[package]]
-name = "reqwest"
-version = "0.13.4"
+name = "rustls-platform-verifier"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-core",
- "futures-util",
- "http",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-util",
- "js-sys",
+ "core-foundation",
+ "core-foundation-sys",
+ "jni 0.22.4",
"log",
- "percent-encoding",
- "pin-project-lite",
- "serde",
- "serde_json",
- "sync_wrapper",
- "tokio",
- "tokio-util",
- "tower",
- "tower-http",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "wasm-streams",
- "web-sys",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
]
[[package]]
-name = "rustc-hash"
-version = "2.1.3"
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
-name = "rustc_version"
-version = "0.4.1"
+name = "rustls-webpki"
+version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
- "semver",
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
]
[[package]]
@@ -2622,6 +3485,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "schemars"
version = "0.8.22"
@@ -2679,6 +3551,48 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "secret-service"
+version = "5.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14"
+dependencies = [
+ "aes",
+ "cbc",
+ "futures-util",
+ "generic-array",
+ "getrandom 0.2.17",
+ "hkdf",
+ "num",
+ "once_cell",
+ "serde",
+ "sha2 0.10.9",
+ "zbus",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "selectors"
version = "0.36.1"
@@ -2867,6 +3781,17 @@ dependencies = [
"stable_deref_trait",
]
+[[package]]
+name = "sha1"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
+]
+
[[package]]
name = "sha2"
version = "0.10.9"
@@ -2874,8 +3799,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
- "cpufeatures",
- "digest",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
]
[[package]]
@@ -2909,6 +3845,22 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -3021,6 +3973,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "swift-rs"
version = "1.0.8"
@@ -3115,7 +4073,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
- "jni",
+ "jni 0.21.1",
"libc",
"log",
"ndk",
@@ -3171,7 +4129,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
- "jni",
+ "jni 0.21.1",
"libc",
"log",
"mime",
@@ -3243,7 +4201,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
- "sha2",
+ "sha2 0.10.9",
"syn 2.0.119",
"tauri-utils",
"thiserror 2.0.20",
@@ -3277,7 +4235,7 @@ dependencies = [
"dpi",
"gtk",
"http",
- "jni",
+ "jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@@ -3300,7 +4258,7 @@ checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
dependencies = [
"gtk",
"http",
- "jni",
+ "jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@@ -3367,6 +4325,19 @@ dependencies = [
"toml 1.1.4+spec-1.1.0",
]
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "tendril"
version = "0.5.1"
@@ -3517,6 +4488,32 @@ dependencies = [
"syn 3.0.4",
]
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tungstenite",
+]
+
[[package]]
name = "tokio-util"
version = "0.7.19"
@@ -3808,6 +4805,24 @@ dependencies = [
"termcolor",
]
+[[package]]
+name = "tungstenite"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand",
+ "rustls",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror 2.0.20",
+]
+
[[package]]
name = "typeid"
version = "1.0.3"
@@ -3820,6 +4835,17 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+[[package]]
+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 = "unic-char-property"
version = "0.9.0"
@@ -3873,6 +4899,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.8"
@@ -4066,6 +5098,16 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "web_atoms"
version = "0.2.6"
@@ -4122,6 +5164,15 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "webview2-com"
version = "0.38.2"
@@ -4297,6 +5348,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-native-keyring-store"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8"
+dependencies = [
+ "byteorder",
+ "keyring-core",
+ "regex",
+ "windows-sys 0.61.2",
+ "zeroize",
+]
+
[[package]]
name = "windows-numerics"
version = "0.2.0"
@@ -4352,6 +5416,15 @@ dependencies = [
"windows-targets 0.42.2",
]
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -4573,7 +5646,7 @@ dependencies = [
"gtk",
"http",
"javascriptcore-rs",
- "jni",
+ "jni 0.21.1",
"libc",
"ndk",
"objc2",
@@ -4585,7 +5658,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"raw-window-handle",
- "sha2",
+ "sha2 0.10.9",
"soup3",
"tao-macros",
"thiserror 2.0.20",
@@ -4643,6 +5716,87 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zbus"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 1.0.4",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus-secret-service-keyring-store"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74801d001b9e7729adb4f1825b67b398185fed424749aa3d8bacf70417137d9a"
+dependencies = [
+ "keyring-core",
+ "secret-service",
+ "zbus",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zvariant",
+]
+
+[[package]]
+name = "zcheapstr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "zerofrom"
version = "0.1.8"
@@ -4664,6 +5818,12 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
[[package]]
name = "zerotrie"
version = "0.2.5"
@@ -4702,3 +5862,44 @@ name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+
+[[package]]
+name = "zvariant"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 1.0.4",
+ "zcheapstr",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 3.0.4",
+ "winnow 1.0.4",
+]
diff --git a/apps/client/src-tauri/Cargo.toml b/apps/client/src-tauri/Cargo.toml
index 2221107..11dcf4b 100644
--- a/apps/client/src-tauri/Cargo.toml
+++ b/apps/client/src-tauri/Cargo.toml
@@ -18,10 +18,14 @@ tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
+reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "json", "rustls"] }
+keyring = "4.1.6"
opencade-adapter-fbneo = { path = "../../../adapters/fbneo" }
+opencade-adapter-retroarch = { path = "../../../adapters/retroarch" }
opencade-emulator-sdk = { path = "../../../packages/emulator-sdk" }
opencade-networking = { path = "../../../packages/networking" }
opencade-protocol = { path = "../../../packages/protocol" }
+opencade-shared = { path = "../../../packages/shared" }
uuid = { version = "1.25.0", features = ["v4"] }
[features]
diff --git a/apps/client/src-tauri/src/commands/match_probe.rs b/apps/client/src-tauri/src/commands/match_probe.rs
index d0e9ad4..a65128d 100644
--- a/apps/client/src-tauri/src/commands/match_probe.rs
+++ b/apps/client/src-tauri/src/commands/match_probe.rs
@@ -1,9 +1,10 @@
use opencade_emulator_sdk::{MatchDescriptor, PeerRole, TransportKind};
use opencade_networking::{
- HolePunchConfig, MatchProbeConfig, MatchProbeReport, UdpPeer, discover_reflexive_address,
- punch_hole, run_match_probe,
+ HolePunchConfig, MatchProbeConfig, MatchProbeReport, RelayPeer, UdpPeer,
+ discover_reflexive_address, punch_hole, run_match_probe, run_relay_match_probe,
};
use opencade_protocol::{MatchCandidateKind, NatMappingState};
+use opencade_shared::RelayTicket;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::future::Future;
@@ -64,6 +65,23 @@ pub struct RunMatchProbeRequest {
pub timeout_ms: u64,
}
+#[derive(Debug, Deserialize)]
+pub struct RunRelayMatchProbeRequest {
+ pub relay_url: String,
+ pub ticket: RelayTicket,
+ pub room_id: String,
+ pub game_id: String,
+ pub local_user_id: String,
+ pub peer_user_id: String,
+ pub role: PeerRole,
+ pub local_nonce: String,
+ pub peer_nonce: String,
+ #[serde(default = "default_frame_count")]
+ pub frame_count: u64,
+ #[serde(default = "default_timeout_ms")]
+ pub timeout_ms: u64,
+}
+
fn default_frame_count() -> u64 {
60
}
@@ -231,6 +249,49 @@ pub async fn run_reserved_match_probe(
Ok(report)
}
+#[tauri::command]
+pub async fn run_relay_match_probe_command(
+ request: RunRelayMatchProbeRequest,
+) -> Result {
+ validate_room_id(&request.room_id)?;
+ if request.ticket.room_id != request.room_id || request.ticket.user_id != request.local_user_id
+ {
+ return Err("relay ticket does not match the local room participant".into());
+ }
+ let session_key = combined_session_key(&request.local_nonce, &request.peer_nonce)?;
+ let descriptor = MatchDescriptor {
+ room_id: request.room_id,
+ game_id: request.game_id,
+ local_user_id: request.local_user_id,
+ peer_user_id: request.peer_user_id,
+ role: request.role,
+ transport: TransportKind::Relay,
+ local_endpoint: "0.0.0.0:0".parse().map_err(|_| "invalid relay endpoint")?,
+ peer_endpoint: "0.0.0.0:0".parse().map_err(|_| "invalid relay endpoint")?,
+ input_delay_frames: 2,
+ };
+ let config = MatchProbeConfig::new(
+ descriptor,
+ session_key,
+ request.frame_count,
+ Duration::from_millis(request.timeout_ms),
+ )
+ .map_err(|error| error.to_string())?;
+ let peer = RelayPeer::connect(&request.relay_url, &request.ticket)
+ .await
+ .map_err(|error| error.to_string())?;
+ let report = run_relay_match_probe(&peer, &config)
+ .await
+ .map_err(|error| error.to_string())?;
+ tracing::info!(
+ room_id = %report.room_id,
+ frames = report.frames_received,
+ elapsed_ms = report.elapsed_ms,
+ "relay match probe completed"
+ );
+ Ok(report)
+}
+
#[tauri::command]
pub async fn cancel_match_probe(
state: tauri::State<'_, MatchProbeState>,
diff --git a/apps/client/src-tauri/src/commands/mod.rs b/apps/client/src-tauri/src/commands/mod.rs
index 565bb65..57cd914 100644
--- a/apps/client/src-tauri/src/commands/mod.rs
+++ b/apps/client/src-tauri/src/commands/mod.rs
@@ -2,3 +2,5 @@ pub mod diag;
pub mod fs;
pub mod match_probe;
pub mod process;
+pub mod runtime;
+pub mod session;
diff --git a/apps/client/src-tauri/src/commands/process.rs b/apps/client/src-tauri/src/commands/process.rs
index 871c727..c86c45c 100644
--- a/apps/client/src-tauri/src/commands/process.rs
+++ b/apps/client/src-tauri/src/commands/process.rs
@@ -1,14 +1,21 @@
use opencade_adapter_fbneo::FbneoAdapter;
-use opencade_emulator_sdk::EmulatorAdapter;
+use opencade_adapter_retroarch::{CompatibilityFingerprint, RetroarchAdapter};
+use opencade_emulator_sdk::{EmulatorAdapter, MatchDescriptor, PeerRole, TransportKind};
+use serde::{Deserialize, Serialize};
use std::collections::HashMap;
+use std::io::Read;
+use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::Child;
-use std::sync::Mutex;
-use tauri::Manager;
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+use tauri::{Emitter, Manager};
+
+const RETROARCH_ALPHA_PORT: u16 = 55_435;
#[derive(Default)]
pub struct ProcessState {
- children: Mutex>,
+ children: Mutex>>>,
}
pub fn fbneo_root(app: &tauri::AppHandle) -> Result {
@@ -18,12 +25,21 @@ pub fn fbneo_root(app: &tauri::AppHandle) -> Result {
.map_err(|_| "application resource directory is unavailable".into())
}
-#[tauri::command]
-pub fn launch_game(
- app: tauri::AppHandle,
- state: tauri::State<'_, ProcessState>,
- game_id: String,
-) -> Result {
+fn retroarch_root(app: &tauri::AppHandle) -> Result {
+ if let Some(root) = std::env::var_os("OPENCADE_RETROARCH_ROOT") {
+ let root = PathBuf::from(root);
+ if !root.is_absolute() {
+ return Err("OPENCADE_RETROARCH_ROOT must be an absolute path".into());
+ }
+ return Ok(root);
+ }
+ app.path()
+ .resource_dir()
+ .map(|root| root.join("emulator").join("retroarch"))
+ .map_err(|_| "application resource directory is unavailable".into())
+}
+
+fn validate_game_id(game_id: &str) -> Result<(), String> {
if game_id.len() < 3
|| game_id.len() > 20
|| !game_id
@@ -32,29 +48,310 @@ pub fn launch_game(
{
return Err("invalid game id".into());
}
+ Ok(())
+}
+
+#[derive(Debug, Deserialize)]
+pub struct RetroarchMatchRequest {
+ api_url: String,
+ session_token: String,
+ launch_grant: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct AuthorizedLaunch {
+ room_id: String,
+ game_id: String,
+ local_user_id: String,
+ peer_user_id: String,
+ role: PeerRole,
+ local_endpoint: SocketAddr,
+ peer_endpoint: SocketAddr,
+ input_delay_frames: u8,
+}
+
+#[derive(Debug, Deserialize)]
+struct ApiEnvelope {
+ payload: T,
+}
+
+#[derive(Debug, Serialize)]
+struct ConsumeLaunchGrant<'a> {
+ grant: &'a str,
+}
+
+#[derive(Debug, Serialize)]
+pub struct RetroarchMatchLaunch {
+ pid: u32,
+ adapter: &'static str,
+ room_id: String,
+ fingerprint: CompatibilityFingerprint,
+}
+
+#[derive(Debug, Clone, Serialize)]
+struct EmulatorExitEvent {
+ pid: u32,
+ room_id: Option,
+ exit_code: Option,
+ success: bool,
+}
+
+struct NativeExitCallback {
+ api_url: String,
+ session_token: String,
+ room_id: String,
+}
+
+#[derive(Serialize)]
+struct FinishRoomBody {
+ exit_code: Option,
+}
+
+#[tauri::command]
+pub fn launch_game(
+ app: tauri::AppHandle,
+ state: tauri::State<'_, ProcessState>,
+ game_id: String,
+) -> Result {
+ validate_game_id(&game_id)?;
let root = fbneo_root(&app)?;
let adapter = FbneoAdapter::new(&root);
let child = adapter
.launch(&root.join("ROMs").join(format!("{game_id}.zip")))
.map_err(|error| error.to_string())?;
- let pid = child.id();
- state
- .children
- .lock()
- .map_err(|_| "process registry unavailable".to_string())?
- .insert(pid, child);
- Ok(pid)
+ register_child(&app, &state, child, None)
+}
+
+#[tauri::command]
+pub async fn launch_retroarch_match(
+ app: tauri::AppHandle,
+ state: tauri::State<'_, ProcessState>,
+ request: RetroarchMatchRequest,
+) -> Result {
+ let authorized = consume_launch_grant(&request).await?;
+ validate_game_id(&authorized.game_id)?;
+ let root = retroarch_root(&app)?;
+ let adapter = RetroarchAdapter::new(&root);
+ let rom = root
+ .join("ROMs")
+ .join(format!("{}.zip", authorized.game_id));
+ let descriptor = MatchDescriptor {
+ room_id: authorized.room_id.clone(),
+ game_id: authorized.game_id,
+ local_user_id: authorized.local_user_id,
+ peer_user_id: authorized.peer_user_id,
+ role: authorized.role,
+ transport: TransportKind::DirectUdp,
+ local_endpoint: authorized.local_endpoint,
+ peer_endpoint: authorized.peer_endpoint,
+ input_delay_frames: authorized.input_delay_frames,
+ };
+ descriptor
+ .transport_lease()
+ .map_err(|error| error.to_string())?;
+ validate_native_endpoints(&descriptor)?;
+ if descriptor.role == PeerRole::Host {
+ ensure_native_port_available(descriptor.local_endpoint)?;
+ }
+ let (fingerprint, child) = tokio::task::spawn_blocking(move || {
+ let fingerprint = adapter.fingerprint(&rom)?;
+ let child = adapter.launch_match(&rom, &descriptor)?;
+ Ok::<_, opencade_emulator_sdk::AdapterError>((fingerprint, child))
+ })
+ .await
+ .map_err(|_| "native launch worker failed".to_string())?
+ .map_err(|error| error.to_string())?;
+ let room_id = authorized.room_id;
+ let callback = NativeExitCallback {
+ api_url: request.api_url,
+ session_token: request.session_token,
+ room_id: room_id.clone(),
+ };
+ let pid = register_child(&app, &state, child, Some(callback))?;
+ Ok(RetroarchMatchLaunch {
+ pid,
+ adapter: "retroarch_fbneo",
+ room_id,
+ fingerprint,
+ })
}
#[tauri::command]
pub fn stop_game(state: tauri::State<'_, ProcessState>, pid: u32) -> Result<(), String> {
- let mut child = state
+ let child = state
.children
.lock()
.map_err(|_| "process registry unavailable".to_string())?
- .remove(&pid)
+ .get(&pid)
+ .cloned()
.ok_or_else(|| "emulator process not found".to_string())?;
- child.kill().map_err(|error| error.to_string())?;
- child.wait().map_err(|error| error.to_string())?;
+ child
+ .lock()
+ .map_err(|_| "emulator process unavailable".to_string())?
+ .kill()
+ .map_err(|error| error.to_string())?;
+ Ok(())
+}
+
+async fn consume_launch_grant(request: &RetroarchMatchRequest) -> Result {
+ let base = reqwest::Url::parse(&request.api_url).map_err(|_| "OpenCade API URL is invalid")?;
+ if !matches!(base.scheme(), "http" | "https")
+ || !base.username().is_empty()
+ || base.password().is_some()
+ || base.query().is_some()
+ || base.fragment().is_some()
+ {
+ return Err("OpenCade API URL is not allowed".into());
+ }
+ if base.scheme() == "http"
+ && !matches!(base.host_str(), Some("localhost" | "127.0.0.1" | "::1"))
+ {
+ return Err("remote OpenCade API URLs must use HTTPS".into());
+ }
+ if request.session_token.is_empty() || request.launch_grant.is_empty() {
+ return Err("authenticated launch grant is required".into());
+ }
+ let endpoint = format!(
+ "{}/api/v1/match-launch-grants/consume",
+ request.api_url.trim_end_matches('/')
+ );
+ let client = reqwest::Client::builder()
+ .timeout(Duration::from_secs(10))
+ .build()
+ .map_err(|_| "launch authorization client unavailable")?;
+ let response = client
+ .post(endpoint)
+ .bearer_auth(&request.session_token)
+ .json(&ConsumeLaunchGrant {
+ grant: &request.launch_grant,
+ })
+ .send()
+ .await
+ .map_err(|_| "launch authorization server is unreachable")?;
+ if !response.status().is_success() {
+ return Err("launch grant was rejected by the server".into());
+ }
+ response
+ .json::>()
+ .await
+ .map(|envelope| envelope.payload)
+ .map_err(|_| "launch authorization response is invalid".into())
+}
+
+fn ensure_native_port_available(endpoint: SocketAddr) -> Result<(), String> {
+ let bind_ip = if endpoint.is_ipv4() {
+ std::net::Ipv4Addr::UNSPECIFIED.into()
+ } else {
+ std::net::Ipv6Addr::UNSPECIFIED.into()
+ };
+ std::net::TcpListener::bind(SocketAddr::new(bind_ip, endpoint.port()))
+ .map(drop)
+ .map_err(|_| format!("native netplay TCP port {} is unavailable", endpoint.port()))
+}
+
+fn validate_native_endpoints(descriptor: &MatchDescriptor) -> Result<(), String> {
+ for endpoint in [descriptor.local_endpoint, descriptor.peer_endpoint] {
+ if endpoint.port() != RETROARCH_ALPHA_PORT
+ || endpoint.ip().is_unspecified()
+ || endpoint.ip().is_multicast()
+ {
+ return Err("native alpha endpoints must be unicast on TCP port 55435".into());
+ }
+ }
Ok(())
}
+
+fn register_child(
+ app: &tauri::AppHandle,
+ state: &ProcessState,
+ mut child: Child,
+ exit_callback: Option,
+) -> Result {
+ if let Some(stdout) = child.stdout.take() {
+ drain_process_output(stdout);
+ }
+ if let Some(stderr) = child.stderr.take() {
+ drain_process_output(stderr);
+ }
+ let pid = child.id();
+ let child = Arc::new(Mutex::new(child));
+ state
+ .children
+ .lock()
+ .map_err(|_| "process registry unavailable".to_string())?
+ .insert(pid, Arc::clone(&child));
+
+ let app = app.clone();
+ std::thread::spawn(move || {
+ let status = loop {
+ let result = child
+ .lock()
+ .map_err(|_| ())
+ .and_then(|mut child| child.try_wait().map_err(|_| ()));
+ match result {
+ Ok(Some(status)) => break Some(status),
+ Ok(None) => std::thread::sleep(Duration::from_millis(200)),
+ Err(()) => break None,
+ }
+ };
+ if let Ok(mut children) = app.state::().children.lock()
+ && children
+ .get(&pid)
+ .is_some_and(|registered| Arc::ptr_eq(registered, &child))
+ {
+ children.remove(&pid);
+ }
+ let exit_code = status.as_ref().and_then(std::process::ExitStatus::code);
+ let room_id = exit_callback
+ .as_ref()
+ .map(|callback| callback.room_id.clone());
+ if let Some(callback) = exit_callback {
+ report_native_exit(&callback, exit_code);
+ }
+ let event = EmulatorExitEvent {
+ pid,
+ room_id,
+ exit_code,
+ success: status.is_some_and(|status| status.success()),
+ };
+ if let Err(error) = app.emit("opencade://emulator-exited", event) {
+ tracing::warn!(%error, pid, "failed to emit emulator exit event");
+ }
+ });
+ Ok(pid)
+}
+
+fn report_native_exit(callback: &NativeExitCallback, exit_code: Option) {
+ let endpoint = format!(
+ "{}/api/v1/rooms/{}/finish",
+ callback.api_url.trim_end_matches('/'),
+ callback.room_id
+ );
+ let Ok(client) = reqwest::blocking::Client::builder()
+ .timeout(Duration::from_secs(10))
+ .build()
+ else {
+ tracing::warn!(room_id = %callback.room_id, "native exit reporter unavailable");
+ return;
+ };
+ for attempt in 1..=3 {
+ let result = client
+ .post(&endpoint)
+ .bearer_auth(&callback.session_token)
+ .json(&FinishRoomBody { exit_code })
+ .send();
+ if result.is_ok_and(|response| response.status().is_success()) {
+ return;
+ }
+ if attempt < 3 {
+ std::thread::sleep(Duration::from_millis(200 * attempt));
+ }
+ }
+ tracing::warn!(room_id = %callback.room_id, "failed to report native process exit");
+}
+
+fn drain_process_output(mut reader: R) {
+ std::thread::spawn(move || {
+ let _ = std::io::copy(&mut reader, &mut std::io::sink());
+ });
+}
diff --git a/apps/client/src-tauri/src/commands/runtime.rs b/apps/client/src-tauri/src/commands/runtime.rs
new file mode 100644
index 0000000..2be02f6
--- /dev/null
+++ b/apps/client/src-tauri/src/commands/runtime.rs
@@ -0,0 +1,60 @@
+use serde::Serialize;
+
+#[derive(Debug, Serialize)]
+pub struct RuntimeConfig {
+ api_url: String,
+ stun_server: Option,
+}
+
+#[tauri::command]
+pub fn runtime_config() -> Result {
+ let api_url =
+ std::env::var("OPENCADE_API_URL").unwrap_or_else(|_| "http://localhost:8080".to_string());
+ validate_api_url(&api_url)?;
+ let stun_server = std::env::var("OPENCADE_STUN_SERVER")
+ .ok()
+ .map(|value| value.trim().to_owned())
+ .filter(|value| !value.is_empty());
+ if let Some(server) = &stun_server {
+ server
+ .parse::()
+ .map_err(|_| "OPENCADE_STUN_SERVER must be a numeric IP:port".to_string())?;
+ }
+ Ok(RuntimeConfig {
+ api_url: api_url.trim_end_matches('/').to_owned(),
+ stun_server,
+ })
+}
+
+fn validate_api_url(value: &str) -> Result<(), String> {
+ let parsed = reqwest::Url::parse(value)
+ .map_err(|_| "OPENCADE_API_URL must be a valid http(s) URL".to_string())?;
+ if !matches!(parsed.scheme(), "http" | "https")
+ || !parsed.username().is_empty()
+ || parsed.password().is_some()
+ || parsed.query().is_some()
+ || parsed.fragment().is_some()
+ {
+ return Err("OPENCADE_API_URL must be a credential-free http(s) origin".into());
+ }
+ if parsed.scheme() == "http"
+ && !matches!(parsed.host_str(), Some("localhost" | "127.0.0.1" | "::1"))
+ {
+ return Err("remote OPENCADE_API_URL values must use HTTPS".into());
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn accepts_http_origins_and_rejects_other_schemes() {
+ assert!(validate_api_url("https://alpha.example").is_ok());
+ assert!(validate_api_url("http://127.0.0.1:8080").is_ok());
+ assert!(validate_api_url("http://192.168.1.10:8080").is_err());
+ assert!(validate_api_url("file:///tmp/server").is_err());
+ assert!(validate_api_url("https://bad host").is_err());
+ }
+}
diff --git a/apps/client/src-tauri/src/commands/session.rs b/apps/client/src-tauri/src/commands/session.rs
new file mode 100644
index 0000000..9298c2f
--- /dev/null
+++ b/apps/client/src-tauri/src/commands/session.rs
@@ -0,0 +1,56 @@
+const SERVICE: &str = "com.opencade.client";
+const ACCOUNT: &str = "session-token";
+
+fn entry() -> Result {
+ keyring::Entry::new(SERVICE, ACCOUNT)
+ .map_err(|_| "operating system credential storage is unavailable".to_string())
+}
+
+fn validate_token(token: &str) -> Result<(), String> {
+ if token.len() < 32 || token.len() > 512 || token.chars().any(char::is_whitespace) {
+ return Err("session token is invalid".into());
+ }
+ Ok(())
+}
+
+#[tauri::command]
+pub fn store_session_token(token: String) -> Result<(), String> {
+ validate_token(&token)?;
+ entry()?
+ .set_password(&token)
+ .map_err(|_| "could not save the session in operating system credential storage".into())
+}
+
+#[tauri::command]
+pub fn load_session_token() -> Result
)}
+ {playableMatch.isError && (
+
+ {playableMatch.error.message}
+
+ )}
+ {playableMatch.data && (
+
+ RetroArch netplay launched · PID {playableMatch.data.pid} · content{" "}
+ {playableMatch.data.fingerprint.content_sha256.slice(0, 12)}
+
+ )}
- {probeError && (
-