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,72 @@
//! Authentication domain logic — OPAQUE registration and login.
//!
//! This module contains the pure business logic for OPAQUE auth,
//! extracted from `node_service/auth_ops.rs`. It operates on domain
//! types and the `Store` trait, with no dependency on Cap'n Proto or Protobuf.
use std::sync::Arc;
use dashmap::DashMap;
use opaque_ke::ServerSetup;
use quicproquo_core::opaque_auth::OpaqueSuite;
use crate::auth::{AuthConfig, PendingLogin, SessionInfo};
use crate::storage::{Store, StorageError};
use super::types::*;
/// Shared state needed by auth operations.
pub struct AuthService {
pub store: Arc<dyn Store>,
pub opaque_setup: Arc<ServerSetup<OpaqueSuite>>,
pub pending_logins: Arc<DashMap<String, PendingLogin>>,
pub sessions: Arc<DashMap<Vec<u8>, SessionInfo>>,
pub auth_cfg: Arc<AuthConfig>,
}
impl AuthService {
/// Validate a session token and return the caller's auth context.
pub fn validate_session(&self, token: &[u8]) -> Option<CallerAuth> {
let info = self.sessions.get(token)?;
if info.expires_at <= crate::auth::current_timestamp() {
self.sessions.remove(token);
return None;
}
Some(CallerAuth {
identity_key: info.identity_key.clone(),
token: token.to_vec(),
device_id: None,
})
}
/// Start OPAQUE registration.
pub fn register_start(&self, req: RegisterStartReq) -> Result<RegisterStartResp, StorageError> {
use opaque_ke::ServerRegistration;
let result = ServerRegistration::<OpaqueSuite>::start(
&self.opaque_setup,
opaque_ke::RegistrationRequest::deserialize(&req.request_bytes)
.map_err(|e| StorageError::Io(format!("bad registration request: {e}")))?,
req.username.as_bytes(),
)
.map_err(|e| StorageError::Io(format!("OPAQUE register start: {e}")))?;
let response_bytes = result.message.serialize().to_vec();
Ok(RegisterStartResp { response_bytes })
}
/// Finish OPAQUE registration — persist user record and identity key.
pub fn register_finish(&self, req: RegisterFinishReq) -> Result<RegisterFinishResp, StorageError> {
let upload = opaque_ke::RegistrationUpload::<OpaqueSuite>::deserialize(&req.upload_bytes)
.map_err(|e| StorageError::Io(format!("bad registration upload: {e}")))?;
let record = opaque_ke::ServerRegistration::<OpaqueSuite>::finish(upload);
let serialized = record.serialize().to_vec();
self.store.store_user_record(&req.username, serialized)?;
self.store
.store_user_identity_key(&req.username, req.identity_key)?;
Ok(RegisterFinishResp { success: true })
}
}

View File

