docs(sdk): add comprehensive SDK documentation and wire format reference

Covers all official SDKs (Rust, Go, Python, TypeScript, C FFI),
the v2 wire format with method ID tables, authentication flow,
and a build-your-own-SDK guide with implementation checklist.
This commit is contained in:
2026-03-04 20:55:24 +01:00
parent 2d56824834
commit cbb76af6b1
8 changed files with 985 additions and 0 deletions

166
docs/sdk/build-your-own.md Normal file
View File

@@ -0,0 +1,166 @@
# Build Your Own SDK
This guide explains how to implement a quicproquo client SDK in any language.
## Two Approaches
### Approach 1: C FFI Wrapper (recommended)
The simplest path. Wrap `libquicproquo_ffi` using your language's FFI mechanism (e.g., Python CFFI, Ruby FFI, JNI, Swift C interop).
**Pros**: Full Rust crypto stack (MLS, OPAQUE, hybrid KEM) with zero reimplementation.
**Cons**: Requires shipping a native library, synchronous API only.
See [C FFI docs](c-ffi.md) for the function signatures.
### Approach 2: Native QUIC + Protobuf
Implement the QUIC transport and protobuf serialization natively in your language.
**Pros**: No Rust dependency, full async support, smaller binary.
**Cons**: Must handle crypto externally (OPAQUE, MLS key packages).
## Native Implementation Checklist
### 1. QUIC Transport
Open a QUIC connection to the server:
- **ALPN**: `qpq`
- **TLS**: 1.3 with server certificate verification
- **Port**: 5001 (default)
Libraries by language:
| Language | Library |
|---|---|
| Go | `github.com/quic-go/quic-go` |
| Python | `aioquic` |
| Java/Kotlin | `netty-quic` or `Kwik` |
| Swift | `Network.framework` (built-in) |
| C/C++ | `quiche`, `msquic`, or `ngtcp2` |
| Rust | `quinn` |
### 2. Wire Framing
Each RPC uses a dedicated QUIC bidirectional stream with a 10-byte header:
```
[method_id:u16][req_id:u32][payload_len:u32][protobuf bytes]
```
All integers are **big-endian** (network byte order).
Implementation:
```python
# Encode
header = struct.pack("!HII", method_id, req_id, len(payload))
frame = header + payload
# Decode
method_id, req_id, length = struct.unpack("!HII", data[:10])
payload = data[10 : 10 + length]
```
### 3. Protobuf Messages
Generate or hand-write protobuf encode/decode for the message types in `proto/qpq/v1/*.proto`.
Minimum required messages for a basic client:
| Proto file | Messages |
|---|---|
| `auth.proto` | `OpaqueLoginStart{Request,Response}`, `OpaqueLoginFinish{Request,Response}` |
| `delivery.proto` | `EnqueueRequest`, `EnqueueResponse`, `FetchRequest`, `FetchResponse` |
| `user.proto` | `ResolveUserRequest`, `ResolveUserResponse` |
| `channel.proto` | `CreateChannelRequest`, `CreateChannelResponse` |
| `p2p.proto` | `HealthRequest`, `HealthResponse` |
| `common.proto` | `DeleteAccountRequest`, `DeleteAccountResponse` |
### 4. RPC Flow
```
Client Server
│ │
│ 1. Open QUIC bidirectional stream │
│ 2. Send framed request │
│ 3. Close send side (end-stream) │
│ ──────────────────────────────────►│
│ │
│ 4. Read framed response │
│ 5. Close receive side │
│ ◄──────────────────────────────────│
```
### 5. Implementation Checklist
Core operations (implement in order):
- [ ] **Connect**: Open QUIC connection with TLS 1.3 + ALPN `qpq`
- [ ] **Health**: Send `HealthRequest` (method 802), verify server is running
- [ ] **Register**: OPAQUE registration (methods 100-101)
- [ ] **Login**: OPAQUE login (methods 102-103), store session token
- [ ] **Upload Key Package**: Upload MLS key package (method 300)
- [ ] **Resolve User**: Look up peer's identity key (method 500)
- [ ] **Create Channel**: Create DM channel with peer (method 400)
- [ ] **Send**: Enqueue encrypted message (method 200)
- [ ] **Receive**: Fetch pending messages (method 201 or 202 for long-poll)
- [ ] **Ack**: Acknowledge received messages (method 204)
- [ ] **Disconnect**: Close QUIC connection gracefully
### 6. OPAQUE Authentication
OPAQUE is a 4-message protocol. You need an OPAQUE library for your language:
| Language | Library |
|---|---|
| Rust | `opaque-ke` |
| Go | `github.com/cloudflare/circl/opaque` |
| Python | `opaque` (pure Python) or bind to Rust via FFI |
| JavaScript | `@nicholasgasior/opaque-wasm` or FFI |
The SDK only needs to transport the OPAQUE bytes -- the crypto is handled by the OPAQUE library. The flow is:
1. Generate `RegistrationRequest` locally
2. Send via `OpaqueRegisterStart` (method 100)
3. Process server's `RegistrationResponse` locally to produce `Upload`
4. Send via `OpaqueRegisterFinish` (method 101)
5. Generate `LoginRequest` locally
6. Send via `OpaqueLoginStart` (method 102)
7. Process server's `LoginResponse` locally to produce `Finalization`
8. Send via `OpaqueLoginFinish` (method 103)
9. Store the returned `session_token`
### 7. Error Handling
The server returns protobuf-encoded error responses. Your SDK should:
- Distinguish authentication errors from transport errors
- Implement timeouts on all RPC calls
- Handle QUIC connection loss and reconnection
## Reference Implementations
Study these SDKs for patterns:
- **Go** (`sdks/go/`): Native QUIC + Cap'n Proto, full OPAQUE flow
- **Python** (`sdks/python/`): Native QUIC + Protobuf v2 wire format
- **TypeScript** (`sdks/typescript/`): WebSocket bridge, WASM crypto
- **C FFI** (`crates/quicproquo-ffi/`): Synchronous wrapper
## Testing
Test your SDK against a local server:
```sh
# Start the server
cargo run -p quicproquo-server
# Run your SDK's tests
```
Verify at minimum:
1. Health check returns status "ok"
2. OPAQUE register + login succeeds
3. Send + receive roundtrip works
4. Error handling for invalid credentials

