feat: Phase 9 — developer experience, extensibility, and community growth

New crates:
- quicproquo-bot: Bot SDK with polling API + JSON pipe mode
- quicproquo-kt: Key Transparency Merkle log (RFC 9162 subset)
- quicproquo-plugin-api: no_std C-compatible plugin vtable API
- quicproquo-gen: scaffolding tool (qpq-gen plugin/bot/rpc/hook)

Server features:
- ServerHooks trait wired into all RPC handlers (enqueue, fetch, auth,
  channel, registration) with plugin rejection support
- Dynamic plugin loader (libloading) with --plugin-dir config
- Delivery proof canary tokens (Ed25519 server signatures on enqueue)
- Key Transparency Merkle log with inclusion proofs on resolveUser

Core library:
- Safety numbers (60-digit HMAC-SHA256 key verification codes)
- Verifiable transcript archive (CBOR + ChaCha20-Poly1305 + hash chain)
- Delivery proof verification utility
- Criterion benchmarks (hybrid KEM, MLS, identity, sealed sender, padding)

Client:
- /verify REPL command for out-of-band key verification
- Full-screen TUI via Ratatui (feature-gated --features tui)
- qpq export / qpq export-verify CLI subcommands
- KT inclusion proof verification on user resolution

Also: ROADMAP Phase 9 added, bot SDK docs, server hooks docs,
crate-responsibilities updated, example plugins (rate_limit, logging).
This commit is contained in:
2026-03-03 22:47:38 +01:00
parent b6483dedbc
commit dc4e4e49a0
62 changed files with 6959 additions and 62 deletions

View File

@@ -11,6 +11,8 @@ use crate::error_codes::*;
use crate::metrics;
use crate::storage::StorageError;
use crate::hooks::AuthEvent;
use super::NodeServiceImpl;
// Audit events in this module must never include secrets (no session tokens, passwords, or raw keys).
@@ -207,6 +209,11 @@ impl NodeServiceImpl {
// Audit: login failure — do not log secrets (no token, no password).
tracing::warn!(user = %username, "audit: auth login failure (no pending login)");
metrics::record_auth_login_failure_total();
self.hooks.on_auth(&AuthEvent {
username: username.clone(),
success: false,
failure_reason: "no pending login".to_string(),
});
return Promise::err(coded_error(E019_NO_PENDING_LOGIN, "no pending login for this username"))
}
};
@@ -236,6 +243,11 @@ impl NodeServiceImpl {
Err(e) => {
tracing::warn!(user = %username, "audit: auth login failure (OPAQUE finish failed)");
metrics::record_auth_login_failure_total();
self.hooks.on_auth(&AuthEvent {
username: username.clone(),
success: false,
failure_reason: format!("OPAQUE finish failed: {e}"),
});
return Promise::err(coded_error(
E010_OPAQUE_ERROR,
format!("OPAQUE login finish failed (bad password?): {e}"),
@@ -255,6 +267,11 @@ impl NodeServiceImpl {
if stored_ik != identity_key {
tracing::warn!(user = %username, "audit: auth login failure (identity mismatch)");
metrics::record_auth_login_failure_total();
self.hooks.on_auth(&AuthEvent {
username: username.clone(),
success: false,
failure_reason: "identity key mismatch".to_string(),
});
return Promise::err(coded_error(
E016_IDENTITY_MISMATCH,
"identity key does not match registered key",
@@ -279,6 +296,13 @@ impl NodeServiceImpl {
results.get().set_session_token(&token_vec);
// Hook: on_auth — fires after successful login.
self.hooks.on_auth(&AuthEvent {
username: username.clone(),
success: true,
failure_reason: String::new(),
});
// Audit: login success — do not log session token or any secrets.
metrics::record_auth_login_success_total();
tracing::info!(user = %username, "audit: auth login success — session token issued");
@@ -356,14 +380,39 @@ impl NodeServiceImpl {
Err(e) => return Promise::err(storage_err(e)),
}
// Hook: on_user_registered — fires after successful registration.
self.hooks.on_user_registered(&username, &identity_key);
if !identity_key.is_empty() {
if let Err(e) = self
.store
.store_user_identity_key(&username, identity_key)
.store_user_identity_key(&username, identity_key.clone())
.map_err(storage_err)
{
return Promise::err(e);
}
// Append (username, identity_key) to the Key Transparency Merkle log.
match self.kt_log.lock() {
Ok(mut log) => {
log.append(&username, &identity_key);
// Persist after each append (small extra cost, but ensures durability).
match log.to_bytes() {
Ok(bytes) => {
if let Err(e) = self.store.save_kt_log(bytes) {
tracing::warn!(user = %username, error = %e, "KT log persist failed");
}
}
Err(e) => {
tracing::warn!(user = %username, error = %e, "KT log serialise failed");
}
}
tracing::info!(user = %username, tree_size = log.len(), "KT: appended identity binding");
}
Err(e) => {
tracing::warn!(user = %username, error = %e, "KT log lock poisoned; skipping append");
}
}
}
results.get().set_success(true);