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:
193
crates/quicproquo-sdk/src/client.rs
Normal file
193
crates/quicproquo-sdk/src/client.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
//! `QpqClient` — the main entry point for the quicproquo SDK.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::info;
|
||||
|
||||
use crate::config::ClientConfig;
|
||||
use crate::conversation::ConversationStore;
|
||||
use crate::error::SdkError;
|
||||
use crate::events::ClientEvent;
|
||||
|
||||
/// The main SDK client. All state is contained within this struct — no globals.
|
||||
pub struct QpqClient {
|
||||
config: ClientConfig,
|
||||
rpc: Option<quicproquo_rpc::client::RpcClient>,
|
||||
event_tx: broadcast::Sender<ClientEvent>,
|
||||
/// The authenticated username, if logged in.
|
||||
username: Option<String>,
|
||||
/// The local identity key (Ed25519 public key, 32 bytes).
|
||||
identity_key: Option<Vec<u8>>,
|
||||
/// Session token from OPAQUE login.
|
||||
session_token: Option<Vec<u8>>,
|
||||
/// Local conversation store (SQLCipher).
|
||||
conv_store: Option<ConversationStore>,
|
||||
}
|
||||
|
||||
impl QpqClient {
|
||||
/// Create a new client with the given configuration.
|
||||
pub fn new(config: ClientConfig) -> Self {
|
||||
let (event_tx, _) = broadcast::channel(256);
|
||||
Self {
|
||||
config,
|
||||
rpc: None,
|
||||
event_tx,
|
||||
username: None,
|
||||
identity_key: None,
|
||||
session_token: None,
|
||||
conv_store: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the server.
|
||||
pub async fn connect(&mut self) -> Result<(), SdkError> {
|
||||
let tls_config = build_tls_config(self.config.accept_invalid_certs)?;
|
||||
|
||||
let rpc_config = quicproquo_rpc::client::RpcClientConfig {
|
||||
server_addr: self.config.server_addr,
|
||||
server_name: self.config.server_name.clone(),
|
||||
tls_config: Arc::new(tls_config),
|
||||
alpn: self.config.alpn.clone(),
|
||||
};
|
||||
|
||||
let client = quicproquo_rpc::client::RpcClient::connect(rpc_config).await?;
|
||||
self.rpc = Some(client);
|
||||
|
||||
// Open local conversation store.
|
||||
let store = ConversationStore::open(
|
||||
&self.config.db_path,
|
||||
self.config.db_password.as_deref(),
|
||||
)?;
|
||||
self.conv_store = Some(store);
|
||||
|
||||
self.emit(ClientEvent::Connected);
|
||||
info!(server = %self.config.server_addr, "connected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribe to client events. Returns a broadcast receiver.
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<ClientEvent> {
|
||||
self.event_tx.subscribe()
|
||||
}
|
||||
|
||||
/// Get the authenticated username, if logged in.
|
||||
pub fn username(&self) -> Option<&str> {
|
||||
self.username.as_deref()
|
||||
}
|
||||
|
||||
/// Get the local identity key.
|
||||
pub fn identity_key(&self) -> Option<&[u8]> {
|
||||
self.identity_key.as_deref()
|
||||
}
|
||||
|
||||
/// Whether the client is connected.
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.rpc.is_some()
|
||||
}
|
||||
|
||||
/// Whether the client is authenticated.
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.session_token.is_some()
|
||||
}
|
||||
|
||||
/// Get a reference to the RPC client (for direct calls).
|
||||
pub fn rpc(&self) -> Result<&quicproquo_rpc::client::RpcClient, SdkError> {
|
||||
self.rpc.as_ref().ok_or(SdkError::NotConnected)
|
||||
}
|
||||
|
||||
/// Get a reference to the conversation store.
|
||||
pub fn conversations(&self) -> Result<&ConversationStore, SdkError> {
|
||||
self.conv_store
|
||||
.as_ref()
|
||||
.ok_or(SdkError::NotConnected)
|
||||
}
|
||||
|
||||
/// Disconnect from the server.
|
||||
pub fn disconnect(&mut self) {
|
||||
if let Some(rpc) = self.rpc.take() {
|
||||
rpc.close();
|
||||
self.emit(ClientEvent::Disconnected {
|
||||
reason: "client closed".into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn emit(&self, event: ClientEvent) {
|
||||
// Ignore send errors (no subscribers).
|
||||
let _ = self.event_tx.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for QpqClient {
|
||||
fn drop(&mut self) {
|
||||
self.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tls_config(accept_invalid_certs: bool) -> Result<rustls::ClientConfig, SdkError> {
|
||||
let builder = rustls::ClientConfig::builder();
|
||||
|
||||
if accept_invalid_certs {
|
||||
let config = builder
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(InsecureVerifier))
|
||||
.with_no_client_auth();
|
||||
Ok(config)
|
||||
} else {
|
||||
let roots = rustls::RootCertStore::empty();
|
||||
let config = builder
|
||||
.with_root_certificates(roots)
|
||||
.with_no_client_auth();
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// A TLS verifier that accepts any certificate (for dev mode only).
|
||||
#[derive(Debug)]
|
||||
struct InsecureVerifier;
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for InsecureVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA384,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA512,
|
||||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
rustls::SignatureScheme::ED25519,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA256,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA384,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA512,
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user