142
docs/sdk/c-ffi.md Normal file
View File

@@ -0,0 +1,142 @@
# C FFI Bindings
The C FFI layer (`crates/quicproquo-ffi/`) provides synchronous C-callable functions that wrap the Rust client library. This is the foundation for language bindings in Python (CFFI), Swift, Kotlin/JNI, Java, and Ruby.
## Building
```sh
cargo build --release -p quicproquo-ffi
```
This produces:
- Linux: `target/release/libquicproquo_ffi.so`
- macOS: `target/release/libquicproquo_ffi.dylib`
- Windows: `target/release/quicproquo_ffi.dll`
## API
### Status Codes
```c
#define QPQ_OK 0
#define QPQ_ERROR 1
#define QPQ_AUTH_FAILED 2
#define QPQ_TIMEOUT 3
#define QPQ_NOT_CONNECTED 4
```
### Functions
```c
// Connect to a server. Returns opaque handle or NULL on failure.
QpqHandle* qpq_connect(
const char* server, // "host:port"
const char* ca_cert, // path to CA certificate PEM
const char* server_name // TLS SNI name
);
// Authenticate with OPAQUE. Returns status code.
int qpq_login(
QpqHandle* handle,
const char* username,
const char* password
);
// Send a message to a recipient (by username).
int qpq_send(
QpqHandle* handle,
const char* recipient, // recipient username
const uint8_t* message, // message bytes
size_t message_len
);
// Receive pending messages. Blocks up to timeout_ms.
// On success, *out_json is a JSON array of strings.
int qpq_receive(
QpqHandle* handle,
uint32_t timeout_ms,
char** out_json // caller must free with qpq_free_string
);
// Disconnect and free the handle.
void qpq_disconnect(QpqHandle* handle);
// Get last error message (valid until next FFI call on this handle).
const char* qpq_last_error(const QpqHandle* handle);
// Free a string returned by qpq_receive.
void qpq_free_string(char* ptr);
```
## Usage Example (C)
```c
#include <stdio.h>
#include <string.h>
// Forward declarations (or include a header).
typedef struct QpqHandle QpqHandle;
extern QpqHandle* qpq_connect(const char*, const char*, const char*);
extern int qpq_login(QpqHandle*, const char*, const char*);
extern int qpq_send(QpqHandle*, const char*, const unsigned char*, size_t);
extern int qpq_receive(QpqHandle*, unsigned int, char**);
extern void qpq_disconnect(QpqHandle*);
extern const char* qpq_last_error(const QpqHandle*);
extern void qpq_free_string(char*);
int main(void) {
QpqHandle* h = qpq_connect("127.0.0.1:5001", "ca.pem", "localhost");
if (!h) {
fprintf(stderr, "connect failed\n");
return 1;
}
int rc = qpq_login(h, "alice", "password123");
if (rc != 0) {
fprintf(stderr, "login failed: %s\n", qpq_last_error(h));
qpq_disconnect(h);
return 1;
}
const char* msg = "hello from C!";
qpq_send(h, "bob", (const unsigned char*)msg, strlen(msg));
char* json = NULL;
rc = qpq_receive(h, 5000, &json);
if (rc == 0 && json) {
printf("received: %s\n", json);
qpq_free_string(json);
}
qpq_disconnect(h);
return 0;
}
```
Compile with:
```sh
gcc -o demo demo.c -L target/release -lquicproquo_ffi
```
## Memory Management
- `qpq_connect` returns a heap-allocated handle. The caller **must** call `qpq_disconnect` to free it.
- `qpq_receive` writes a heap-allocated JSON string to `*out_json`. The caller **must** call `qpq_free_string` to free it.
- `qpq_last_error` returns a pointer owned by the handle. Do **not** free it. It is valid until the next FFI call on the same handle.
## Thread Safety
Each `QpqHandle` owns its own Tokio runtime. Concurrent calls on the **same** handle are not safe. Create separate handles for concurrent use.
## Internals
The FFI layer bridges synchronous C callers to the async Rust client:
```
C caller ─── qpq_login() ───► QpqHandle ─── runtime.block_on() ───► async Rust client
```
Each handle contains:
- A Tokio runtime for blocking on async operations
- Server connection parameters
- Login state and error buffer

