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:
259
docs/src/internals/server-hooks.md
Normal file
259
docs/src/internals/server-hooks.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Server Hooks
|
||||
|
||||
The `ServerHooks` trait provides a plugin system for extending the quicproquo
|
||||
server. Hooks fire at key points in the request lifecycle — message delivery,
|
||||
authentication, channel creation, and message fetch — allowing you to inspect,
|
||||
log, rate-limit, or reject operations without modifying server internals.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
```text
|
||||
Client RPC request
|
||||
└─ Validation (auth, rate limits, wire format)
|
||||
└─ Hook fires (on_message_enqueue, on_auth, etc.)
|
||||
├─ HookAction::Continue → proceed to storage/delivery
|
||||
└─ HookAction::Reject("reason") → error returned to client
|
||||
```
|
||||
|
||||
Hooks are called **synchronously** in the RPC handler path after validation
|
||||
but before storage. Keep hook implementations fast — offload heavy work
|
||||
(HTTP calls, disk I/O, analytics) to background tasks.
|
||||
|
||||
---
|
||||
|
||||
## The `ServerHooks` trait
|
||||
|
||||
```rust,ignore
|
||||
pub trait ServerHooks: Send + Sync {
|
||||
/// Called before a message is stored in the delivery queue.
|
||||
/// Return HookAction::Reject to prevent delivery.
|
||||
fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
|
||||
HookAction::Continue
|
||||
}
|
||||
|
||||
/// Called after a batch of messages is enqueued.
|
||||
fn on_batch_enqueue(&self, events: &[MessageEvent]) {}
|
||||
|
||||
/// Called after a successful or failed login attempt.
|
||||
fn on_auth(&self, event: &AuthEvent) {}
|
||||
|
||||
/// Called after a channel is created or looked up.
|
||||
fn on_channel_created(&self, event: &ChannelEvent) {}
|
||||
|
||||
/// Called after messages are fetched from the delivery queue.
|
||||
fn on_fetch(&self, event: &FetchEvent) {}
|
||||
|
||||
/// Called when a user completes OPAQUE registration.
|
||||
fn on_user_registered(&self, username: &str, identity_key: &[u8]) {}
|
||||
}
|
||||
```
|
||||
|
||||
All methods have default no-op implementations. Override only the events you
|
||||
care about.
|
||||
|
||||
---
|
||||
|
||||
## Hook action
|
||||
|
||||
```rust,ignore
|
||||
pub enum HookAction {
|
||||
/// Allow the operation to proceed.
|
||||
Continue,
|
||||
/// Reject the operation with a reason (returned to the client as an error).
|
||||
Reject(String),
|
||||
}
|
||||
```
|
||||
|
||||
Currently only `on_message_enqueue` can reject operations. Other hooks are
|
||||
observational (fire-and-forget).
|
||||
|
||||
---
|
||||
|
||||
## Event types
|
||||
|
||||
### `MessageEvent`
|
||||
|
||||
Fired on `enqueue` and `batch_enqueue` RPC calls.
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------|-------------------|-------------|
|
||||
| `sender_identity` | `Option<Vec<u8>>` | Sender's 32-byte identity key (None in sealed sender mode). |
|
||||
| `recipient_key` | `Vec<u8>` | Recipient's 32-byte identity key. |
|
||||
| `channel_id` | `Vec<u8>` | 16-byte channel ID. |
|
||||
| `payload_len` | `usize` | Length of the encrypted payload in bytes. |
|
||||
| `seq` | `u64` | Server-assigned sequence number. |
|
||||
|
||||
### `AuthEvent`
|
||||
|
||||
Fired after OPAQUE login completes (success or failure).
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------------|----------|-------------|
|
||||
| `username` | `String` | The username that attempted to authenticate. |
|
||||
| `success` | `bool` | Whether authentication succeeded. |
|
||||
| `failure_reason` | `String` | Failure reason (empty on success). |
|
||||
|
||||
### `ChannelEvent`
|
||||
|
||||
Fired after a `createChannel` RPC call.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------------|------------|-------------|
|
||||
| `channel_id` | `Vec<u8>` | 16-byte channel ID. |
|
||||
| `initiator_key` | `Vec<u8>` | Identity key of the channel initiator. |
|
||||
| `peer_key` | `Vec<u8>` | Identity key of the peer. |
|
||||
| `was_new` | `bool` | True if this is a newly created channel. |
|
||||
|
||||
### `FetchEvent`
|
||||
|
||||
Fired after a `fetch` or `fetchWait` RPC call.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------------|------------|-------------|
|
||||
| `recipient_key` | `Vec<u8>` | Identity key of the fetcher. |
|
||||
| `channel_id` | `Vec<u8>` | Channel ID being fetched from. |
|
||||
| `message_count` | `usize` | Number of messages returned. |
|
||||
|
||||
---
|
||||
|
||||
## Built-in implementations
|
||||
|
||||
### `NoopHooks`
|
||||
|
||||
Does nothing. This is the default when no hooks are configured.
|
||||
|
||||
```rust,ignore
|
||||
pub struct NoopHooks;
|
||||
impl ServerHooks for NoopHooks {}
|
||||
```
|
||||
|
||||
### `TracingHooks`
|
||||
|
||||
Logs all events via the `tracing` crate at info/debug level.
|
||||
|
||||
```rust,ignore
|
||||
pub struct TracingHooks;
|
||||
|
||||
impl ServerHooks for TracingHooks {
|
||||
fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
|
||||
tracing::info!(
|
||||
recipient_prefix = %hex_prefix(&event.recipient_key),
|
||||
payload_len = event.payload_len,
|
||||
seq = event.seq,
|
||||
"hook: message enqueued"
|
||||
);
|
||||
HookAction::Continue
|
||||
}
|
||||
|
||||
fn on_auth(&self, event: &AuthEvent) {
|
||||
if event.success {
|
||||
tracing::info!(username = %event.username, "hook: login success");
|
||||
} else {
|
||||
tracing::warn!(
|
||||
username = %event.username,
|
||||
reason = %event.failure_reason,
|
||||
"hook: login failure"
|
||||
);
|
||||
}
|
||||
}
|
||||
// ... other methods log similarly
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Writing a custom hook
|
||||
|
||||
### Example: payload size limiter
|
||||
|
||||
```rust,ignore
|
||||
use quicproquo_server::hooks::{ServerHooks, HookAction, MessageEvent};
|
||||
|
||||
struct PayloadLimiter {
|
||||
max_bytes: usize,
|
||||
}
|
||||
|
||||
impl ServerHooks for PayloadLimiter {
|
||||
fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
|
||||
if event.payload_len > self.max_bytes {
|
||||
return HookAction::Reject(format!(
|
||||
"payload too large: {} > {} bytes",
|
||||
event.payload_len, self.max_bytes
|
||||
));
|
||||
}
|
||||
HookAction::Continue
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: login auditor
|
||||
|
||||
```rust,ignore
|
||||
use quicproquo_server::hooks::{ServerHooks, AuthEvent};
|
||||
|
||||
struct LoginAuditor;
|
||||
|
||||
impl ServerHooks for LoginAuditor {
|
||||
fn on_auth(&self, event: &AuthEvent) {
|
||||
if !event.success {
|
||||
eprintln!(
|
||||
"AUDIT: failed login for '{}': {}",
|
||||
event.username, event.failure_reason
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: composing multiple hooks
|
||||
|
||||
```rust,ignore
|
||||
use quicproquo_server::hooks::*;
|
||||
|
||||
struct CompositeHooks {
|
||||
hooks: Vec<Box<dyn ServerHooks>>,
|
||||
}
|
||||
|
||||
impl ServerHooks for CompositeHooks {
|
||||
fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction {
|
||||
for hook in &self.hooks {
|
||||
if let HookAction::Reject(reason) = hook.on_message_enqueue(event) {
|
||||
return HookAction::Reject(reason);
|
||||
}
|
||||
}
|
||||
HookAction::Continue
|
||||
}
|
||||
|
||||
fn on_auth(&self, event: &AuthEvent) {
|
||||
for hook in &self.hooks {
|
||||
hook.on_auth(event);
|
||||
}
|
||||
}
|
||||
// ... delegate other methods similarly
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important considerations
|
||||
|
||||
- **E2E encryption**: Message payloads are encrypted end-to-end. Hooks cannot
|
||||
inspect plaintext content — they see only metadata (sender, recipient,
|
||||
payload size, sequence number).
|
||||
- **Performance**: Hooks run synchronously in the RPC handler. A slow hook
|
||||
blocks the RPC response. Use `tokio::spawn` for async work.
|
||||
- **Thread safety**: `ServerHooks` requires `Send + Sync`. Use `Arc<Mutex<_>>`
|
||||
or lock-free structures for shared mutable state.
|
||||
- **Reject semantics**: Only `on_message_enqueue` supports rejection. Other
|
||||
hooks are informational — the operation proceeds regardless of what the hook
|
||||
does.
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Delivery Service Internals](delivery-service.md) -- how messages flow through the server
|
||||
- [Authentication Service Internals](authentication-service.md) -- OPAQUE auth flow
|
||||
- [Bot SDK](../getting-started/bot-sdk.md) -- build bots that interact with the server
|
||||
Reference in New Issue
Block a user