feat: Sprint 1 — production hardening, TLS lifecycle, CI coverage, lint cleanup

- Fix 3 client panics: replace .unwrap()/.expect() with proper error
  handling in rpc.rs (AUTH_CONTEXT lock), repl.rs (pending_member),
  and retry.rs (last_err)
- Add --danger-accept-invalid-certs flag with InsecureServerCertVerifier
  for development TLS bypass, plus mdBook TLS documentation
- Add CI coverage job (cargo-tarpaulin) and Docker build validation
  to GitHub Actions workflow, plus README CI badge
- Add [workspace.lints] config, fix 46 clippy warnings across 8 crates,
  zero warnings on all buildable crates
- Update Dockerfile for all 11 workspace members
This commit is contained in:
2026-03-03 23:19:11 +01:00
parent dc4e4e49a0
commit 612b06aa8e
33 changed files with 388 additions and 67 deletions

View File

@@ -284,6 +284,7 @@ async fn ensure_server(
// ── REPL entry point ─────────────────────────────────────────────────────────
#[allow(clippy::too_many_arguments)]
pub async fn run_repl(
state_path: &Path,
server: &str,
@@ -497,6 +498,7 @@ async fn auto_upload_keys(
}
/// Determine the access token, performing OPAQUE registration/login as needed.
#[allow(clippy::too_many_arguments)]
async fn resolve_access_token(
state_path: &Path,
server: &str,
@@ -715,13 +717,11 @@ fn cmd_list(session: &SessionState) -> anyhow::Result<()> {
fn cmd_switch(session: &mut SessionState, target: &str) -> anyhow::Result<()> {
let target = target.trim();
let conv = if target.starts_with('@') {
let username = &target[1..];
let conv = if let Some(username) = target.strip_prefix('@') {
session.conv_store.list_conversations()?.into_iter().find(|c| {
matches!(&c.kind, ConversationKind::Dm { peer_username: Some(u), .. } if u == username)
})
} else if target.starts_with('#') {
let name = &target[1..];
} else if let Some(name) = target.strip_prefix('#') {
session.conv_store.find_group_by_name(name)?
} else {
// Try as display name
@@ -861,7 +861,7 @@ async fn cmd_dm(
display_name: format!("@{username}"),
mls_group_blob: member
.group_ref()
.map(|g| bincode::serialize(g))
.map(bincode::serialize)
.transpose()
.context("serialize group")?,
keystore_blob: None,
@@ -905,7 +905,7 @@ fn cmd_create_group(session: &mut SessionState, name: &str) -> anyhow::Result<()
display_name: format!("#{name}"),
mls_group_blob: member
.group_ref()
.map(|g| bincode::serialize(g))
.map(bincode::serialize)
.transpose()
.context("serialize group")?,
keystore_blob: None,
@@ -1099,7 +1099,7 @@ async fn cmd_join(
// Try to process with existing groups first
let mut handled = false;
for (_cid, member) in &mut session.members {
for member in session.members.values_mut() {
match member.receive_message(&mls_payload) {
Ok(_) => { handled = true; break; }
Err(_) => continue,
@@ -1147,7 +1147,7 @@ async fn cmd_join(
display_name: format!("#{display}"),
mls_group_blob: new_member
.group_ref()
.map(|g| bincode::serialize(g))
.map(bincode::serialize)
.transpose()
.context("serialize joined group")?,
keystore_blob: None,
@@ -1570,7 +1570,13 @@ async fn try_auto_join(
}
// Take ownership of the pending member.
let member = session.pending_member.take().unwrap();
let member = match session.pending_member.take() {
Some(m) => m,
None => {
tracing::error!("pending_member disappeared after successful join");
return false;
}
};
let member_keys = member.member_identities();
// Figure out the peer (any member that isn't us).