@@ -0,0 +1,110 @@
//! Delivery domain logic — enqueue, fetch, peek, ack.
//!
//! Pure business logic operating on `Store` trait and domain types.
use std::sync::Arc;
use dashmap::DashMap;
use tokio::sync::Notify;
use crate::storage::Store;
use super::types::*;
/// Shared state needed by delivery operations.
pub struct DeliveryService {
pub store: Arc<dyn Store>,
pub waiters: Arc<DashMap<Vec<u8>, Arc<Notify>>>,
}
impl DeliveryService {
/// Enqueue a payload for delivery.
pub fn enqueue(&self, req: EnqueueReq) -> Result<EnqueueResp, crate::storage::StorageError> {
let ttl = if req.ttl_secs > 0 {
Some(req.ttl_secs)
} else {
None
};
let seq = self.store.enqueue(
&req.recipient_key,
&req.channel_id,
req.payload,
ttl,
)?;
// Wake any long-polling waiter for this recipient.
if let Some(notify) = self.waiters.get(&req.recipient_key) {
notify.notify_one();
}
Ok(EnqueueResp {
seq,
delivery_proof: Vec::new(), // TODO: sign in Phase 2
})
}
/// Fetch and drain queued messages.
pub fn fetch(&self, req: FetchReq) -> Result<FetchResp, crate::storage::StorageError> {
let messages = if req.limit > 0 {
self.store
.fetch_limited(&req.recipient_key, &req.channel_id, req.limit as usize)?
} else {
self.store.fetch(&req.recipient_key, &req.channel_id)?
};
Ok(FetchResp {
payloads: messages
.into_iter()
.map(|(seq, data)| Envelope { seq, data })
.collect(),
})
}
/// Peek at messages without removing them.
pub fn peek(&self, req: PeekReq) -> Result<PeekResp, crate::storage::StorageError> {
let messages = self.store.peek(
&req.recipient_key,
&req.channel_id,
if req.limit > 0 { req.limit as usize } else { 0 },
)?;
Ok(PeekResp {
payloads: messages
.into_iter()
.map(|(seq, data)| Envelope { seq, data })
.collect(),
})
}
/// Acknowledge messages up to a sequence number.
pub fn ack(&self, req: AckReq) -> Result<(), crate::storage::StorageError> {
self.store
.ack(&req.recipient_key, &req.channel_id, req.seq_up_to)?;
Ok(())
}
/// Batch enqueue to multiple recipients.
pub fn batch_enqueue(
&self,
req: BatchEnqueueReq,
) -> Result<BatchEnqueueResp, crate::storage::StorageError> {
let ttl = if req.ttl_secs > 0 {
Some(req.ttl_secs)
} else {
None
};
let mut seqs = Vec::with_capacity(req.recipient_keys.len());
for rk in &req.recipient_keys {
let seq = self.store.enqueue(rk, &req.channel_id, req.payload.clone(), ttl)?;
seqs.push(seq);
if let Some(notify) = self.waiters.get(rk) {
notify.notify_one();
}
}
Ok(BatchEnqueueResp { seqs })
}
}

View File

@@ -0,0 +1,10 @@
//! Domain types and service logic — protocol-agnostic.
//!
//! These types define the server's business logic independently of any
//! serialization format (Cap'n Proto, Protobuf). RPC handlers translate
//! wire-format messages into these types, call service functions, and
//! translate the results back.
pub mod types;
pub mod auth;
pub mod delivery;

View File

