From 01a4a9ed28b7f0db5017ca713576f4537ea4e06c Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Thu, 11 Jun 2026 11:09:12 +0900 Subject: [PATCH] docs: add README files to 9 crates and update verification plan (TODO-030,036) Add purpose, architecture, and public API summary to each crate README following chanora_resolver pattern. Update verification master plan with new evidence sources and entry/exit criteria. --- core/chanora_core/README.md | 74 +++++++++++++++++ crates/chanora_audio/README.md | 66 +++++++++++++++ crates/chanora_bridge/README.md | 80 +++++++++++++++++++ crates/chanora_cache/README.md | 39 +++++++++ crates/chanora_diagnostics/README.md | 80 +++++++++++++++++++ crates/chanora_prefetch/README.md | 48 +++++++++++ crates/chanora_protocol/README.md | 52 ++++++++++++ crates/chanora_state/README.md | 64 +++++++++++++++ crates/chanora_storage/README.md | 68 ++++++++++++++++ docs/verification/verification-master-plan.md | 9 ++- 10 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 core/chanora_core/README.md create mode 100644 crates/chanora_audio/README.md create mode 100644 crates/chanora_bridge/README.md create mode 100644 crates/chanora_cache/README.md create mode 100644 crates/chanora_diagnostics/README.md create mode 100644 crates/chanora_prefetch/README.md create mode 100644 crates/chanora_protocol/README.md create mode 100644 crates/chanora_state/README.md create mode 100644 crates/chanora_storage/README.md diff --git a/core/chanora_core/README.md b/core/chanora_core/README.md new file mode 100644 index 0000000..83f6634 --- /dev/null +++ b/core/chanora_core/README.md @@ -0,0 +1,74 @@ +# chanora_core + +Top-level Rust API and orchestration layer for the Chanora client. Composes subsystem crates behind a stable, typed API consumed by `chanora_bridge`. Owns no protocol, audio, or storage logic directly. + +## Architecture + +Per SAD §7.2, `chanora_core` is the integration point: + +- **`ChanoraSession`** — the primary public type. Owns at most one active server connection (DEC-006). Provides connect, disconnect, snapshot, audio lifecycle, PTT, bookmarks, and diagnostics methods. +- **Supervisor** — a per-connection tokio task that monitors connection health via a loss notifier and a watchdog probe, and auto-reconnects with exponential backoff (1 s → 60 s capped). Re-attaches the audio engine if it was running prior to the loss. +- **`SessionEvent`** — broadcast enum emitted on connect/lost/reconnecting/disconnected/audio-started/audio-stopped/voice-state/chat/route changes. Subscribers consume via `subscribe_events()`. +- **File transfer** — avatar/icon download routed through a cacache-backed blob cache with LRU eviction. +- **Channel join state machine** — reducer-based state tracking for voice channel joins, with optimistic commands, snapshot reconciliation, and error projection. +- **PTT controller** — platform input backend management, binding persistence, and release-tail timer wiring (SDD-088/094/096). + +## Public API Summary + +### Core types + +| Type | Role | +|---|---| +| `ChanoraSession` | Top-level session handle; cloneable, thread-safe | +| `CoreError` | Unified error enum covering all subsystem errors | +| `SessionEvent` | Broadcast lifecycle event enum | +| `ConnectConfig` | Typed connection parameters | +| `NetworkState` | OS connectivity state enum | + +### Key methods on `ChanoraSession` + +- `new()` — construct an empty session (no I/O) +- `init_storage(dir)` — wire identity + bookmark stores +- `init_cache(dir)` — wire the blob cache for avatars/icons +- `connect(cfg)` → `ServerSnapshot` — dial a server (single-connection invariant) +- `disconnect()` — clean teardown including supervisor +- `is_connected()` — check connection state +- `snapshot()` → `ServerSnapshot` — refresh server state +- `client_profile(client_id)` — rich profile for one client +- `voice_join(channel_id, password)` / `voice_leave()` — audio lifecycle +- `start_audio(cfg)` — initialize audio subsystem +- `set_input_device(id)` / `set_output_device(id)` — device selection +- `set_output_gain(gain)` / `set_client_volume(client_id, volume)` — volume control +- `set_transmit_mode(mode)` / `get_transmit_mode()` — transmit mode +- `set_hard_mute(muted)` — hard-mute clamp +- `set_release_tail_ms(ms)` / `get_release_tail_ms()` — release-tail config +- `set_ptt(active)` / `set_ptt_binding(binding)` / `ptt_descriptor()` — PTT control +- `send_text_message(message, target)` — chat +- `move_to_channel(id, password)` / `set_self_muted(input, output)` — channel + mute +- `subscribe_events()` — broadcast receiver for `SessionEvent` +- `drain_protocol_events()` / `protocol_events_snapshot()` — protocol event access +- `export_diagnostics()` — redacted diagnostic bundle (includes network stats) +- `audio_stats()` — audio subsystem telemetry +- `network_diagnostics_summary()` — network statistics +- `prefetch_server(host)` — warm server-address resolution +- `set_audio_processing_config(cfg)` / `get_audio_processing_config()` — audio DSP config +- `set_audio_debug_wav_dump(enabled)` — WAV dump toggle +- `set_vad_model_path(path)` — Silero model path +- `transmit_selector()` / `release_tail_timer()` — subsystem accessors + +### Re-exports + +Re-exports selected types from `chanora_protocol`, `chanora_audio`, `chanora_storage`, and `chanora_diagnostics` so the bridge only depends on `chanora_core`. + +## Platform notes + +- iOS/macOS-specific methods (`ios_handle_route_change`, `ios_handle_interruption_began`, etc.) are gated behind `cfg(target_os = "ios" | "macos")` inside method bodies. +- Android-specific reconnect paths are similarly gated. +- The crate itself compiles on all targets; platform-specific code is runtime- or cfg-gated. + +## Invariants + +- Single active connection at runtime (DEC-006) +- `tsclientlib` types never cross out of `chanora_protocol` (SAD-067) +- Secret material never lands in non-secret storage (DEC-013.2) +- Audio engine construction failure preserves the previous engine state diff --git a/crates/chanora_audio/README.md b/crates/chanora_audio/README.md new file mode 100644 index 0000000..5d020ad --- /dev/null +++ b/crates/chanora_audio/README.md @@ -0,0 +1,66 @@ +# chanora_audio + +Real-time audio subsystem: capture, Opus encoding/decoding, voice rendering, PTT gating, and audio processing. Promoted from `poc/audio-capture-playback-spike`. + +## Architecture + +### Engine + +- **`AudioEngine`** — the primary type. Starts a platform audio backend (capture + playback), wires an `AudioTransmitGate` for PTT gating, and feeds encoded Opus frames to the protocol layer via `voice_out`. Inbound voice packets are decoded and mixed by `tsclientlib::audio::AudioHandler` and pulled by the platform output callback at 48 kHz stereo. + +### Platform backends (cfg-gated) + +| Target | Backend | Notes | +|---|---|---| +| Android | Oboe (via `android_voice_unit`) | Requires `ndk_context` before start | +| iOS/macOS | Apple VoiceProcessingIO (`ios_voice_unit`) | Platform AEC/AGC/NS, route-change handling | +| Linux | SDL (`sdl_output`) | PulseAudio/ALSA via SDL | +| Other desktop | cpal | Fallback | + +### Key modules + +- **`audio_processing`** — P1 audio processing config, stats, route policy, effect ownership (Platform/Sonora/WebRTC APM) +- **`opus_voice`** — 20 ms / 48 kHz mono Opus encode/decode via `audiopus` +- **`transmit_mode`** — `TransmitMode` enum: Ptt, Continuous, VoiceActivity +- **`transmit_selector`** — `TransmitModeSelector` combining mode, hard-mute, PTT gate, permission gate, and in-channel state +- **`ptt`** — `AudioTransmitGate` (atomic bool), `PttCapabilityLevel`, `PttBackendDescriptor` +- **`ptt_backends`** — platform PTT backends: `DesktopPttBackend` (Linux portal), `FocusedPttBackend` (in-app fallback) +- **`release_tail`** — `ReleaseTailTimer` for configurable PTT release delay (default 200 ms, max 500 ms) +- **`vad`** — Voice-activity detection: Silero ONNX (desktop), WebRTC fallback, energy debug +- **`voice_render`** — mixes per-client decoded f32 PCM into the output buffer +- **`debug_wav`** — optional WAV file dump for diagnostics (DIAG_002/003) +- **`mobile_voice_backend`** — shared mobile voice-unit lifecycle abstraction +- **`frame`** — frame-aligned buffer utilities + +## Public API Summary + +### Types + +| Type | Role | +|---|---| +| `AudioEngine` | Start/stop audio, set gain/mute/volume, read stats | +| `AudioEngineConfig` | Capture/playback device selection, PTT initial state, processing config | +| `AudioDeviceInfo` / `AudioDeviceList` | Device enumeration | +| `AudioTransmitGate` | Atomic PTT gate | +| `TransmitMode` / `TransmitModeSelector` | Mode selection with hard-mute clamp | +| `ReleaseTailTimer` | Configurable release delay (SDD-096) | +| `PttBinding` / `PttInputClass` | PTT key binding types | +| `PttBackendDescriptor` / `PttCapabilityLevel` | Capability query | +| `AudioProcessingConfig` / `AudioProcessingStats` | P1 processing control and telemetry | +| `AudioRoute` | Speaker/Earpiece/Wired/Bluetooth enum | +| `AudioEffects` | Effect toggles (AEC/AGC/NS/HPF), all enabled by default (DEC-007..010) | +| `AudioError` | Typed error catalogue | + +### Key functions + +- `AudioEngine::start_with_gate(cfg, voice_out, voice_in, gate)` — construct and start +- `AudioEngine::stop()` — tear down +- `list_audio_devices()` — enumerate available input/output devices +- `select_ptt_backend()` — choose the best PTT backend for the current platform + +## Platform notes + +- Android requires `initChanoraContext` (NDK context) before engine start. +- iOS/macOS uses VoiceProcessingIO for platform AEC/AGC/NS in the default route. +- Desktop can use Silero ONNX VAD when the model file is available. +- `bench_seam` is exposed (`#[doc(hidden)]`) for criterion benchmarks on non-mobile targets. diff --git a/crates/chanora_bridge/README.md b/crates/chanora_bridge/README.md new file mode 100644 index 0000000..378a6bb --- /dev/null +++ b/crates/chanora_bridge/README.md @@ -0,0 +1,80 @@ +# chanora_bridge + +Typed Flutter/Rust bridge — schema-controlled DTOs for commands, results, and events. Backed by `flutter_rust_bridge` 2.x per DEC-014. + +## Architecture + +- **`api` module** — all public functions exposed to Dart. Each function runs on a shared tokio runtime and delegates to `chanora_core::ChanoraSession`. Input/output types are owned primitives or `String`s — no backend types cross the boundary (SAD-067, SDD-079). +- **`frb_generated`** — auto-generated `flutter_rust_bridge` glue. Contains `unsafe` for the FFI boundary; hand-written code must not use `unsafe`. +- **`android_init`** (Android only) — NDK context initialization +- **`permission_jni`** (Android only) — JNI hook for Android permission state changes (SDD-106) + +### DTO pattern + +Every Dart-facing type is a `Bridge*` DTO with primitive fields. `From` impls convert between bridge DTOs and `chanora_core` types. Most types do not carry `serde` derives — FRB generates its own SSE encoders/decoders. + +### Event streaming + +`BridgeEvent` enum is streamed to Dart via FRB's `StreamSink`. Events include: Connected, Disconnected, Lost, Reconnecting, ChatMessage, VoiceState, AudioStarted/Stopped, ClientJoined/Left/Moved/Updated, ChannelAdded/Removed/Updated, PttCapability, PermissionState, ServerActivity, InterruptionState, AudioRouteChanged. + +## Public API Summary + +### Commands (api.rs) + +| Command | Description | +|---|---| +| `bridge_init()` | One-time init: logging, panic hook | +| `connect(host, nickname, password)` | Connect to a server | +| `disconnect()` | Clean disconnect | +| `snapshot()` | Refresh server state | +| `client_profile(client_id)` | Rich profile for one client | +| `is_connected()` | Connection check | +| `prefetch_server(host)` | Warm server resolution | +| `voice_join(channel_id, password)` | Join voice channel | +| `voice_leave()` | Leave voice channel | +| `set_transmit_mode(mode)` | Ptt/Continuous/VoiceActivity | +| `get_transmit_mode()` | Read current mode | +| `set_hard_mute(muted)` | Hard-mute clamp | +| `set_ptt(active)` | Manual PTT press/release | +| `set_ptt_binding(input_class, platform_key)` | Bind a PTT key | +| `ptt_descriptor()` | Current PTT capability | +| `get_ptt_binding()` | Persisted PTT binding | +| `set_release_tail_ms(ms)` | Release-tail config | +| `get_release_tail_ms()` | Read release-tail | +| `move_to_channel(channel_id, password)` | Move to a channel | +| `set_input_muted(muted)` / `set_output_muted(muted)` | Server-side mute | +| `set_output_gain(gain)` | Master volume | +| `set_client_volume(client_id, volume)` | Per-client volume | +| `send_chat_message(message, target)` | Send text | +| `set_audio_processing_config(config)` | P1 audio processing | +| `get_audio_processing_config()` | Read P1 config | +| `audio_processing_stats()` | P1 telemetry | +| `enable_audio_debug_wav_dump(enabled)` | WAV dump toggle | +| `set_vad_model_path(path)` | Silero model path | +| `set_input_device(id)` / `set_output_device(id)` | Device selection | +| `export_diagnostics()` | Redacted export bundle (includes network stats) | +| `audio_stats()` | Audio subsystem statistics | +| `input_level_stream()` | Mic level metering stream | +| `events_stream()` | Bridge event stream | +| `log_file_path_str()` | Log file path for platform | +| `init_storage()` / `init_cache()` | Storage/cache initialization | +| `set_ios_voice_processing_mode(mode)` | iOS audio processing mode | +| `set_audio_output_route(route)` | Audio output route selection | +| `list_audio_devices()` | Enumerate audio devices | +| `list_bookmarks()` / `add_bookmark` / `update_bookmark` / `delete_bookmark` | Bookmark CRUD | +| `download_avatar(hash, uid)` / `download_icon(id)` | Avatar/icon download | +| `clear_file_cache()` / `file_cache_size()` | Cache management | +| `handle_route_change(route)` | iOS audio route change | +| `handle_media_services_reset_with_route(route_class)` | iOS media reset | +| `handle_interruption_began()` / `handle_interruption_ended(should_resume)` | iOS interruption | +| `lifecycle_event(state)` | Platform lifecycle | + +### Bridge DTOs + +`BridgeSnapshot`, `BridgeChannel`, `BridgeClient`, `BridgeClientProfile`, `BridgeAudioStats`, `BridgeAudioProcessingConfig`, `BridgeAudioProcessingStats`, `BridgeAudioRoute`, `BridgeTransmitMode`, `BridgePttInputClass`, `BridgePttDescriptor`, `BridgePttBinding`, `BridgeMessageTarget`, `BridgeBookmark`, `PermissionStateKind`, `BridgeError`. + +## Platform notes + +- Cannot use `#![forbid(unsafe_code)]` because FRB-generated glue legitimately uses `unsafe` for the FFI boundary. +- Android: includes `android_init` and `permission_jni` modules gated behind `cfg(target_os = "android")`. +- iOS: route-change and interruption handlers are synchronous (`#[frb(sync)]`), dispatched to the tokio runtime via an ordered channel. diff --git a/crates/chanora_cache/README.md b/crates/chanora_cache/README.md new file mode 100644 index 0000000..764bd9a --- /dev/null +++ b/crates/chanora_cache/README.md @@ -0,0 +1,39 @@ +# chanora_cache + +Disposable content-addressed blob cache for avatar and icon files. Wraps `cacache` for crash safety and integrity verification. Separated from `chanora_storage` because cache owns reconstructible, disposable blob data with different durability and backup semantics. + +## Architecture + +- **`BlobCache`** — async blob store backed by cacache's content-v2 / index-v2 on-disk layout. +- Keys are protocol identifiers prefixed by type: `av_<32-char-hex>` for avatars (MD5), `ic_` for icons (CRC32). +- Cacache handles dedup and SSRI integrity verification on every read. +- Corrupt entries are automatically removed on read failure. +- LRU eviction by timestamp when total size exceeds the configured cap. + +## Public API Summary + +### Types + +| Type | Role | +|---|---| +| `BlobCache` | Content-addressed blob cache | +| `BlobCacheError` | Io, InvalidKey | + +### Key methods on `BlobCache` + +- `new(cache_dir, max_bytes)` — create or open the cache. `max_bytes = 0` disables eviction. +- `put(prefix, key, data)` — store a blob (async) +- `get(prefix, key)` → `Option>` — read a blob, with integrity check (async) +- `remove(prefix, key)` — delete a specific blob (async) +- `clear()` — delete all blobs (async) +- `total_size()` → `u64` — sum of all blob sizes (async) +- `evict()` — remove oldest entries until under `max_bytes` cap (async) + +### Constants + +- `PREFIX_AVATAR` = `"av_"` — avatar key prefix +- `PREFIX_ICON` = `"ic_"` — icon key prefix + +## Key validation + +Avatar keys must be exactly 32 hex characters. Icon keys must be non-empty decimal digits. Unknown prefixes are rejected. This prevents malformed entries from polluting the cache. diff --git a/crates/chanora_diagnostics/README.md b/crates/chanora_diagnostics/README.md new file mode 100644 index 0000000..773e710 --- /dev/null +++ b/crates/chanora_diagnostics/README.md @@ -0,0 +1,80 @@ +# chanora_diagnostics + +Application diagnostics: log redaction, in-memory log capture, and user-initiated diagnostic export. Per DEC-016, export is **user-initiated only**; there is no automatic upload. + +## Architecture + +### Redaction policy + +`Redactor` applies the production policy (REDACT-TC-001..010): + +1. Known-secret registry — substring match → `[REDACTED]` +2. `$HOME` prefix → `[home]` +3. IPv4 addresses → `[ip]` +4. IPv6 addresses → `[ip]` +5. Email-shaped strings → `[email]` +6. Long opaque tokens (base64 ≥32 chars, ≥75% alnum) → `[token]` + +### PTT sanitiser + +`PttSanitizer` — a `tracing-subscriber` Layer decorator that drops any record containing field names from the banned list (`key_code`, `scan_code`, `virtual_key`, `keysym`, etc.) per DEC-027 / REDACT-PTT-001..006. Allocation-free on the success path. + +### Log capture + +`InMemoryLogSink` — bounded ring buffer that passes every line through the redactor before storing. Capacity differs by build: 4096 lines (debug), 256 lines (release) per SRS-122. + +### Event recorder + +`ProtocolEventRecorder` — ring buffer of protocol-level events (connect, disconnect, reconnect, snapshot changes, channel joins) for diagnostic export and state-sync replay verification (SRS-097/098). + +### Export + +`DiagnosticExport` — serialisable bundle containing: +- Client metadata (version, platform) +- Redacted recent logs +- Known-secret count (values never exported) +- Optional Android audio diagnostics YAML +- Optional network diagnostics summary +- Protocol event trace + +## Public API Summary + +### Types + +| Type | Role | +|---|---| +| `Redactor` | Production redaction policy (cheap to clone) | +| `KnownSecretRegistry` | Cross-spike secret registry for defence in depth (SS-AUD-003) | +| `InMemoryLogSink` | Bounded ring buffer of redacted log lines | +| `RedactingLogLayer` | `tracing-subscriber` Layer feeding `InMemoryLogSink` | +| `PttSanitizer` | Layer decorator dropping PTT-sensitive records | +| `DiagnosticExport` | User-facing export bundle | +| `ProtocolEventRecorder` | Protocol event ring buffer (SRS-097) | +| `DiagnosticsError` | Export, Io | +| `REDACTION_MARKER` | `"[REDACTED]"` | + +### Key methods + +**Redactor:** +- `with_default_policy()` / `with_secrets(registry)` — construct +- `redact(s)` → `String` — apply policy +- `secrets()` → `&KnownSecretRegistry` — register secrets + +**KnownSecretRegistry:** +- `register(secret)` — add a known-secret value (≥4 chars) +- `contains_substr(haystack)` → `bool` — substring check + +**InMemoryLogSink:** +- `new(capacity, redactor)` — construct +- `push(raw)` — redact and store a line +- `snapshot()` → `Vec` — current buffer contents + +**DiagnosticExport:** +- `from_sink(sink, metadata)` — build from log sink +- `with_android_audio(yaml)` / `with_network_info(info)` / `with_protocol_events(events)` — attach optional sections +- `to_text()` → `String` — render as multi-line plaintext + +**ProtocolEventRecorder:** +- `new(capacity)` — construct +- `record_connected(server_name)` / `record_disconnected(reason)` / `record_reconnecting(attempt, delay)` +- `drain()` → `Vec` / `snapshot()` → `Vec` diff --git a/crates/chanora_prefetch/README.md b/crates/chanora_prefetch/README.md new file mode 100644 index 0000000..b40c867 --- /dev/null +++ b/crates/chanora_prefetch/README.md @@ -0,0 +1,48 @@ +# chanora_prefetch + +Server-address prefetch cache and policy. Owns speculative server-resolution warming so that when the user presses Connect, a fresh DNS/SRV result may already be available, reducing perceived join latency. + +## Architecture + +### Cache model + +`ServerPrefetchCache` holds at most one entry — the latest prefetched resolution. A generation counter prevents stale async completions from overwriting newer results. TTL is 120 seconds. + +### Flow + +1. Flutter typing triggers `prefetch_server(host)` via the bridge. +2. `ServerPrefetcher::prefetch()` normalizes the host, bumps the generation, and spawns a fire-and-forget tokio task that calls `chanora_resolver::ChanoraResolver::resolve_client_address()`. +3. On success, the result is stored if its generation is still current. +4. When `chanora_core::connect()` is called, it checks `fresh_match(host)`. If a fresh (non-expired) entry matches, it's used as the `resolved_address` in `ConnectConfig`, bypassing a second DNS round trip. + +### Generation guard + +If the user types another host while the first prefetch is in flight, the generation advances. The slower completion is discarded because its generation no longer matches. The most recent entry always wins. + +## Public API Summary + +### Types + +| Type | Role | +|---|---| +| `ServerPrefetcher` | Public API: schedule prefetches, query fresh matches | +| `ServerPrefetchError` | ResolverInit, Resolution, InvalidSocketAddress | + +### Key methods on `ServerPrefetcher` + +- `new()` — construct with empty cache +- `prefetch(host)` — schedule a fire-and-forget resolution (async). Only reports synchronous setup failures; DNS failures are logged. +- `fresh_match(host)` → `Option` — return a cached address if it matches and is within TTL (async) + +### Test-only methods (behind `cfg(test)` or `feature = "test-support"`) + +- `begin_for_test(host)` — bump generation +- `store_success_for_test(generation, host, addr, instant)` — inject a result +- `latest_generation_for_test()` — read current generation +- `fail_next_prefetch_setup_for_test(error)` — inject a setup failure + +## Design notes + +- Blank/whitespace-only hosts are silently skipped. +- Hosts are normalized to lowercase trimmed strings before matching. +- A fresh entry remains usable while a newer prefetch is in flight; stale completions are rejected by the generation guard. diff --git a/crates/chanora_protocol/README.md b/crates/chanora_protocol/README.md new file mode 100644 index 0000000..f76a060 --- /dev/null +++ b/crates/chanora_protocol/README.md @@ -0,0 +1,52 @@ +# chanora_protocol + +TeamSpeak-compatible protocol adapter. Isolates `tsclientlib` behind a typed boundary so the rest of Chanora is decoupled from the upstream library's types (SAD-067). + +## Architecture + +- **`adapter` module** — wraps `tsclientlib::Connection` into an async `ProtocolClient` handle. Owns the connection task, loss notifier, snapshot probe, and voice channel endpoints. +- **`dto` module** — plain-data types (`ServerSnapshot`, `ChannelInfo`, `ClientInfo`, `ClientProfile`, `ChatMessage`) containing only `String`s and primitives. No `tsclientlib` types leak out. +- **`poke_limiter`** — rate-limiter for poke messages to prevent spam. + +## Public API Summary + +### Types + +| Type | Role | +|---|---| +| `ProtocolClient` | Async handle owning the TS3 connection task | +| `ConnectConfig` | Connection parameters: address, nickname, password, identity, timeout, resolved_address | +| `ServerSnapshot` | Full server state: channels, clients, metadata | +| `ChannelInfo` / `ClientInfo` | Channel and client DTOs | +| `ClientProfile` | Rich per-client profile (unique_id, country, ping, groups, etc.) | +| `ChatMessage` | Inbound text message with target enum | +| `MessageTarget` | Server / Channel / Client(id) / Poke(id) | +| `ProtocolDelta` | Live state changes: client joined/left/moved/updated, channel added/removed/updated | +| `ServerActivity` | Server-wide broadcast messages | +| `DisconnectReason` | UserRequested / StreamEnded / Error | +| `ProtocolError` | Typed error catalogue: Invalid, DnsFailed, Connect, Lost, Identity, Timeout, ServerRejected, FileTransfer | +| `PokeLimiter` | Rate-limiting poke sends | + +### Key methods on `ProtocolClient` + +- `connect(cfg)` — dial a server and return a connected client +- `snapshot()` — fetch current server state +- `client_profile(id)` — rich profile for one client +- `send_text_message(msg, target)` — send chat +- `move_to_channel(id, password)` — move to a channel +- `queue_move_to_channel(id, password)` — async move with typed error reply +- `set_muted(input, output)` — server-side mute +- `download_avatar(uid)` / `download_icon(id)` — fetch protocol-owned assets +- `voice_out()` / `take_voice_in()` — voice packet endpoints +- `take_loss_notifier()` — oneshot channel that fires on connection loss +- `snapshot_probe()` — watchdog probe handle +- `generate_identity()` — create a fresh TS3 identity string +- `disconnect()` — clean shutdown + +### Re-exports + +The crate deliberately re-exports `tsproto_packets::packets::{AudioData, CodecType, Direction, InAudioBuf, OutAudio, OutPacket}` — the single permitted exception so `chanora_audio` can build voice packets without a direct `tsclientlib` dependency (SAD-067 performance carve-out). + +## Address resolution + +`chanora_resolver` owns TeamSpeak client address resolution (SRV, TSDNS, DNS fallback). This crate feeds the resulting `SocketAddr` to `tsclientlib::Connection::build`, bypassing tsclientlib's own resolver. diff --git a/crates/chanora_state/README.md b/crates/chanora_state/README.md new file mode 100644 index 0000000..5d0f4a1 --- /dev/null +++ b/crates/chanora_state/README.md @@ -0,0 +1,64 @@ +# chanora_state + +Authoritative client-side mirror of server state: channel tree, client list, and connection lifecycle. Owns the reducers that fold protocol events into state and produce deltas for the bridge (per SAD §7.2 and SDD §5). + +## Architecture + +### Core reducer pattern + +The crate exposes a single `reduce(state: &mut Option, event: StateEvent) -> Reduction` function. Callers own state storage and pass it by `&mut`. The reducer returns a `Reduction` containing only the emitted `Delta` values. This satisfies: + +- **SRS-056** — deterministic deltas: the same `(state, event)` always produces the same `Reduction` +- **SRS-057** — per-connection ordering +- **SRS-058** — reducer functions are pure + +### Module: `channel_join` + +A more specialized reducer for voice-channel join/leave state tracking with: +- Optimistic `UserJoinRequested` events +- `AuthoritativeSelfMove` confirmation from live deltas +- `SnapshotReady` reconciliation after connects/reconnects +- `ChannelJoinProjection` for UI rendering (in_channel, can_join, can_leave, sync_state) +- `ConnectionEpoch` tracking to disambiguate stale events across reconnects + +### State model + +- `ServerState` — owned `HashMap` and `HashMap` with stable ordering vectors. Built from `ServerSnapshot`, updated incrementally via `StateEvent`s. +- `ConnectionState` — enum: Idle / Connecting / Ready / Reconnecting / Lost + +## Public API Summary + +### Types + +| Type | Role | +|---|---| +| `ServerState` | Authoritative mirror of connected server state | +| `ConnectionState` | Lifecycle enum (Idle, Connecting, Ready, Reconnecting, Lost) | +| `StateEvent` | Protocol-layer input events (Snapshot, ChannelChanged, ClientChanged, etc.) | +| `Delta` | Bridge output events (SnapshotApplied, ChannelUpserted, ClientRemoved, etc.) | +| `Reduction` | Result of `reduce()`: a `Vec` | +| `StateError` | Reducer errors (Unknown entity, invariant violation) | + +### Key functions + +- `reduce(state, event)` → `Reduction` — apply a protocol event, return deltas +- `reduce_reconnect_snapshot(state, snap)` → `Reduction` — replace all state on reconnect (SRS-059) + +### `ServerState` methods + +- `channel(id)` / `client(id)` — lookup by id +- `channels()` / `clients()` — ordered iterators +- `own_channel()` — the channel our client is in +- `clients_in_channel(channel_id)` — filtered iterator + +### `channel_join` module + +- `reduce(state, event)` → `JoinReduction` — channel-join state machine +- `project(state)` → `ChannelJoinProjection` — UI-ready snapshot +- `ChannelJoinEvent`, `ChannelJoinState`, `ChannelJoinProjection` — state machine types + +## Design notes + +- Events are ignored when state is `None` (disconnected), except `Snapshot` (creates state) and `ConnectionChanged`. +- Deleting a channel also removes all clients in that channel. +- Duplicate IDs in snapshots are deduplicated deterministically. diff --git a/crates/chanora_storage/README.md b/crates/chanora_storage/README.md new file mode 100644 index 0000000..d326a6f --- /dev/null +++ b/crates/chanora_storage/README.md @@ -0,0 +1,68 @@ +# chanora_storage + +Two strictly separated storage concerns per SAD-067: + +1. **`BookmarkRepository`** — non-secret bookmark state via SQLite with optional encrypted password fields (`rusqlite` bundled, DEC-013.1). +2. **`IdentityFileStore`** — Beta fallback storage for identity material while platform secure-storage backends mature. + +## Architecture + +### IdentityFileStore + +- Persists a single TS3 identity to `/identity.tskey` encrypted with ChaCha20-Poly1305. +- The Data Encryption Key (DEK) is 32 random bytes stored in the platform keyring (Linux Secret Service, macOS Keychain, Windows Credential Manager, iOS Keychain) when available, with a best-effort file fallback at `identity.dek` (mode 0600 on Unix). +- Legacy plaintext files from pre-Beta are still readable; the next `save()` upgrades them to encrypted form. +- Audio metadata (`transmit_mode`, `release_tail_ms`, PTT binding) is persisted alongside as `audio_meta.json` (plaintext, non-secret). + +### BookmarkRepository + +- SQLite-backed store at `/chanora.db`. +- Schema v1: basic bookmark columns. Schema v2: adds `password_blob` for encrypted passwords. +- When constructed via `with_crypto()`, the `password` column is replaced by a ChaCha20-Poly1305 envelope under the same per-install DEK. +- Legacy plaintext passwords are transparently read and upgraded on the next `update()`. + +### Crypto abstraction + +- `Crypto` trait: `encrypt(plaintext)` / `decrypt(blob)` — callers see only the encrypt/decrypt pair. +- `DekCrypto` — concrete implementation sharing the same per-install DEK with `IdentityFileStore`. + +## Public API Summary + +### Types + +| Type | Role | +|---|---| +| `IdentityFileStore` | Encrypted identity file store | +| `BookmarkRepository` | SQLite bookmark store with optional password encryption | +| `Bookmark` | Bookmark DTO: id, display_name, host, nickname, password | +| `PttBindingMeta` | Persisted PTT binding metadata | +| `StorageError` | NotFound, Migration, Sqlite, SecureStore, Io, Crypto | +| `Crypto` trait | Encrypt/decrypt abstraction | + +### IdentityFileStore methods + +- `new(dir)` — create or open store, ensure DEK exists +- `load()` → `Option` — read identity (handles legacy plaintext) +- `save(identity)` — persist encrypted (ChaCha20-Poly1305, atomic write) +- `clear()` — remove identity file +- `crypto()` — obtain a `Crypto` handle sharing the DEK +- `set_transmit_mode(mode)` / `get_transmit_mode()` — audio settings persistence +- `set_release_tail_ms(ms)` / `get_release_tail_ms()` — release-tail persistence +- `set_ptt_binding(...)` / `get_ptt_binding()` — PTT binding persistence + +### BookmarkRepository methods + +- `new(dir)` / `with_crypto(dir, crypto)` — open (plain or encrypted) +- `add(bookmark)` → `i64` — insert, return id +- `update(bookmark)` — replace by id +- `delete(id)` — remove by id +- `list()` → `Vec` — all bookmarks ordered by id +- `upsert_or_add(bookmark)` — insert or update by host, preserves user's display name +- `encrypts_passwords()` — whether password encryption is active + +## Platform notes + +- Unix: files written with mode 0600. +- Keyring access can be disabled via `CHANORA_DISABLE_KEYRING=1` for tests/headless environments. +- Android: file in app-private storage (not encrypted at rest — documented Beta gap). +- iOS/Windows/macOS: caller provides the storage directory; platform sandbox handles access control. diff --git a/docs/verification/verification-master-plan.md b/docs/verification/verification-master-plan.md index 4866a85..817764c 100644 --- a/docs/verification/verification-master-plan.md +++ b/docs/verification/verification-master-plan.md @@ -1,9 +1,12 @@ # Chanora Verification Master Plan **Document status:** DV meeting baseline candidate -**Date:** 2026-05-29 +**Date:** 2026-06-11 **Applies to:** Chanora Rust workspace `0.2.0-beta.1`, Flutter app `0.3.0+100`, and current DV/release-candidate evidence **Primary upstream documents:** `docs/sysrs.md`, `docs/sysdes.md`, `docs/srs.md`, `docs/architecture/sad.md`, `docs/architecture/sdd.md`, `docs/implementation-status-2026-05-28.md` +**Change log:** +- 2026-05-29 — initial baseline +- 2026-06-11 — refreshed evidence sources, added per-crate documentation evidence, updated CI capabilities ## 1. Purpose @@ -46,6 +49,8 @@ No document in this pack may convert an implementation gap into a pass. Gaps mus | Rust crate tests and benches | Workspace tests plus audio benchmark harnesses | SWE.4/SWE.5 performance and component evidence | | `tools/windows-smoke.md` | Windows source-build smoke procedure | SYS.4/SWE.5 manual platform evidence when executed | | `docs/release/ios-build.md` | Unsigned iOS verification build note | Release and platform build evidence | +| Per-crate README.md files | 10 crates with documented purpose, architecture, public API, and platform notes (chanora_core, chanora_protocol, chanora_state, chanora_audio, chanora_storage, chanora_cache, chanora_diagnostics, chanora_bridge, chanora_prefetch, chanora_resolver) | SDD traceability: each module's public surface and invariants are documented in-tree alongside the source | +| `docs/verification/` | SWE.4/SWE.5/SWE.6/SYS.4 verification plans, traceability matrix, waiver register, release-readiness record | Full DV documentation pack | ## 5. Entry Criteria for DV Review @@ -60,6 +65,7 @@ No document in this pack may convert an implementation gap into a pass. Gaps mus | Traceability summary available | Met by this pack | `docs/governance/traceability-matrix.md` | | Release decision record available | Met by this pack | `docs/release/release-readiness-go-nogo-record.md` | | Open gates represented as waivers or blockers | Met by this pack | `docs/release/dv-waiver-register.md` | +| Per-crate documentation available | Met | README.md in each of 10 crate directories | ## 6. Exit Criteria for DV Meeting @@ -72,6 +78,7 @@ The DV meeting can pass the documentation baseline if reviewers agree that: | Verification scope is honest | Known gaps are not marked as passed | | Waivers are explicit | Each release-affecting gap has owner, impact, mitigation, and unblock condition | | Release recommendation is clear | Current candidate is not represented as public-release-ready while DEC-012 and other gates remain open | +| Module documentation is traceable | Each crate's README describes its actual purpose, architecture, and public API as built | ## 7. Open Gates Affecting Release