95
docs/sdk/go.md Normal file
View File

@@ -0,0 +1,95 @@
# Go SDK
The Go SDK provides a native QUIC client with Cap'n Proto serialization.
Location: `sdks/go/`
## Installation
```sh
go get quicproquo.dev/sdk/go
```
## Quick Start
```go
package main
import (
"context"
"fmt"
"quicproquo.dev/sdk/go/qpq"
)
func main() {
ctx := context.Background()
client, err := qpq.Connect(ctx, qpq.Options{
Addr: "127.0.0.1:5001",
InsecureSkipVerify: true, // dev only
})
if err != nil {
panic(err)
}
defer client.Close()
// Health check
status, _ := client.Health(ctx)
fmt.Println("Server:", status)
// OPAQUE login (requires external OPAQUE library)
resp, _ := client.LoginStart(ctx, "alice", opaqueRequest)
// ... process resp with OPAQUE library ...
client.LoginFinish(ctx, "alice", finalization, identityKey)
// Resolve user
key, _ := client.ResolveUser(ctx, "bob")
// Create channel
chID, wasNew, _ := client.CreateChannel(ctx, key)
// Send
seq, _ := client.Send(ctx, key, []byte("hello"))
// Receive (long-poll)
msgs, _ := client.ReceiveWait(ctx, myKey, 5000)
for _, m := range msgs {
fmt.Printf("[%d] %s\n", m.Seq, m.Data)
}
}
```
## API
| Method | Description |
|---|---|
| `qpq.Connect(ctx, opts)` | Connect to server |
| `client.Close()` | Disconnect |
| `client.Health(ctx)` | Health check |
| `client.SetSessionToken(token)` | Set pre-existing token |
| `client.RegisterStart(ctx, username, request)` | OPAQUE register start |
| `client.RegisterFinish(ctx, username, upload, key)` | OPAQUE register finish |
| `client.LoginStart(ctx, username, request)` | OPAQUE login start |
| `client.LoginFinish(ctx, username, finalization, key)` | OPAQUE login finish |
| `client.ResolveUser(ctx, username)` | Look up identity key |
| `client.CreateChannel(ctx, peerKey)` | Create DM channel |
| `client.Send(ctx, recipientKey, payload)` | Send message |
| `client.SendWithTTL(ctx, recipientKey, payload, ttl)` | Disappearing message |
| `client.Receive(ctx, recipientKey)` | Fetch messages |
| `client.ReceiveWait(ctx, recipientKey, timeoutMs)` | Long-poll |
| `client.DeleteAccount(ctx)` | Delete account |
## Structure
- `qpq/` -- High-level client API
- `transport/` -- QUIC + TLS 1.3 transport, Cap'n Proto RPC framing
- `proto/node/` -- Generated Cap'n Proto Go types
- `cmd/example/` -- Example usage
## Transport
The Go SDK uses `quic-go` for QUIC transport and `capnproto.org/go/capnp/v3` for Cap'n Proto RPC. The transport layer handles:
- TLS 1.3 with configurable CA certificates
- Cap'n Proto RPC bootstrap over QUIC streams
- Automatic connection management

