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:
72
crates/quicproquo-server/src/domain/auth.rs
Normal file
72
crates/quicproquo-server/src/domain/auth.rs
Normal 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 })
|
||||
}
|
||||
}
|
||||
110
crates/quicproquo-server/src/domain/delivery.rs
Normal file
110
crates/quicproquo-server/src/domain/delivery.rs
Normal 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 })
|
||||
}
|
||||
}
|
||||
10
crates/quicproquo-server/src/domain/mod.rs
Normal file
10
crates/quicproquo-server/src/domain/mod.rs
Normal 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;
|
||||
260
crates/quicproquo-server/src/domain/types.rs
Normal file
260
crates/quicproquo-server/src/domain/types.rs
Normal 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,
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use tokio::task::LocalSet;
|
||||
|
||||
mod auth;
|
||||
mod config;
|
||||
pub mod domain;
|
||||
mod error_codes;
|
||||
mod federation;
|
||||
pub mod hooks;
|
||||
|
||||
Reference in New Issue
Block a user