Files
chanora/crates/chanora_protocol/src/lib.rs
T
EdisonJwa 50768a8f48 feat(mvp): v1.0.0-rc.1 — keyring-backed DEK, encrypted bookmarks, MVP release-gate docs
Closes the v0.4 dual-file weakness in identity-at-rest and turns the
release into an MVP public release candidate. The remaining work
before `v1.0.0` is DEC-012 legal sign-off — see
`docs/governance/legal-review-readiness.md` — and the staged
platform promotions in `docs/governance/staged-release-plan.md`.
No decision rows in `product-decision-register.md` change; the
register's change-history advances to 0.9.8.

`chanora_storage`
-----------------

* New public `Crypto` trait + `IdentityFileStore::crypto()` give
  callers an encrypt / decrypt pair anchored on the per-install
  32-byte DEK without exposing the key material.
* `IdentityFileStore` keyring-first DEK retrieval (Linux Secret
  Service via D-Bus, macOS Keychain, Windows Credential Manager,
  iOS Keychain via the `keyring` crate). Pre-existing
  `identity.dek` files are opportunistically migrated into the
  keyring on first run; the on-disk DEK copy is removed once the
  keyring acknowledges. `CHANORA_DISABLE_KEYRING=1` forces the
  file-fallback path for tests and headless / CI hosts where a
  real keyring call would prompt the user or block on a missing
  D-Bus session.
* `BookmarkRepository::with_crypto(dir, crypto)` encrypts the
  server password into a new `password_blob` BLOB column under
  the same per-install DEK. Schema v2 migration is idempotent —
  legacy v0.4 rows with a plain `password TEXT` are read
  transparently and lifted into `password_blob` on the next
  `update()`. `BookmarkRepository::new` (no crypto) is preserved
  for tests and as a documented fallback when the DEK is
  unreachable.
* Storage tests rise from 8 to 10: encrypted bookmark password
  round-trip + legacy-plaintext-bookmark upgrade.

`chanora_core`
--------------

* `ChanoraSession::init_storage(dir)` wires the bookmark
  repository with crypto by default. On any crypto-derivation
  failure it falls back to the plain-password repository and
  logs the gap — better than hard-failing init.
* `supervisor_loop` now tracks a 64-bit `snapshot_signature` over
  channels (id + parent + order + name) and clients (id + channel
  + name) instead of the old `(channel_count, client_count)`
  tuple. Any in-channel client move, channel rename, or reorder
  now fires `SessionEvent::SnapshotChanged`. The signature sorts
  by id before hashing so it's stable under input-vector
  reordering.
* Two new unit tests cover the signature behaviour; new
  `tests/mvp_storage.rs` integration test drives
  `ChanoraSession::init_storage` end-to-end and verifies the
  bookmark `password_blob` does not contain the plaintext.
* Re-export `ChannelId` + `ClientId` from `chanora_protocol` so
  downstream callers and tests can construct DTOs directly.

Flutter
-------

* New About dialog (info icon in the AppBar) surfaces DEC-018
  (public name "Chanora"), DEC-019 (non-affiliation statement),
  and DEC-020 (Apache-2.0 OR MIT dual license). New ARB keys in
  `app_en.arb` and `app_zh.arb`: `aboutAction`, `aboutVersion`,
  `aboutNonAffiliation`, `aboutLicenseHeading`, `aboutLicenseBody`,
  `aboutThirdPartyHeading`, `aboutThirdPartyBody`.
* `pubspec.yaml` version bumps to `1.0.0-rc.1+5`.

Governance
----------

* `docs/governance/legal-review-readiness.md` — DEC-012 handoff
  package. Enumerates trademark / non-affiliation / license-text
  / third-party-attribution / `tsclientlib`-posture / crypto-
  export / data-handling items the legal reviewer must confirm,
  and lists the concrete engineering deliverables they block on
  (`cargo about generate`, `cargo deny check licenses`,
  Flutter `LicenseRegistry` dump).
* `docs/governance/staged-release-plan.md` — DEC-002 channel
  schedule. Linux + Android sideload promote to GA on DEC-012
  sign-off; Play Store / Windows / macOS / iOS gate on per-
  platform signed-build availability. Rollback policy included.
* `product-decision-register.md` change-history advances to
  0.9.8 with a single entry summarising v0.3, v0.4, and v1.0-rc.1
  progress against DEC-001. No decision rows mutate.

Build + ops
-----------

* `NOTICE` refreshed for the MVP product-code dependency set:
  adds `chacha20poly1305`, `rand`, `zeroize`, `base64`,
  `keyring`, `connectivity_plus`, `path_provider`,
  `freezed_annotation`; drops PoC-only entries.
