Replaces the diagnostics scaffold with the production redaction policy + a user-initiated export path that satisfies DEC-016 (no automatic uploads). * `chanora_diagnostics::Redactor` applies the six policy rules to every captured log line: `$HOME` paths → `[home]`; IPv4 + IPv6 literals → `[ip]`; email-shaped strings → `[email]`; long base64-ish tokens → `[token]`; substrings registered with `KnownSecretRegistry` → `[REDACTED]`. The registry implements SS-AUD-003 defence-in-depth: storage adapters can register secrets as they cross out of the keyring so an accidental `Debug` print is still scrubbed at write time. * `InMemoryLogSink` is a bounded ring buffer (cap 500 lines in the bridge) that always passes lines through the redactor before storing them. `RedactingLogLayer` plugs it into `tracing- subscriber` alongside the existing logcat / fmt layers. * `DiagnosticExport::from_sink` builds a plaintext blob — already redacted — combining free-form metadata (crate version, target os/arch) with the retained log tail. `bridge::api:: export_diagnostics()` is the Flutter-facing entrypoint (`#[frb(sync)]`). * `bridge_init` now installs the redaction layer on both Android and desktop hosts, switching from the global `fmt::init()` shortcut to a layered `Registry` so the in-memory sink can sit side-by-side with the platform sink. * Flutter adds a bug-report icon to the AppBar; tapping it opens a scrollable monospace dialog with Copy and Close actions. New `diagnosticsAction` / `copyAction` / `closeAction` strings land in `app_en.arb` + `app_zh.arb`. Tests cover the redaction matrix (IPv4, IPv6, email, long tokens, known secret), the ring buffer capacity, and the full `DiagnosticExport::to_text()` round-trip — 9/9 green. Live-verified on Moto G: the dialog rendered a multi-line transcript with `[ip]`, `[token]`, `[home]` substitutions, the metadata block showed `target_os=android` `target_arch=aarch64`, and Copy placed the same text on the clipboard.
279 lines
8.7 KiB
Dart
279 lines
8.7 KiB
Dart
// This file is automatically generated, so please do not edit it.
|
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
|
|
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
|
|
|
import 'frb_generated.dart';
|
|
import 'lib.dart';
|
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
|
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
|
|
part 'api.freezed.dart';
|
|
|
|
// These functions are ignored because they are not marked as `pub`: `log_sink`, `runtime`, `session`
|
|
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`
|
|
|
|
/// Connect to a TeamSpeak-compatible server and return the initial
|
|
/// state snapshot. Honours the DEC-006 single-connection invariant
|
|
/// via [`BridgeError::AlreadyConnected`].
|
|
Future<BridgeSnapshot> connect({
|
|
required String host,
|
|
required String nickname,
|
|
}) => RustLib.instance.api.crateApiConnect(host: host, nickname: nickname);
|
|
|
|
/// Re-fetch a fresh snapshot from the active connection.
|
|
Future<BridgeSnapshot> snapshot() => RustLib.instance.api.crateApiSnapshot();
|
|
|
|
/// Disconnect from the server. No-op if not connected.
|
|
Future<void> disconnect() => RustLib.instance.api.crateApiDisconnect();
|
|
|
|
/// True if a connection is currently active.
|
|
Future<bool> isConnected() => RustLib.instance.api.crateApiIsConnected();
|
|
|
|
/// Start the audio engine on the active connection. Requires a
|
|
/// connection; idempotent (will replace any previous engine).
|
|
Future<void> startAudio() => RustLib.instance.api.crateApiStartAudio();
|
|
|
|
/// Set the push-to-talk state.
|
|
Future<void> setPtt({required bool active}) =>
|
|
RustLib.instance.api.crateApiSetPtt(active: active);
|
|
|
|
/// User-initiated diagnostic export. Returns a multi-line text
|
|
/// blob, redacted per the production policy, that the user can
|
|
/// share or copy. DEC-016 forbids automatic uploads — this is the
|
|
/// only path that surfaces logs.
|
|
String exportDiagnostics() => RustLib.instance.api.crateApiExportDiagnostics();
|
|
|
|
/// Wire the identity persistence store to a platform-private
|
|
/// directory. Should be called once on app start after Flutter has
|
|
/// resolved `getApplicationSupportDirectory()` (or equivalent).
|
|
///
|
|
/// Subsequent [`connect`] calls will reuse the persisted identity,
|
|
/// or generate-and-persist a fresh one on first use. This keeps the
|
|
/// server-visible UID stable across app restarts.
|
|
///
|
|
/// Beta caveat: the identity is stored as a plain file (mode 0600
|
|
/// on Unix). It is *not* encrypted at rest. RISK-PoC-002 documents
|
|
/// this gap; the v0.4 storage rework lands the proper Secret
|
|
/// Service + Android Keystore + iOS Keychain backends.
|
|
Future<void> initStorage({required String dir}) =>
|
|
RustLib.instance.api.crateApiInitStorage(dir: dir);
|
|
|
|
/// Notify the core of the latest OS-reported connectivity state.
|
|
/// Called by the Flutter side from `connectivity_plus` callbacks.
|
|
/// The core's supervisor uses this to (a) pre-charge the watchdog
|
|
/// on Offline and (b) short-circuit reconnect backoff on Online.
|
|
void setNetworkState({required BridgeNetworkState state}) =>
|
|
RustLib.instance.api.crateApiSetNetworkState(state: state);
|
|
|
|
/// Subscribe to lifecycle events. Each call yields a fresh
|
|
/// subscription; multiple subscribers are supported. On slow
|
|
/// consumers, events are dropped rather than blocking the supervisor
|
|
/// (consistent with `tokio::sync::broadcast::Receiver` semantics).
|
|
Stream<BridgeEvent> eventsStream() =>
|
|
RustLib.instance.api.crateApiEventsStream();
|
|
|
|
/// Read audio statistics. Errors if no connection or audio not started.
|
|
Future<BridgeAudioStats> audioStats() =>
|
|
RustLib.instance.api.crateApiAudioStats();
|
|
|
|
/// Statistics from the audio engine.
|
|
class BridgeAudioStats {
|
|
/// Number of Opus frames sent since audio started.
|
|
final int framesSent;
|
|
|
|
/// Number of inbound voice packets decoded.
|
|
final int framesReceived;
|
|
|
|
/// Current push-to-talk state.
|
|
final bool pttActive;
|
|
|
|
const BridgeAudioStats({
|
|
required this.framesSent,
|
|
required this.framesReceived,
|
|
required this.pttActive,
|
|
});
|
|
|
|
@override
|
|
int get hashCode =>
|
|
framesSent.hashCode ^ framesReceived.hashCode ^ pttActive.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeAudioStats &&
|
|
runtimeType == other.runtimeType &&
|
|
framesSent == other.framesSent &&
|
|
framesReceived == other.framesReceived &&
|
|
pttActive == other.pttActive;
|
|
}
|
|
|
|
/// Channel as seen by Dart. Matches `chanora_protocol::ChannelInfo`
|
|
/// but with primitive `u64` ids so the Dart side gets `BigInt`s
|
|
/// without any wrapper-type ceremony.
|
|
class BridgeChannel {
|
|
/// Stable channel id.
|
|
final BigInt id;
|
|
|
|
/// Parent channel id; 0 means top-level.
|
|
final BigInt parent;
|
|
|
|
/// Display name.
|
|
final String name;
|
|
|
|
/// Server-side ordering hint.
|
|
final PlatformInt64 order;
|
|
|
|
const BridgeChannel({
|
|
required this.id,
|
|
required this.parent,
|
|
required this.name,
|
|
required this.order,
|
|
});
|
|
|
|
@override
|
|
int get hashCode =>
|
|
id.hashCode ^ parent.hashCode ^ name.hashCode ^ order.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeChannel &&
|
|
runtimeType == other.runtimeType &&
|
|
id == other.id &&
|
|
parent == other.parent &&
|
|
name == other.name &&
|
|
order == other.order;
|
|
}
|
|
|
|
/// Client as seen by Dart.
|
|
class BridgeClient {
|
|
/// Stable client id.
|
|
final BigInt id;
|
|
|
|
/// Channel id the client is currently in.
|
|
final BigInt channel;
|
|
|
|
/// Nickname.
|
|
final String name;
|
|
|
|
const BridgeClient({
|
|
required this.id,
|
|
required this.channel,
|
|
required this.name,
|
|
});
|
|
|
|
@override
|
|
int get hashCode => id.hashCode ^ channel.hashCode ^ name.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeClient &&
|
|
runtimeType == other.runtimeType &&
|
|
id == other.id &&
|
|
channel == other.channel &&
|
|
name == other.name;
|
|
}
|
|
|
|
@freezed
|
|
sealed class BridgeEvent with _$BridgeEvent {
|
|
const BridgeEvent._();
|
|
|
|
/// Initial connect succeeded, or a reconnect attempt succeeded.
|
|
const factory BridgeEvent.connected({
|
|
/// Server name reported by the server snapshot.
|
|
required String serverName,
|
|
}) = BridgeEvent_Connected;
|
|
|
|
/// Connection lost; supervisor will retry.
|
|
const factory BridgeEvent.lost({
|
|
/// Reason classification from the protocol layer.
|
|
required String reason,
|
|
}) = BridgeEvent_Lost;
|
|
|
|
/// Supervisor is sleeping before its next reconnect attempt.
|
|
const factory BridgeEvent.reconnecting({
|
|
/// 1-based attempt counter for the current outage.
|
|
required int attempt,
|
|
|
|
/// Seconds the supervisor will sleep before this attempt.
|
|
required int delaySecs,
|
|
}) = BridgeEvent_Reconnecting;
|
|
|
|
/// Session ended (user-requested disconnect or unrecoverable).
|
|
const factory BridgeEvent.disconnected({
|
|
/// Reason classification.
|
|
required String reason,
|
|
}) = BridgeEvent_Disconnected;
|
|
|
|
/// Audio engine started.
|
|
const factory BridgeEvent.audioStarted() = BridgeEvent_AudioStarted;
|
|
|
|
/// Audio engine stopped.
|
|
const factory BridgeEvent.audioStopped() = BridgeEvent_AudioStopped;
|
|
}
|
|
|
|
/// Coarse OS-reported network state. Mirrors
|
|
/// [`chanora_core::NetworkState`] across the bridge.
|
|
enum BridgeNetworkState {
|
|
/// No signal seen yet.
|
|
unknown,
|
|
|
|
/// OS reports a usable network.
|
|
online,
|
|
|
|
/// OS reports no network.
|
|
offline,
|
|
}
|
|
|
|
/// Server snapshot as seen by Dart.
|
|
class BridgeSnapshot {
|
|
/// Server name.
|
|
final String serverName;
|
|
|
|
/// Welcome banner text.
|
|
final String welcomeMessage;
|
|
|
|
/// Server platform (e.g. "Linux").
|
|
final String platform;
|
|
|
|
/// Server version string.
|
|
final String version;
|
|
|
|
/// Channels currently known.
|
|
final List<BridgeChannel> channels;
|
|
|
|
/// Clients currently known.
|
|
final List<BridgeClient> clients;
|
|
|
|
const BridgeSnapshot({
|
|
required this.serverName,
|
|
required this.welcomeMessage,
|
|
required this.platform,
|
|
required this.version,
|
|
required this.channels,
|
|
required this.clients,
|
|
});
|
|
|
|
@override
|
|
int get hashCode =>
|
|
serverName.hashCode ^
|
|
welcomeMessage.hashCode ^
|
|
platform.hashCode ^
|
|
version.hashCode ^
|
|
channels.hashCode ^
|
|
clients.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BridgeSnapshot &&
|
|
runtimeType == other.runtimeType &&
|
|
serverName == other.serverName &&
|
|
welcomeMessage == other.welcomeMessage &&
|
|
platform == other.platform &&
|
|
version == other.version &&
|
|
channels == other.channels &&
|
|
clients == other.clients;
|
|
}
|