Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .clippy.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Clippy configuration for OpenCade
# See https://doc.rust-lang.org/clippy/configuration.html
# This file is read by `cargo clippy` at workspace root.
# Rust 1.78 reads this dotfile directly; keep a single config to avoid ambiguity.
# Rust 1.98 reads this dotfile directly; keep a single config to avoid ambiguity.

msrv = "1.78"
msrv = "1.98"
cognitive-complexity-threshold = 30
too-many-arguments-threshold = 7
type-complexity-threshold = 250
Expand Down
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -13,3 +15,6 @@ OPENCADE_ENV=production
# POSTGRES_USER=opencade
# POSTGRES_PASSWORD=opencade
# 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
# RELAY_URL=wss://relay.example.com/relay
37 changes: 29 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.78"
toolchain: "1.98"
components: rustfmt, clippy

- name: Cache cargo registry + target
Expand All @@ -60,7 +60,7 @@ jobs:
- name: Setup Node.js for protocol drift check
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: pnpm

- name: Install protocol formatter
Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: pnpm

- name: Install dependencies
Expand Down Expand Up @@ -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)
Expand All @@ -136,7 +137,7 @@ jobs:
- name: Install Rust MSRV
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.78"
toolchain: "1.98"
components: rustfmt, clippy

- name: Cache Cargo
Expand All @@ -149,14 +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
name: opencade-windows-alpha
path: alpha-kit
if-no-files-found: error
retention-days: 14

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ out/

# ── Tauri ────────────────────────────────────────────────────
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
Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ OpenCade — open-source arcade netplay platform, clean-room alternative to prop
## Architecture & Data Flow

- **System:** `apps/client` (Tauri) ↔ `apps/server` (Axum) over HTTPS/WS ` /ws` → P2P/relay between peers. Server never sees game inputs except when relaying as fallback.
- **Client:** Tauri 1.x hosts React routes `Games | Lobbies | Friends | Servers | Settings`. Rust core owns `fs` (ROM scan under `emulator/<core>/ROMs/`), `process` (spawn via `tauri::api::process::Command` with arg escaping, no shell), `diag` (Network Test). State: TanStack Query + Zustand, WS client with typed `packages/protocol` discriminated unions and reconnect backoff.
- **Client:** Tauri 2.x hosts React routes `Games | Lobbies | Friends | Servers | Settings`. Rust core owns `fs` (ROM scan under `emulator/<core>/ROMs/`), `process` (safe direct process spawning with arg escaping, no shell), `diag` (Network Test). State: TanStack Query + Zustand, WS client with typed `packages/protocol` discriminated unions and reconnect backoff.
- **Server:** Single Axum monolith + `postgres:16-alpine` (no Redis in MVP). Routes `POST /api/v1/auth/*`, `GET /api/v1/games`, `GET /api/v1/lobbies/:game`, `POST /api/v1/rooms` + `.../:id/{accept,decline,cancel}`, WS `/ws` with versioned envelope `{type,version,request_id,timestamp,payload}` (`presence.update`, `chat.message`, `challenge.*`, `session.offer/answer/candidate`, `room.*`). In-memory presence Hub, Postgres for durable state.
- **Networking:** Signaling relayed verbatim through server; client-driven UDP hole-punch → STUN hint (`GET /servers` returns `stun:host:port` when configured) → WS relay fallback (in-process `relay` module, future `services/relay` binary). Room states `WAITING → CHALLENGING → CONNECTING → PLAYING → FINISHED|CANCELLED`. Latency `rtt_ms/loss/jitter` via `presence.update`.
- **Reference only:** `docs/reference-fightcade-install.md` describes the opaque PyInstaller launcher (`emulator/fcade.exe`/`frm.exe` → `fightcade/launcher.py`) and `fbneo-training-mode` Lua surface — not used at runtime.
Expand All @@ -31,7 +31,7 @@ OpenCade — open-source arcade netplay platform, clean-room alternative to prop
## Development Commands