* `CHANGELOG.md` restructured: explicit version sections for
  v0.3.0-beta.1, v0.4.0-beta.2, v1.0.0-rc.1. Previous "Unreleased"
  contents migrated into their respective milestone sections.
* `.github/workflows/ci.yml` exports `CHANORA_DISABLE_KEYRING=1`
  for the cargo-test job — CI runners have no D-Bus session and
  the keyring crate would otherwise block.
* `run-chanora.sh` reads `CHANORA_BUNDLE_FLAVOUR` (default
  `release`) and self-copies the latest cdylib into the bundle's
  `lib/` if missing.

Verification
------------

* `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`: all
  green (49 unit tests across the workspace; up from 36 at
  v0.4.0-beta.2).
* `cargo test -p chanora_core --release -- --ignored alpha_smoke`
  passes against the live `cn.teamspeak.app` (DNS → connect →
  snapshot → disconnect in ~2.5 s).
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` + `flutter build
  linux --release` produce a working Linux x86_64 bundle.

No Android live test in this commit per the user's note that the
physical device was removed; the Android arm64-v8a build path is
mechanically identical to v0.4.0-beta.2.
2026-05-15 02:24:42 +08:00

103 lines
3.6 KiB
Rust

//! # `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, SysDes-011, SysDes-029).
//!
//! ## What this crate exposes
//!
//! * [`ConnectConfig`] — typed connection parameters.
//! * [`ProtocolClient`] — async handle owning the connection task.
//! * [`ServerSnapshot`], [`ChannelInfo`], [`ClientInfo`] — opaque
//! DTOs containing only `String`s and primitives.
//! * [`ProtocolError`] — typed error catalogue.
//!
//! ## What this crate does NOT expose
//!
//! * `tsclientlib::*` types.
//! * `tsproto::*` types.
//! * Any audio-related types — those live in `chanora_audio`.
//!
//! Promoted from `poc/tsclientlib-connect-spike` on 2026-05-14
//! as part of the Alpha build.
//!
//! ## Hostname resolution (A.1)
//!
//! Upstream `tsclientlib` uses `hickory-resolver` which reads
//! `/etc/resolv.conf`. That file does not exist on Android or iOS,
//! and the Beta UI surfaced the resulting cryptic "connection task
//! exited before signalling ready" errors. We side-step the issue by
//! resolving hostnames ourselves with `tokio::net::lookup_host`,
//! which uses platform `getaddrinfo` (works correctly on every
//! supported platform), and feeding the resulting `SocketAddr`
//! directly to `tsclientlib::Connection::build`. A small in-process
//! positive-result cache keeps reconnects fast.
#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod adapter;
mod dto;
mod resolver;
pub use adapter::{ConnectConfig, DisconnectReason, InboundVoice, ProtocolClient, SnapshotProbe};
pub use dto::{ChannelId, ChannelInfo, ClientId, ClientInfo, ServerSnapshot};
// Re-export the upstream voice types so chanora_audio can build outbound
// voice packets without taking a direct dependency on tsclientlib /
// tsproto_packets. Per SAD-067 this is the *one* deliberate
// re-export: the audio path is performance-sensitive and a parallel
// type hierarchy would force copies for every 20 ms frame.
pub use tsproto_packets::packets::{
AudioData, CodecType, Direction, InAudioBuf, OutAudio, OutPacket,
};
use thiserror::Error;
/// Errors surfaced by the protocol adapter. None of these expose
/// `tsclientlib`-specific types; raw upstream errors are mapped here
/// to typed arms.
#[derive(Debug, Error)]
pub enum ProtocolError {
/// Configuration is invalid before any I/O is attempted (bad
/// hostname, missing identity, etc.).
#[error("invalid protocol configuration: {0}")]
Invalid(String),
/// Hostname resolution failed. Distinct from [`Self::Connect`]
/// so the UI can show a meaningful "Server not found" message
/// instead of a generic connection error.
#[error("dns lookup failed for '{host}': {reason}")]
DnsFailed {
/// The hostname (or `host:port`) the caller submitted.
host: String,
/// Reason from the platform resolver.
reason: String,
},
/// Failed to dial / handshake with the server.
#[error("connect failed: {0}")]
Connect(String),
/// The connection ended before becoming ready.
#[error("disconnected before ready: {0}")]
DisconnectedEarly(String),
/// Connection lost after becoming ready.
#[error("connection lost: {0}")]
Lost(String),
/// Identity parsing failed.
#[error("identity error: {0}")]
Identity(String),
/// Operation timed out.
#[error("protocol timeout")]
Timeout,
/// A backend error escaped the mapping. Production callers
/// should never see this; if they do, it is a mapping bug here.
#[error("protocol backend: {0}")]
Backend(String),
}