|
1 | 1 | # SpiritStream Architecture |
2 | 2 |
|
3 | | -## Monorepo Layout |
4 | | - |
| 3 | +## Layered architecture |
| 4 | + |
| 5 | +```text |
| 6 | +┌──────────────────────────────────────────────────────────────────┐ |
| 7 | +│ ONE React frontend, shipped through THREE shells │ |
| 8 | +│ • Tauri 2 desktop (macOS, Linux, Windows) │ |
| 9 | +│ • Tauri 2 mobile (iOS, Android) │ |
| 10 | +│ • Docker / browser (HTTP) │ |
| 11 | +│ Plus: spiritstream-cli (headless, integration-test substrate) │ |
| 12 | +├──────────────────────────────────────────────────────────────────┤ |
| 13 | +│ Transport adapters — implement core::traits::transport::Transport│ |
| 14 | +│ • HttpTransport (Axum + utoipa, /api/v1/*) │ |
| 15 | +│ • CliTransport (in-process dispatch) │ |
| 16 | +│ • VeilidTransport (contract-validation spike; see BLOCKERS) │ |
| 17 | +├──────────────────────────────────────────────────────────────────┤ |
| 18 | +│ crates/core/spiritstream-core (Rust library, NO transport deps)│ |
| 19 | +│ • services/, models/, traits/, errors/ │ |
| 20 | +└──────────────────────────────────────────────────────────────────┘ |
5 | 21 | ``` |
| 22 | + |
| 23 | +**Frontends are thin.** UI-only concerns stay in React: focus management, animations, modal open/close, form input state during editing. Every business rule — validation, orchestration, side-effect decisions — lives in `crates/core`. |
| 24 | + |
| 25 | +## Monorepo layout |
| 26 | + |
| 27 | +```text |
6 | 28 | spiritstream/ |
7 | | -├── apps/web/ # React frontend (standalone) |
8 | | -├── apps/desktop/ # Tauri 2.x launcher (minimal) |
9 | | -├── server/ # Standalone Rust HTTP server (Axum 0.7) |
10 | | -├── docker/ # Docker distribution |
11 | | -├── themes/ # Theme files |
12 | | -├── scripts/ # Build & utility scripts |
13 | | -└── packages/ # Shared packages (future) |
| 29 | +├── apps/ |
| 30 | +│ ├── web/ # React frontend |
| 31 | +│ └── tauri/ # Tauri 2 shell — desktop + mobile |
| 32 | +│ └── src-tauri/ # gen/apple + gen/android for mobile targets |
| 33 | +├── crates/ |
| 34 | +│ ├── core/ # spiritstream-core (transport-agnostic library) |
| 35 | +│ ├── transport-http/ # Axum + utoipa adapter |
| 36 | +│ ├── transport-cli/ # spiritstream-cli binary |
| 37 | +│ └── transport-veilid/ # Contract-validation spike — see BLOCKERS.md |
| 38 | +├── server/ # Thin binary wiring core + transport-http |
| 39 | +├── packages/ |
| 40 | +│ ├── types/ # Rust → TS via ts-rs (auto-generated) |
| 41 | +│ ├── api-client/ # ApiClient interface + HttpClient impl |
| 42 | +│ ├── validation/ # JSON Schemas from utoipa |
| 43 | +│ └── ui/ # Shared React primitives |
| 44 | +├── deploy/ |
| 45 | +│ ├── docker/ # Docker image build context |
| 46 | +│ ├── compose/ # docker-compose with Caddy + Let's Encrypt |
| 47 | +│ └── helm/ # K8s Helm chart |
| 48 | +├── tests/ |
| 49 | +│ ├── integration/ # CLI-driven golden-file tests |
| 50 | +│ ├── threat-model/ # Doxxing/EXIF/PII fixtures |
| 51 | +│ └── a11y/ # Playwright + axe-core |
| 52 | +├── docs/ # User and operator docs |
| 53 | +└── themes/ # Theme bundle data |
14 | 54 | ``` |
15 | 55 |
|
16 | | -## Server (`server/`) |
17 | | - |
18 | | -Rust + Axum 0.7 HTTP server. Single binary, no Tauri dependency. |
19 | | - |
20 | | -**Entry**: `server/src/main.rs` — route definitions, middleware, server startup |
21 | | - |
22 | | -**Routes**: |
23 | | -- `POST /api/invoke/:command` — command dispatch (all business logic) |
24 | | -- `GET /ws` — WebSocket for real-time events |
25 | | -- `GET /health` / `GET /ready` — health checks |
26 | | -- `POST /auth/login` / `POST /auth/logout` / `GET /auth/check` — cookie-based auth |
27 | | -- `GET /api/files/browse` / `GET /api/files/home` / `POST /api/files/open` — file browser |
28 | | - |
29 | | -**Services** (`server/src/services/`): |
30 | | -- `profile_manager` — Profile CRUD, encryption |
31 | | -- `ffmpeg_handler` — Stream process management |
32 | | -- `ffmpeg_downloader` — FFmpeg binary download/update |
33 | | -- `encryption` — AES-256-GCM, Argon2id key derivation |
34 | | -- `settings_manager` — App settings persistence |
35 | | -- `theme_manager` / `embedded_themes` — Theme management |
36 | | -- `log_manager` — Logging, log rotation |
37 | | -- `chat_manager` / `chat` — Multi-platform chat integration |
38 | | -- `obs_websocket` — OBS WebSocket integration |
39 | | -- `discord_webhook` — Discord notifications |
40 | | -- `oauth` — OAuth 2.0 for multiple providers |
41 | | -- `events` — Event bus, WebSocket broadcasting |
42 | | -- `path_validator` — Path traversal prevention |
43 | | -- `platform_registry` — Platform-specific utilities |
44 | | - |
45 | | -**Models** (`server/src/models/`): Domain types with `#[derive(Serialize, Deserialize, Clone)]` |
46 | | - |
47 | | -## Frontend (`apps/web/`) |
48 | | - |
49 | | -React 19 + Zustand 5 + Tailwind CSS v4 + Vite 7 + i18next (11 locales) |
50 | | - |
51 | | -**Key paths**: |
52 | | -- `src/components/` — React components (ui/, layout/, stream/, modals/) |
53 | | -- `src/stores/` — Zustand stores, one per domain |
54 | | -- `src/lib/backend/` — Transport abstraction layer |
55 | | -- `src/types/` — TypeScript type definitions |
56 | | -- `src/locales/` — i18n translations (af, ar, de, en, es, fr, ja, ko, ru, uk, zh-CN) |
57 | | -- `src/views/` — Page views |
58 | | - |
59 | | -**Transport abstraction** (`src/lib/backend/`): |
60 | | -- `api.ts` — selects `httpApi` (default) or `tauriApi` (legacy) based on detected mode |
61 | | -- `httpApi.ts` — `POST /api/invoke/{command}` with `safeFetch()` + retry + cookie auth |
62 | | -- `httpEvents.ts` — WebSocket event handler with auto-reconnect |
63 | | -- `httpDialogs.ts` — File dialog abstraction for HTTP mode |
64 | | -- `env.ts` — Mode detection, URL management, health checks |
65 | | - |
66 | | -## Desktop (`apps/desktop/`) |
67 | | - |
68 | | -Minimal Tauri 2.x wrapper. **No business logic** — `generate_handler![]` is intentionally empty. |
69 | | - |
70 | | -**Entry**: `apps/desktop/src-tauri/src/main.rs` |
71 | | -- Spawns `spiritstream-server` sidecar binary |
72 | | -- Waits for `/health` endpoint |
73 | | -- Opens webview to `http://127.0.0.1:8008` |
74 | | - |
75 | | -## Deployment Modes |
76 | | - |
77 | | -| Mode | How it runs | |
78 | | -|------|-------------| |
79 | | -| **Desktop** | Tauri launcher spawns server sidecar, UI in embedded webview | |
80 | | -| **Docker** | Server container, UI served or accessed via browser | |
81 | | -| **Web browser** | Remote access to running server instance | |
| 56 | +## Rules for any new code |
| 57 | + |
| 58 | +1. **No business logic in the frontend.** If the code answers "should this action happen?" it belongs in `crates/core`. The frontend answers "how should this look?" |
| 59 | +2. **No transport types in core.** `crates/core` must compile without `axum`, `tower`, `hyper`, `tauri`, `clap`, or any HTTP/CLI/Tauri crate. |
| 60 | +3. **All API calls through `@spiritstream/api-client`.** Never `fetch()` directly from a component. |
| 61 | +4. **Domain types live once.** Define in `crates/core/src/models/`, derive `ts_rs::TS`, consume from `@spiritstream/types`. Do not redeclare in TypeScript. |
| 62 | +5. **Errors are structured.** Use `CoreError` variants; do not return new `Result<T, String>` from core services. |
| 63 | +6. **Transports map errors.** `transport-http` maps `CoreError → HTTP status`; `transport-cli` maps `CoreError → exit code`. Mapping lives in the transport, not in core. |
| 64 | +7. **Every UI operation must be reachable from `spiritstream-cli`.** If it isn't, that's a contract bug — fix core first. |
| 65 | +8. **Mobile constraints inform core design.** Sidecar binaries are restricted on iOS/Android; the core links into the Tauri 2 mobile shell as a library and the in-process Axum binds `localhost:8008`. |
| 66 | +9. **No runtime fallback chains.** Where a primary/secondary impl exists (e.g., keyring vs. encrypted-file secret store), the choice is made once at startup via platform probe or env override and held for the process lifetime. Never `try_primary().or_else(secondary)` at the call site. |
| 67 | +10. **No legacy/transitional dual-mount.** Forward-only architecture. There are no unversioned URL aliases, no `/api/invoke/:command` fallback, no schema-version migration framework. |
| 68 | +11. **No `#[allow(dead_code)]`; no TODO/FIXME markers in source.** Adapters, fields, and helpers must have a live caller in the same change. Future work tracks in plans, issues, or `BLOCKERS.md`. |
| 69 | +12. **Per-transport security hardening is allowed even though business logic isn't.** CSRF, cookie attributes, and CORS are HTTP-shaped concerns that legitimately live in `transport-http`. The Veilid analogue (peer-keypair verification) would live in its transport. See `crates/transport-veilid/BLOCKERS.md` for the boundary discussion. |
| 70 | + |
| 71 | +## Service catalog (`crates/core/src/services/`) |
| 72 | + |
| 73 | +| Service | Responsibility | |
| 74 | +|---|---| |
| 75 | +| `profile_service` | Profile CRUD, validation, encryption boundaries (delegated to `SecretStore`). | |
| 76 | +| `stream_service` | Stream lifecycle, reconnection, retry policy. Uses `MediaProcessor` trait (FFmpeg on desktop; HaishinKit/RootEncoder follow-up branch on mobile). | |
| 77 | +| `chat_service` | Multi-platform chat connect/send/receive. Applies the PII filter on inbound and `check_outbound_pii` on outbound. | |
| 78 | +| `obs_service` | OBS WebSocket integration. Owns trigger orchestration state machine. | |
| 79 | +| `oauth_service` | OAuth 2.0 flows with token refresh. | |
| 80 | +| `settings_service` | Global settings persistence with bound-checked setters. | |
| 81 | +| `theme_service` | Theme catalog, install, hot-reload. | |
| 82 | +| `audit_log_service` | HMAC-chained append-only audit trail. `verify_chain()` detects tamper. | |
| 83 | +| `safety_service` | Panic disconnect, PII blocklist, EXIF stripping, anonymous-mode pseudonymizer. | |
| 84 | +| `auth` | Brute-force defense (exponential backoff + sliding-window lockout). | |
| 85 | +| `auth_surveillance` | OAuth refresh-frequency anomaly detection. | |
| 86 | +| `secret_store` | `KeyringSecretStore` or `EncryptedFileSecretStore`, selected once at startup. | |
| 87 | +| `encryption` | AES-256-GCM-SIV envelope (V2 writes, V1 read-only) under HKDF-derived keys. | |
| 88 | +| `pii_filter` | Strict + fuzzy matchers; returns `phrase_id` for audit, never the phrase. | |
| 89 | +| `pseudonymizer` | HMAC-SHA256 keyed-hash for anonymous mode. | |
| 90 | +| `media_sanitizer` | EXIF / IPTC / XMP / PNG-text strip, thumbnail removal. | |
| 91 | +| `confirm_token` | One-shot intent-scoped 30s-TTL tokens for destructive ops. | |
| 92 | +| `secure_io` | `write_owner_only_atomic` (temp + rename + 0600 on Unix). | |
| 93 | + |
| 94 | +Each service is constructed once, held in `ServiceRegistry`, and shared as `Arc<...>` across all active transports. |
| 95 | + |
| 96 | +## Transports |
| 97 | + |
| 98 | +### `transport-http` (Axum + utoipa) |
| 99 | + |
| 100 | +Versioned REST under `/api/v1/*`. utoipa annotations on every handler generate an OpenAPI spec served at `/api/v1/openapi.json`; `@hey-api/openapi-ts` consumes that to produce `@spiritstream/api-client`. |
| 101 | + |
| 102 | +Middleware stack: `request_id_middleware` → `csrf_middleware` (Sec-Fetch-Site + Origin allow-list, WS upgrades guarded) → `rate_limit_middleware` (per-endpoint, keyed by SHA-256 hash of auth subject) → `auth_middleware` (cookie + Bearer, `verify_token` hashes before `ct_eq`). |
| 103 | + |
| 104 | +Cookie attributes via `SessionCookieMode` enum: `SameOrigin` (Tauri webview), `CrossOrigin` (browser-served), `LocalhostDev`. Pure-function `detect(host, deploy_mode, explicit)` for parallel-test safety. |
| 105 | + |
| 106 | +Cloud-mode startup guard `enforce_cloud_mode_preconditions(token, tls_declared)` refuses to start without TLS and a ≥32-char token. |
| 107 | + |
| 108 | +WebSocket: `GET /api/v1/events`, one-way server-push only. CSRF guard runs on upgrade. |
| 109 | + |
| 110 | +### `transport-cli` |
| 111 | + |
| 112 | +`spiritstream-cli` binary calls into `ServiceRegistry` directly — **no HTTP shell-out**. JSON output by default, `--pretty` for humans, `--quiet` for scripting. Exit codes derived from `CoreError` variants. Integration tests under `tests/integration/` are CLI-driven and golden-compared. |
| 113 | + |
| 114 | +### `transport-veilid` (contract-validation spike) |
| 115 | + |
| 116 | +Stub implementing `Transport` to prove the contract holds for a non-HTTP transport. `VeilidTransport::serve` returns `CoreError::NotImplemented`. The deliverable is `crates/transport-veilid/BLOCKERS.md` — 10 enumerated HTTP-shaped contract gaps (the biggest is **URL paths → method namespace**) that a real Veilid implementation must close. The crate compiles against `crates/core` alone with no Axum, Tauri, or Veilid SDK. |
| 117 | + |
| 118 | +## Deployment modes |
| 119 | + |
| 120 | +| Mode | Shell | Transport | Notes | |
| 121 | +|---|---|---|---| |
| 122 | +| **Desktop** | Tauri 2 | HTTP sidecar (`server/` spawned as a subprocess) | macOS / Linux / Windows. | |
| 123 | +| **Mobile** | Tauri 2 | HTTP in-process (core linked into Tauri Rust shell; Axum on `localhost:8008`) | iOS / Android. Sidecars don't work on either. | |
| 124 | +| **Docker / browser** | None | HTTP | Same `server/` binary; serves UI bundle + API. Caddy + Let's Encrypt in front. | |
| 125 | +| **CLI** | None | In-process dispatch | First-class client; integration-test substrate. | |
| 126 | + |
| 127 | +## Pointers |
| 128 | + |
| 129 | +- Coding standards: `.claude/rules/coding-standards.md` |
| 130 | +- Git workflow: `.claude/rules/git-workflow.md` |
| 131 | +- Documentation rules: `.claude/rules/documentation.md` |
| 132 | +- Veilid contract gaps: `crates/transport-veilid/BLOCKERS.md` |
| 133 | +- Per-crate READMEs: `crates/*/README.md` |
0 commit comments