# 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 ```capnp # 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](auth-schema.md) 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](#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](auth-schema.md) for full single-use semantics and [ADR-005](../design-rationale/adr-005-single-use-keypackages.md) 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](delivery-schema.md) `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](delivery-schema.md) 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: 1. One or more payloads become available in the queue, **or** 2. The `timeoutMs` duration 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 (`auth` is 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 ```capnp 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):** `createChannel` returns `(channelId :Data, wasNew :Bool)` for 1:1 DM creation with deduplication. - **File transfer (@21-@22):** `uploadBlob` accepts 256 KB chunks with SHA-256 content addressing; `downloadBlob` retrieves 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:** `enqueue` and `batchEnqueue` accept an optional `ttlSecs` parameter for disappearing messages with server-side garbage collection. - **P2P discovery (@25-@26):** `publishEndpoint` and `resolveEndpoint` for 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: 1. **New methods can be added** by appending with a new ordinal. Old clients ignore unknown methods; new clients can call them. 2. **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. 3. **The `version` field** 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](overview.md) -- serialisation pipeline context - [Auth Schema](auth-schema.md) -- standalone Authentication Service interface (subset of NodeService) - [Delivery Schema](delivery-schema.md) -- standalone Delivery Service interface (subset of NodeService) - [Envelope Schema](envelope-schema.md) -- legacy M1 framing that NodeService replaced - [Architecture Overview](../architecture/overview.md) -- system-level view showing NodeService in context - [ADR-005: Single-Use KeyPackages](../design-rationale/adr-005-single-use-keypackages.md) -- why fetchKeyPackage is destructive - [ADR-004: MLS-Unaware DS](../design-rationale/adr-004-mls-unaware-ds.md) -- why payloads are opaque