63
docs/sdk/index.md Normal file
View File

@@ -0,0 +1,63 @@
# quicproquo SDK Documentation
This guide covers how to build clients for the quicproquo E2E encrypted messenger using the official SDKs or by implementing your own.
## Official SDKs
| Language | Location | Transport | Status |
|---|---|---|---|
| **Rust** | `crates/quicproquo-client` | QUIC + Cap'n Proto | Production |
| **Go** | `sdks/go/` | QUIC + Cap'n Proto | Production |
| **TypeScript** | `sdks/typescript/` | WebSocket bridge + WASM crypto | Production |
| **Python** | `sdks/python/` | QUIC + Protobuf (v2) / Rust FFI | Production |
| **C** | `crates/quicproquo-ffi/` | Rust FFI (synchronous) | Production |
| **Swift** | `sdks/swift/` | C FFI wrapper | In progress |
| **Kotlin** | `sdks/kotlin/` | JNI + C FFI | In progress |
| **Java** | `sdks/java/` | JNI + C FFI | In progress |
| **Ruby** | `sdks/ruby/` | FFI gem | In progress |
## Documentation
- [Wire Format](wire-format.md) -- v2 QUIC + Protobuf wire protocol
- [Rust SDK](rust.md) -- Native Rust client
- [Go SDK](go.md) -- Go client with QUIC transport
- [Python SDK](python.md) -- Python client (async QUIC + sync FFI)
- [TypeScript SDK](typescript.md) -- Browser/Node.js client with WASM crypto
- [C FFI](c-ffi.md) -- C bindings for language integrations
- [Build Your Own SDK](build-your-own.md) -- Guide for implementing a new language SDK
## Architecture Overview
```
Client SDK Server
───────── ──────
┌──────────┐ QUIC/TLS 1.3 ┌──────────┐
│ App code │ ◄──────────────► │ RPC │
│ │ v2 wire frames │ dispatch │
│ SDK API │ │ │
│ │ [method_id:u16] │ handlers │
│ Proto │ [req_id:u32] │ │
│ encode/ │ [len:u32] │ storage │
│ decode │ [protobuf] │ │
│ │ │ │
│ QUIC │ │ QUIC │
│ transport│ │ listener │
└──────────┘ └──────────┘
```
Each RPC call opens a new QUIC bidirectional stream. The request and response use the same 10-byte framing header followed by a protobuf payload.
## Quick Start
1. Choose an SDK for your language
2. Connect to the server over QUIC (or WebSocket bridge for browsers)
3. Authenticate with OPAQUE (register or login)
4. Upload MLS key packages for E2E encryption
5. Send and receive encrypted messages
## Canonical Schemas
- **Protobuf** (v2): `proto/qpq/v1/*.proto` -- 14 service definitions
- **Cap'n Proto** (v1): `schemas/*.capnp` -- legacy RPC interface
The protobuf schemas in `proto/qpq/v1/` are the canonical API contract for the v2 protocol. New SDKs should implement against these definitions.

120
docs/sdk/python.md Normal file
View File

