chore: fix all clippy warnings across workspace

This commit is contained in:
2026-03-04 14:13:58 +01:00
parent 4013b223ff
commit 5a66c2e954
43 changed files with 2124 additions and 57 deletions

1
Cargo.lock generated
View File

@@ -4387,6 +4387,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"tempfile",
"thiserror 1.0.69",
"tokio",
"tracing",

891
ROADMAP.html Normal file
View File

@@ -0,0 +1,891 @@
<!DOCTYPE HTML>
<html lang="en" class="navy sidebar-visible" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Full Roadmap (Phases 18) - quicproquo</title>
<!-- Custom HTML head -->
<meta name="description" content="End-to-end encrypted group messaging over QUIC + TLS 1.3 + MLS (RFC 9420)">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="favicon-de23e50b.svg">
<link rel="shortcut icon" href="favicon-8114d1fc.png">
<link rel="stylesheet" href="css/variables-8adf115d.css">
<link rel="stylesheet" href="css/general-2459343d.css">
<link rel="stylesheet" href="css/chrome-ae938929.css">
<link rel="stylesheet" href="css/print-9e4910d8.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="fonts/fonts-9644e21d.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" id="mdbook-highlight-css" href="highlight-493f70e1.css">
<link rel="stylesheet" id="mdbook-tomorrow-night-css" href="tomorrow-night-4c0ae647.css">
<link rel="stylesheet" id="mdbook-ayu-highlight-css" href="ayu-highlight-3fdfc3ac.css">
<!-- Custom theme stylesheets -->
<!-- Provide site root and default themes to javascript -->
<script>
const path_to_root = "";
const default_light_theme = "navy";
const default_dark_theme = "navy";
window.path_to_searchindex_js = "searchindex-92ce38c7.js";
</script>
<!-- Start loading toc.js asap -->
<script src="toc-4c7c920d.js"></script>
</head>
<body>
<div id="mdbook-help-container">
<div id="mdbook-help-popup">
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
<div>
<p>Press <kbd></kbd> or <kbd></kbd> to navigate between chapters</p>
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
<p>Press <kbd>?</kbd> to show this help</p>
<p>Press <kbd>Esc</kbd> to hide this help</p>
</div>
</div>
</div>
<div id="mdbook-body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
let theme = localStorage.getItem('mdbook-theme');
let sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
let theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('navy')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="mdbook-sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
let sidebar = null;
const sidebar_toggle = document.getElementById("mdbook-sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
sidebar_toggle.checked = false;
}
if (sidebar === 'visible') {
sidebar_toggle.checked = true;
} else {
html.classList.remove('sidebar-visible');
}
</script>
<nav id="mdbook-sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
</noscript>
<div id="mdbook-sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="mdbook-page-wrapper" class="page-wrapper">
<div class="page">
<div id="mdbook-menu-bar-hover-placeholder"></div>
<div id="mdbook-menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="mdbook-sidebar-toggle" class="icon-button" for="mdbook-sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="mdbook-sidebar">
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></span>
</label>
<button id="mdbook-theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="mdbook-theme-list">
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M371.3 367.1c27.3-3.9 51.9-19.4 67.2-42.9L600.2 74.1c12.6-19.5 9.4-45.3-7.6-61.2S549.7-4.4 531.1 9.6L294.4 187.2c-24 18-38.2 46.1-38.4 76.1L371.3 367.1zm-19.6 25.4l-116-104.4C175.9 290.3 128 339.6 128 400c0 3.9 .2 7.8 .6 11.6c1.8 17.5-10.2 36.4-27.8 36.4H96c-17.7 0-32 14.3-32 32s14.3 32 32 32H240c61.9 0 112-50.1 112-112c0-2.5-.1-5-.2-7.5z"/></svg></span>
</button>
<ul id="mdbook-theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-default_theme">Auto</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-ayu">Ayu</button></li>
</ul>
<button id="mdbook-search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="mdbook-searchbar">
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z"/></svg></span>
</button>
</div>
<h1 class="menu-title">quicproquo</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<span class=fa-svg id="print-button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M128 0C92.7 0 64 28.7 64 64v96h64V64H354.7L384 93.3V160h64V93.3c0-17-6.7-33.3-18.7-45.3L400 18.7C388 6.7 371.7 0 354.7 0H128zM384 352v32 64H128V384 368 352H384zm64 32h32c17.7 0 32-14.3 32-32V256c0-35.3-28.7-64-64-64H64c-35.3 0-64 28.7-64 64v96c0 17.7 14.3 32 32 32H64v64c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V384zm-16-88c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24z"/></svg></span>
</a>
</div>
</div>
<div id="mdbook-search-wrapper" class="hidden">
<form id="mdbook-searchbar-outer" class="searchbar-outer">
<div class="search-wrapper">
<input type="search" id="mdbook-searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="mdbook-searchresults-outer" aria-describedby="searchresults-header">
<div class="spinner-wrapper">
<span class=fa-svg id="fa-spin"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M304 48c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zm0 416c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM48 304c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48zm464-48c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM142.9 437c18.7-18.7 18.7-49.1 0-67.9s-49.1-18.7-67.9 0s-18.7 49.1 0 67.9s49.1 18.7 67.9 0zm0-294.2c18.7-18.7 18.7-49.1 0-67.9S93.7 56.2 75 75s-18.7 49.1 0 67.9s49.1 18.7 67.9 0zM369.1 437c18.7 18.7 49.1 18.7 67.9 0s18.7-49.1 0-67.9s-49.1-18.7-67.9 0s-18.7 49.1 0 67.9z"/></svg></span>
</div>
</div>
</form>
<div id="mdbook-searchresults-outer" class="searchresults-outer hidden">
<div id="mdbook-searchresults-header" class="searchresults-header"></div>
<ul id="mdbook-searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('mdbook-sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('mdbook-sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#mdbook-sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="mdbook-content" class="content">
<main>
<h1 id="roadmap--quicproquo"><a class="header" href="#roadmap--quicproquo">Roadmap — quicproquo</a></h1>
<blockquote>
<p>From proof-of-concept to production-grade E2E encrypted messaging.</p>
<p>Each phase is designed to be tackled sequentially. Items within a phase
can be parallelised. Check the box when done.</p>
</blockquote>
<hr>
<h2 id="phase-1--production-hardening-critical"><a class="header" href="#phase-1--production-hardening-critical">Phase 1 — Production Hardening (Critical)</a></h2>
<p>Eliminate all crash paths, enforce secure defaults, fix deployment blockers.</p>
<ul>
<li>
<p><input disabled="" type="checkbox"> <strong>1.1 Remove <code>.unwrap()</code> / <code>.expect()</code> from production paths</strong></p>
<ul>
<li>Replace <code>AUTH_CONTEXT.read().expect()</code> in client RPC with proper <code>Result</code></li>
<li>Replace <code>"0.0.0.0:0".parse().unwrap()</code> in client with fallible parse</li>
<li>Replace <code>Mutex::lock().unwrap()</code> in server storage with <code>.map_err()</code></li>
<li>Audit: <code>grep -rn 'unwrap()\|expect(' crates/</code> outside <code>#[cfg(test)]</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>1.2 Enforce secure defaults in production mode</strong></p>
<ul>
<li>Reject startup if <code>QPQ_PRODUCTION=true</code> and <code>auth_token</code> is empty or <code>"devtoken"</code></li>
<li>Require non-empty <code>db_key</code> when using SQL backend in production</li>
<li>Refuse to auto-generate TLS certs in production mode (require existing cert+key)</li>
<li>Already partially implemented — verify and harden the validation in <code>config.rs</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>1.3 Fix <code>.gitignore</code></strong></p>
<ul>
<li>Add <code>data/</code>, <code>*.der</code>, <code>*.pem</code>, <code>*.db</code>, <code>*.bin</code> (state files), <code>*.ks</code> (keystores)</li>
<li>Verify no secrets are already tracked: <code>git ls-files data/ *.der *.db</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>1.4 Fix Dockerfile</strong></p>
<ul>
<li>Sync workspace members (handle excluded <code>p2p</code> crate)</li>
<li>Create dedicated user/group instead of <code>nobody</code></li>
<li>Set writable <code>QPQ_DATA_DIR</code> with correct permissions</li>
<li>Test: <code>docker build . &amp;&amp; docker run --rm -it qpq-server --help</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>1.5 TLS certificate lifecycle</strong></p>
<ul>
<li>Document CA-signed cert setup (Lets Encrypt / custom CA)</li>
<li>Add <code>--tls-required</code> flag that refuses to start without valid cert</li>
<li>Log clear warning when using self-signed certs</li>
<li>Document certificate rotation procedure</li>
</ul>
</li>
</ul>
<hr>
<h2 id="phase-2--test--ci-maturity"><a class="header" href="#phase-2--test--ci-maturity">Phase 2 — Test &amp; CI Maturity</a></h2>
<p>Build confidence before adding features.</p>
<ul>
<li>
<p><input disabled="" type="checkbox"> <strong>2.1 Expand E2E test coverage</strong></p>
<ul>
<li>Auth failure scenarios (wrong password, expired token, invalid token)</li>
<li>Message ordering verification (send N messages, verify seq numbers)</li>
<li>Concurrent clients (3+ members in group, simultaneous send/recv)</li>
<li>OPAQUE registration + login full flow</li>
<li>Queue full behavior (&gt;1000 messages)</li>
<li>Rate limiting behavior (&gt;100 enqueues/minute)</li>
<li>Reconnection after server restart</li>
<li>KeyPackage exhaustion (fetch when none available)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>2.2 Add unit tests for untested paths</strong></p>
<ul>
<li>Client retry logic (exponential backoff, jitter, retriable classification)</li>
<li>REPL input parsing edge cases (empty input, special characters, <code>/</code> commands)</li>
<li>State file encryption/decryption round-trip with bad password</li>
<li>Token cache expiry</li>
<li>Conversation store migrations</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>2.3 CI hardening</strong></p>
<ul>
<li>Add <code>.github/CODEOWNERS</code> (crypto, auth, wire-format require 2 reviewers)</li>
<li>Ensure <code>cargo deny check</code> runs on every PR (already in CI — verify)</li>
<li>Add <code>cargo audit</code> as blocking check (already in CI — verify)</li>
<li>Add coverage reporting (tarpaulin or llvm-cov)</li>
<li>Add CI job for Docker build validation</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>2.4 Clean up build warnings</strong></p>
<ul>
<li>Fix Capn Proto generated <code>unused_parens</code> warnings</li>
<li>Remove dead code / unused imports</li>
<li>Address <code>openmls</code> future-incompat warnings</li>
<li>Target: <code>cargo clippy --workspace -- -D warnings</code> passes clean</li>
</ul>
</li>
</ul>
<hr>
<h2 id="phase-3--client-sdks-native-quic--capn-proto-everywhere"><a class="header" href="#phase-3--client-sdks-native-quic--capn-proto-everywhere">Phase 3 — Client SDKs: Native QUIC + Capn Proto Everywhere</a></h2>
<p><strong>No REST gateway. No protocol dilution.</strong> The <code>.capnp</code> schemas are the
interface definition. Every SDK speaks native QUIC + Capn Proto. The
project name stays honest.</p>
<h3 id="why-this-matters"><a class="header" href="#why-this-matters">Why this matters</a></h3>
<p>The name is <strong>quic</strong>n<strong>proto</strong>chat — the protocol IS the product. Instead
of adding an HTTP translation layer that loses zero-copy performance and
adds base64 overhead, we invest in making the native protocol accessible
from every language that has QUIC + Capn Proto support, and provide
WASM/FFI for the crypto layer.</p>
<h3 id="architecture"><a class="header" href="#architecture">Architecture</a></h3>
<pre><code> Server: QUIC + Cap'n Proto (single protocol, no gateway)
Client SDKs:
┌─── Rust quinn + capnp-rpc (existing, reference impl)
├─── Go quic-go + go-capnp (native, high confidence)
├─── Python aioquic + pycapnp (native QUIC, manual framing)
├─── C/C++ msquic/ngtcp2 + capnproto (reference impl, full RPC)
└─── Browser WebTransport + capnp (WASM) (QUIC transport, no HTTP needed)
Crypto layer (client-side MLS, shared across all SDKs):
┌─── Rust crate (native, existing)
├─── WASM module (browsers, Node.js, Deno)
└─── C FFI (Swift, Kotlin, Python, Go via cgo)
</code></pre>
<h3 id="language-support-reality-check"><a class="header" href="#language-support-reality-check">Language support reality check</a></h3>
<div class="table-wrapper">
<table>
<thead>
<tr><th>Language</th><th>QUIC</th><th>Capn Proto</th><th>RPC</th><th>Confidence</th></tr>
</thead>
<tbody>
<tr><td><strong>Rust</strong></td><td>quinn ✅</td><td>capnp-rpc ✅</td><td>Full ✅</td><td>Existing</td></tr>
<tr><td><strong>Go</strong></td><td>quic-go ✅</td><td>go-capnp ✅</td><td>Level 1 ✅</td><td>High</td></tr>
<tr><td><strong>Python</strong></td><td>aioquic ✅</td><td>pycapnp ⚠️</td><td>Manual framing</td><td>Medium</td></tr>
<tr><td><strong>C/C++</strong></td><td>msquic/ngtcp2 ✅</td><td>capnproto ✅</td><td>Full ✅</td><td>High</td></tr>
<tr><td><strong>Browser</strong></td><td>WebTransport ✅</td><td>WASM ✅</td><td>Via WASM bridge</td><td>Medium</td></tr>
</tbody>
</table>
</div>
<h3 id="implementation"><a class="header" href="#implementation">Implementation</a></h3>
<ul>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>3.1 Go SDK (<code>quicproquo-go</code>)</strong></p>
<ul>
<li>Generated Go types from <code>node.capnp</code> (6487-line codegen, all 24 RPC methods)</li>
<li>QUIC transport via <code>quic-go</code> with TLS 1.3 + ALPN <code>"capnp"</code></li>
<li>High-level <code>qpq</code> package: Connect, Health, ResolveUser, CreateChannel, Send/SendWithTTL, Receive/ReceiveWait, DeleteAccount, OPAQUE auth</li>
<li>Example CLI in <code>sdks/go/cmd/example/</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>3.2 Python SDK (<code>quicproquo-py</code>)</strong></p>
<ul>
<li>QUIC transport: <code>aioquic</code> with custom Capn Proto stream handler</li>
<li>Capn Proto serialization: <code>pycapnp</code> for message types</li>
<li>Manual RPC framing: length-prefixed request/response over QUIC stream</li>
<li>Async/await API matching the Rust client patterns</li>
<li>Crypto: PyO3 bindings to <code>quicproquo-core</code> for MLS operations</li>
<li>Publish: PyPI <code>quicproquo</code></li>
<li>Example: async bot client</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>3.3 C FFI layer (<code>quicproquo-ffi</code>)</strong></p>
<ul>
<li><code>crates/quicproquo-ffi</code> with 7 extern “C” functions: connect, login, send, receive, disconnect, last_error, free_string</li>
<li>Builds as <code>libquicproquo_ffi.so</code> / <code>.dylib</code> / <code>.dll</code></li>
<li>Python ctypes wrapper in <code>examples/python/qpq_client.py</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>3.4 WASM compilation of <code>quicproquo-core</code></strong></p>
<ul>
<li><code>wasm-pack build</code> target producing 175 KB WASM bundle (LTO + opt-level=s)</li>
<li>13 <code>wasm_bindgen</code> functions: Ed25519 identity, hybrid KEM, safety numbers, sealed sender, padding</li>
<li>Browser-ready with <code>crypto.getRandomValues()</code> RNG</li>
<li>Published as <code>sdks/typescript/wasm-crypto/</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>3.5 WebTransport server endpoint</strong></p>
<ul>
<li>Add HTTP/3 + WebTransport listener to server (same QUIC stack via quinn)</li>
<li>Capn Proto RPC framed over WebTransport bidirectional streams</li>
<li>Same auth, same storage, same RPC handlers — just a different stream source</li>
<li>Browsers connect via <code>new WebTransport("https://server:7443")</code></li>
<li>ALPN negotiation: <code>"h3"</code> for WebTransport, <code>"capnp"</code> for native QUIC</li>
<li>Configurable port: <code>--webtransport-listen 0.0.0.0:7443</code></li>
<li>Feature-flagged: <code>--features webtransport</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>3.6 TypeScript/JavaScript SDK (<code>@quicproquo/client</code>)</strong></p>
<ul>
<li><code>QpqClient</code> class: connect, offline, health, resolveUser, createChannel, send/sendWithTTL, receive, deleteAccount</li>
<li>WASM crypto wrapper: generateIdentity, sign/verify, hybridEncrypt/Decrypt, computeSafetyNumber, sealedSend, pad</li>
<li>WebSocket transport with request/response correlation and reconnection</li>
<li>Browser demo: interactive crypto playground + chat UI (<code>sdks/typescript/demo/index.html</code>)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>3.7 SDK documentation and schema publishing</strong></p>
<ul>
<li>Publish <code>.capnp</code> schemas as the canonical API contract</li>
<li>Document the QUIC + Capn Proto connection pattern for each language</li>
<li>Provide a “build your own SDK” guide (QUIC stream → Capn Proto RPC bootstrap)</li>
<li>Reference implementation checklist: connect, auth, upload key, enqueue, fetch</li>
</ul>
</li>
</ul>
<hr>
<h2 id="phase-4--trust--security-infrastructure"><a class="header" href="#phase-4--trust--security-infrastructure">Phase 4 — Trust &amp; Security Infrastructure</a></h2>
<p>Address the security gaps required for real-world deployment.</p>
<ul>
<li>
<p><input disabled="" type="checkbox"> <strong>4.1 Third-party cryptographic audit</strong></p>
<ul>
<li>Scope: MLS integration, OPAQUE flow, hybrid KEM, key lifecycle, zeroization</li>
<li>Firms: NCC Group, Trail of Bits, Cure53</li>
<li>Budget and timeline: typically 4-6 weeks, $50K$150K</li>
<li>Publish report publicly (builds trust)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>4.2 Key Transparency / revocation</strong></p>
<ul>
<li>Replace <code>BasicCredential</code> with X.509-based MLS credentials</li>
<li>Or: verifiable key directory (Merkle tree, auditable log)</li>
<li>Users can verify peer keys havent been substituted (MITM detection)</li>
<li>Revocation mechanism for compromised keys</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>4.3 Client authentication on Delivery Service</strong></p>
<ul>
<li>DS sender identity binding with explicit audit logging</li>
<li><code>sender_prefix</code> tracking in enqueue/batch_enqueue RPCs</li>
<li>Sender identity derived from authenticated session</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>4.4 M7 — Post-quantum MLS integration</strong></p>
<ul>
<li>Integrate hybrid KEM (X25519 + ML-KEM-768) into the OpenMLS crypto provider</li>
<li>Group key material gets post-quantum confidentiality</li>
<li>Full test suite with PQ ciphersuite</li>
<li>Ref: existing <code>hybrid_kem.rs</code> and <code>hybrid_crypto.rs</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>4.5 Username enumeration mitigation</strong></p>
<ul>
<li>5 ms timing floor on <code>resolveUser</code> responses</li>
<li>Rate limiting to prevent bulk enumeration attacks</li>
</ul>
</li>
</ul>
<hr>
<h2 id="phase-5--features--ux"><a class="header" href="#phase-5--features--ux">Phase 5 — Features &amp; UX</a></h2>
<p>Make it a product people want to use.</p>
<ul>
<li>
<p><input disabled="" type="checkbox"> <strong>5.1 Multi-device support</strong></p>
<ul>
<li>Account → multiple devices, each with own Ed25519 key + MLS KeyPackages</li>
<li>Device graph management (add device, remove device, list devices)</li>
<li>Messages delivered to all devices of a user</li>
<li><code>device_id</code> field already in Auth struct — wire it through</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>5.2 Account recovery</strong></p>
<ul>
<li>Recovery codes or backup key (encrypted, stored by user)</li>
<li>Option: server-assisted recovery with security questions (lower security)</li>
<li>MLS state re-establishment after device loss</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>5.3 Full MLS lifecycle</strong></p>
<ul>
<li>Member removal (Remove proposal → Commit → fan-out)</li>
<li>Credential update (Update proposal for key rotation)</li>
<li>Explicit proposal handling (queue proposals, batch commit)</li>
<li>Group metadata (name, description, avatar hash)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>5.4 Message editing and deletion</strong></p>
<ul>
<li><code>Edit</code> (0x06) and <code>Delete</code> (0x07) message types in <code>AppMessage</code></li>
<li><code>/edit &lt;index&gt; &lt;text&gt;</code> and <code>/delete &lt;index&gt;</code> REPL commands (own messages only)</li>
<li>Database update/removal on incoming edit/delete</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>5.5 File and media transfer</strong></p>
<ul>
<li><code>uploadBlob</code> / <code>downloadBlob</code> RPCs with 256 KB chunked streaming</li>
<li>SHA-256 content-addressable storage with hash verification</li>
<li><code>FileRef</code> (0x08) message type with blob_id, filename, file_size, mime_type</li>
<li><code>/send-file &lt;path&gt;</code> and <code>/download &lt;index&gt;</code> REPL commands with progress bars</li>
<li>50 MB max file size, automatic MIME detection via <code>mime_guess</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>5.6 Abuse prevention and moderation</strong></p>
<ul>
<li>Block user (client-side, suppress display)</li>
<li>Report message (encrypted report to admin key)</li>
<li>Admin tools: ban user, delete account, audit log</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>5.7 Offline message queue (client-side)</strong></p>
<ul>
<li>Queue messages when disconnected, send on reconnect</li>
<li>Idempotent message IDs to prevent duplicates</li>
<li>Gap detection: compare local seq with server seq</li>
</ul>
</li>
</ul>
<hr>
<h2 id="phase-6--scale--operations"><a class="header" href="#phase-6--scale--operations">Phase 6 — Scale &amp; Operations</a></h2>
<p>Prepare for real traffic.</p>
<ul>
<li>
<p><input disabled="" type="checkbox"> <strong>6.1 Distributed rate limiting</strong></p>
<ul>
<li>Current: in-memory per-process, lost on restart</li>
<li>Move to Redis or shared state for multi-node deployments</li>
<li>Sliding window with configurable thresholds</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>6.2 Multi-node / horizontal scaling</strong></p>
<ul>
<li>Stateless server design (already mostly there — state is in storage backend)</li>
<li>Shared PostgreSQL or CockroachDB backend (replace SQLite)</li>
<li>Message queue fan-out (Redis pub/sub or NATS for cross-node notification)</li>
<li>Load balancer health check via QUIC RPC <code>health()</code> or Prometheus <code>/metrics</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>6.3 Operational runbook</strong></p>
<ul>
<li>Backup / restore procedures (SQLCipher, file backend)</li>
<li>Key rotation (auth token, TLS cert, DB encryption key)</li>
<li>Incident response playbook</li>
<li>Scaling guide (when to add nodes, resource sizing)</li>
<li>Monitoring dashboard templates (Grafana + Prometheus)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>6.4 Connection draining and graceful shutdown</strong></p>
<ul>
<li>Stop accepting new connections on SIGTERM</li>
<li>Wait for in-flight RPCs (configurable timeout, default 30s)</li>
<li>Drain WebTransport sessions with close frame</li>
<li>Document expected behavior for load balancers (health → unhealthy first)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>6.5 Request-level timeouts</strong></p>
<ul>
<li>Per-RPC timeout (prevent slow clients from holding resources)</li>
<li>Database query timeout</li>
<li>Overall request deadline propagation</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>6.6 Observability enhancements</strong></p>
<ul>
<li>Request correlation IDs (trace across RPC → storage)</li>
<li>Storage operation latency metrics</li>
<li>Per-endpoint latency histograms</li>
<li>Structured audit log to persistent storage (not just stdout)</li>
<li>OpenTelemetry integration</li>
</ul>
</li>
</ul>
<hr>
<h2 id="phase-7--platform-expansion--research"><a class="header" href="#phase-7--platform-expansion--research">Phase 7 — Platform Expansion &amp; Research</a></h2>
<p>Long-term vision for wide adoption.</p>
<ul>
<li>
<p><input disabled="" type="checkbox"> <strong>7.1 Mobile clients (iOS + Android)</strong></p>
<ul>
<li>Use C FFI (Phase 3.3) for crypto + transport (single library)</li>
<li>Push notifications via APNs / FCM (server sends notification on enqueue)</li>
<li>Background QUIC connection for message polling</li>
<li>Biometric auth for local key storage (Keychain / Android Keystore)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>7.2 Web client (browser)</strong></p>
<ul>
<li>Use WASM (Phase 3.4) for crypto</li>
<li>Use WebTransport (Phase 3.5) for native QUIC transport</li>
<li>Capn Proto via WASM bridge (Phase 3.6)</li>
<li>IndexedDB for local state persistence</li>
<li>Service Worker for background notifications</li>
<li>Progressive Web App (PWA) support</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>7.3 Federation</strong></p>
<ul>
<li>Server-to-server protocol via Capn Proto RPC over QUIC (see <code>federation.capnp</code>)</li>
<li><code>relayEnqueue</code>, <code>proxyFetchKeyPackage</code>, <code>federationHealth</code> methods</li>
<li>Identity resolution across federated servers</li>
<li>MLS group spanning multiple servers</li>
<li>Trust model for federated deployments</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>7.4 Sealed Sender</strong></p>
<ul>
<li>Sender identity inside MLS ciphertext only (server cant see who sent)</li>
<li><code>sealed_sender</code> module in quicproquo-core with seal/unseal API</li>
<li>WASM-accessible via <code>wasm_bindgen</code> for browser use</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>7.5 Additional language SDKs</strong></p>
<ul>
<li>Java/Kotlin: JNI bindings to C FFI (Phase 3.3) + native QUIC (netty-quic)</li>
<li>Swift: Swift wrapper over C FFI + Network.framework QUIC</li>
<li>Ruby: FFI bindings via <code>quicproquo-ffi</code></li>
<li>Evaluate demand-driven — only build SDKs people request</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>7.6 P2P / NAT traversal</strong></p>
<ul>
<li>Direct peer-to-peer via iroh (foundation exists in <code>quicproquo-p2p</code>)</li>
<li>Server as fallback relay only</li>
<li>Reduces latency and single-point-of-failure</li>
<li>Ref: <code>FUTURE-IMPROVEMENTS.md § 6.1</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>7.7 Traffic analysis resistance</strong></p>
<ul>
<li>Padding messages to uniform size</li>
<li>Decoy traffic to mask timing patterns</li>
<li>Optional Tor/I2P routing for IP privacy</li>
<li>Ref: <code>FUTURE-IMPROVEMENTS.md § 5.4, 6.3</code></li>
</ul>
</li>
</ul>
<hr>
<h2 id="phase-8--freifunk--community-mesh-networking"><a class="header" href="#phase-8--freifunk--community-mesh-networking">Phase 8 — Freifunk / Community Mesh Networking</a></h2>
<p>Make qpq a first-class citizen on decentralised, community-operated wireless
networks (Freifunk, BATMAN-adv/Babel routing, OpenWrt). Multiple qpq nodes form
a federated mesh; clients auto-discover nearby nodes via mDNS; the network
functions without any central infrastructure or internet uplink.</p>
<h3 id="architecture-1"><a class="header" href="#architecture-1">Architecture</a></h3>
<pre><code> Client A ─── mDNS discovery ──► nearby qpq node (LAN / mesh)
Cap'n Proto federation
remote qpq node (across mesh)
</code></pre>
<ul>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>F0 — Re-include <code>quicproquo-p2p</code> in workspace; fix ALPN strings</strong></p>
<ul>
<li>Moved <code>crates/quicproquo-p2p</code> from <code>exclude</code> back into <code>[workspace] members</code></li>
<li>Fixed ALPN <code>b"quicnprotochat/p2p/1"</code><code>b"quicproquo/p2p/1"</code> (breaking wire change)</li>
<li>Fixed federation ALPN <code>b"qnpc-fed"</code><code>b"quicproquo/federation/1"</code></li>
<li>Feature-gated behind <code>--features mesh</code> on client (keeps iroh out of default builds)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>F1 — Federation routing in message delivery</strong></p>
<ul>
<li><code>handle_enqueue</code> and <code>handle_batch_enqueue</code> call <code>federation::routing::resolve_destination()</code></li>
<li>Recipients with a remote home server are relayed via <code>FederationClient::relay_enqueue()</code></li>
<li>mTLS mutual authentication between nodes (both present client certs, validated against shared CA)</li>
<li>Config: <code>QPQ_FEDERATION_LISTEN</code>, <code>QPQ_LOCAL_DOMAIN</code>, <code>QPQ_FEDERATION_CERT/KEY/CA</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>F2 — mDNS local peer discovery</strong></p>
<ul>
<li>Server announces <code>_quicproquo._udp.local.</code> on startup via <code>mdns-sd</code></li>
<li>Client: <code>MeshDiscovery::start()</code> browses for nearby nodes (feature-gated)</li>
<li>REPL commands: <code>/mesh peers</code> (scan + list), <code>/mesh server &lt;host:port&gt;</code> (note address)</li>
<li>Nodes announce: <code>ver=1</code>, <code>server=&lt;host:port&gt;</code>, <code>domain=&lt;local_domain&gt;</code> TXT records</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>F3 — Self-sovereign mesh identity</strong></p>
<ul>
<li>Ed25519 keypair-based identity independent of AS registration</li>
<li>JSON-persisted seed + known peers directory</li>
<li>Sign/verify operations for mesh authenticity (<code>crates/quicproquo-p2p/src/identity.rs</code>)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>F4 — Store-and-forward with TTL</strong></p>
<ul>
<li><code>MeshEnvelope</code> with TTL-based expiry, hop_count tracking, max_hops routing limit</li>
<li>SHA-256 deduplication ID prevents relay loops</li>
<li>Ed25519 signature verification on envelopes</li>
<li><code>MeshStore</code> in-memory queue with per-recipient capacity limits and TTL-based GC</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>F5 — Lightweight broadcast channels</strong></p>
<ul>
<li>Symmetric ChaCha20-Poly1305 encrypted channels (no MLS overhead)</li>
<li>Topic-based pub/sub via <code>BroadcastChannel</code> and <code>BroadcastManager</code></li>
<li>Subscribe/unsubscribe, create, publish API on <code>P2pNode</code></li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>F6 — Extended <code>/mesh</code> REPL commands</strong></p>
<ul>
<li><code>/mesh send &lt;peer_id&gt; &lt;msg&gt;</code> — direct P2P message via iroh</li>
<li><code>/mesh broadcast &lt;topic&gt; &lt;msg&gt;</code> — publish to broadcast channel</li>
<li><code>/mesh subscribe &lt;topic&gt;</code> — join broadcast channel</li>
<li><code>/mesh route</code> — show routing table</li>
<li><code>/mesh identity</code> — show mesh identity info</li>
<li><code>/mesh store</code> — show store-and-forward statistics</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>F7 — OpenWrt cross-compilation guide</strong></p>
<ul>
<li>Musl static builds: <code>x86_64-unknown-linux-musl</code>, <code>armv7-unknown-linux-musleabihf</code>, <code>mips-unknown-linux-musl</code></li>
<li>Strip binary: <code>--release</code> + <code>strip</code> → target size &lt; 5 MB for flash storage</li>
<li><code>opkg</code> package manifest for OpenWrt feed</li>
<li><code>procd</code> init script + <code>uci</code> config file for OpenWrt integration</li>
<li>CI job: cross-compile and size-check on every release tag</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>F8 — Traffic analysis resistance for mesh</strong></p>
<ul>
<li>Uniform message padding to nearest 256-byte boundary (hides message size)</li>
<li>Configurable decoy traffic rate (fake messages to mask send timing)</li>
<li>Optional onion routing: 3-hop relay through other mesh nodes (no Tor dependency)</li>
<li>Ref: Phase 7.7 for server-side traffic analysis resistance</li>
</ul>
</li>
</ul>
<hr>
<h2 id="phase-9--developer-experience--community-growth"><a class="header" href="#phase-9--developer-experience--community-growth">Phase 9 — Developer Experience &amp; Community Growth</a></h2>
<p>Features designed to attract contributors, create demo/showcase potential,
and lower the barrier to entry for non-crypto developers.</p>
<ul>
<li>
<p><input disabled="" type="checkbox"> <strong>9.1 Criterion Benchmark Suite (<code>qpq-bench</code>)</strong></p>
<ul>
<li>Criterion benchmarks for all crypto primitives: hybrid KEM encap/decap,
MLS group-add at 10/100/1000 members, epoch rotation, Noise_XX handshake</li>
<li>CI publishes HTML benchmark reports as GitHub Actions artifacts</li>
<li>Citable numbers — no other project benchmarks MLS + PQ-KEM in Rust</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>9.2 Safety Numbers (key verification)</strong></p>
<ul>
<li>60-digit numeric code derived from two identity keys (Signal-style)</li>
<li><code>/verify &lt;username&gt;</code> REPL command for out-of-band verification</li>
<li>Available in WASM via <code>compute_safety_number</code> binding</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>9.3 Full-Screen TUI (Ratatui + Crossterm)</strong></p>
<ul>
<li><code>qpq tui</code> launches a full-screen terminal UI: message pane, input bar,
channel sidebar with unread counts, MLS epoch indicator</li>
<li>Feature-gated <code>--features tui</code> to keep ratatui/crossterm out of default builds</li>
<li>Existing REPL and CLI subcommands are unaffected</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>9.4 Delivery Proof Canary Tokens</strong></p>
<ul>
<li>Server signs <code>Ed25519(SHA-256(message_id || recipient || timestamp))</code> on enqueue</li>
<li>Sender stores proof locally — cryptographic evidence the server queued the message</li>
<li>Capn Proto schema gains optional <code>deliveryProof: Data</code> on enqueue response</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>9.5 Verifiable Transcript Archive</strong></p>
<ul>
<li><code>GroupMember::export_transcript(path, password)</code> writes encrypted, tamper-evident
message archive (CBOR records, Argon2id + ChaCha20-Poly1305, Merkle chain)</li>
<li><code>qpq export verify</code> CLI command independently verifies chain integrity</li>
<li>Useful for legal discovery, audit, or personal backup</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>9.6 Key Transparency (Merkle-Log Identity Binding)</strong></p>
<ul>
<li>Append-only Merkle log of (username, identity_key) bindings in the AS</li>
<li>Clients receive inclusion proofs alongside key fetches</li>
<li>Any client can independently audit the full identity history</li>
<li>Lightweight subset of RFC 9162 adapted for identity keys</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox" checked=""> <strong>9.7 Dynamic Server Plugin System</strong></p>
<ul>
<li>Server loads <code>.so</code>/<code>.dylib</code> plugins at runtime via <code>--plugin-dir</code></li>
<li>C-compatible <code>HookVTable</code> via <code>extern "C"</code> — plugins in any language</li>
<li>6 hook points: on_message_enqueue, on_batch_enqueue, on_auth, on_channel_created, on_fetch, on_user_registered</li>
<li>Example plugins: logging plugin, rate limit plugin (512 KiB payload enforcement)</li>
</ul>
</li>
<li>
<p><input disabled="" type="checkbox"> <strong>9.8 PQ Noise Transport Layer</strong></p>
<ul>
<li>Hybrid <code>Noise_XX + ML-KEM-768</code> handshake for post-quantum transport security</li>
<li>Closes the harvest-now-decrypt-later gap on handshake metadata (ADR-006)</li>
<li>Feature-gated <code>--features pq-noise</code>; classical Noise_XX default preserved</li>
<li>May require extending or forking <code>snow</code> crates <code>CryptoResolver</code></li>
</ul>
</li>
</ul>
<hr>
<h2 id="summary-timeline"><a class="header" href="#summary-timeline">Summary Timeline</a></h2>
<div class="table-wrapper">
<table>
<thead>
<tr><th>Phase</th><th>Focus</th><th>Estimated Effort</th></tr>
</thead>
<tbody>
<tr><td><strong>1</strong></td><td>Production Hardening</td><td>12 days</td></tr>
<tr><td><strong>2</strong></td><td>Test &amp; CI Maturity</td><td>23 days</td></tr>
<tr><td><strong>3</strong></td><td>Client SDKs (Go, Python, WASM, FFI, WebTransport)</td><td>58 days</td></tr>
<tr><td><strong>4</strong></td><td>Trust &amp; Security Infrastructure</td><td>24 days (excl. audit)</td></tr>
<tr><td><strong>5</strong></td><td>Features &amp; UX</td><td>57 days</td></tr>
<tr><td><strong>6</strong></td><td>Scale &amp; Operations</td><td>35 days</td></tr>
<tr><td><strong>7</strong></td><td>Platform Expansion &amp; Research</td><td>ongoing</td></tr>
<tr><td><strong>8</strong></td><td>Freifunk / Community Mesh</td><td>ongoing</td></tr>
<tr><td><strong>9</strong></td><td>Developer Experience &amp; Community Growth</td><td>35 days</td></tr>
</tbody>
</table>
</div>
<hr>
<h2 id="related-documents"><a class="header" href="#related-documents">Related Documents</a></h2>
<ul>
<li><a href="docs/FUTURE-IMPROVEMENTS.html">Future Improvements</a> — consolidated improvement list</li>
<li><a href="docs/PRODUCTION-READINESS-AUDIT.html">Production Readiness Audit</a> — specific blockers</li>
<li><a href="docs/SECURITY-AUDIT.html">Security Audit</a> — findings and recommendations</li>
<li><a href="docs/src/roadmap/milestones.html">Milestone Tracker</a> — M1M7 status</li>
<li><a href="docs/src/roadmap/authz-plan.html">Auth, Devices, and Tokens</a> — authorization design</li>
<li><a href="docs/src/roadmap/dm-channels.html">DM Channel Design</a> — 1:1 channel spec</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="roadmap/future-research.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg></span>
</a>
<a rel="next prefetch" href="contributing/coding-standards.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"/></svg></span>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="roadmap/future-research.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg></span>
</a>
<a rel="next prefetch" href="contributing/coding-standards.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"/></svg></span>
</a>
</nav>
</div>
<template id=fa-eye><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"/></svg></span></template>
<template id=fa-eye-slash><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"/></svg></span></template>
<template id=fa-copy><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M502.6 70.63l-61.25-61.25C435.4 3.371 427.2 0 418.7 0H255.1c-35.35 0-64 28.66-64 64l.0195 256C192 355.4 220.7 384 256 384h192c35.2 0 64-28.8 64-64V93.25C512 84.77 508.6 76.63 502.6 70.63zM464 320c0 8.836-7.164 16-16 16H255.1c-8.838 0-16-7.164-16-16L239.1 64.13c0-8.836 7.164-16 16-16h128L384 96c0 17.67 14.33 32 32 32h47.1V320zM272 448c0 8.836-7.164 16-16 16H63.1c-8.838 0-16-7.164-16-16L47.98 192.1c0-8.836 7.164-16 16-16H160V128H63.99c-35.35 0-64 28.65-64 64l.0098 256C.002 483.3 28.66 512 64 512h192c35.2 0 64-28.8 64-64v-32h-47.1L272 448z"/></svg></span></template>
<template id=fa-play><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg></span></template>
<template id=fa-clock-rotate-left><span class=fa-svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M75 75L41 41C25.9 25.9 0 36.6 0 57.9V168c0 13.3 10.7 24 24 24H134.1c21.4 0 32.1-25.9 17-41l-30.8-30.8C155 85.5 203 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4c-14.5-10.1-34.4-6.6-44.6 7.9s-6.6 34.4 7.9 44.6C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C185.3 0 121.3 28.7 75 75zm181 53c-13.3 0-24 10.7-24 24V256c0 6.4 2.5 12.5 7 17l72 72c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-65-65V152c0-13.3-10.7-24-24-24z"/></svg></span></template>
<script>
window.playground_copyable = true;
</script>
<script src="elasticlunr-ef4e11c1.min.js"></script>
<script src="mark-09e88c2c.min.js"></script>
<script src="searcher-c2a407aa.js"></script>
<script src="clipboard-1626706a.min.js"></script>
<script src="highlight-abc7f01d.js"></script>
<script src="book-a0b12cfe.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>

