{
  "asyncapi": "3.1.0",
  "info": {
    "title": "Chatbox Backend WebSocket API",
    "version": "1.0.0",
    "description": "The two WebSocket endpoints of the Chatbox backend: durable chat (/v1/chat/ws) and WebRTC signaling against the ICE-lite SFU (/v1/rtc/ws). These live here rather than in openapi.json because an HTTP specification renders them as callable GET operations, which generates a client that performs a plain GET against an upgrade endpoint. Wire shapes are defined once in ws-schemas.json and referenced from this document.",
    "license": { "name": "Apache-2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0" }
  },
  "servers": {
    "hosted": {
      "host": "api.ollacore.com",
      "protocol": "wss",
      "description": "Ollacore hosted API",
      "security": [{ "$ref": "#/components/securitySchemes/subprotocolBearer" }]
    },
    "selfHosted": {
      "host": "{host}",
      "protocol": "wss",
      "description": "Self-hosted: the PUBLIC_HOST of your deployment",
      "variables": { "host": { "default": "api.example.com" } },
      "security": [{ "$ref": "#/components/securitySchemes/subprotocolBearer" }]
    }
  },
  "channels": {
    "chat": {
      "address": "/v1/chat/ws",
      "title": "Durable room chat",
      "description": "One socket per session token. Server frames carry a monotonic event_seq per room; a client that reconnects sends a catchup frame with its last seq. Catch-up deliberately overlaps by CATCHUP_OVERLAP events, so a client may see an event it already has and must de-duplicate on event_seq. Ephemeral events sit outside that sequence (event_id 0): typing and presence from the server, and the client's own `broadcast` frame — a body of at most 1 KiB fanned out to the members connected right now, or to one member with `to`, never stored and never replayed, 20 a second per socket, refused with an error ack rather than a close.",
      "messages": {
        "clientEnvelope": { "$ref": "#/components/messages/chatClientEnvelope" },
        "ack": { "$ref": "#/components/messages/chatAck" },
        "serverEnvelope": { "$ref": "#/components/messages/chatServerEnvelope" }
      }
    },
    "rtc": {
      "address": "/v1/rtc/ws",
      "title": "WebRTC signaling",
      "description": "Signaling for the ICE-lite SFU. The server is always the controlled agent and generates no connectivity checks, so the browser is always controlling. ICE servers issued at token mint time carry no ?transport= query, because WebKit rejects the entire RTCPeerConnection configuration when any TURN URL has one.",
      "messages": {
        "clientFrame": { "$ref": "#/components/messages/rtcClientFrame" },
        "serverFrame": { "$ref": "#/components/messages/rtcServerFrame" }
      }
    }
  },
  "operations": {
    "sendChatFrame": {
      "action": "send",
      "channel": { "$ref": "#/channels/chat" },
      "summary": "Client sends a chat command.",
      "messages": [{ "$ref": "#/channels/chat/messages/clientEnvelope" }]
    },
    "receiveChatFrame": {
      "action": "receive",
      "channel": { "$ref": "#/channels/chat" },
      "summary": "Server acknowledges a command, or broadcasts a room event.",
      "messages": [
        { "$ref": "#/channels/chat/messages/ack" },
        { "$ref": "#/channels/chat/messages/serverEnvelope" }
      ]
    },
    "sendRtcFrame": {
      "action": "send",
      "channel": { "$ref": "#/channels/rtc" },
      "summary": "Client sends an offer, answer or ICE candidate.",
      "messages": [{ "$ref": "#/channels/rtc/messages/clientFrame" }]
    },
    "receiveRtcFrame": {
      "action": "receive",
      "channel": { "$ref": "#/channels/rtc" },
      "summary": "Server replies with SDP or signaling state. Note that the server flattens SDP onto the frame itself rather than nesting it under an sdp object.",
      "messages": [{ "$ref": "#/channels/rtc/messages/serverFrame" }]
    }
  },
  "components": {
    "securitySchemes": {
      "subprotocolBearer": {
        "type": "httpApiKey",
        "in": "header",
        "name": "Sec-WebSocket-Protocol",
        "description": "Authentication happens at upgrade, not with an Authorization header, because browsers cannot set one on a WebSocket. Send two subprotocols: chatbox, bearer.<JWT>. The JWT is the room-scoped Ed25519 token minted by POST /internal/v1/session-tokens. On the RTC channel a client may list chatbox.msid-principal ahead of chatbox: the server then echoes it as the selected subprotocol and every track forwarded to that socket carries the publisher's verified principal_id as the msid stream id and the publisher's own track id, instead of the default peer-<client_id>-<original> stamp. Optional and per socket; a server that does not know it answers chatbox."
      }
    },
    "messages": {
      "chatClientEnvelope": {
        "name": "chatClientEnvelope",
        "title": "Chat command",
        "contentType": "application/json",
        "payload": { "$ref": "./ws-schemas.json#/$defs/chatClientEnvelope" }
      },
      "chatAck": {
        "name": "chatAck",
        "title": "Command acknowledgement",
        "contentType": "application/json",
        "payload": { "$ref": "./ws-schemas.json#/$defs/chatAck" }
      },
      "chatServerEnvelope": {
        "name": "chatServerEnvelope",
        "title": "Room event",
        "contentType": "application/json",
        "payload": { "$ref": "./ws-schemas.json#/$defs/chatServerEnvelope" }
      },
      "rtcClientFrame": {
        "name": "rtcClientFrame",
        "title": "Signaling frame from the client",
        "contentType": "application/json",
        "payload": { "$ref": "./ws-schemas.json#/$defs/rtcClientFrame" }
      },
      "rtcServerFrame": {
        "name": "rtcServerFrame",
        "title": "Signaling frame from the server",
        "contentType": "application/json",
        "payload": { "$ref": "./ws-schemas.json#/$defs/rtcServerFrame" }
      }
    }
  }
}