@@ -0,0 +1,120 @@
# Python SDK
The Python SDK provides both async QUIC transport and synchronous Rust FFI backends.
Location: `sdks/python/`
## Installation
```sh
pip install quicproquo
```
## Transport Backends
### Async QUIC (pure Python)
Uses [aioquic](https://github.com/aiortc/aioquic) for native QUIC transport with the v2 protobuf wire format. No Rust build required.
```python
import asyncio
from quicproquo import QpqClient, ConnectOptions
async def main():
client = await QpqClient.connect(ConnectOptions(
addr="127.0.0.1:5001",
ca_cert_path="ca.pem",
))
health = await client.health()
print(f"Server: {health.status}")
# OPAQUE auth (requires external OPAQUE library)
resp = await client.login_start("alice", opaque_request)
token = await client.login_finish("alice", finalization, identity_key)
# Messaging
key, proof = await client.resolve_user("bob")
seq, proof = await client.send(key, b"hello")
messages = await client.receive_wait(my_key, timeout_ms=5000)
await client.close()
asyncio.run(main())
```
### Rust FFI (synchronous)
Wraps `libquicproquo_ffi` via CFFI for full Rust crypto stack at native speed.
```sh
cargo build --release -p quicproquo-ffi
```
```python
from quicproquo import QpqClient, ConnectOptions
client = QpqClient.connect_ffi(ConnectOptions(
addr="127.0.0.1:5001",
ca_cert_path="ca.pem",
))
client.ffi_login("alice", "password123")
client.ffi_send("bob", b"hello")
messages = client.ffi_receive(timeout_ms=5000)
client.close_sync()
```
## Async API (QUIC)
| Method | Description |
|---|---|
| `QpqClient.connect(opts)` | Connect via QUIC |
| `client.health()` | Health check |
| `client.register_start(username, request)` | OPAQUE register start |
| `client.register_finish(username, upload, key)` | OPAQUE register finish |
| `client.login_start(username, request)` | OPAQUE login start |
| `client.login_finish(username, finalization, key)` | OPAQUE login finish |
| `client.resolve_user(username)` | Look up identity key |
| `client.resolve_identity(key)` | Reverse look up username |
| `client.create_channel(peer_key)` | Create DM channel |
| `client.send(recipient_key, payload)` | Send message |
| `client.receive(recipient_key)` | Fetch messages |
| `client.receive_wait(recipient_key, timeout_ms=5000)` | Long-poll |
| `client.ack(recipient_key, seq_up_to)` | Acknowledge messages |
| `client.upload_key_package(key, package)` | Upload MLS key package |
| `client.fetch_key_package(key)` | Fetch MLS key package |
| `client.upload_hybrid_key(key, hybrid_pk)` | Upload hybrid key |
| `client.fetch_hybrid_key(key)` | Fetch hybrid key |
| `client.delete_account()` | Delete account |
| `client.close()` | Disconnect (async) |
## FFI API (synchronous)
| Method | Description |
|---|---|
| `QpqClient.connect_ffi(opts)` | Connect via Rust FFI |
| `client.ffi_login(username, password)` | Full OPAQUE login |
| `client.ffi_send(recipient, message)` | Send by username |
| `client.ffi_receive(timeout_ms=5000)` | Receive messages |
| `client.close_sync()` | Disconnect (sync) |
## Exceptions
| Exception | Description |
|---|---|
| `QpqError` | Base exception for all SDK errors |
| `AuthError` | OPAQUE authentication failed |
| `TimeoutError` | Operation timed out |
| `ConnectionError` | Server connection failure |
## Structure
| File | Purpose |
|---|---|
| `quicproquo/client.py` | High-level client API |
| `quicproquo/transport.py` | QUIC transport (aioquic) |
| `quicproquo/ffi.py` | Rust FFI transport (CFFI) |
| `quicproquo/proto.py` | Protobuf encode/decode |
| `quicproquo/wire.py` | v2 wire format framing |
| `quicproquo/types.py` | Data types and exceptions |

63
docs/sdk/rust.md Normal file
View File

@@ -0,0 +1,63 @@
# Rust SDK
The Rust client is the reference implementation, located in `crates/quicproquo-client/`.
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
quicproquo-client = { path = "crates/quicproquo-client" }
```
## Connection
The Rust client connects directly over QUIC with Cap'n Proto RPC:
```rust
use quicproquo_client::{cmd_health, cmd_login, cmd_send, connect_node};
// Health check
cmd_health("127.0.0.1:5001", &ca_cert_path, "localhost").await?;
// Login via OPAQUE
cmd_login(
"127.0.0.1:5001", &ca_cert_path, "localhost",
"alice", "password123",
None, // identity_key_hex
Some(&state_path), // state persistence
None, // state_password
).await?;
```
## Key Features
- Full MLS (RFC 9420) group encryption
- Hybrid post-quantum KEM (X25519 + ML-KEM-768)
- OPAQUE authentication with zeroizing credential storage
- SQLCipher local state with Argon2id key derivation
- Sealed sender metadata protection
- v2 QUIC + Protobuf transport (via `quicproquo-sdk` crate)
## v2 SDK Crate
The `quicproquo-sdk` crate provides the higher-level v2 API:
```rust
use quicproquo_sdk::QpqClient;
let client = QpqClient::connect("127.0.0.1:5001", &tls_config).await?;
let health = client.health().await?;
```
## Crate Structure
| Crate | Purpose |
|---|---|
| `quicproquo-core` | Crypto primitives, MLS, hybrid KEM |
| `quicproquo-proto` | Protobuf + Cap'n Proto generated types |
| `quicproquo-rpc` | QUIC RPC framework (framing, dispatch) |
| `quicproquo-sdk` | High-level client SDK |
| `quicproquo-client` | CLI/TUI client application |
| `quicproquo-ffi` | C FFI bindings |

128
docs/sdk/typescript.md Normal file
View File

@@ -0,0 +1,128 @@
# TypeScript SDK
The TypeScript SDK provides a browser-compatible client with WASM-based crypto.
Location: `sdks/typescript/`
## Installation
```sh
npm install quicproquo
```
## Quick Start
```typescript
import { QpqClient } from "quicproquo";
// Connect via WebSocket bridge
const client = await QpqClient.connect({
addr: "ws://127.0.0.1:5002",
});
const status = await client.health();
console.log("Server:", status);
// Set session token (obtained via OPAQUE externally)
client.setSessionToken(token);
// Resolve user
const peerKey = await client.resolveUser("bob");
// Create channel
const { channelId, wasNew } = await client.createChannel(peerKey);
// Send message
const seq = await client.send(peerKey, new TextEncoder().encode("hello"));
// Receive messages
const messages = await client.receive(myKey);
for (const msg of messages) {
console.log(`[${msg.seq}] ${new TextDecoder().decode(msg.data)}`);
}
client.close();
```
## Offline Crypto
The SDK includes WASM-compiled crypto primitives that work without a server:
```typescript
const client = await QpqClient.offline();
// Ed25519 identity
const { seed, publicKey } = client.generateIdentity();
const sig = client.sign(seed, message);
const valid = client.verify(publicKey, message, sig);
// Hybrid KEM (X25519 + ML-KEM-768)
const kp = client.hybridGenerateKeypair();
const ciphertext = client.hybridEncrypt(kp.publicKey, plaintext);
const decrypted = client.hybridDecrypt(kp.blob, ciphertext);
// Safety numbers
const safetyNumber = client.computeSafetyNumber(keyA, keyB);
// Sealed sender
const sealed = client.seal(seed, payload);
const unsealed = client.unseal(sealed);
// Message padding
const padded = client.padMessage(data);
const unpadded = client.unpadMessage(padded);
```
## API
### RPC Methods
| Method | Description |
|---|---|
| `QpqClient.connect(opts)` | Connect via WebSocket |
| `QpqClient.offline()` | Crypto-only mode |
| `client.health()` | Health check |
| `client.resolveUser(username)` | Look up identity key |
| `client.createChannel(peerKey)` | Create DM channel |
| `client.send(recipientKey, payload)` | Send message |
| `client.sendWithTTL(recipientKey, payload, ttl)` | Disappearing message |
| `client.receive(recipientKey)` | Fetch messages |
| `client.deleteAccount()` | Delete account |
| `client.close()` | Disconnect |
### Crypto Methods
| Method | Description |
|---|---|
| `client.generateIdentity()` | Generate Ed25519 keypair |
| `client.identityPublicKey(seed)` | Derive public key |
| `client.sign(seed, message)` | Ed25519 sign |
| `client.verify(publicKey, message, sig)` | Ed25519 verify |
| `client.hybridGenerateKeypair()` | Generate hybrid keypair |
| `client.hybridEncrypt(pk, plaintext)` | Hybrid encrypt |
| `client.hybridDecrypt(blob, ciphertext)` | Hybrid decrypt |
| `client.computeSafetyNumber(keyA, keyB)` | Safety number |
| `client.seal(seed, payload)` | Sealed sender |
| `client.unseal(envelope)` | Unseal |
| `client.padMessage(data)` | Pad message |
| `client.unpadMessage(data)` | Unpad message |
## Architecture
The TypeScript SDK uses a WebSocket bridge proxy because browsers cannot open raw QUIC connections. The bridge translates JSON-over-WebSocket to Cap'n Proto RPC:
```
Browser ─── WebSocket ───► Bridge Proxy ─── QUIC/capnp ───► Server
```
Crypto operations (Ed25519, hybrid KEM, sealed sender) run entirely in WASM, compiled from the Rust `quicproquo-core` crate.
## Structure
| File | Purpose |
|---|---|
| `src/client.ts` | High-level client API |
| `src/transport.ts` | WebSocket transport with auto-reconnect |
| `src/crypto.ts` | WASM crypto bindings |
| `src/types.ts` | TypeScript interfaces |
| `wasm-crypto/` | Rust WASM crypto source |

208
docs/sdk/wire-format.md Normal file
View File

@@ -0,0 +1,208 @@
# v2 Wire Format
The quicproquo v2 protocol uses QUIC (RFC 9000) with TLS 1.3 as the transport layer and Protocol Buffers for message serialization.
## Connection
- **Protocol**: QUIC with TLS 1.3
- **ALPN**: `qpq`
- **Port**: 5001 (default)
- **Certificate**: Server presents a TLS certificate; clients verify against a CA cert
## Frame Format
Every RPC request and response is wrapped in a 10-byte binary header:
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| method_id (u16) | req_id (u32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| req_id (cont.) | payload_len (u32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| payload_len (cont.) | protobuf payload ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
| Field | Type | Bytes | Description |
|---|---|---|---|
| `method_id` | `u16` | 0-1 | RPC method identifier (network byte order) |
| `req_id` | `u32` | 2-5 | Client-generated request correlation ID (network byte order) |
| `payload_len` | `u32` | 6-9 | Length of the protobuf payload (network byte order) |
| payload | bytes | 10+ | Protobuf-encoded request or response message |
All multi-byte integers are **big-endian** (network byte order).
## Stream Model
Each RPC call uses a **dedicated QUIC bidirectional stream**:
1. Client opens a new stream
2. Client sends the request frame and marks end-of-stream
3. Server reads the request, processes it, and sends the response frame
4. Server marks end-of-stream
This allows concurrent RPCs without head-of-line blocking.
## Method IDs
### Auth (100-103)
| ID | Method | Request | Response |
|---|---|---|---|
| 100 | `OpaqueRegisterStart` | `OpaqueRegisterStartRequest` | `OpaqueRegisterStartResponse` |
| 101 | `OpaqueRegisterFinish` | `OpaqueRegisterFinishRequest` | `OpaqueRegisterFinishResponse` |
| 102 | `OpaqueLoginStart` | `OpaqueLoginStartRequest` | `OpaqueLoginStartResponse` |
| 103 | `OpaqueLoginFinish` | `OpaqueLoginFinishRequest` | `OpaqueLoginFinishResponse` |
### Delivery (200-205)
| ID | Method | Request | Response |
|---|---|---|---|
| 200 | `Enqueue` | `EnqueueRequest` | `EnqueueResponse` |
| 201 | `Fetch` | `FetchRequest` | `FetchResponse` |
| 202 | `FetchWait` | `FetchWaitRequest` | `FetchWaitResponse` |
| 203 | `Peek` | `PeekRequest` | `PeekResponse` |
| 204 | `Ack` | `AckRequest` | `AckResponse` |
| 205 | `BatchEnqueue` | `BatchEnqueueRequest` | `BatchEnqueueResponse` |
### Keys (300-304)
| ID | Method | Request | Response |
|---|---|---|---|
| 300 | `UploadKeyPackage` | `UploadKeyPackageRequest` | `UploadKeyPackageResponse` |
| 301 | `FetchKeyPackage` | `FetchKeyPackageRequest` | `FetchKeyPackageResponse` |
| 302 | `UploadHybridKey` | `UploadHybridKeyRequest` | `UploadHybridKeyResponse` |
| 303 | `FetchHybridKey` | `FetchHybridKeyRequest` | `FetchHybridKeyResponse` |
| 304 | `FetchHybridKeys` | `FetchHybridKeysRequest` | `FetchHybridKeysResponse` |
### Channel (400)
| ID | Method | Request | Response |
|---|---|---|---|
| 400 | `CreateChannel` | `CreateChannelRequest` | `CreateChannelResponse` |
### Group Management (410-413)
| ID | Method | Request | Response |
|---|---|---|---|
| 410 | `RemoveMember` | `RemoveMemberRequest` | `RemoveMemberResponse` |
| 411 | `UpdateGroupMetadata` | `UpdateGroupMetadataRequest` | `UpdateGroupMetadataResponse` |
| 412 | `ListGroupMembers` | `ListGroupMembersRequest` | `ListGroupMembersResponse` |
| 413 | `RotateKeys` | `RotateKeysRequest` | `RotateKeysResponse` |
### User (500-501)
| ID | Method | Request | Response |
|---|---|---|---|
| 500 | `ResolveUser` | `ResolveUserRequest` | `ResolveUserResponse` |
| 501 | `ResolveIdentity` | `ResolveIdentityRequest` | `ResolveIdentityResponse` |
### Blob (600-601)
| ID | Method | Request | Response |
|---|---|---|---|
| 600 | `UploadBlob` | `UploadBlobRequest` | `UploadBlobResponse` |
| 601 | `DownloadBlob` | `DownloadBlobRequest` | `DownloadBlobResponse` |
### Device (700-702)
| ID | Method | Request | Response |
|---|---|---|---|
| 700 | `RegisterDevice` | `RegisterDeviceRequest` | `RegisterDeviceResponse` |
| 701 | `ListDevices` | `ListDevicesRequest` | `ListDevicesResponse` |
| 702 | `RevokeDevice` | `RevokeDeviceRequest` | `RevokeDeviceResponse` |
### P2P / Health (800-802)
| ID | Method | Request | Response |
|---|---|---|---|
| 800 | `PublishEndpoint` | `PublishEndpointRequest` | `PublishEndpointResponse` |
| 801 | `ResolveEndpoint` | `ResolveEndpointRequest` | `ResolveEndpointResponse` |
| 802 | `Health` | `HealthRequest` | `HealthResponse` |
### Federation (900-905)
| ID | Method | Request | Response |
|---|---|---|---|
| 900 | `RelayEnqueue` | `RelayEnqueueRequest` | `RelayEnqueueResponse` |
| 901-905 | Reserved | -- | -- |
### Account (950)
| ID | Method | Request | Response |
|---|---|---|---|
| 950 | `DeleteAccount` | `DeleteAccountRequest` | `DeleteAccountResponse` |
## Protobuf Definitions
All message types are defined in `proto/qpq/v1/*.proto`:
| File | Services |
|---|---|
| `auth.proto` | OPAQUE registration and login |
| `common.proto` | Auth context, account deletion |
| `delivery.proto` | Message enqueue, fetch, peek, ack |
| `keys.proto` | MLS key packages, hybrid keys |
| `channel.proto` | Channel creation |
| `user.proto` | User/identity resolution |
| `group.proto` | Group management |
| `blob.proto` | Binary object storage |
| `device.proto` | Multi-device management |
| `p2p.proto` | P2P endpoints, health |
| `federation.proto` | Cross-server relay |
| `push.proto` | Push notifications |
| `recovery.proto` | Account recovery |
| `moderation.proto` | Content moderation |
## Authentication Flow
Authentication uses the OPAQUE protocol (asymmetric PAKE):
```
Client Server
│ │
│ OpaqueRegisterStart(username, │
│ registration_request) │
│ ──────────────────────────────────►│
│ │
│ OpaqueRegisterStartResponse( │
│ registration_response) │
│ ◄──────────────────────────────────│
│ │
│ OpaqueRegisterFinish(username, │
│ upload, identity_key) │
│ ──────────────────────────────────►│
│ │
│ OpaqueRegisterFinishResponse( │
│ success) │
│ ◄──────────────────────────────────│
│ │
│ OpaqueLoginStart(username, │
│ login_request) │
│ ──────────────────────────────────►│
│ │
│ OpaqueLoginStartResponse( │
│ login_response) │
│ ◄──────────────────────────────────│
│ │
│ OpaqueLoginFinish(username, │
│ finalization, identity_key) │
│ ──────────────────────────────────►│
│ │
│ OpaqueLoginFinishResponse( │
│ session_token) │
│ ◄──────────────────────────────────│
```
The `session_token` is used for subsequent authenticated RPCs.
## Error Handling
The server returns protobuf-encoded error responses on the same stream. Error conditions include:
- Invalid method ID: stream reset
- Authentication failure: error response with details
- Rate limiting: error response with retry-after hint
- Internal errors: generic error response