# 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 --- ## 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 may be: - `ws://host:port/path` - `wss://host:port/path` - or raw `host:port` if normalized by implementation Recommended canonical config: - prefer full WebSocket URL --- ## 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; } ``` 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":"", "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: ``` 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":"", "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":"", "publicKey":"" } } ``` 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`. 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: ``` idle -> connecting -> connected -> (pairing_required | authenticating) pairing_required -> pairing_pending -> paired -> authenticating -> authenticated authenticated -> reconnecting -> connecting ``` On `re_pair_required`: ``` 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 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 ``` 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 ``` 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 ``` 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 ``` 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.