26
assets/left.ansi Normal file
View File

@@ -0,0 +1,26 @@
registering 'alice'...
user 'alice' registered
logging in as 'alice'...
logged in, session cached
identity: c1e1f6df17eeb6..2816
KeyPackage uploaded
hybrid key uploaded
type /help for commands, Ctrl+D to exit
[no conversation] > /dm bob
resolving bob...
creating channel...
fetching peer's key package...
DM with @bob created. Start typing!
[@bob] > Hey Bob, testing our E2E encrypted channel!
[bob] Works great -- the server never sees plaintext?
[@bob] > Right. MLS forward secrecy + post-quantum KEM.
[bob] Impressive. How do I verify your identity?
[@bob] > Run /verify alice -- compare the safety number out-of-band.
[@bob] > /group-info
 Conversation: @bob
 Type: DM
 Members: 2
 alice (you), bob
 MLS epoch: 3
[@bob] >

24
assets/right.ansi Normal file
View File

@@ -0,0 +1,24 @@
registering 'bob'...
user 'bob' registered
logging in as 'bob'...
logged in, session cached
identity: a8c2f19f1b0806..c73f
KeyPackage uploaded
hybrid key uploaded
type /help for commands, Ctrl+D to exit
[system] new conversation: @alice
[@alice] > [alice] Hey Bob, testing our E2E encrypted channel!
[@alice] > Works great -- the server never sees plaintext?
[alice] Right. MLS forward secrecy + post-quantum KEM.
[@alice] > Impressive. How do I verify your identity?
[alice] Run /verify alice -- compare the safety number out-of-band.
[@alice] > /verify alice
 Safety number for @alice:
 096482 731945 208376
 571039 284617 950283
