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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user