feat: Phase 9 — developer experience, extensibility, and community growth
New crates: - quicproquo-bot: Bot SDK with polling API + JSON pipe mode - quicproquo-kt: Key Transparency Merkle log (RFC 9162 subset) - quicproquo-plugin-api: no_std C-compatible plugin vtable API - quicproquo-gen: scaffolding tool (qpq-gen plugin/bot/rpc/hook) Server features: - ServerHooks trait wired into all RPC handlers (enqueue, fetch, auth, channel, registration) with plugin rejection support - Dynamic plugin loader (libloading) with --plugin-dir config - Delivery proof canary tokens (Ed25519 server signatures on enqueue) - Key Transparency Merkle log with inclusion proofs on resolveUser Core library: - Safety numbers (60-digit HMAC-SHA256 key verification codes) - Verifiable transcript archive (CBOR + ChaCha20-Poly1305 + hash chain) - Delivery proof verification utility - Criterion benchmarks (hybrid KEM, MLS, identity, sealed sender, padding) Client: - /verify REPL command for out-of-band key verification - Full-screen TUI via Ratatui (feature-gated --features tui) - qpq export / qpq export-verify CLI subcommands - KT inclusion proof verification on user resolution Also: ROADMAP Phase 9 added, bot SDK docs, server hooks docs, crate-responsibilities updated, example plugins (rate_limit, logging).
This commit is contained in:
@@ -2,14 +2,17 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use quicproquo_client::{
|
||||
cmd_chat, cmd_check_key, cmd_create_group, cmd_demo_group, cmd_fetch_key, cmd_health,
|
||||
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, init_auth, run_repl,
|
||||
ClientAuth,
|
||||
cmd_chat, cmd_check_key, cmd_create_group, cmd_demo_group, cmd_export, cmd_export_verify,
|
||||
cmd_fetch_key, cmd_health, 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,
|
||||
init_auth, run_repl, ClientAuth,
|
||||
};
|
||||
#[cfg(feature = "tui")]
|
||||
use quicproquo_client::client::tui::run_tui;
|
||||
|
||||
// ── CLI ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -310,6 +313,26 @@ enum Command {
|
||||
no_server: bool,
|
||||
},
|
||||
|
||||
/// Full-screen Ratatui TUI (requires --features tui).
|
||||
/// Channels sidebar, scrollable message view, and inline input bar.
|
||||
#[cfg(feature = "tui")]
|
||||
Tui {
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "qpq-state.bin",
|
||||
env = "QPQ_STATE"
|
||||
)]
|
||||
state: PathBuf,
|
||||
#[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
|
||||
server: String,
|
||||
/// OPAQUE username for automatic registration/login.
|
||||
#[arg(long, env = "QPQ_USERNAME")]
|
||||
username: Option<String>,
|
||||
/// OPAQUE password (prompted securely if --username is set but --password is not).
|
||||
#[arg(long, env = "QPQ_PASSWORD")]
|
||||
password: Option<String>,
|
||||
},
|
||||
|
||||
/// Interactive 1:1 chat: type to send, incoming messages printed as [peer] <msg>. Ctrl+D to exit.
|
||||
/// In a two-person group, peer is chosen automatically; use --peer-key only with 3+ members.
|
||||
Chat {
|
||||
@@ -328,6 +351,39 @@ enum Command {
|
||||
#[arg(long, default_value_t = 500)]
|
||||
poll_interval_ms: u64,
|
||||
},
|
||||
|
||||
/// Export a conversation's message history to an encrypted, tamper-evident transcript file.
|
||||
///
|
||||
/// The output file uses Argon2id + ChaCha20-Poly1305 encryption with a SHA-256 hash chain
|
||||
/// linking every record. Use `qpq export verify` to check chain integrity without decrypting.
|
||||
Export {
|
||||
/// Path to the conversation database (.convdb file).
|
||||
#[arg(long, default_value = "qpq-convdb.sqlite", env = "QPQ_CONV_DB")]
|
||||
conv_db: PathBuf,
|
||||
|
||||
/// Conversation ID to export (32 hex chars = 16 bytes).
|
||||
#[arg(long)]
|
||||
conv_id: String,
|
||||
|
||||
/// Output path for the .qpqt transcript file.
|
||||
#[arg(long, default_value = "transcript.qpqt")]
|
||||
output: PathBuf,
|
||||
|
||||
/// Password used to encrypt the transcript (separate from the state/DB password).
|
||||
#[arg(long, env = "QPQ_TRANSCRIPT_PASSWORD")]
|
||||
transcript_password: Option<String>,
|
||||
|
||||
/// Password for the encrypted conversation database (if any).
|
||||
#[arg(long, env = "QPQ_STATE_PASSWORD")]
|
||||
db_password: Option<String>,
|
||||
},
|
||||
|
||||
/// Verify the hash-chain integrity of a transcript file without decrypting content.
|
||||
ExportVerify {
|
||||
/// Path to the .qpqt transcript file to verify.
|
||||
#[arg(long)]
|
||||
input: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -361,9 +417,12 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
// For the REPL, defer init_auth so it can resolve its own token via OPAQUE.
|
||||
// For the REPL and TUI, defer init_auth so they can resolve their own token via OPAQUE.
|
||||
// For all other subcommands, initialize auth immediately.
|
||||
#[cfg(not(feature = "tui"))]
|
||||
let is_repl = matches!(args.command, None | Some(Command::Repl { .. }));
|
||||
#[cfg(feature = "tui")]
|
||||
let is_repl = matches!(args.command, None | Some(Command::Repl { .. }) | Some(Command::Tui { .. }));
|
||||
if !is_repl {
|
||||
let auth_ctx = ClientAuth::from_parts(args.access_token.clone(), args.device_id.clone());
|
||||
init_auth(auth_ctx);
|
||||
@@ -615,5 +674,53 @@ async fn main() -> anyhow::Result<()> {
|
||||
))
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "tui")]
|
||||
Command::Tui {
|
||||
state,
|
||||
server,
|
||||
username,
|
||||
password,
|
||||
} => {
|
||||
let state = derive_state_path(state, username.as_deref());
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local
|
||||
.run_until(run_tui(
|
||||
&state,
|
||||
&server,
|
||||
&args.ca_cert,
|
||||
&args.server_name,
|
||||
state_pw,
|
||||
username.as_deref(),
|
||||
password.as_deref(),
|
||||
&args.access_token,
|
||||
args.device_id.as_deref(),
|
||||
))
|
||||
.await
|
||||
}
|
||||
Command::Export {
|
||||
conv_db,
|
||||
conv_id,
|
||||
output,
|
||||
transcript_password,
|
||||
db_password,
|
||||
} => {
|
||||
// Prompt for transcript password if not provided.
|
||||
let tp = match transcript_password {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
eprint!("Transcript password: ");
|
||||
rpassword::read_password()
|
||||
.context("failed to read transcript password")?
|
||||
}
|
||||
};
|
||||
cmd_export(
|
||||
&conv_db,
|
||||
&conv_id,
|
||||
&output,
|
||||
&tp,
|
||||
db_password.as_deref().or(state_pw),
|
||||
)
|
||||
}
|
||||
Command::ExportVerify { input } => cmd_export_verify(&input),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user