Update README, ROADMAP, and mdBook to reflect all sprint deliverables: rich messaging, file transfer, disappearing messages, Go/TypeScript SDKs, C FFI, mesh networking (identity, store-and-forward, broadcast), and security hardening. Add 6 new mdBook guides (REPL reference, Go SDK, TypeScript SDK + browser demo, rich messaging, file transfer, mesh networking). Check off 16 completed ROADMAP items across phases 3-9.
12 KiB
NodeService Schema
Schema file: schemas/node.capnp
File ID: @0xd5ca5648a9cc1c28
The NodeService interface is the unified Cap'n Proto RPC surface that every quicproquo client talks to. It combines the Authentication Service and Delivery Service into a single interface, adds long-polling support (fetchWait), a health probe (health), and hybrid KEM key management. Every method that mutates state or accesses per-user data accepts an Auth struct for versioned authentication.
Full schema listing
# node.capnp -- Unified quicproquo node RPC interface.
#
# Combines Authentication and Delivery operations into a single service.
#
# ID generated with: capnp id
@0xd5ca5648a9cc1c28;
interface NodeService {
# Upload a single-use KeyPackage for later retrieval by peers.
# identityKey : Ed25519 public key bytes (32 bytes)
# package : TLS-encoded openmls KeyPackage
# auth : Auth context (version=1, non-empty accessToken required).
uploadKeyPackage @0 (identityKey :Data, package :Data, auth :Auth)
-> (fingerprint :Data);
# Fetch and atomically remove one KeyPackage for a given identity key.
# Returns empty Data if none are stored.
fetchKeyPackage @1 (identityKey :Data, auth :Auth) -> (package :Data);
# Enqueue an opaque payload for delivery to a recipient.
# channelId : Optional channel identifier (empty for legacy). A 16-byte UUID
# is recommended for 1:1 channels.
# version : Schema/wire version. Must be 1.
enqueue @2 (recipientKey :Data, payload :Data, channelId :Data,
version :UInt16, auth :Auth) -> ();
# Fetch and drain all queued payloads for the recipient.
fetch @3 (recipientKey :Data, channelId :Data, version :UInt16, auth :Auth)
-> (payloads :List(Data));
# Long-poll: wait up to timeoutMs for new payloads, then drain queue.
fetchWait @4 (recipientKey :Data, channelId :Data, version :UInt16,
timeoutMs :UInt64, auth :Auth) -> (payloads :List(Data));
# Health probe for readiness/liveness.
health @5 () -> (status :Text);
# Upload the hybrid (X25519 + ML-KEM-768) public key for sealed envelope
# encryption.
uploadHybridKey @6 (identityKey :Data, hybridPublicKey :Data) -> ();
# Fetch a peer's hybrid public key (for post-quantum envelope encryption).
fetchHybridKey @7 (identityKey :Data) -> (hybridPublicKey :Data);
}
struct Auth {
version @0 :UInt16; # 1 = token-based auth (required)
accessToken @1 :Data; # opaque bearer token issued at login
deviceId @2 :Data; # optional UUID bytes for auditing/rate limiting
}
Interface methods
Authentication methods
uploadKeyPackage @0
uploadKeyPackage (identityKey :Data, package :Data, auth :Auth) -> (fingerprint :Data)
Uploads a single-use MLS KeyPackage. Identical semantics to the standalone AuthenticationService method, with the addition of the auth parameter for access control.
| Parameter | Type | Size | Description |
|---|---|---|---|
identityKey |
Data |
32 bytes | Uploader's raw Ed25519 public key |
package |
Data |
Variable | TLS-encoded openmls KeyPackage blob |
auth |
Auth |
Struct | Authentication context (see Auth struct below) |
Returns: fingerprint :Data -- 32-byte SHA-256 digest of the stored package.
fetchKeyPackage @1
fetchKeyPackage (identityKey :Data, auth :Auth) -> (package :Data)
Fetches and atomically removes one KeyPackage for the specified identity key. Returns empty Data if no packages are stored. See Auth Schema for full single-use semantics and ADR-005 for the design rationale.
Delivery methods
enqueue @2
enqueue (recipientKey :Data, payload :Data, channelId :Data, version :UInt16, auth :Auth) -> ()
Enqueues an opaque payload for delivery. Identical semantics to the standalone DeliveryService enqueue method, with the addition of the auth parameter.
| Parameter | Type | Size | Description |
|---|---|---|---|
recipientKey |
Data |
32 bytes | Recipient's raw Ed25519 public key |
payload |
Data |
Variable | Opaque byte string (typically MLS ciphertext) |
channelId |
Data |
0 or 16 bytes | Channel identifier (empty for legacy, UUID recommended) |
version |
UInt16 |
2 bytes | Wire version: 1 = current (required) |
auth |
Auth |
Struct | Authentication context |
fetch @3
fetch (recipientKey :Data, channelId :Data, version :UInt16, auth :Auth) -> (payloads :List(Data))
Fetches and atomically drains all queued payloads for the specified recipient and channel. Returns an empty list if no messages are pending. See Delivery Schema for full queue semantics.
fetchWait @4
fetchWait (recipientKey :Data, channelId :Data, version :UInt16, timeoutMs :UInt64, auth :Auth)
-> (payloads :List(Data))
Long-polling variant of fetch. This method blocks on the server side until either:
- One or more payloads become available in the queue, or
- The
timeoutMsduration expires.
In case (1), the method returns all available payloads and drains the queue, identical to fetch. In case (2), the method returns an empty list.
| Parameter | Type | Description |
|---|---|---|
timeoutMs |
UInt64 |
Maximum wait time in milliseconds. A value of 0 means return immediately (equivalent to fetch). |
Why long-polling? Without fetchWait, clients must poll the server at a fixed interval, which wastes bandwidth when no messages are pending and introduces latency equal to half the polling interval on average. Long-polling provides near-real-time delivery while avoiding busy-wait overhead.
Server implementation: The server holds the RPC response open until a payload is enqueued for the recipient or the timeout fires. The underlying mechanism is a tokio::sync::Notify per recipient, which is woken by enqueue.
Infrastructure methods
health @5
health () -> (status :Text)
A readiness/liveness probe that takes no parameters and returns a human-readable status string (e.g., "ok"). This method:
- Does not require authentication (
authis not a parameter). - Is suitable for use as a Kubernetes or Docker health check endpoint.
- Can be extended in future versions to report more detailed status (e.g., queue depth, uptime).
Hybrid KEM methods
uploadHybridKey @6
uploadHybridKey (identityKey :Data, hybridPublicKey :Data) -> ()
Uploads the client's hybrid (X25519 + ML-KEM-768) public key for post-quantum sealed envelope encryption. Peers fetch this key to encrypt payloads with post-quantum protection before enqueuing them.
| Parameter | Type | Description |
|---|---|---|
identityKey |
Data |
Uploader's 32-byte Ed25519 public key (index key) |
hybridPublicKey |
Data |
Concatenated X25519 public key (32 bytes) + ML-KEM-768 encapsulation key |
fetchHybridKey @7
fetchHybridKey (identityKey :Data) -> (hybridPublicKey :Data)
Fetches a peer's hybrid public key. Unlike fetchKeyPackage, this is not a destructive operation -- the hybrid key persists across fetches because it is a long-lived public key, not a single-use package.
Auth struct
struct Auth {
version @0 :UInt16;
accessToken @1 :Data;
deviceId @2 :Data;
}
The Auth struct is attached to every mutating or per-user method call. It provides a versioned authentication context that supports clean schema evolution.
Fields
| Field | Type | Description |
|---|---|---|
version |
UInt16 |
Authentication protocol version. Determines how accessToken and deviceId are interpreted. |
accessToken |
Data |
Opaque bearer token issued at login. The server validates this token against its auth backend. |
deviceId |
Data |
Optional device identifier (UUID bytes). Used for auditing, rate limiting, and per-device session management. |
Version semantics
| Version | Behavior |
|---|---|
1 |
Token-based authentication (required). The server validates accessToken (static token or OPAQUE session) and rejects requests with missing or invalid tokens. deviceId is used for audit logging. |
Auth version 0 is no longer supported; clients must send version=1 and a valid token.
Method ordinal summary
| Ordinal | Method | Category |
|---|---|---|
@0 |
uploadKeyPackage |
Auth |
@1 |
fetchKeyPackage |
Auth |
@2 |
enqueue |
Delivery |
@3 |
fetch |
Delivery |
@4 |
fetchWait |
Delivery |
@5 |
health |
Infrastructure |
@6 |
uploadHybridKey |
Auth / PQ |
@7 |
fetchHybridKey |
Auth / PQ |
@8 |
fetchHybridKeys |
Auth / PQ (batch) |
@9 |
opaqueRegisterStart |
Auth / OPAQUE |
@10 |
opaqueRegisterFinish |
Auth / OPAQUE |
@11 |
opaqueLoginStart |
Auth / OPAQUE |
@12 |
opaqueLoginFinish |
Auth / OPAQUE |
@13 |
peek |
Delivery (non-destructive read) |
@14 |
ack |
Delivery (acknowledge after peek) |
@15 |
batchEnqueue |
Delivery (fan-out) |
@16 |
createChannel |
Channels |
@17 |
resolveUser |
Discovery |
@18 |
resolveIdentity |
Discovery (reverse lookup) |
@19 |
registerDevice |
Devices |
@20 |
listDevices |
Devices |
@21 |
uploadBlob |
File transfer |
@22 |
downloadBlob |
File transfer |
@23 |
deleteAccount |
Account management |
@24 |
revokeDevice |
Devices |
@25 |
publishEndpoint |
P2P discovery |
@26 |
resolveEndpoint |
P2P discovery |
Ordinals are stable and must not be reused. New methods are appended with the next available ordinal. This is a fundamental Cap'n Proto schema evolution rule: removing a method does not free its ordinal.
Notable additions since initial release
- OPAQUE (@9-@12): Password-authenticated key exchange. The password never leaves the client.
- Channels (@16):
createChannelreturns(channelId :Data, wasNew :Bool)for 1:1 DM creation with deduplication. - File transfer (@21-@22):
uploadBlobaccepts 256 KB chunks with SHA-256 content addressing;downloadBlobretrieves chunks with hash verification. Max 50 MB. - Account deletion (@23): Transactional purge of all user data (user record, identity keys, key packages, hybrid keys, queued deliveries, channel memberships).
- TTL support:
enqueueandbatchEnqueueaccept an optionalttlSecsparameter for disappearing messages with server-side garbage collection. - P2P discovery (@25-@26):
publishEndpointandresolveEndpointfor iroh node address exchange.
Schema evolution
Cap'n Proto supports forward-compatible schema evolution through several mechanisms, all of which are used in the NodeService interface:
- New methods can be added by appending with a new ordinal. Old clients ignore unknown methods; new clients can call them.
- New struct fields can be added to
Auth(or any other struct) by appending with a new field number. Old structs that lack the new field will read the default value. - The
versionfield provides application-level versioning on top of Cap'n Proto's structural versioning, allowing the server to change validation behavior without changing the schema.
Further reading
- Wire Format Overview -- serialisation pipeline context
- Auth Schema -- standalone Authentication Service interface (subset of NodeService)
- Delivery Schema -- standalone Delivery Service interface (subset of NodeService)
- Envelope Schema -- legacy M1 framing that NodeService replaced
- Architecture Overview -- system-level view showing NodeService in context
- ADR-005: Single-Use KeyPackages -- why fetchKeyPackage is destructive
- ADR-004: MLS-Unaware DS -- why payloads are opaque