[@alice] > /whoami
 identity: a8c2f19f1b0806..c73f
 hybrid key: yes
 conversations: 1
[@alice] >

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

59
assets/screenshot.txt Normal file
View File

@@ -0,0 +1,59 @@
=== Alice (left) ===
./target/debug/qpq repl --username alice --password de
opass1 --server 127.0.0.1:17123 --ca-cert /tmp/tmp.adbXG
OrPY/server-cert.der --state /tmp/tmp.adbXGLOrPY/alice.b
n
registering 'alice'...
user 'alice' registered
logging in as 'alice'...
logged in, session cached
identity: c1e1f6df17eeb6f539d7fbea94129fa32fc02ca40e5c
7a7c95cfc94161d5f628
KeyPackage uploaded
hybrid key uploaded
type /help for commands, Ctrl+D to exit
[no conversation] > /dm bob
resolving bob...
creating channel...
fetching peer's key package...
DM with @bob created. Start typing!
[@bob] > ^LHey Bob, testing our E2E encrypted channel!
[@bob] > Right. MLS forward secrecy + post-quantum KEM.
[@bob] > /group-info
Conversation: @bob
Type: DM
Members: 2
alice (you), bob
MLS epoch: 1
[@bob] >
=== Bob (right) ===
./target/debug/qpq repl --username bob --password demop
ass2 --server 127.0.0.1:17123 --ca-cert /tmp/tmp.adbXGLOr
PY/server-cert.der --state /tmp/tmp.adbXGLOrPY/bob.bin
registering 'bob'...
user 'bob' registered
logging in as 'bob'...
logged in, session cached
identity: a8c2f19f1b080616b7206e02244fd14c2ab8821367392
af5ff9c89c69750c73f
KeyPackage uploaded
hybrid key uploaded
type /help for commands, Ctrl+D to exit
[no conversation] > /list
no conversations yet. Try /dm <username> or /create-gro
up <name>
[no conversation] > /switch @alice
error: conversation not found: @alice
[no conversation] > ^LWorks great -- the server never see
s plaintext?
error: no active conversation; use /dm or /create-group
first
[no conversation] > /whoami
identity: a8c2f19f1b080616b7206e02244fd14c2ab8821367392
af5ff9c89c69750c73f
hybrid key: yes
conversations: 0
[no conversation] >

