//! quicproquo CLI client library. //! //! # KeyPackage expiry and refresh //! //! KeyPackages are single-use (consumed when someone fetches them for an invite) and the server //! may enforce a TTL (e.g. 24 hours). To stay invitable, run `qpq refresh-keypackage` //! periodically (e.g. before the server TTL) or after your KeyPackage was consumed: //! //! ```bash //! qpq refresh-keypackage --state qpq-state.bin --server 127.0.0.1:7000 //! ``` //! //! Use the same `--access-token` (or `QPQ_ACCESS_TOKEN`) as for other authenticated //! commands. See the [running-the-client](https://docs.quicproquo.dev/getting-started/running-the-client) //! docs for details. use std::sync::RwLock; use std::sync::atomic::{AtomicBool, Ordering}; use zeroize::Zeroizing; pub mod client; #[cfg(feature = "v2")] pub mod v2_commands; pub use client::commands::{ cmd_chat, cmd_check_key, cmd_create_group, cmd_demo_group, cmd_export, cmd_export_verify, cmd_fetch_key, cmd_health, cmd_health_json, cmd_invite, cmd_join, cmd_login, cmd_ping, cmd_recv, cmd_register, cmd_register_state, cmd_refresh_keypackage, cmd_register_user, cmd_send, cmd_whoami, opaque_login, receive_pending_plaintexts, whoami_json, }; pub use client::command_engine::{Command, CommandRegistry, CommandResult}; #[cfg(feature = "playbook")] pub use client::playbook::{Playbook, PlaybookReport, PlaybookRunner}; pub use client::repl::run_repl; pub use client::rpc::{connect_node, connect_node_opt, create_channel, enqueue, fetch_wait, resolve_user}; // ── ClientContext: structured holder for session-scoped auth + TLS config ──── /// Holds the authentication credentials and TLS policy for a client session. /// /// Prefer constructing a `ClientContext` and passing it explicitly where /// possible. The global `AUTH_CONTEXT` / `INSECURE_SKIP_VERIFY` statics /// delegate to a `ClientContext` under the hood and exist only for backward /// compatibility with call-sites that have not yet been migrated. pub struct ClientContext { auth: RwLock>, insecure_skip_verify: AtomicBool, } impl ClientContext { /// Create a new context with no auth and TLS verification enabled. pub fn new() -> Self { Self { auth: RwLock::new(None), insecure_skip_verify: AtomicBool::new(false), } } /// Create a context pre-loaded with auth credentials. pub fn with_auth(auth: ClientAuth) -> Self { Self { auth: RwLock::new(Some(auth)), insecure_skip_verify: AtomicBool::new(false), } } /// Set (or replace) the auth credentials. /// /// # Panics /// Panics if the RwLock is poisoned (a thread panicked while holding it). /// A poisoned lock indicates unrecoverable state corruption. #[allow(clippy::expect_used)] pub fn set_auth(&self, ctx: ClientAuth) { let mut guard = self.auth.write().expect("ClientContext auth lock poisoned"); *guard = Some(ctx); } /// Read the current auth snapshot (cloned). /// /// # Panics /// Panics if the RwLock is poisoned (a thread panicked while holding it). /// A poisoned lock indicates unrecoverable state corruption. #[allow(clippy::expect_used)] pub fn get_auth(&self) -> Option { let guard = self.auth.read().expect("ClientContext auth lock poisoned"); guard.clone() } /// Returns true if auth credentials have been set. /// /// # Panics /// Panics if the RwLock is poisoned (a thread panicked while holding it). /// A poisoned lock indicates unrecoverable state corruption. #[allow(clippy::expect_used)] pub fn is_authenticated(&self) -> bool { let guard = self.auth.read().expect("ClientContext auth lock poisoned"); guard.is_some() } /// Enable or disable insecure TLS mode. pub fn set_insecure_skip_verify(&self, enabled: bool) { self.insecure_skip_verify.store(enabled, Ordering::Relaxed); } /// Read the current insecure-skip-verify flag. pub fn insecure_skip_verify(&self) -> bool { self.insecure_skip_verify.load(Ordering::Relaxed) } } impl Default for ClientContext { fn default() -> Self { Self::new() } } // ── Global statics (thin wrappers, kept for backward compat) ───────────────── /// Global auth context — delegates to a process-wide `ClientContext`. /// Prefer passing `&ClientContext` explicitly in new code. pub(crate) static AUTH_CONTEXT: RwLock> = RwLock::new(None); /// When `true`, [`connect_node`] skips TLS certificate verification. /// Prefer `ClientContext::set_insecure_skip_verify` in new code. pub(crate) static INSECURE_SKIP_VERIFY: AtomicBool = AtomicBool::new(false); /// Enable or disable insecure (no-verify) TLS mode globally. /// /// **Development only.** When enabled, all outgoing connections skip certificate /// verification, making them vulnerable to MITM attacks. pub fn set_insecure_skip_verify(enabled: bool) { INSECURE_SKIP_VERIFY.store(enabled, Ordering::Relaxed); } #[derive(Clone, Debug)] pub struct ClientAuth { pub(crate) version: u16, /// Bearer or OPAQUE session token. Zeroized on drop. (M8) pub(crate) access_token: Zeroizing>, pub(crate) device_id: Vec, } impl ClientAuth { /// Build a client auth context from optional token and device id. pub fn from_parts(access_token: String, device_id: Option) -> Self { let token = access_token.into_bytes(); let device = device_id.unwrap_or_default().into_bytes(); Self { version: 1, access_token: Zeroizing::new(token), device_id: device, } } /// Build from raw token bytes (e.g. a 32-byte OPAQUE session token). pub fn from_raw(raw_token: Vec, device_id: Option) -> Self { let device = device_id.unwrap_or_default().into_bytes(); Self { version: 1, access_token: Zeroizing::new(raw_token), device_id: device, } } } /// Set (or replace) the global auth context. /// /// # Panics /// Panics if the RwLock is poisoned (a thread panicked while holding it). /// A poisoned lock indicates unrecoverable state corruption. #[allow(clippy::expect_used)] pub fn init_auth(ctx: ClientAuth) { let mut guard = AUTH_CONTEXT.write().expect("AUTH_CONTEXT poisoned"); *guard = Some(ctx); }