@@ -0,0 +1,260 @@
//! Plain Rust request/response types for server domain logic.
//!
//! No proto, no capnp — just Rust structs.
// ── Auth ─────────────────────────────────────────────────────────────────────
/// Caller authentication context (resolved from session token).
#[derive(Debug, Clone)]
pub struct CallerAuth {
/// Ed25519 identity key of the authenticated caller (32 bytes).
pub identity_key: Vec<u8>,
/// Session token bytes.
pub token: Vec<u8>,
/// Device ID (optional, for auditing).
pub device_id: Option<Vec<u8>>,
}
/// OPAQUE registration start.
pub struct RegisterStartReq {
pub username: String,
pub request_bytes: Vec<u8>,
}
pub struct RegisterStartResp {
pub response_bytes: Vec<u8>,
}
/// OPAQUE registration finish.
pub struct RegisterFinishReq {
pub username: String,
pub upload_bytes: Vec<u8>,
pub identity_key: Vec<u8>,
}
pub struct RegisterFinishResp {
pub success: bool,
}
/// OPAQUE login start.
pub struct LoginStartReq {
pub username: String,
pub request_bytes: Vec<u8>,
}
pub struct LoginStartResp {
pub response_bytes: Vec<u8>,
}
/// OPAQUE login finish.
pub struct LoginFinishReq {
pub username: String,
pub finalization_bytes: Vec<u8>,
pub identity_key: Vec<u8>,
}
pub struct LoginFinishResp {
pub session_token: Vec<u8>,
}
// ── Delivery ─────────────────────────────────────────────────────────────────
/// An envelope pairing a sequence number with an opaque payload.
#[derive(Debug, Clone)]
pub struct Envelope {
pub seq: u64,
pub data: Vec<u8>,
}
pub struct EnqueueReq {
pub recipient_key: Vec<u8>,
pub payload: Vec<u8>,
pub channel_id: Vec<u8>,
pub ttl_secs: u32,
}
pub struct EnqueueResp {
pub seq: u64,
pub delivery_proof: Vec<u8>,
}
pub struct FetchReq {
pub recipient_key: Vec<u8>,
pub channel_id: Vec<u8>,
pub limit: u32,
}
pub struct FetchResp {
pub payloads: Vec<Envelope>,
}
pub struct PeekReq {
pub recipient_key: Vec<u8>,
pub channel_id: Vec<u8>,
pub limit: u32,
}
pub struct PeekResp {
pub payloads: Vec<Envelope>,
}
pub struct AckReq {
pub recipient_key: Vec<u8>,
pub channel_id: Vec<u8>,
pub seq_up_to: u64,
}
pub struct BatchEnqueueReq {
pub recipient_keys: Vec<Vec<u8>>,
pub payload: Vec<u8>,
pub channel_id: Vec<u8>,
pub ttl_secs: u32,
}
pub struct BatchEnqueueResp {
pub seqs: Vec<u64>,
}
// ── Keys ─────────────────────────────────────────────────────────────────────
pub struct UploadKeyPackageReq {
pub identity_key: Vec<u8>,
pub package: Vec<u8>,
}
pub struct UploadKeyPackageResp {
pub fingerprint: Vec<u8>,
}
pub struct FetchKeyPackageReq {
pub identity_key: Vec<u8>,
}
pub struct FetchKeyPackageResp {
pub package: Vec<u8>,
}
pub struct UploadHybridKeyReq {
pub identity_key: Vec<u8>,
pub hybrid_public_key: Vec<u8>,
}
pub struct FetchHybridKeyReq {
pub identity_key: Vec<u8>,
}
pub struct FetchHybridKeyResp {
pub hybrid_public_key: Vec<u8>,
}
pub struct FetchHybridKeysReq {
pub identity_keys: Vec<Vec<u8>>,
}
pub struct FetchHybridKeysResp {
pub keys: Vec<Vec<u8>>,
}
// ── Channel ──────────────────────────────────────────────────────────────────
pub struct CreateChannelReq {
pub peer_key: Vec<u8>,
}
pub struct CreateChannelResp {
pub channel_id: Vec<u8>,
pub was_new: bool,
}
// ── User ─────────────────────────────────────────────────────────────────────
pub struct ResolveUserReq {
pub username: String,
}
pub struct ResolveUserResp {
pub identity_key: Vec<u8>,
pub inclusion_proof: Vec<u8>,
}
pub struct ResolveIdentityReq {
pub identity_key: Vec<u8>,
}
pub struct ResolveIdentityResp {
pub username: String,
}
// ── Blob ─────────────────────────────────────────────────────────────────────
pub struct UploadBlobReq {
pub blob_hash: Vec<u8>,
pub chunk: Vec<u8>,
pub offset: u64,
pub total_size: u64,
pub mime_type: String,
}
pub struct UploadBlobResp {
pub blob_id: Vec<u8>,
}
pub struct DownloadBlobReq {
pub blob_id: Vec<u8>,
pub offset: u64,
pub length: u32,
}
pub struct DownloadBlobResp {
pub chunk: Vec<u8>,
pub total_size: u64,
pub mime_type: String,
}
// ── Device ───────────────────────────────────────────────────────────────────
pub struct RegisterDeviceReq {
pub device_id: Vec<u8>,
pub device_name: String,
}
pub struct RegisterDeviceResp {
pub success: bool,
}
pub struct DeviceInfo {
pub device_id: Vec<u8>,
pub device_name: String,
pub registered_at: u64,
}
pub struct ListDevicesResp {
pub devices: Vec<DeviceInfo>,
}
pub struct RevokeDeviceReq {
pub device_id: Vec<u8>,
}
pub struct RevokeDeviceResp {
pub success: bool,
}
// ── P2P ──────────────────────────────────────────────────────────────────────
pub struct PublishEndpointReq {
pub identity_key: Vec<u8>,
pub node_addr: Vec<u8>,
}
pub struct ResolveEndpointReq {
pub identity_key: Vec<u8>,
}
pub struct ResolveEndpointResp {
pub node_addr: Vec<u8>,
}
pub struct HealthResp {
pub status: String,
}

View File

@@ -17,6 +17,7 @@ use tokio::task::LocalSet;
mod auth;
mod config;
pub mod domain;
mod error_codes;
mod federation;
pub mod hooks;