Files
quicproquo/docs/src/wire-format/node-service-schema.md
Christian Nennemann 4694a3098b docs: comprehensive update for sprints 1-9
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.
2026-03-04 02:10:20 +01:00

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:

  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

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