```bash
# prerequisites: Rust 1.78+, pnpm 9+, Docker, Postgres 16
# prerequisites: Rust 1.98+, Node 24+, pnpm 11+, Docker, Postgres 16
pnpm install # workspace install (apps/*, packages/*)
pnpm -C apps/client tauri dev # Vite + Tauri dev (Windows)
pnpm -C apps/client build # TS check + Vite build
Expand Down Expand Up @@ -80,9 +80,9 @@ pnpm tauri dev -- --log opencade # client logs to %APPDATA%/OpenCade/logs/openc

## Runtime/Tooling Preferences

- **Runtime:** Rust 1.78+ (MSRV), Tauri 1.x (WebView2 on Windows), Node 20+ with **pnpm 9** (not npm/yarn), Postgres 16-alpine (sqlx compile-time checked). No Bun. No Electron.
- **Runtime:** Rust 1.98+ (MSRV), Tauri 2.x (WebView2 on Windows), Node 24+ with **pnpm 11** (not npm/yarn), Postgres 16-alpine (sqlx compile-time checked). No Bun. No Electron.
- **Package manager:** `pnpm` at root (`pnpm-workspace.yaml` covers `apps/*`, `packages/*`, `adapters/*`, `services/*`) — `pnpm install` only, commit `pnpm-lock.yaml`.
- **Build:** `cargo build --workspace`, `pnpm -C apps/client build`, `tauri build` (MSI/NSIS on Windows). Docker multi-stage `rust:1.78 → debian:bookworm-slim`.
- **Build:** `cargo build --workspace`, `pnpm -C apps/client build`, `tauri build` (MSI/NSIS on Windows). Docker multi-stage `rust:1.98-bookworm → debian:bookworm-slim`.
- **Env:** `DATABASE_URL=postgres://opencade:opencade@db:5432/opencade`, `SESSION_SECRET` (32B CSPRNG), `RUST_LOG=info,opencade_server=debug`. Never commit `.env` (see `.env.example`).
- **OS:** Windows 10/11 primary (Tauri), Linux/macOS viable via same stack — no `.lnk` shortcuts, use `tauri::path`.
- **Tooling constraints:** keep `disableDevTools` off in dev, on in prod via Tauri `tauri.conf.json > build > devPath`. No `shell` permission in `tauri.conf.json`; use `process` allowlist.
Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Changelog

All notable changes to OpenCade are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2026-08-24

First auditable vertical slice: two authenticated users can challenge, negotiate transport, run a deterministic direct-UDP match, and produce a correlated, privacy-minimized report. See `docs/ARCHITECTURE.md` (authoritative), `docs/adr/0001-proof-of-match-boundaries.md`, and `docs/IMPLEMENTATION_STATUS.md`.

### Added

- **Runtime composition** — Axum `build_app(state)` with automatic `sqlx::migrate!` on boot and `--migrate` CLI, liveness `GET /health` and readiness `GET /ready` (DB-checked, redacted errors), strict production `SESSION_SECRET` (>=32 chars) and `STUN_*`/`ALLOWED_ORIGINS` validation, scoped CORS, structured `tracing` logs.
- **Auth** — Argon2id registration/login (`POST /api/v1/auth/*`), SHA-256-hashed opaque sessions, `AuthUser` extractor, revocation/expiry, authenticated REST and `GET /ws` (query `token` + bounded queues/rate-limit, envelope validation).
- **Challenges & rooms** — Durable `challenges` table with owner rules (create/accept/decline/cancel), transactional room and `matches` lifecycle (`WAITING -> CHALLENGING -> CONNECTING -> PLAYING -> FINISHED|CANCELLED`), pure `room_state` transition function with unit tests.
- **Protocol** — Rust-authoritative `packages/protocol` envelope `{type, version, request_id, timestamp, payload}` (canonical "1.0", compat "1"), `PROTOCOL_VERSION` + `is_supported_version`, `ts-rs` generated TypeScript under `packages/protocol/src/generated/` with CI drift gate. Payloads: `presence.update`, `chat.message`, `challenge.*`, `session.offer/answer/candidate`, `match.endpoint`, `match.probe_completed`, `room.state`.
- **Client** — React + Tauri 1.x shell (`apps/client`): login/register, games list with local availability, lobby presence, challenge send/accept, connecting/playing/finished screen, typed REST (`lib/api.ts`) and WS (`lib/ws.ts`) with reconnect backoff and `request_id` correlation, diagnostics button and redacted report export.
- **Emulator SDK** — `packages/emulator-sdk` trait `EmulatorAdapter` + `LaunchSpec`, `ProcessLauncher` abstraction, `PathBuf`/`OsString` args, `canonicalize_below` root check, no shell, `ChildHandle` tracking, `MockAdapter` with `MatchDescriptor` and explicit `AdapterCapabilities`.
- **FBNeo adapter** — `adapters/fbneo` detection of `emulator/fbneo/fcadefbneo.exe`, version check, `required_files` validation, safe `build_command` with per-arg sanitization, traversal rejected.
- **Game definitions** — `packages/game-definitions` declarative TOML (`schema_version=1`, `id ^[a-z0-9_]{3,20}$`, `launch.args` with `{rom}` placeholder), JSON schema, loader, and build-time importer.
- **Networking data plane** — `packages/networking`: deterministic in-memory transport + nonce-bound direct UDP (`UdpPeer`, `MatchProbeConfig`, 60-frame transcript + FNV checksum), `NatTraversal`/`FallbackOrder` (direct UDP -> hole-punch 3x500ms -> STUN -> WS/relay), `diagnose_network` reporting `nat`, `rtt_ms` (EWMA alpha 0.2 over 30 samples), `loss`, `jitter_ms`, `relay_reachable`, `stun_reachable`, and `stun:host:port` hint in `GET /servers`.
- **Relay** — Standalone `services/relay` (`opencade-relay`) Axum WS relay on `/relay` with `GET /health`/`ready`, room-bucket forwarding, envelope validation, graceful shutdown; wired in `docker-compose.yml` on `3478`/`3478:udp`.
- **LAN evidence kit** — Canonical redacted `MatchReport` (schema v1) from desktop and CLI probe (`opencade-match-probe`/`opencade-match-verify`), fail-closed paired verifier, two-process localhost test verifying identical 60-frame transcripts, and `docs/alpha/LAN_TEST.md` manual gate (10/10 loopback verified on CI).
- **Desktop diagnostics** — `diagnose_network` + `diagnose_adapter` + `diagnose_roms` + `get_logs`, typed wrapper `apps/client/src/lib/diagnostics.ts`, Tauri allowlist (no `shell.open`, restrictive CSP), `withGlobalTauri: false`.
- **CI** — `ci.yml`: Rust `fmt --check` + `clippy -D warnings` + `cargo test --workspace` (Postgres 16), `pnpm format:check`/`typecheck`/`test`/`build`, bindings drift, `docker compose config` lint, Windows MSRV + `opencade-lan-tools-windows` artifact.

### Changed

- Server routing consolidated from `main.rs` stubs into reusable `build_app` wiring `config`, `state`, `routes`, and `ws`. All SQL aligned to canonical schema (TEXT game ids, `host_user_id`, uppercase-at-rest state). Tauri `shell.open` removed, CSP set, production secret checks enforced.

### Fixed

- Schema drift (slug/UUID/lowercase states) eliminated; DB errors no longer masked as seeded responses. UDP probe startup races and Windows `WSAECONNRESET 10054` handled.

### Not claimed (deferred)

- FBNeo netplay (`netplay: false` until ADR 0001 bridge), symmetric-NAT TURN allocation and production STUN/TURN deployment, production packaging/signing, friends/chat/rankings/replays, or public MVP release beyond verifiable LAN direct-UDP proof.

[0.1.0]: https://github.com/opencade/opencade/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
Loading
Loading