# 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
