feat: v2 Phase 1 — foundation, proto schemas, RPC framework, SDK skeleton

New workspace structure with 9 crates. Adds:

- proto/qpq/v1/*.proto: 11 protobuf schemas covering all 33 RPC methods
- quicproquo-proto: dual codegen (capnp legacy + prost v2)
- quicproquo-rpc: QUIC RPC framework (framing, server, client, middleware)
- quicproquo-sdk: client SDK (QpqClient, events, conversation store)
- quicproquo-server/domain/: protocol-agnostic domain types and services
- justfile: build commands

Wire format: [method_id:u16][req_id:u32][len:u32][protobuf] per QUIC stream.
All 151 existing tests pass. Backward compatible with v1 capnp code.
This commit is contained in:
2026-03-04 12:02:07 +01:00
parent 394199b19b
commit a5864127d1
37 changed files with 3115 additions and 2778 deletions

View File

@@ -0,0 +1,96 @@
//! Tower-based middleware layers for the RPC server.
//!
//! - `AuthLayer`: validates session tokens and attaches identity to context.
//! - `RateLimitLayer`: per-IP request rate limiting.
use std::time::{Duration, Instant};
use dashmap::DashMap;
// ── Auth middleware ──────────────────────────────────────────────────────────
/// Validates bearer tokens and resolves identity keys.
pub trait SessionValidator: Send + Sync + 'static {
/// Validate a session token, returning the identity key if valid.
fn validate(&self, token: &[u8]) -> Option<Vec<u8>>;
}
/// Auth context extracted from a validated session.
#[derive(Debug, Clone)]
pub struct AuthContext {
/// The Ed25519 identity key of the authenticated caller.
pub identity_key: Vec<u8>,
}
// ── Rate limiter ─────────────────────────────────────────────────────────────
/// Simple per-key sliding-window rate limiter.
pub struct RateLimiter {
/// Max requests per window.
max_requests: u32,
/// Window duration.
window: Duration,
/// Map from key → (count, window_start).
state: DashMap<Vec<u8>, (u32, Instant)>,
}
impl RateLimiter {
/// Create a new rate limiter.
pub fn new(max_requests: u32, window: Duration) -> Self {
Self {
max_requests,
window,
state: DashMap::new(),
}
}
/// Check if a request from `key` is allowed. Returns `true` if allowed.
pub fn check(&self, key: &[u8]) -> bool {
let now = Instant::now();
let mut entry = self.state.entry(key.to_vec()).or_insert((0, now));
let (count, window_start) = entry.value_mut();
if now.duration_since(*window_start) >= self.window {
// Reset window.
*count = 1;
*window_start = now;
true
} else if *count < self.max_requests {
*count += 1;
true
} else {
false
}
}
/// Remove expired entries (call periodically for memory hygiene).
pub fn gc(&self) {
let now = Instant::now();
self.state.retain(|_, (_, start)| now.duration_since(*start) < self.window * 2);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rate_limiter_allows_within_limit() {
let rl = RateLimiter::new(3, Duration::from_secs(60));
let key = b"test-key";
assert!(rl.check(key));
assert!(rl.check(key));
assert!(rl.check(key));
assert!(!rl.check(key)); // 4th request denied
}
#[test]
fn rate_limiter_resets_after_window() {
let rl = RateLimiter::new(1, Duration::from_millis(1));
let key = b"test-key";
assert!(rl.check(key));
assert!(!rl.check(key));
std::thread::sleep(Duration::from_millis(5));
assert!(rl.check(key)); // window expired
}
}