feat: wire traffic resistance, implement v2 CLI commands, add auth expiry detection

Server:
- Wire traffic resistance decoy generator into main.rs startup behind
  --traffic-resistance flag + --decoy-interval-ms config (feature-gated)

Client:
- Implement v2 CLI one-shot commands: send, recv, dm, group create, group invite
  All previously printed "coming soon" — now fully functional with MLS state
  restoration, peer resolution, KeyPackage fetch, and MLS encryption pipeline

SDK:
- Add SdkError::SessionExpired variant + is_auth_expired() helper for
  detecting expired session tokens (RpcStatus::Unauthorized)
- Add ClientEvent::AuthExpired for UI-layer session expiry notification
This commit is contained in:
2026-04-05 00:03:12 +02:00
parent f58ce2529d
commit a856f9bb53
4 changed files with 175 additions and 21 deletions

View File

@@ -24,6 +24,21 @@ pub enum SdkError {
#[error("storage error: {0}")]
Storage(String),
#[error("session expired — re-login required")]
SessionExpired,
#[error("{0}")]
Other(#[from] anyhow::Error),
}
impl SdkError {
/// Returns `true` if the error indicates the session token has expired
/// and the user needs to re-authenticate.
pub fn is_auth_expired(&self) -> bool {
matches!(self, SdkError::SessionExpired)
|| matches!(self, SdkError::Rpc(quicprochat_rpc::error::RpcError::Server {
status: quicprochat_rpc::error::RpcStatus::Unauthorized,
..
}))
}
}

View File

@@ -82,6 +82,10 @@ pub enum ClientEvent {
received_seq: u64,
},
/// Session token expired — the user must re-authenticate.
/// Emitted when an RPC returns Unauthorized after a previously valid session.
AuthExpired,
/// A peer's identity key changed — possible re-registration, new device,
/// or MITM attack. The UI MUST alert the user (like Signal's "safety number changed").
IdentityKeyChanged {
@@ -241,6 +245,7 @@ mod tests {
expected_seq: 0,
received_seq: 1,
},
ClientEvent::AuthExpired,
ClientEvent::IdentityKeyChanged {
username: "u".into(),
old_fingerprint: "old".into(),
@@ -261,6 +266,6 @@ mod tests {
for event in &events {
let _ = event.clone();
}
assert_eq!(events.len(), 20);
assert_eq!(events.len(), 21);
}
}