Files
Yonexus/PROTOCOL.md

867 lines
17 KiB
Markdown

# Yonexus Protocol Specification
Version: draft v0.3
Status: planning
---
## 1. Purpose
This document defines the built-in Yonexus communication protocol used between:
- `Yonexus.Server`
- `Yonexus.Client`
The protocol covers:
- connection setup
- pairing
- authentication
- heartbeat
- status/lifecycle events
- protocol-level errors
- transport of application rule messages over the same WebSocket channel
Important security rule:
- pairing codes must **not** be delivered to `Yonexus.Client` over the Yonexus WebSocket channel
- pairing codes must be delivered out-of-band to a human administrator via Discord DM
---
## 1.1 Canonical Terminology
These names are treated as protocol-level canonical terms:
- `identifier`: unique logical identity of a Yonexus client instance.
- `rule_identifier`: exact-match routing key for application messages.
- `builtin`: reserved protocol namespace used only for Yonexus control frames.
- `pairingCode`: short-lived out-of-band code generated by the server for human-mediated pairing.
- `secret`: server-issued shared secret used in reconnect authentication proof construction.
- `publicKey` / `privateKey`: client signing keypair.
- `nextAction`: the server's directed next step in `hello_ack`.
The protocol and implementation repos should prefer these exact names over synonyms.
---
## 2. Transport
Transport is WebSocket.
- `Yonexus.Server` listens as WebSocket server
- `Yonexus.Client` connects as WebSocket client
- protocol frames are UTF-8 text in v1
- binary frames are not required in v1
Client connects to configured `mainHost`, which in v1 should be a full WebSocket URL:
- `ws://host:port/path`
- `wss://host:port/path`
Recommended canonical config:
- require/prefer a full WebSocket URL in v1 rather than raw `host:port`
---
## 3. Message Categories
## 3.1 Builtin Protocol Messages
Builtin messages always use:
```text
builtin::${json_payload}
```
`builtin` is reserved and must not be registered by user code.
## 3.2 Application Rule Messages
Application messages use:
```text
${rule_identifier}::${message_content}
```
When `Yonexus.Server` receives a rule message from a client, it must rewrite it before dispatch to:
```text
${rule_identifier}::${sender_identifier}::${message_content}
```
---
## 4. Builtin Envelope
Builtin wire format:
```text
builtin::{JSON}
```
Canonical envelope:
```ts
interface BuiltinEnvelope {
type: string;
requestId?: string;
timestamp?: number; // UTC unix seconds
payload?: Record<string, unknown>;
}
```
Rules:
- `timestamp` uses UTC unix seconds
- `requestId` is used for correlation where needed
- `payload` content depends on `type`
---
## 5. Builtin Message Types
## 5.1 Session Setup
### `hello`
Sent by `Yonexus.Client` immediately after WebSocket connection opens.
Purpose:
- declare identifier
- advertise current auth material state
- announce protocol version
Example:
```text
builtin::{
"type":"hello",
"requestId":"req_001",
"timestamp":1711886400,
"payload":{
"identifier":"client-a",
"hasSecret":true,
"hasKeyPair":true,
"publicKey":"<optional-public-key>",
"protocolVersion":"1"
}
}
```
### `hello_ack`
Sent by `Yonexus.Server` in response to `hello`.
Possible `nextAction` values:
- `pair_required`
- `auth_required`
- `rejected`
- `waiting_pair_confirm`
Example:
```text
builtin::{
"type":"hello_ack",
"requestId":"req_001",
"timestamp":1711886401,
"payload":{
"identifier":"client-a",
"nextAction":"pair_required"
}
}
```
---
## 5.2 Pairing Flow
## 5.2.1 Pairing Design Rule
`Yonexus.Server` must never send the actual `pairingCode` to `Yonexus.Client` through the Yonexus WebSocket channel.
The pairing code must be delivered to the configured human administrator using:
- `notifyBotToken`
- `adminUserId`
Specifically:
- `Yonexus.Server` sends a Discord DM to the configured admin user
- the DM contains the client identifier and pairing code
- the human relays the code to the client side by some trusted out-of-band path
## 5.2.2 Pairing Request Creation
When pairing is required, `Yonexus.Server` generates:
- `pairingCode`
- `expiresAt`
- `ttlSeconds`
The admin DM must include at minimum:
- `identifier`
- `pairingCode`
- `expiresAt` or TTL
Example DM body:
```text
Yonexus pairing request
identifier: client-a
pairingCode: ABCD-1234-XYZ
expiresAt: 1711886702
```
### `pair_request`
Sent by `Yonexus.Server` to `Yonexus.Client` after pairing starts.
Purpose:
- indicate that pairing has started
- indicate whether admin notification succeeded
- provide expiry metadata without revealing the code
Example:
```text
builtin::{
"type":"pair_request",
"requestId":"req_002",
"timestamp":1711886402,
"payload":{
"identifier":"client-a",
"expiresAt":1711886702,
"ttlSeconds":300,
"adminNotification":"sent",
"codeDelivery":"out_of_band"
}
}
```
Allowed `adminNotification` values:
- `sent`
- `failed`
If notification failed, pairing must not proceed until retried successfully.
### `pair_confirm`
Sent by `Yonexus.Client` to confirm pairing.
Purpose:
- submit the pairing code obtained out-of-band
Example:
```text
builtin::{
"type":"pair_confirm",
"requestId":"req_002",
"timestamp":1711886410,
"payload":{
"identifier":"client-a",
"pairingCode":"ABCD-1234-XYZ"
}
}
```
### `pair_success`
Sent by `Yonexus.Server` after successful pairing.
Purpose:
- return generated secret
- confirm trusted pairing state
Example:
```text
builtin::{
"type":"pair_success",
"requestId":"req_002",
"timestamp":1711886411,
"payload":{
"identifier":"client-a",
"secret":"<random-secret>",
"pairedAt":1711886411
}
}
```
### `pair_failed`
Sent by `Yonexus.Server` when pairing fails.
Typical reasons:
- `expired`
- `invalid_code`
- `identifier_not_allowed`
- `admin_notification_failed`
- `internal_error`
Example:
```text
builtin::{
"type":"pair_failed",
"requestId":"req_002",
"timestamp":1711886710,
"payload":{
"identifier":"client-a",
"reason":"expired"
}
}
```
---
## 5.3 Authentication Flow
After pairing, reconnect authentication uses:
- stored `secret`
- 24-character random nonce
- current UTC unix timestamp
- client private key
## 5.3.1 Proof Construction
Logical proof content:
```text
secret + nonce + timestamp
```
Implementation recommendation:
- use canonical serialized object bytes for signing
Recommended logical form:
```json
{
"secret":"...",
"nonce":"...",
"timestamp":1711886500
}
```
## 5.3.2 Signature Primitive
Recommended primitive:
- digital signature using client private key
- verification using stored client public key on server
### `auth_request`
Sent by `Yonexus.Client` after pairing or on reconnect.
Example:
```text
builtin::{
"type":"auth_request",
"requestId":"req_003",
"timestamp":1711886500,
"payload":{
"identifier":"client-a",
"nonce":"RANDOM24CHARACTERSTRINGX",
"proofTimestamp":1711886500,
"signature":"<base64-signature>",
"publicKey":"<optional-public-key-if-rotating>"
}
}
```
Server validation:
1. identifier is allowlisted
2. identifier exists in registry
3. client is in paired state
4. public key matches expected key if provided
5. signature verifies successfully
6. proof contains correct secret
7. `abs(now - proofTimestamp) < 10`
8. nonce has not appeared in recent nonce window
9. handshake attempts in last 10 seconds do not exceed 10
### `auth_success`
Sent by `Yonexus.Server` on success.
Example:
```text
builtin::{
"type":"auth_success",
"requestId":"req_003",
"timestamp":1711886501,
"payload":{
"identifier":"client-a",
"authenticatedAt":1711886501,
"status":"online"
}
}
```
### `auth_failed`
Sent by `Yonexus.Server` on auth failure.
Allowed reasons include:
- `unknown_identifier`
- `not_paired`
- `invalid_signature`
- `invalid_secret`
- `stale_timestamp`
- `future_timestamp`
- `nonce_collision`
- `rate_limited`
- `re_pair_required`
### `re_pair_required`
Sent by `Yonexus.Server` when trust state must be reset.
Example:
```text
builtin::{
"type":"re_pair_required",
"requestId":"req_004",
"timestamp":1711886510,
"payload":{
"identifier":"client-a",
"reason":"nonce_collision"
}
}
```
---
## 5.4 Heartbeat
### `heartbeat`
Sent by `Yonexus.Client` every 5 minutes after authentication.
Example:
```text
builtin::{
"type":"heartbeat",
"timestamp":1711886800,
"payload":{
"identifier":"client-a",
"status":"alive"
}
}
```
### `heartbeat_ack`
Optional response by `Yonexus.Server`.
v1 policy:
- `heartbeat_ack` may be enabled by the server but clients must not require it for healthy operation
Example:
```text
builtin::{
"type":"heartbeat_ack",
"timestamp":1711886801,
"payload":{
"identifier":"client-a",
"status":"online"
}
}
```
---
## 5.5 Status / Lifecycle Notifications
### `status_update`
Sent by `Yonexus.Server` when client state changes.
Example:
```text
builtin::{
"type":"status_update",
"timestamp":1711887220,
"payload":{
"identifier":"client-a",
"status":"unstable",
"reason":"heartbeat_timeout_7m"
}
}
```
### `disconnect_notice`
Sent by `Yonexus.Server` before deliberate close.
Example:
```text
builtin::{
"type":"disconnect_notice",
"timestamp":1711887460,
"payload":{
"identifier":"client-a",
"reason":"heartbeat_timeout_11m"
}
}
```
---
## 5.6 Errors
### `error`
Generic protocol-level error.
Recommended builtin error codes:
- `MALFORMED_MESSAGE`
- `UNSUPPORTED_PROTOCOL_VERSION`
- `IDENTIFIER_NOT_ALLOWED`
- `PAIRING_REQUIRED`
- `PAIRING_EXPIRED`
- `ADMIN_NOTIFICATION_FAILED`
- `AUTH_FAILED`
- `NONCE_COLLISION`
- `RATE_LIMITED`
- `RE_PAIR_REQUIRED`
- `CLIENT_OFFLINE`
- `INTERNAL_ERROR`
---
## 6. State Machines
## 6.1 Client State Machine
Suggested `Yonexus.Client` states:
- `idle`
- `connecting`
- `connected`
- `pairing_required`
- `pairing_pending`
- `paired`
- `authenticating`
- `authenticated`
- `reconnecting`
- `error`
Typical transitions:
```text
idle
-> connecting
-> connected
-> (pairing_required | authenticating)
pairing_required
-> pairing_pending
-> paired
-> authenticating
-> authenticated
authenticated
-> reconnecting
-> connecting
```
On `re_pair_required`:
```text
authenticated | authenticating -> pairing_required
```
## 6.2 Server-Side Client State
Per client trust state:
- `unpaired`
- `pending`
- `paired`
- `revoked`
Per client liveness state:
- `online`
- `unstable`
- `offline`
---
## 7. Security Windows and Replay Protection
## 7.1 Nonce Requirements
Nonce rules:
- exactly 24 random characters
- fresh per auth attempt
- must not repeat within recent security window
## 7.2 Recent Nonce Window
Server stores for each client:
- the last 10 nonces seen within the recent validity window
If a nonce collides:
- authentication fails
- server marks condition unsafe
- client must re-pair
## 7.3 Handshake Attempt Window
Server stores recent handshake attempt timestamps.
If more than 10 handshake attempts occur within 10 seconds:
- authentication fails
- server marks condition unsafe
- client must re-pair
## 7.4 Time Drift Validation
Server validates:
```text
abs(current_utc_unix_time - proofTimestamp) < 10
```
If validation fails:
- auth fails
- no session is established
---
## 8. Rule Message Dispatch
All non-builtin messages use:
```text
${rule_identifier}::${message_content}
```
Client to server example:
```text
chat_sync::{"conversationId":"abc","body":"hello"}
```
Server rewrites before matching:
```text
chat_sync::client-a::{"conversationId":"abc","body":"hello"}
```
Dispatch algorithm:
1. parse first delimiter section as `rule_identifier`
2. if `rule_identifier === builtin`, route to builtin protocol handler
3. otherwise iterate registered rules in registration order
4. invoke the first exact match
5. ignore/log if no match is found
v1 policy:
- rule matching is exact string match only; prefix, wildcard, and regex routing are out of scope
Processor input:
- on client: `${rule_identifier}::${message_content}`
- on server for client-originated messages: `${rule_identifier}::${sender_identifier}::${message_content}`
---
## 9. Connection Rules
Server should reject connection attempts when:
- identifier is absent
- identifier is not in configured allowlist
- protocol version is unsupported
- hello/auth payload is malformed
Recommended v1 policy:
- only one active authenticated connection per client identifier
- terminate old connection and accept new one after successful auth
---
## 10. Persistence Semantics
## 10.1 Yonexus.Server Persists
At minimum:
- identifier
- public key
- secret
- trust state
- pairing code + expiry if pending
- pairing notification metadata
- last known liveness status
- metadata timestamps
May persist or reset on restart:
- recent nonces
- recent handshake attempts
Recommended v1:
- clear rolling security windows on restart
- keep long-lived trust records
## 10.2 Yonexus.Client Persists
At minimum:
- identifier
- private key
- secret
- optional last successful pair/auth metadata
---
## 11. Versioning
Protocol version is advertised during `hello`.
Initial version:
```text
1
```
---
## 12. Canonical JSON Shapes
```ts
interface HelloPayload {
identifier: string;
hasSecret: boolean;
hasKeyPair: boolean;
publicKey?: string;
protocolVersion: string;
}
interface PairRequestPayload {
identifier: string;
expiresAt: number;
ttlSeconds: number;
adminNotification: "sent" | "failed";
codeDelivery: "out_of_band";
}
interface PairConfirmPayload {
identifier: string;
pairingCode: string;
}
interface PairSuccessPayload {
identifier: string;
secret: string;
pairedAt: number;
}
interface AuthRequestPayload {
identifier: string;
nonce: string;
proofTimestamp: number;
signature: string;
publicKey?: string;
}
interface HeartbeatPayload {
identifier: string;
status: "alive";
}
```
---
## 13. Example Flows
## 13.1 First-Time Pairing Flow
```text
Client connects WS
Client -> builtin::hello
Server sends Discord DM to configured admin with identifier + pairingCode
Server -> builtin::hello_ack(nextAction=pair_required)
Server -> builtin::pair_request(expiresAt, adminNotification=sent, codeDelivery=out_of_band)
Human reads DM and relays pairingCode to client side
Client -> builtin::pair_confirm(pairingCode)
Server -> builtin::pair_success(secret)
Client stores secret
Client -> builtin::auth_request(signature over secret+nonce+timestamp)
Server -> builtin::auth_success
Client enters authenticated state
```
## 13.2 Normal Reconnect Flow
```text
Client connects WS
Client -> builtin::hello(hasSecret=true)
Server -> builtin::hello_ack(nextAction=auth_required)
Client -> builtin::auth_request(...)
Server -> builtin::auth_success
Client begins heartbeat schedule
```
## 13.3 Unsafe Replay / Collision Flow
```text
Client -> builtin::auth_request(nonce collision)
Server detects unsafe condition
Server -> builtin::re_pair_required(reason=nonce_collision)
Server invalidates existing secret/session trust
Server sends fresh Discord DM pairing notification on next pairing start
Client returns to pairing_required state
```
## 13.4 Heartbeat Timeout Flow
```text
Client authenticated
No heartbeat for 7 min -> server marks unstable
No heartbeat for 11 min -> server marks offline
Server -> builtin::disconnect_notice
Server closes WS connection
```
---
## 14. Implementation Notes
## 14.1 Parsing
Because the wire format is string-based with `::` delimiters:
- only the first delimiter split should determine the `rule_identifier`
- for `builtin`, the remainder is parsed as JSON once
- message content itself may contain `::`, so avoid naive full split logic
## 14.2 Discord DM Notification
When pairing starts on `Yonexus.Server`:
- use configured `notifyBotToken`
- send DM to `adminUserId`
- include only required pairing data
- if DM send fails, surface pairing notification failure
Sensitive values that must never be logged in plaintext:
- `secret`
- private key
- raw proof material
---
## 15. Open Clarifications
1. Exact signing algorithm: Ed25519 is a strong default candidate
2. Secret length and generation requirements
3. Pairing code format and length
4. Is human code relay enough for v1, or should admin approve/deny controls be added later?
5. Should `heartbeat_ack` be mandatory or optional?
6. Should client reconnect use exponential backoff?
7. Should duplicate active connections replace old sessions or be rejected in stricter modes?
---
## 16. Reserved Builtin Types
Reserved builtin `type` values:
- `hello`
- `hello_ack`
- `pair_request`
- `pair_confirm`
- `pair_success`
- `pair_failed`
- `auth_request`
- `auth_success`
- `auth_failed`
- `re_pair_required`
- `heartbeat`
- `heartbeat_ack`
- `status_update`
- `disconnect_notice`
- `error`
These names are reserved by Yonexus and must not be repurposed by user rules.