View File

@@ -1376,12 +1376,12 @@ pub fn cmd_export(
///
/// Prints a summary. Does not require the encryption password (structural check only).
pub fn cmd_export_verify(input: &Path) -> anyhow::Result<()> {
use quicproquo_core::{verify_transcript_chain, ChainVerdict};
use quicproquo_core::{validate_transcript_structure, ChainVerdict};
let data = std::fs::read(input)
.with_context(|| format!("read transcript file '{}'", input.display()))?;
match verify_transcript_chain(&data)? {
match validate_transcript_structure(&data)? {
ChainVerdict::Ok { records } => {
println!(
"OK: transcript '{}' is structurally valid. {} record(s) found, hash chain intact.",

View File

@@ -169,6 +169,7 @@ impl ConversationStore {
let salt = get_or_create_salt(&salt_path)?;
let key = derive_convdb_key(password, &salt)?;
#[allow(clippy::needless_borrows_for_generic_args)]
let hex_key = Zeroizing::new(hex::encode(&*key));
let conn = Connection::open(db_path).context("open conversation db")?;
@@ -188,6 +189,7 @@ impl ConversationStore {
) -> anyhow::Result<()> {
let salt = get_or_create_salt(salt_path)?;
let key = derive_convdb_key(password, &salt)?;
#[allow(clippy::needless_borrows_for_generic_args)]
let hex_key = Zeroizing::new(hex::encode(&*key));
let enc_path = db_path.with_extension("convdb-enc");

View File

@@ -914,11 +914,11 @@ fn parse_duration_secs(s: &str) -> Option<u32> {
/// Format a TTL in seconds into a human-friendly string.
fn format_ttl(secs: u32) -> String {
if secs >= 86400 && secs % 86400 == 0 {
if secs >= 86400 && secs.is_multiple_of(86400) {
format!("{} day(s)", secs / 86400)
} else if secs >= 3600 && secs % 3600 == 0 {
} else if secs >= 3600 && secs.is_multiple_of(3600) {
format!("{} hour(s)", secs / 3600)
} else if secs >= 60 && secs % 60 == 0 {
} else if secs >= 60 && secs.is_multiple_of(60) {
format!("{} minute(s)", secs / 60)
} else {
format!("{} second(s)", secs)

View File

@@ -85,6 +85,7 @@ pub fn anyhow_is_retriable(err: &anyhow::Error) -> bool {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -152,7 +152,7 @@ pub fn set_auth(auth: &mut auth::Builder<'_>) -> anyhow::Result<()> {
)
})?;
auth.set_version(ctx.version);
auth.set_access_token(&*ctx.access_token);
auth.set_access_token(&ctx.access_token);
auth.set_device_id(&ctx.device_id);
Ok(())
}

View File

@@ -217,6 +217,7 @@ pub fn sha256(bytes: &[u8]) -> Vec<u8> {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -93,6 +93,7 @@ pub fn clear_cached_session(state_path: &Path) {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -1,5 +1,7 @@
// cargo_bin! only works for current package's binary; we spawn qpq-server from another package.
#![allow(deprecated)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::await_holding_lock)] // AUTH_LOCK intentionally held across await to serialize tests
use std::{path::PathBuf, process::Command, sync::Mutex, time::Duration};
@@ -8,7 +10,6 @@ use portpicker::pick_unused_port;
use rand::RngCore;
use tempfile::TempDir;
use tokio::time::sleep;
use hex;
// Required by rustls 0.23 when QUIC/TLS is used from this process (e.g. client in test).
fn ensure_rustls_provider() {
@@ -46,7 +47,7 @@ impl Drop for ChildGuard {
}
}
async fn wait_for_health(server: &str, ca_cert: &PathBuf, server_name: &str) -> anyhow::Result<()> {
async fn wait_for_health(server: &str, ca_cert: &std::path::Path, server_name: &str) -> anyhow::Result<()> {
let local = tokio::task::LocalSet::new();
for _ in 0..30 {
if local
@@ -1090,7 +1091,7 @@ async fn e2e_key_rotation_update_path() -> anyhow::Result<()> {
let alice_seed = bincode::deserialize::<StoredStateCompat>(&std::fs::read(&alice_state)?)?.identity_seed;
let bob_seed = bincode::deserialize::<StoredStateCompat>(&std::fs::read(&bob_state)?)?.identity_seed;
let alice_pk = IdentityKeypair::from_seed(alice_seed).public_key_bytes().to_vec();
let _alice_pk = IdentityKeypair::from_seed(alice_seed).public_key_bytes().to_vec();
let bob_pk = IdentityKeypair::from_seed(bob_seed).public_key_bytes().to_vec();
let bob_pk_hex = hex_encode(&bob_pk);
@@ -1372,7 +1373,7 @@ async fn e2e_file_upload_download() -> anyhow::Result<()> {
// Build 2 KB of known data.
let pattern = b"hello-world-file-test\n";
let repeat_count = (2048 + pattern.len() - 1) / pattern.len();
let repeat_count = 2048_usize.div_ceil(pattern.len());
let file_data: Vec<u8> = pattern.iter().copied().cycle().take(repeat_count * pattern.len()).collect();
let file_data = &file_data[..2048]; // exactly 2 KB
@@ -1472,7 +1473,7 @@ async fn e2e_file_upload_download() -> anyhow::Result<()> {
.await?;
anyhow::ensure!(
partial == &file_data[100..300],
partial == file_data[100..300],
"partial download [100..300] does not match expected slice"
);

View File

@@ -1,3 +1,4 @@
#![allow(clippy::unwrap_used)]
//! Benchmark: Identity keypair operations, sealed sender, and message padding.
//!
//! Covers:
@@ -34,14 +35,12 @@ fn bench_identity_verify(c: &mut Criterion) {
c.bench_function("identity_verify", |b| {
b.iter(|| {
black_box(
IdentityKeypair::verify_raw(
black_box(&pk),
black_box(payload),
black_box(&sig),
)
.unwrap()
IdentityKeypair::verify_raw(
black_box(&pk),
black_box(payload),
black_box(&sig),
)
.unwrap();
});
});
}

View File

@@ -1,3 +1,4 @@
#![allow(clippy::unwrap_used)]
//! Benchmark: Hybrid KEM (X25519 + ML-KEM-768) vs classical-only encryption.
//!
//! Compares keypair generation, encryption, and decryption times for the

View File

@@ -1,3 +1,4 @@
#![allow(clippy::unwrap_used)]
//! Benchmark: MLS group operations at various group sizes.
//!
//! Measures KeyPackage generation, group creation, member addition,

View File

@@ -1,3 +1,4 @@
#![allow(clippy::unwrap_used)]
//! Benchmark: Cap'n Proto vs Protobuf serialization for chat message envelopes.
//!
//! Compares serialization/deserialization speed and encoded size at three

View File

@@ -349,6 +349,7 @@ fn parse_file_ref(payload: &[u8]) -> Result<AppMessage, CoreError> {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -631,6 +631,7 @@ impl GroupMember {
// ── Unit tests ────────────────────────────────────────────────────────────────
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -364,6 +364,7 @@ impl OpenMlsCryptoProvider for HybridCryptoProvider {
// ── Tests ───────────────────────────────────────────────────────────────────
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use openmls_traits::types::HpkeKdfType;

View File

@@ -476,6 +476,7 @@ fn derive_aead_key(x25519_ss: &[u8], mlkem_ss: &[u8], extra_info: &[u8]) -> Key
// ── Tests ───────────────────────────────────────────────────────────────────
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -151,7 +151,43 @@ pub fn verify_delivery_proof(
Ok(true)
}
impl Serialize for IdentityKeypair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(&self.seed[..])
}
}
impl<'de> Deserialize<'de> for IdentityKeypair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes: Vec<u8> = serde::Deserialize::deserialize(deserializer)?;
let seed: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| serde::de::Error::custom("identity seed must be 32 bytes"))?;
Ok(IdentityKeypair::from_seed(seed))
}
}
impl std::fmt::Debug for IdentityKeypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fp = self.fingerprint();
f.debug_struct("IdentityKeypair")
.field(
"fingerprint",
&format!("{:02x}{:02x}{:02x}{:02x}", fp[0], fp[1], fp[2], fp[3]),
)
.finish_non_exhaustive()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod proof_tests {
use super::*;
use sha2::{Digest, Sha256};
@@ -207,38 +243,3 @@ mod proof_tests {
assert!(verify_delivery_proof(&pk, &proof).is_err());
}
}
impl Serialize for IdentityKeypair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(&self.seed[..])
}
}
impl<'de> Deserialize<'de> for IdentityKeypair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes: Vec<u8> = serde::Deserialize::deserialize(deserializer)?;
let seed: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| serde::de::Error::custom("identity seed must be 32 bytes"))?;
Ok(IdentityKeypair::from_seed(seed))
}
}
impl std::fmt::Debug for IdentityKeypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fp = self.fingerprint();
f.debug_struct("IdentityKeypair")
.field(
"fingerprint",
&format!("{:02x}{:02x}{:02x}{:02x}", fp[0], fp[1], fp[2], fp[3]),
)
.finish_non_exhaustive()
}
}

View File

@@ -62,6 +62,7 @@ pub fn unpad(padded: &[u8]) -> Result<Vec<u8>, CoreError> {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -85,6 +85,7 @@ pub fn is_sealed(bytes: &[u8]) -> bool {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -95,6 +95,7 @@ fn recompute_root(leaf: [u8; 32], path: &[PathStep]) -> Result<[u8; 32], KtError
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tree::MerkleLog;

View File

@@ -182,6 +182,7 @@ fn largest_power_of_two_less_than(n: usize) -> usize {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -77,7 +77,7 @@ impl MeshIdentity {
/// contains the Ed25519 seed in the clear.
pub fn save(&self, path: &Path) -> anyhow::Result<()> {
let file = IdentityFile {
seed: hex::encode(&*self.keypair.seed_bytes()),
seed: hex::encode(self.keypair.seed_bytes()),
peers: self.known_peers.clone(),
};
let json = serde_json::to_string_pretty(&file)?;

View File

@@ -114,6 +114,7 @@ impl ConversationStore {
if let Some(pw) = password {
let key = derive_db_key(pw, db_path)?;
#[allow(clippy::needless_borrows_for_generic_args)]
let hex_key = Zeroizing::new(hex::encode(&*key));
conn.pragma_update(None, "key", format!("x'{}'", &*hex_key))
.context("set SQLCipher key")?;
@@ -561,6 +562,7 @@ fn row_to_message(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -30,6 +30,7 @@ use crate::error::SdkError;
/// Returns `(conversation_id, was_new)`.
/// - `was_new = true` — caller created the MLS group and sent the Welcome.
/// - `was_new = false` — peer is the MLS initiator; caller should wait for Welcome.
#[allow(clippy::too_many_arguments)]
pub async fn create_dm(
rpc: &RpcClient,
conv_store: &ConversationStore,
@@ -177,6 +178,7 @@ pub fn create_group(
/// Invite a peer to an existing group.
///
/// Sends the Welcome to the new peer and the Commit to all existing members.
#[allow(clippy::too_many_arguments)]
pub async fn invite_to_group(
rpc: &RpcClient,
conv_store: &ConversationStore,

View File

@@ -143,6 +143,7 @@ pub fn load_state(path: &Path, password: Option<&str>) -> Result<StoredState, Sd
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -48,7 +48,7 @@ impl BlobService {
if req
.offset
.checked_add(req.chunk.len() as u64)
.map_or(true, |end| end > req.total_size)
.is_none_or(|end| end > req.total_size)
{
return Err(DomainError::BadParams(format!(
"chunk out of bounds: offset={} + chunk_len={} > total_size={}",

View File

@@ -29,6 +29,7 @@ pub fn resolve_destination(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -86,7 +86,7 @@ impl NodeServiceImpl {
}
// Validate chunk bounds.
if offset.checked_add(chunk.len() as u64).map_or(true, |end| end > total_size) {
if offset.checked_add(chunk.len() as u64).is_none_or(|end| end > total_size) {
return Promise::err(coded_error(
E020_BAD_PARAMS,
format!(

View File

@@ -263,7 +263,7 @@ impl NodeServiceImpl {
if self.redact_logs {
let redacted_sender = sender_identity
.as_deref()
.map(|id| redacted_prefix(id))
.map(redacted_prefix)
.unwrap_or_else(|| "sealed".to_string());
tracing::info!(
sender_prefix = %redacted_sender,

View File

@@ -1004,6 +1004,7 @@ impl<T> OptionalExt<T> for Result<T, rusqlite::Error> {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::path::PathBuf;

View File

@@ -320,6 +320,7 @@ pub struct FileBackedStore {
identity_keys: Mutex<HashMap<String, Vec<u8>>>,
endpoints: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
/// Device registry: identity_key -> Vec<(device_id, device_name, registered_at)>
#[allow(clippy::type_complexity)]
devices: Mutex<HashMap<Vec<u8>, Vec<(Vec<u8>, String, u64)>>>,
}
@@ -958,6 +959,7 @@ impl Store for FileBackedStore {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use tempfile::TempDir;

380
docs/V2-DESIGN-ANALYSIS.md Normal file
View File

@@ -0,0 +1,380 @@
# quicproquo v2 — Design Analysis & Recommendations
> Multi-perspective retrospective of the v1 architecture.
> Produced 2026-03-04 by four parallel analysis agents examining server,
> client/UX, crypto/security, and project structure/DX.
---
## Executive Summary
quicproquo v1 demonstrates strong fundamentals: QUIC-native transport, RFC 9420
MLS group encryption, post-quantum hybrid KEM, OPAQUE zero-knowledge auth, and a
working multi-language SDK surface. These are the right bets and put the project
ahead of most open-source messengers on the crypto front.
However, three architectural choices limit the path to production:
1. **capnp-rpc is `!Send`** — forces single-threaded RPC handling, blocking
scalability.
2. **Monolithic client with global state** — business logic is tangled into the
REPL, duplicated across TUI/GUI/Web, and cannot be used as a library.
3. **Poll-based delivery** — 1-second polling wastes bandwidth and adds latency;
no server-push channel exists.
A v2 should keep the crypto stack (MLS + hybrid PQ KEM + OPAQUE), keep QUIC, but
rearchitect the RPC layer, extract an SDK crate, and add push-based delivery.
---
## Part 1 — What Works Well
### Transport & Protocol
- **QUIC (quinn) + TLS 1.3** — correct choice. Built-in encryption, connection
migration, 0-RTT potential. No reason to change.
- **Cap'n Proto schemas as API contract** — zero-copy wire format, compact
binary, schema evolution via ordinals. The *schemas* are good; the *RPC
runtime* is the problem.
### Cryptography
- **MLS (RFC 9420, openmls)** — only IETF-standard group E2E protocol. No
realistic alternative for groups > 2 members. Test suite is thorough (1005
lines covering 2-party, 3-party, hybrid, removal, leave, stale epoch).
- **Hybrid PQ KEM (X25519 + ML-KEM-768)** — forward-thinking dual-algorithm
protection. Well-implemented with versioned wire format, proper zeroization,
and 12 targeted tests. Ahead of Signal (PQXDH, late 2023) and Matrix (no PQ).
- **OPAQUE (RFC 9497)** — server never sees passwords. Ristretto255 + Argon2id
is best-in-class.
- **Sealed sender, safety numbers, message padding** — all clean, simple,
correct. Safety numbers match Signal's 5200-iteration HMAC-SHA256 cost.
- **Zeroization discipline** — secrets wrapped in `Zeroizing`, Debug impls
redact keys, no `.unwrap()` in crypto paths.
- **WASM feature gating** — `core/native` cleanly separates WASM-safe crypto
from native-only modules (MLS, OPAQUE, filesystem).
### Server Design
- **Store trait abstraction** — 30+ methods, clean backend swap (SqlStore vs
FileBackedStore). Well-factored.
- **OPAQUE auth with timing floors** — `resolveUser`/`resolveIdentity` mask
lookup timing to prevent username enumeration.
- **Delivery proofs** — Ed25519-signed receipt of server acceptance. Clients get
cryptographic evidence.
- **`wasNew` flag on createChannel** — elegantly solves the dual-MLS-group race
condition where both DM parties try to initialize.
- **Plugin hooks (C-ABI)** — `#![no_std]` vtable, zero dependencies, chained
hooks with continue/reject protocol. Clean extensibility.
- **Production config validation** — enforces encrypted storage, strong auth
tokens, pre-existing TLS certs.
### Client & DX
- **Zero-config local dev** — `qpq --username alice --password pass` auto-starts
server, generates TLS certs, registers, and logs in. Genuinely excellent.
- **Encrypted-at-rest everything** — state file (QPCE), conversation DB
(SQLCipher), session cache. Argon2id + ChaCha20-Poly1305 throughout.
- **Playbook system** — YAML-scripted command execution with assertions. Great
for CI/integration testing.
- **Conversation store** — SQLite with deduplication, outbox for offline
queuing, activity tracking.
- **Conventional commits, GPG-signed** — consistent `feat:`/`fix:`/`docs:`
discipline.
- **Security lints enforced by build** — `clippy::unwrap_used = "deny"`,
`unsafe_code = "warn"`.
---
## Part 2 — What Needs Rethinking
### 2.1 RPC Layer: capnp-rpc is the #1 Scalability Bottleneck
**Problem:** `capnp-rpc` uses `Rc` internally and is `!Send`. Everything runs on
a `LocalSet` with `spawn_local`. All 27 RPC methods serialize through a single
thread. No work-stealing, no multi-core utilization.
**Impact:** With 1000+ concurrent clients, the single-threaded executor cannot
keep up. A slow `fetchWait` (30s timeout) blocks the entire connection.
**Also:** The WebSocket bridge (`ws_bridge.rs`, 645 lines) exists solely because
Cap'n Proto cannot run in browsers. This duplicates handler logic and creates
maintenance burden.
### 2.2 Client Architecture: Monolith with Global State
**Problem:** `AUTH_CONTEXT` is a process-wide `RwLock<Option<ClientAuth>>`.
Business logic (MLS processing, sealed sender, hybrid decryption, message
routing) lives inside `repl.rs`'s `poll_messages()` — a 100-line function that
mixes transport, crypto, routing, and storage.
**Impact:** Every frontend (REPL, TUI, GUI, Web) must reimplement message
processing. The TUI already duplicates it. The GUI stub and mobile PoC would need
yet another copy. Client cannot be used as a library.
### 2.3 Delivery Model: Poll-Based, No Push Channel
**Problem:** Client polls every 1 second with `fetch_wait(timeout_ms=0)` — never
actually long-polls. Constant network traffic even when idle. ~1 second latency
for message delivery.
**Also:** `fetch` is destructive (drains queue). If the client crashes between
receive and processing, messages are lost.
### 2.4 Connection Model: Single Stream
**Problem:** `max_concurrent_bidi_streams(1)` means the entire QUIC connection is
effectively single-stream. A blocking `fetchWait` prevents all other RPCs.
### 2.5 Storage: Single Mutex-Guarded SQLite Connection
**Problem:** `SqlStore` uses `Mutex<Connection>`. Every database operation
acquires a global lock. Under concurrent load, all storage access serializes.
**Also:** `FileBackedStore` flushes the entire map on every write (O(n) I/O).
Sessions are in-memory only — server restart forces all clients to re-login.
### 2.6 Key Management Gaps
- **DiskKeyStore** — HPKE private keys stored as plaintext bincode on disk. No
encryption at rest.
- **MLS group state** — `GroupMember` holds `MlsGroup` in memory only. Process
crash loses all group state.
- **Token zeroization** — `AuthContext.token`, `ClientAuth.access_token` are not
wrapped in `Zeroizing`.
### 2.7 Workspace Bloat
12 crates for a project at this maturity is excessive. Several are thin stubs
(`quicproquo-gen`, `quicproquo-bot` at 354 lines) or broken (`quicproquo-gui`
fails `cargo build --workspace`).
---
## Part 3 — v2 Architecture Recommendations
### 3.1 Replace capnp-rpc with a Send-Compatible RPC Framework
**Recommendation:** Switch to **tonic (gRPC)** or a custom framing layer.
| Dimension | capnp-rpc (v1) | tonic/gRPC (v2) |
|-----------|---------------|-----------------|
| Threading | `!Send`, single-threaded | `Send + Sync`, multi-threaded |
| Browser | Requires WS bridge | grpc-web native |
| Streaming | Not supported | Built-in |
| Middleware | None (copy-paste auth) | Interceptors/layers |
| Ecosystem | Niche | Massive (every language) |
**Alternative:** Keep Cap'n Proto *schemas* for serialization (zero-copy
advantage) but replace capnp-rpc with custom framing over QUIC streams. This
preserves the wire format while gaining `Send` compatibility.
The WS bridge would be eliminated entirely — grpc-web or WebTransport gives
browsers direct access.
### 3.2 Extract an SDK Crate (Most Important Client Change)
Create `quicproquo-sdk` that owns all business logic:
```
quicproquo-sdk/
src/
client.rs -- QpqClient: connect, login, send, receive
events.rs -- ClientEvent enum (push-based)
conversation.rs -- ConversationHandle, group management
crypto.rs -- MLS pipeline, sealed sender, hybrid decryption
sync.rs -- message sync, offline queue, retry
```
All frontends become thin shells:
```
CLI/REPL -> calls sdk
TUI -> calls sdk
Tauri GUI -> calls sdk (via Tauri commands)
Mobile -> calls sdk (via C FFI)
Web/WASM -> calls sdk (compiled to wasm32)
```
**Key API shape:**
```rust
pub struct QpqClient { /* session, rpc, crypto pipeline */ }
impl QpqClient {
pub async fn connect(config: ClientConfig) -> Result<Self>;
pub async fn login(username: &str, password: &str) -> Result<Self>;
pub async fn dm(&mut self, username: &str) -> Result<ConversationHandle>;
pub async fn create_group(&mut self, name: &str) -> Result<ConversationHandle>;
pub async fn send(&mut self, text: &str) -> Result<MessageId>;
pub fn subscribe(&self) -> Receiver<ClientEvent>;
}
```
No global state. No `AUTH_CONTEXT`. Auth context is per-`QpqClient` instance.
### 3.3 Add Push-Based Delivery
**Recommendation:** Dedicated QUIC unidirectional stream for server-push
notifications.
```
Client opens bidi stream 0 -> RPC channel (request/response)
Server opens uni stream 1 -> push notifications (new message, typing, etc.)
```
Benefits:
- Zero-latency message delivery (no polling)
- No idle network traffic
- Typing indicators delivered in real-time
- Graceful degradation: fall back to long-poll if push stream fails
**Also:** Make `peek` + `ack` the default delivery pattern (not destructive
`fetch`). Add idempotency keys to prevent duplicate messages on retry.
### 3.4 Multi-Stream Connections
Allow 4-8 concurrent bidirectional QUIC streams per connection. This enables:
- Pipelined RPCs (send while fetching)
- Concurrent blob upload + chat
- `fetchWait` on one stream without blocking others
### 3.5 Storage Improvements
| Change | Rationale |
|--------|-----------|
| Drop `FileBackedStore` | O(n) flush per write, no federation support |
| Connection pool for SQLite | Replace `Mutex<Connection>` with r2d2/deadpool |
| Persist sessions to DB | Server restart shouldn't force re-login |
| Encrypt DiskKeyStore at rest | HPKE private keys in plaintext is a real vuln |
| Persist MLS group state | Process crash shouldn't lose group state |
| Atomic keystore writes | tempfile-then-rename pattern |
### 3.6 Crypto Stack Refinements
The algorithms are correct. The refinements are operational:
| Change | Rationale |
|--------|-----------|
| Typed MLS error variants | Stop losing error info via `format!("{e:?}")` |
| Formalize hybrid PQ ciphersuite ID | Replace length-based key detection |
| Remove all InsecureServerCertVerifier | No TLS bypass on any platform |
| Add passkey/WebAuthn alt-auth | Better UX for GUI/mobile, no password to forget |
| Consider Double Ratchet for 1:1 DMs | MLS is over-engineered for 2-party; DR gives better per-message forward secrecy |
| Token/session secret zeroization | `AuthContext.token` et al. need `Zeroizing` wrappers |
| Fix serde deserialization of secrets | Intermediate non-zeroized `Vec<u8>` in `IdentityKeypair::deserialize` |
### 3.7 Workspace Restructuring
**Reduce from 12 to 8 crates:**
```
quicproquo-core -- crypto primitives (keep)
quicproquo-proto -- schema codegen (keep)
quicproquo-plugin-api -- #![no_std] C-ABI (keep)
quicproquo-kt -- key transparency (keep)
quicproquo-sdk -- NEW: business logic library
quicproquo-server -- server binary (keep)
quicproquo-client -- CLI/TUI binary, depends on sdk (keep, slimmed)
quicproquo-p2p -- mesh networking (keep, feature-flagged)
```
**Merge/remove:**
- `bot` -> `sdk::bot` module
- `ffi` -> `sdk` with `--features c-ffi`
- `gen` -> `scripts/` or `xtask`
- `gui` -> `apps/gui/` outside workspace (Tauri project)
- `mobile` -> `examples/` (research spike)
**Add `[workspace.default-members]`** so `cargo build` doesn't attempt GUI.
**Add `justfile`** with `build`, `test`, `test-e2e`, `build-wasm`, `docker`.
### 3.8 Plugin System Evolution
| Change | Rationale |
|--------|-----------|
| Add `version: u32` to `HookVTable` | ABI stability — check version on load |
| Config passthrough | `qpq_plugin_init(vtable, config_json)` |
| Async hooks | Plugins that call external services shouldn't block Tokio |
| Evaluate WASM plugins | Sandboxed community plugins (keep C-ABI for first-party) |
### 3.9 Federation Improvements
| Change | Rationale |
|--------|-----------|
| DNS SRV / .well-known discovery | Static peer config doesn't scale |
| Persistent relay queue with retry | Messages to offline peers are currently lost |
| Deterministic channel ID derivation | Avoid cross-server channel conflicts |
| Keep mDNS as optional mesh feature | Not for internet-scale, but good for LAN |
### 3.10 Test & CI Improvements
| Change | Rationale |
|--------|-----------|
| Per-client auth context | Removes `--test-threads 1` constraint |
| Mock server for client unit tests | Fast tests without spawning real server |
| Fuzz testing (cargo-fuzz) | Hybrid KEM, sealed sender, padding, Cap'n Proto deser |
| WS bridge unit tests | 645 lines, zero tests, security-critical |
| WASM + Go SDK in CI | Currently untested in CI |
| Separate E2E from unit test CI job | Different speed, different failure modes |
| macOS CI | FFI/mobile cross-compilation validation |
| Release automation | Binary artifacts, Docker tags, WASM npm publish |
---
## Part 4 — Ecosystem Positioning
### Don't compete with Signal or Matrix directly.
**Target: Privacy-first messaging infrastructure for developers and
organizations.**
quicproquo's differentiators — QUIC-native transport, post-quantum crypto, MLS,
plugin system, multi-language SDKs, embeddable architecture — point toward an
infrastructure play, not a consumer app.
Think: *"the Postgres of E2E encrypted messaging"* — a high-quality open-source
server and protocol that other projects build on.
| Segment | Value Proposition |
|---------|-------------------|
| **Developer tool** | API-first messenger for encrypted bots and integrations |
| **Embeddable** | C FFI + WASM + Go SDK for embedding in other apps |
| **Enterprise** | On-prem, plugins for compliance/audit, OPAQUE zero-knowledge auth |
| **Research** | Post-quantum crypto, MLS reference implementation, mesh networking |
---
## Part 5 — Priority Ordering
### Phase 1: Foundation (unblocks everything else)
1. Replace capnp-rpc with Send-compatible framework
2. Extract SDK crate from client
3. Per-client auth context (no global state)
### Phase 2: Reliability
4. Push-based delivery (QUIC uni-stream)
5. Multi-stream connections
6. Persist sessions + MLS group state
7. Encrypt DiskKeyStore at rest
8. peek+ack as default delivery
### Phase 3: Polish
9. Workspace restructuring (12 -> 8 crates)
10. TUI as primary interactive mode (built on SDK)
11. Plugin system v2 (versioning, config, async)
12. Federation retry queue + discovery
### Phase 4: Ecosystem
13. Full MLS in WASM (browser E2E)
14. WebTransport (eliminate WS bridge)
15. Tauri GUI (built on SDK)
16. Release automation + expanded CI
---
## Appendix — Analysis Sources
This document was produced by four parallel analysis agents:
| Agent | Scope | Files Read |
|-------|-------|-----------|
| server-analyst | Transport, RPC, delivery, storage, federation | 27 server .rs files, 4 schemas, core transport |
| client-analyst | REPL, UX, state, multi-platform, SDK design | All client .rs, GUI, mobile, TS demo |
| security-analyst | MLS, OPAQUE, hybrid KEM, keystore, identity | All core .rs, review doc |
| dx-analyst | Workspace, build, tests, plugins, CI, ecosystem | All Cargo.toml, tests, CI, plugins, SDKs |

328
docs/V2-MASTER-PLAN.md Normal file
View File

@@ -0,0 +1,328 @@
# quicproquo v2 — Master Implementation Plan
> Created 2026-03-04. This is the authoritative plan for the v2 rewrite.
> See also: `docs/V2-DESIGN-ANALYSIS.md` for the detailed retrospective.
## Context
The v1 codebase has strong crypto foundations (MLS, hybrid PQ KEM, OPAQUE) but three
architectural bottlenecks: capnp-rpc is `!Send` (single-threaded), client business logic
is trapped in a monolithic REPL with global state, and delivery is poll-based.
This plan creates v2 on a new branch, keeping the crypto stack intact and replacing
the RPC/transport layer, extracting an SDK, and restructuring the workspace.
**Key decisions:**
- Transport: Protobuf (prost) + custom framing over QUIC (quinn)
- Mobile: Tauri 2 (same Rust SDK backend, web UI)
- Branch strategy: `v2` branch from main, not a fresh repo
- Constraints: Rust, QUIC, GPG-signed commits, zeroize secrets, no stubs
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────┐
│ Frontends │
│ CLI/TUI │ Tauri GUI/Mobile │ Web (WebTransport)│
└─────┬─────┴────────┬───────────┴──────────┬─────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ quicproquo-sdk │
│ QpqClient { connect, login, send, recv, subscribe } │
│ Event system (tokio broadcast) │
│ Crypto pipeline (MLS, sealed sender, hybrid) │
│ Conversation store (SQLCipher) │
└──────────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ quicproquo-rpc │
│ QUIC framing: [method:u16][req_id:u32][len:u32][pb] │
│ Multi-stream (1 RPC per stream) │
│ Server-push via uni-streams │
│ tower middleware (auth, rate-limit) │
└──────────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ quicproquo-server │
│ Domain services (auth, delivery, channel, blob) │
│ Store trait → SqlStore (connection pool) │
│ Plugin hooks, federation, KT │
└─────────────────────────────────────────────────────┘
```
### Wire Format
Per QUIC bidirectional stream (request/response):
```
Request: [method_id: u16][request_id: u32][payload_len: u32][protobuf bytes]
Response: [status: u8][request_id: u32][payload_len: u32][protobuf bytes]
```
Per QUIC unidirectional stream (server → client push):
```
Push: [event_type: u16][payload_len: u32][protobuf bytes]
```
Each RPC opens its own QUIC bidi stream → natural multi-stream, no head-of-line blocking.
---
## Workspace Structure (v2: 9 crates)
```
quicproquo/
├── crates/
│ ├── quicproquo-core/ # KEEP AS-IS — crypto primitives, MLS, hybrid KEM
│ ├── quicproquo-kt/ # KEEP AS-IS — key transparency
│ ├── quicproquo-plugin-api/ # KEEP AS-IS — #![no_std] C-ABI
│ ├── quicproquo-proto/ # REWRITE — protobuf schemas + prost codegen
│ ├── quicproquo-rpc/ # NEW — QUIC RPC framework (framing, dispatch, tower)
│ ├── quicproquo-sdk/ # NEW — client business logic library
│ ├── quicproquo-server/ # REWRITE — domain services + RPC handlers
│ ├── quicproquo-client/ # REWRITE — thin CLI/TUI shell over SDK
│ └── quicproquo-p2p/ # KEEP — iroh mesh (feature-flagged, later)
├── apps/
│ └── gui/ # Tauri 2 desktop + mobile app (outside workspace)
├── proto/ # .proto source files
│ └── qpq/v1/
│ ├── auth.proto # OPAQUE registration + login (4 methods)
│ ├── delivery.proto # enqueue, fetch, peek, ack, batch (6 methods)
│ ├── keys.proto # key package + hybrid key CRUD (5 methods)
│ ├── channel.proto # channel create (1 method)
│ ├── user.proto # resolve user/identity (2 methods)
│ ├── blob.proto # upload/download (2 methods)
│ ├── device.proto # register/list/revoke (3 methods)
│ ├── p2p.proto # endpoint publish/resolve + health (3 methods)
│ ├── federation.proto # relay + proxy (6 methods)
│ ├── push.proto # server-push events (NEW)
│ └── common.proto # shared types (Auth, Envelope, Error)
├── sdks/
│ ├── go/ # Go SDK (regenerate from .proto)
│ └── typescript/ # TS SDK (WebTransport client)
├── justfile # NEW — build commands
└── Cargo.toml # workspace root
```
**Removed from workspace:**
- `quicproquo-bot``sdk::bot` module
- `quicproquo-ffi``sdk` with `--features c-ffi`
- `quicproquo-gen``scripts/`
- `quicproquo-gui``apps/gui/` (Tauri project, outside workspace)
- `quicproquo-mobile` → merged into `apps/gui/` (Tauri 2 mobile)
---
## Crate Reuse Assessment
| v1 Crate | capnp deps? | v2 Action | Effort |
|----------|:-----------:|-----------|--------|
| **quicproquo-core** | None | Copy as-is | Zero |
| **quicproquo-kt** | None | Copy as-is | Zero |
| **quicproquo-plugin-api** | None | Copy as-is | Zero |
| **quicproquo-p2p** | None | Copy as-is | Zero |
| **quicproquo-proto** | 100% capnp | Replace with prost codegen | Medium |
| **quicproquo-server** | 16/20 files | Extract domain logic, rewrite handlers | High |
| **quicproquo-client** | 6/10 files | Extract to SDK, thin CLI shell | High |
### Key Files to Reuse Directly
| Source (v1) | Destination (v2) | Notes |
|-------------|------------------|-------|
| `crates/quicproquo-core/` (entire) | same path | Zero changes |
| `crates/quicproquo-kt/` (entire) | same path | Zero changes |
| `crates/quicproquo-plugin-api/` (entire) | same path | Zero changes |
| `server/src/storage.rs` | `server/src/storage.rs` | Store trait — keep |
| `server/src/sql_store.rs` | `server/src/sql_store.rs` | Add connection pool |
| `server/src/hooks.rs` | `server/src/hooks.rs` | Plugin system — keep |
| `server/src/plugin_loader.rs` | `server/src/plugin_loader.rs` | Keep |
| `server/src/error_codes.rs` | `server/src/error_codes.rs` | Keep |
| `server/src/config.rs` | `server/src/config.rs` | Update for new transport |
| `client/src/conversation.rs` | `sdk/src/conversation.rs` | Move to SDK |
| `client/src/token_cache.rs` | `sdk/src/token_cache.rs` | Move to SDK |
| `client/src/display.rs` | `client/src/display.rs` | Keep in CLI |
| `schemas/*.capnp` | reference only | Translate to .proto |
---
## Phased Implementation
### Phase 1: Foundation
**Goal:** v2 branch with new workspace, proto schemas, RPC framework skeleton, SDK skeleton.
**Scope:** Compiles, no runtime functionality yet.
1. **Create v2 branch** from main
2. **Restructure workspace** — update root Cargo.toml, create new crate dirs, add justfile
3. **Write .proto files** — translate all 33 RPC methods + push events from Cap'n Proto
4. **Create quicproquo-proto crate** — prost-build codegen
5. **Create quicproquo-rpc crate** — QUIC RPC framework:
- `framing.rs` — wire format encode/decode (request, response, push)
- `server.rs` — accept QUIC connections, dispatch to handlers
- `client.rs` — connect, send requests, receive responses + push events
- `middleware.rs` — tower-based auth + rate-limit layers
- `method.rs` — method registry (method_id → async handler fn)
6. **Create quicproquo-sdk crate** — public API skeleton:
- `client.rs``QpqClient` struct
- `events.rs``ClientEvent` enum
- `conversation.rs``ConversationHandle`, `ConversationStore`
- `config.rs``ClientConfig`
7. **Extract server domain types**`server/src/domain/` module:
- `types.rs` — plain Rust request/response types
- `auth.rs` — OPAQUE logic extracted from auth_ops.rs
- `delivery.rs` — enqueue/fetch logic extracted from delivery.rs
**Verification:**
- `cargo build --workspace` succeeds
- `cargo test -p quicproquo-core` passes (72 tests)
- Proto codegen works
- RPC framework compiles
---
### Phase 2: Server Core
**Goal:** Working server with all 33 RPC handlers over QUIC.
1. **RPC dispatch** — method registry, connection lifecycle
2. **Domain handlers** — all 33 methods as `async fn(Request) -> Result<Response>`
- Auth (4): OPAQUE register start/finish, login start/finish
- Delivery (6): enqueue, fetch, fetchWait, peek, ack, batchEnqueue
- Keys (5): upload/fetch key package, upload/fetch/batch-fetch hybrid key
- Channels (1): createChannel
- Users (2): resolveUser, resolveIdentity
- Blobs (2): uploadBlob, downloadBlob
- Devices (3): registerDevice, listDevices, revokeDevice
- P2P (3): health, publishEndpoint, resolveEndpoint
- Federation (6): relay enqueue/batch, proxy fetch/resolve, health
3. **Server-push** — notification stream via QUIC uni-stream
4. **Storage upgrades:**
- Drop `FileBackedStore`
- Connection pool (deadpool-sqlite)
- Persist sessions to SQLite
- Atomic queue depth check + enqueue
5. **Tower middleware** — auth validation, rate limiting, audit logging
6. **Multi-stream** — concurrent RPCs per connection (remove 1-stream limit)
**Verification:**
- Server starts, accepts QUIC connections
- Health check RPC works
- OPAQUE registration + login works
- Message enqueue + fetch round-trip
---
### Phase 3: SDK
**Goal:** Complete client SDK library — the heart of v2.
1. **QpqClient** — connect, OPAQUE auth, session management (no global state)
2. **Crypto pipeline** — MLS processing, sealed sender unwrap, hybrid decrypt
(extracted from repl.rs `poll_messages()`)
3. **Conversation management** — create DM, create group, invite, remove, send, receive
4. **Event system**`tokio::broadcast<ClientEvent>` replacing poll loop
- `MessageReceived`, `TypingIndicator`, `ConversationCreated`
- `MemberJoined`, `MemberLeft`, `ConnectionLost`, `Reconnected`
5. **Offline support** — outbox queue, retry with backoff, sync on reconnect
6. **ConversationStore** — SQLCipher local DB (migrate from client/conversation.rs)
7. **Key management** — encrypted DiskKeyStore, MLS group state persistence
8. **Token/secret zeroization**`AuthContext.token` etc. wrapped in `Zeroizing`
**Verification:**
- SDK integration test: connect → login → create DM → send → receive
- No global state (`AUTH_CONTEXT` eliminated)
- Event subscription works
- Offline outbox drains on reconnect
---
### Phase 4: Client
**Goal:** CLI and TUI as thin shells over SDK.
1. **CLI binary** (`qpq`) — clap subcommands calling `QpqClient`
2. **REPL** — readline with tab-completion (rustyline), categorized `/help`
3. **TUI** — ratatui, subscribes to `QpqClient::subscribe()` events
4. **Simplified commands:**
- Hide MLS/KeyPackage internals (auto-refresh)
- Message references by short ID (not index)
- Batch operations (`/create-group team alice bob`)
- Categorized help (Chat, Groups, Security, System)
5. **Auto-server-launch** — keep zero-config DX from v1
6. **Playbook system** — keep YAML-based test scripting
**Verification:**
- `qpq --username alice --password pass` starts REPL (same UX as v1)
- TUI mode works with live event updates
- Tab-completion for commands and usernames
- E2E test: two clients exchange messages
---
### Phase 5: Desktop & Mobile
**Goal:** Tauri 2 app for all platforms.
1. **Tauri 2 project** in `apps/gui/`
2. **Rust backend** — Tauri commands wrapping `QpqClient`
3. **Web frontend** — Svelte or vanilla HTML/JS
4. **Desktop** — Linux, macOS, Windows
5. **Mobile** — iOS, Android via Tauri 2 mobile
6. **QUIC connection migration** — automatic wifi↔cellular handoff
**Verification:**
- Desktop app builds and runs on Linux
- Mobile app builds for Android (emulator)
- Send message from CLI → received in GUI
---
### Phase 6: Polish & Ecosystem
**Goal:** Production readiness.
1. **Federation improvements** — DNS SRV discovery, persistent relay queue with retry
2. **Plugin system v2** — version field, config passthrough, async hooks, WASM plugins
3. **WebTransport** — browser clients over HTTP/3 (same quinn endpoint)
4. **WASM MLS** — compile openmls to wasm32 for browser E2E encryption
5. **CI/CD** — release automation, WASM CI, multi-platform (Linux + macOS)
6. **Security hardening:**
- Fuzz testing (hybrid KEM, sealed sender, padding, protobuf deser)
- Remove all `InsecureServerCertVerifier` paths
- Certificate pinning
- Add passkey/WebAuthn as alternative auth
7. **Double Ratchet for 1:1 DMs** — better per-message forward secrecy than MLS for 2-party
---
## RPC Method Inventory (33 total)
| Category | Methods | Proto File |
|----------|---------|-----------|
| Auth (OPAQUE) | opaqueRegisterStart, opaqueRegisterFinish, opaqueLoginStart, opaqueLoginFinish | auth.proto |
| Delivery | enqueue, fetch, fetchWait, peek, ack, batchEnqueue | delivery.proto |
| Keys | uploadKeyPackage, fetchKeyPackage, uploadHybridKey, fetchHybridKey, fetchHybridKeys | keys.proto |
| Channel | createChannel | channel.proto |
| User | resolveUser, resolveIdentity | user.proto |
| Blob | uploadBlob, downloadBlob | blob.proto |
| Device | registerDevice, listDevices, revokeDevice | device.proto |
| P2P | health, publishEndpoint, resolveEndpoint | p2p.proto |
| Federation | relayEnqueue, relayBatchEnqueue, proxyFetchKeyPackage, proxyFetchHybridKey, proxyResolveUser, federationHealth | federation.proto |
**New in v2:**
| Push Events | Description | Proto File |
|-------------|-------------|-----------|
| MessageNotification | New message available | push.proto |
| TypingNotification | Peer is typing | push.proto |
| ChannelUpdate | Channel created/member changed | push.proto |
| SessionExpired | Auth session expired | push.proto |
---
## Engineering Standards (carried from v1)
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`
- GPG-signed commits only
- No `Co-authored-by` trailers
- No `.unwrap()` on crypto or I/O in non-test paths
- Secrets: zeroize on drop, never in logs
- No stubs / `todo!()` / `unimplemented!()` in production code
- `clippy::unwrap_used = "deny"` at workspace level

View File

@@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "logging_plugin"
version = "0.1.0"
dependencies = [
"quicproquo-plugin-api",
]
[[package]]
name = "quicproquo-plugin-api"
version = "0.1.0"

View File

@@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "quicproquo-plugin-api"
version = "0.1.0"
[[package]]
name = "rate_limit_plugin"
version = "0.1.0"
dependencies = [
"quicproquo-plugin-api",
]

212
scripts/render_terminal.py Executable file
View File

@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""Render two terminal pane captures (with ANSI escapes) into a single PNG.
Usage:
python3 scripts/render_terminal.py left.ansi right.ansi -o assets/screenshot.png
python3 scripts/render_terminal.py left.ansi right.ansi --labels "alice" "bob" -o out.png
"""
import argparse
import re
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# ── Theme (dark terminal) ────────────────────────────────────────────────────
BG = (30, 30, 46) # base (catppuccin mocha-ish)
FG = (205, 214, 244) # default text
DIM = (108, 112, 134) # dim/grey
GREEN = (166, 227, 161)
CYAN = (137, 220, 235)
YELLOW = (249, 226, 175)
RED = (243, 139, 168)
BLUE = (137, 180, 250)
MAGENTA = (203, 166, 247)
BOLD_WHITE = (255, 255, 255)
BORDER = (69, 71, 90)
TITLE_BG = (49, 50, 68)
ANSI_COLORS = {
30: (30, 30, 46), 31: RED, 32: GREEN, 33: YELLOW,
34: BLUE, 35: MAGENTA, 36: CYAN, 37: FG,
90: DIM, 91: RED, 92: GREEN, 93: YELLOW,
94: BLUE, 95: MAGENTA, 96: CYAN, 97: BOLD_WHITE,
}
# ── ANSI parsing ─────────────────────────────────────────────────────────────
ESC_RE = re.compile(r'\x1b\[([0-9;]*)m')
def parse_ansi_line(line):
"""Yield (text, fg_color, bold) spans from an ANSI-escaped line."""
fg = FG
bold = False
dim = False
pos = 0
for m in ESC_RE.finditer(line):
if m.start() > pos:
color = fg
if dim and color == FG:
color = DIM
yield (line[pos:m.start()], color, bold)
codes = m.group(1).split(';') if m.group(1) else ['0']
for code_s in codes:
code = int(code_s) if code_s else 0
if code == 0:
fg, bold, dim = FG, False, False
elif code == 1:
bold = True
elif code == 2:
dim = True
elif code in ANSI_COLORS:
fg = ANSI_COLORS[code]
pos = m.end()
tail = line[pos:]
if tail:
color = fg
if dim and color == FG:
color = DIM
yield (tail, color, bold)
def load_font(size):
"""Try to load a monospace font."""
candidates = [
"/usr/share/fonts/google-noto/NotoSansMono-Regular.ttf",
"/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf",
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
"/usr/share/fonts/liberation-mono/LiberationMono-Regular.ttf",
"/usr/share/fonts/google-droid-sans-mono-fonts/DroidSansMono.ttf",
]
for path in candidates:
if Path(path).exists():
return ImageFont.truetype(path, size)
# fallback
return ImageFont.load_default()
def load_bold_font(size):
candidates = [
"/usr/share/fonts/google-noto/NotoSansMono-Bold.ttf",
"/usr/share/fonts/truetype/noto/NotoSansMono-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf",
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono-Bold.ttf",
"/usr/share/fonts/TTF/DejaVuSansMono-Bold.ttf",
"/usr/share/fonts/liberation-mono/LiberationMono-Bold.ttf",
]
for path in candidates:
if Path(path).exists():
return ImageFont.truetype(path, size)
return load_font(size)
def strip_ansi(s):
return ESC_RE.sub('', s)
def render_pane(lines, width_chars, font, bold_font, font_size, line_height):
"""Render terminal lines to an Image."""
char_w = font.getbbox("M")[2]
img_w = char_w * width_chars + 24 # 12px padding each side
img_h = line_height * len(lines) + 16 # 8px padding top+bottom
img = Image.new("RGB", (img_w, img_h), BG)
draw = ImageDraw.Draw(img)
y = 8
for line in lines:
x = 12
for text, color, bold in parse_ansi_line(line):
f = bold_font if bold else font
draw.text((x, y), text, fill=color, font=f)
x += f.getlength(text)
y += line_height
return img
def main():
ap = argparse.ArgumentParser(description="Render terminal panes to PNG")
ap.add_argument("left", help="Left pane ANSI capture file")
ap.add_argument("right", help="Right pane ANSI capture file")
ap.add_argument("-o", "--output", default="assets/screenshot.png")
ap.add_argument("--labels", nargs=2, default=["alice", "bob"],
help="Labels for the two panes")
ap.add_argument("--font-size", type=int, default=14)
ap.add_argument("--width", type=int, default=58,
help="Width of each pane in characters")
args = ap.parse_args()
font_size = args.font_size
line_height = int(font_size * 1.5)
font = load_font(font_size)
bold_font = load_bold_font(font_size)
char_w = font.getbbox("M")[2]
pane_w = char_w * args.width + 24
left_lines = Path(args.left).read_text().splitlines()
right_lines = Path(args.right).read_text().splitlines()
# Render each pane
left_img = render_pane(left_lines, args.width, font, bold_font,
font_size, line_height)
right_img = render_pane(right_lines, args.width, font, bold_font,
font_size, line_height)
# Composite: title bar + two panes side by side
title_h = 32
gap = 2
max_h = max(left_img.height, right_img.height)
total_w = left_img.width + gap + right_img.width
total_h = title_h + max_h
# Window chrome
canvas = Image.new("RGB", (total_w, total_h), BORDER)
draw = ImageDraw.Draw(canvas)
# Title bar
draw.rectangle([(0, 0), (total_w, title_h - 1)], fill=TITLE_BG)
# Traffic lights
for i, color in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
cx = 16 + i * 22
cy = title_h // 2
draw.ellipse([(cx - 6, cy - 6), (cx + 6, cy + 6)], fill=color)
# Pane labels
label_font = load_font(font_size - 1)
left_label = args.labels[0]
right_label = args.labels[1]
left_label_w = label_font.getlength(left_label)
right_label_w = label_font.getlength(right_label)
draw.text((left_img.width // 2 - left_label_w // 2, 7),
left_label, fill=DIM, font=label_font)
draw.text((left_img.width + gap + right_img.width // 2 - right_label_w // 2, 7),
right_label, fill=DIM, font=label_font)
# Paste panes
canvas.paste(left_img, (0, title_h))
canvas.paste(right_img, (left_img.width + gap, title_h))
# Round corners (simple mask)
radius = 10
mask = Image.new("L", canvas.size, 255)
mask_draw = ImageDraw.Draw(mask)
mask_draw.rectangle([(0, 0), (radius, radius)], fill=0)
mask_draw.pieslice([(0, 0), (radius * 2, radius * 2)], 180, 270, fill=255)
mask_draw.rectangle([(total_w - radius, 0), (total_w, radius)], fill=0)
mask_draw.pieslice([(total_w - radius * 2, 0), (total_w, radius * 2)], 270, 360, fill=255)
# Apply rounded corners with transparent background
final = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
final.paste(canvas, mask=mask)
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
final.save(args.output)
print(f"Saved {args.output} ({final.width}x{final.height})")
if __name__ == "__main__":
main()

92
scripts/screenshot.sh Executable file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# scripts/screenshot.sh — generate a README screenshot automatically
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
INTERACTIVE=false
[[ "${1:-}" == "--interactive" ]] && INTERACTIVE=true
SESSION="qpq-screenshot"
SERVER_PORT=17123
SERVER_ADDR="127.0.0.1:${SERVER_PORT}"
DATA_DIR=$(mktemp -d)
CERT="${DATA_DIR}/server-cert.der"
KEY="${DATA_DIR}/server-key.der"
QPQ="./target/debug/qpq"
SERVER="./target/debug/qpq-server"
SLOG="${DATA_DIR}/server.log"
cleanup() {
[[ -n "${SERVER_PID:-}" ]] && kill "$SERVER_PID" 2>/dev/null || true
tmux kill-session -t "$SESSION" 2>/dev/null || true
# Keep DATA_DIR for debugging
echo "Data dir: $DATA_DIR"
}
trap cleanup EXIT
# ── Build ────────────────────────────────────────────────────────────────────
echo "Building binaries..."
cargo build --bin qpq --bin qpq-server 2>&1 | tail -1
# ── Start server ─────────────────────────────────────────────────────────────
echo "Starting server on ${SERVER_ADDR}..."
RUST_LOG=debug "$SERVER" \
--allow-insecure-auth \
--listen "$SERVER_ADDR" \
--tls-cert "$CERT" \
--tls-key "$KEY" \
--data-dir "$DATA_DIR" \
&>"$SLOG" &
SERVER_PID=$!
for _ in $(seq 1 30); do [[ -f "$CERT" ]] && break; sleep 0.2; done
if [[ ! -f "$CERT" ]]; then echo "ERROR: server did not start"; cat "$SLOG"; exit 1; fi
echo "Server ready (PID ${SERVER_PID})"
# ── tmux session ─────────────────────────────────────────────────────────────
tmux new-session -d -s "$SESSION" -x 114 -y 28
send_alice() { tmux send-keys -t "${SESSION}:0.0" "$1" Enter; }
send_bob() { tmux send-keys -t "${SESSION}:0.1" "$1" Enter; }
# Start Alice (left pane)
tmux send-keys -t "$SESSION" \
"RUST_LOG=debug $QPQ repl --username alice --password demopass1 --server $SERVER_ADDR --ca-cert $CERT --state ${DATA_DIR}/alice.bin 2>${DATA_DIR}/alice-debug.log" Enter
# Start Bob (right pane)
tmux split-window -h -t "$SESSION"
tmux send-keys -t "$SESSION" \
"RUST_LOG=debug $QPQ repl --username bob --password demopass2 --server $SERVER_ADDR --ca-cert $CERT --state ${DATA_DIR}/bob.bin 2>${DATA_DIR}/bob-debug.log" Enter
tmux select-layout -t "$SESSION" even-horizontal
sleep 5
# Alice creates DM with Bob
send_alice "/dm bob"
sleep 4
# Wait for Bob's poller (8 seconds = 8 poll cycles)
echo "Waiting for Bob to poll Welcome..."
sleep 10
# Check Bob's list
send_bob "/list"
sleep 2
# Capture both panes
{
echo "=== Alice ==="
tmux capture-pane -t "${SESSION}:0.0" -p
echo ""
echo "=== Bob ==="
tmux capture-pane -t "${SESSION}:0.1" -p
} > "${DATA_DIR}/capture.txt"
echo ""
cat "${DATA_DIR}/capture.txt"
echo ""
echo "Server log tail:"
tail -30 "$SLOG" 2>/dev/null || true
echo ""
echo "Debug logs in: $DATA_DIR"
echo "Check: $DATA_DIR/alice-debug.log and $DATA_DIR/bob-debug.log"