{
  "asyncapi": "3.0.0",
  "info": {
    "title": "CodeB /signal.ashx WebSocket signaling",
    "version": "2026-09-28.r5",
    "description": "AsyncAPI 3.0 contract for the WebSocket half of CodeB Sovereign Communications' /signal.ashx. Covers the full frame catalog (client to server and server to client), authentication paths a non-browser client can use, mesh vs SFU cascade frames, the outbound-dial frame, the knock/admit flow, and the additive remote-control frame family for a native helper controlling a participant's screen. Every frame here is verified against the running implementation in signal.ashx (WS message loop starting at line ~1425). Tenant is always the request Host header; the server does not run cross-tenant.\n\nFor REST and webhook surfaces see /openapi.json (OpenAPI 3.1). For a human-readable walk-through see /signal_api.html; for the remote-control channel see /remote_control_api.html.",
    "contact": {
      "name": "Aloaha Limited",
      "url": "https://www.aloaha.com/contact.html"
    },
    "license": {
      "name": "Proprietary - Aloaha Limited",
      "url": "https://www.aloaha.com/legal.html"
    }
  },
  "servers": {
    "development": {
      "host": "phone.codeb.io",
      "pathname": "/signal.ashx",
      "protocol": "wss",
      "description": "Development / conformance tenant. Use this for third-party client build and test — the RDPLauncher team, external SDK builds, anything that is not final acceptance against a customer tenant. First to receive protoVersion=2 features."
    },
    "production": {
      "host": "phone.aloaha.com",
      "pathname": "/signal.ashx",
      "protocol": "wss",
      "description": "Primary production tenant (Aloaha operator). Each customer has its own host name; the frame contract is identical."
    },
    "tenant": {
      "host": "{tenant}",
      "pathname": "/signal.ashx",
      "protocol": "wss",
      "description": "Any provisioned tenant.",
      "variables": {
        "tenant": { "default": "phone.aloaha.com" }
      }
    }
  },
  "defaultContentType": "application/json",
  "channels": {
    "signal": {
      "address": "/signal.ashx",
      "title": "Signaling channel",
      "description": "Bidirectional WebSocket. All application frames are UTF-8 JSON text frames (WebSocket opcode 0x1). Binary frames are ignored. Maximum aggregate message size is 256 KiB; the server closes with code 1009 (Message Too Big) when exceeded. Keepalive: the client sends `{ type: 'ping' }` about every 25 s and the server replies `{ type: 'pong' }`. The server also emits its own IIS-level Ping every 30 s independent of the application ping. Idle sockets without any traffic for approximately 60 s are dropped by IIS.",
      "messages": {
        "join":                  { "$ref": "#/components/messages/join" },
        "welcome":               { "$ref": "#/components/messages/welcome" },
        "peer_joined":           { "$ref": "#/components/messages/peer_joined" },
        "peer_left":             { "$ref": "#/components/messages/peer_left" },
        "signal":                { "$ref": "#/components/messages/signal" },
        "leave":                 { "$ref": "#/components/messages/leave" },
        "ping":                  { "$ref": "#/components/messages/ping" },
        "pong":                  { "$ref": "#/components/messages/pong" },
        "dial":                  { "$ref": "#/components/messages/dial" },
        "dial_result":           { "$ref": "#/components/messages/dial_result" },
        "ring":                  { "$ref": "#/components/messages/ring" },
        "ring_cleared":          { "$ref": "#/components/messages/ring_cleared" },
        "kicked":                { "$ref": "#/components/messages/kicked" },
        "error":                 { "$ref": "#/components/messages/error" },
        "lock":                  { "$ref": "#/components/messages/lock" },
        "unlock":                { "$ref": "#/components/messages/unlock" },
        "room_locked":           { "$ref": "#/components/messages/room_locked" },
        "room_unlocked":         { "$ref": "#/components/messages/room_unlocked" },
        "knock":                 { "$ref": "#/components/messages/knock" },
        "knock_pending":         { "$ref": "#/components/messages/knock_pending" },
        "admit":                 { "$ref": "#/components/messages/admit" },
        "deny":                  { "$ref": "#/components/messages/deny" },
        "denied":                { "$ref": "#/components/messages/denied" },
        "mode_changed":          { "$ref": "#/components/messages/mode_changed" },
        "sfu_offer":             { "$ref": "#/components/messages/sfu_offer" },
        "sfu_answer":            { "$ref": "#/components/messages/sfu_answer" },
        "sfu_offer_failed":      { "$ref": "#/components/messages/sfu_offer_failed" },
        "sfu_subscribe_offer":   { "$ref": "#/components/messages/sfu_subscribe_offer" },
        "sfu_subscribe_answer":  { "$ref": "#/components/messages/sfu_subscribe_answer" },
        "sfu_subscribe_failed":  { "$ref": "#/components/messages/sfu_subscribe_failed" },
        "bandwidth_report":      { "$ref": "#/components/messages/bandwidth_report" },
        "dup_ip_warning":        { "$ref": "#/components/messages/dup_ip_warning" },
        "control_request":       { "$ref": "#/components/messages/control_request" },
        "control_grant":         { "$ref": "#/components/messages/control_grant" },
        "control_revoke":        { "$ref": "#/components/messages/control_revoke" },
        "control_lock":          { "$ref": "#/components/messages/control_lock" },
        "control_lock_changed":  { "$ref": "#/components/messages/control_lock_changed" },
        "identity_attest_req":   { "$ref": "#/components/messages/identity_attest_req" },
        "identity_attest":       { "$ref": "#/components/messages/identity_attest" }
      }
    },
    "codeb_control_v1": {
      "address": "codeb-control-v1",
      "title": "Remote-control data channel (codeb-control-v1)",
      "description": "The RTCDataChannel labelled `codeb-control-v1` that both peers open on their existing PeerConnection after control-grant. Messages are UTF-8 JSON, one event per SCTP SDU, `ordered:true`, `maxRetransmits:null` (fully reliable). Every event carries `s` (sessionId) and `seq` (strictly increasing per session; targets MUST drop events with `seq <= last`). See /remote_control_api.html for a walk-through.",
      "messages": {
        "dc_screen_info": { "$ref": "#/components/messages/dc_screen_info" },
        "dc_pointer":     { "$ref": "#/components/messages/dc_pointer" },
        "dc_button":      { "$ref": "#/components/messages/dc_button" },
        "dc_wheel":       { "$ref": "#/components/messages/dc_wheel" },
        "dc_key":         { "$ref": "#/components/messages/dc_key" },
        "dc_clip_read":   { "$ref": "#/components/messages/dc_clip_read" },
        "dc_clip_write":  { "$ref": "#/components/messages/dc_clip_write" },
        "dc_clip":        { "$ref": "#/components/messages/dc_clip" },
        "dc_file_offer":  { "$ref": "#/components/messages/dc_file_offer" },
        "dc_file_chunk":  { "$ref": "#/components/messages/dc_file_chunk" },
        "dc_hb":          { "$ref": "#/components/messages/dc_hb" },
        "dc_end":         { "$ref": "#/components/messages/dc_end" }
      }
    }
  },
  "operations": {
    "clientToServer": {
      "action": "send",
      "channel": { "$ref": "#/channels/signal" },
      "summary": "Frames the client sends to /signal.ashx.",
      "messages": [
        { "$ref": "#/channels/signal/messages/join" },
        { "$ref": "#/channels/signal/messages/signal" },
        { "$ref": "#/channels/signal/messages/leave" },
        { "$ref": "#/channels/signal/messages/ping" },
        { "$ref": "#/channels/signal/messages/dial" },
        { "$ref": "#/channels/signal/messages/lock" },
        { "$ref": "#/channels/signal/messages/unlock" },
        { "$ref": "#/channels/signal/messages/admit" },
        { "$ref": "#/channels/signal/messages/deny" },
        { "$ref": "#/channels/signal/messages/sfu_offer" },
        { "$ref": "#/channels/signal/messages/sfu_subscribe_offer" },
        { "$ref": "#/channels/signal/messages/bandwidth_report" },
        { "$ref": "#/channels/signal/messages/control_request" },
        { "$ref": "#/channels/signal/messages/control_grant" },
        { "$ref": "#/channels/signal/messages/control_revoke" },
        { "$ref": "#/channels/signal/messages/control_lock" },
        { "$ref": "#/channels/signal/messages/identity_attest_req" }
      ]
    },
    "serverToClient": {
      "action": "receive",
      "channel": { "$ref": "#/channels/signal" },
      "summary": "Frames the server sends to a connected peer.",
      "messages": [
        { "$ref": "#/channels/signal/messages/welcome" },
        { "$ref": "#/channels/signal/messages/peer_joined" },
        { "$ref": "#/channels/signal/messages/peer_left" },
        { "$ref": "#/channels/signal/messages/signal" },
        { "$ref": "#/channels/signal/messages/pong" },
        { "$ref": "#/channels/signal/messages/dial_result" },
        { "$ref": "#/channels/signal/messages/ring" },
        { "$ref": "#/channels/signal/messages/ring_cleared" },
        { "$ref": "#/channels/signal/messages/kicked" },
        { "$ref": "#/channels/signal/messages/error" },
        { "$ref": "#/channels/signal/messages/room_locked" },
        { "$ref": "#/channels/signal/messages/room_unlocked" },
        { "$ref": "#/channels/signal/messages/knock" },
        { "$ref": "#/channels/signal/messages/knock_pending" },
        { "$ref": "#/channels/signal/messages/denied" },
        { "$ref": "#/channels/signal/messages/mode_changed" },
        { "$ref": "#/channels/signal/messages/sfu_answer" },
        { "$ref": "#/channels/signal/messages/sfu_offer_failed" },
        { "$ref": "#/channels/signal/messages/sfu_subscribe_answer" },
        { "$ref": "#/channels/signal/messages/sfu_subscribe_failed" },
        { "$ref": "#/channels/signal/messages/dup_ip_warning" },
        { "$ref": "#/channels/signal/messages/control_request" },
        { "$ref": "#/channels/signal/messages/control_grant" },
        { "$ref": "#/channels/signal/messages/control_revoke" },
        { "$ref": "#/channels/signal/messages/control_lock_changed" },
        { "$ref": "#/channels/signal/messages/identity_attest" }
      ]
    },
    "helperToTargetDc": {
      "action": "send",
      "channel": { "$ref": "#/channels/codeb_control_v1" },
      "summary": "Helper -> target events on the codeb-control-v1 RTCDataChannel after a control-grant.",
      "messages": [
        { "$ref": "#/channels/codeb_control_v1/messages/dc_pointer" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_button" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_wheel" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_key" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_clip_read" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_clip_write" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_file_offer" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_file_chunk" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_hb" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_end" }
      ]
    },
    "targetToHelperDc": {
      "action": "receive",
      "channel": { "$ref": "#/channels/codeb_control_v1" },
      "summary": "Target -> helper events on the codeb-control-v1 RTCDataChannel.",
      "messages": [
        { "$ref": "#/channels/codeb_control_v1/messages/dc_screen_info" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_clip" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_hb" },
        { "$ref": "#/channels/codeb_control_v1/messages/dc_end" }
      ]
    }
  },
  "components": {
    "schemas": {
      "PeerId":       { "type": "string", "pattern": "^[a-f0-9]{12}$", "description": "12 lowercase hex chars. Server-assigned in welcome. Immutable for the lifetime of one WebSocket." },
      "RoomCode":     { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._-]{1,128}$", "description": "Sanitized room name. Pattern enforced server-side by SanitizeRoom(). Case-insensitive matching. Clients SHOULD apply the same regex before sending." },
      "DisplayName":  { "type": "string", "minLength": 1, "maxLength": 64 },
      "IceServer": {
        "type": "object",
        "description": "Standard WebRTC ICE server entry consumable by RTCPeerConnection.",
        "properties": {
          "urls":       { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] },
          "username":   { "type": "string", "description": "TURN REST ephemeral username = '<expiryUnixSec>:<peerId>'. Present only on TURN entries." },
          "credential": { "type": "string", "description": "Base64(HMAC-SHA1(sharedSecret, username)). Present only on TURN entries." }
        },
        "required": [ "urls" ]
      },
      "PeerRef": {
        "type": "object",
        "description": "Reference to a peer in the room, as broadcast in welcome/peer-joined.",
        "properties": {
          "id":       { "$ref": "#/components/schemas/PeerId" },
          "name":     { "$ref": "#/components/schemas/DisplayName" },
          "verified": { "type": "boolean", "description": "true when the peer presented a valid OIDC access token or the reverse proxy set X-CodeB-Identity." },
          "role":     { "type": "string", "enum": [ "human", "ai-vnum", "sip-bridge" ], "default": "human" }
        },
        "required": [ "id", "name" ]
      },
      "SdpDescription": {
        "type": "object",
        "description": "Standard WebRTC RTCSessionDescriptionInit.",
        "properties": {
          "type": { "type": "string", "enum": [ "offer", "answer", "pranswer", "rollback" ] },
          "sdp":  { "type": "string" }
        },
        "required": [ "type", "sdp" ]
      },
      "IceCandidateInit": {
        "type": "object",
        "description": "Standard WebRTC RTCIceCandidateInit. An empty candidate string (or end-of-candidates set) is the trickle terminator.",
        "properties": {
          "candidate":     { "type": "string" },
          "sdpMid":        { "type": [ "string", "null" ] },
          "sdpMLineIndex": { "type": [ "integer", "null" ] },
          "usernameFragment": { "type": [ "string", "null" ] }
        }
      },
      "SignalPayload": {
        "oneOf": [
          { "type": "object", "properties": { "sdp":       { "$ref": "#/components/schemas/SdpDescription"  } }, "required": [ "sdp" ]       },
          { "type": "object", "properties": { "candidate": { "$ref": "#/components/schemas/IceCandidateInit" } }, "required": [ "candidate" ] },
          { "type": "object", "description": "Freeform payload the SDK forwards opaquely between peers (chat metadata, cursor coords for the 'Remote pointer' setting, and so on). The server does not inspect this branch and never dispatches on its shape." }
        ]
      },
      "ControlSessionId": {
        "type": "string",
        "pattern": "^[a-z0-9]{16,64}$",
        "description": "Opaque control-session identifier. The grant-side peer picks it and both sides echo it on subsequent frames."
      },
      "ControlFeatureFlag": {
        "type": "string",
        "enum": [ "pointer", "keyboard", "wheel", "clipboard-read", "clipboard-write", "file-transfer" ],
        "description": "Fine-grained capability the helper is asking for. Grant-side may return a subset."
      }
    },
    "messages": {
      "join": {
        "name": "join",
        "title": "join",
        "summary": "First frame the client sends after the WebSocket upgrade. Nothing else works until join has completed.",
        "payload": {
          "type": "object",
          "properties": {
            "type":          { "const": "join" },
            "room":          { "$ref": "#/components/schemas/RoomCode" },
            "name":          { "$ref": "#/components/schemas/DisplayName" },
            "authToken":     { "type": "string", "description": "Optional OIDC access token or 'ak_' API key. When present and valid, the peer's verifiedIdentity is set from the token's sub claim. If unset, or invalid, or the reverse proxy already set X-CodeB-Identity, this field is ignored." },
            "role":          { "type": "string", "enum": [ "human", "ai-vnum", "sip-bridge" ], "default": "human", "description": "Bridge-side legs set 'sip-bridge' or 'ai-vnum'. Browsers leave it unset. Unknown values are clamped to 'human'." },
            "preferredMode": { "type": "string", "enum": [ "auto", "prefer-mesh", "prefer-sfu" ], "default": "auto", "description": "Nudges the auto-promote decision. Admin pin (ForceMode) still wins." },
            "protoVersion":  { "type": "string", "default": "1", "description": "Client-declared protocol revision. The server echoes back its supported version in welcome.protoVersion. Version '2' unlocks the control-request / control-grant frame family." },
            "client":        { "type": "string", "description": "Free-form client identifier (e.g. 'AloahaRDPSystemTray/1.4.0'). Logged; no behavior gated by it." }
          },
          "required": [ "type", "room" ]
        }
      },
      "welcome": {
        "name": "welcome",
        "title": "welcome",
        "summary": "Server's answer to a successful join. Carries the assigned peerId, the current peer roster and the ICE server list the client must feed to RTCPeerConnection.",
        "payload": {
          "type": "object",
          "properties": {
            "type":          { "const": "welcome" },
            "peerId":        { "$ref": "#/components/schemas/PeerId" },
            "peers":         { "type": "array", "items": { "$ref": "#/components/schemas/PeerRef" }, "description": "Peers already in the room, not including self." },
            "iceServers":    { "type": "array", "items": { "$ref": "#/components/schemas/IceServer" } },
            "suggestedName": { "type": [ "string", "null" ], "description": "When the peer authenticated, this is the OIDC sub. Consent dialogs display it." },
            "mode":          { "type": "string", "enum": [ "mesh", "sfu" ] },
            "caps": {
              "type": "object",
              "properties": {
                "dial":          { "type": "boolean" },
                "maxPeers":      { "type": "integer" },
                "sfuEnabled":    { "type": "boolean" },
                "controlLocked": { "type": "boolean", "description": "true when a moderator has set the room-wide control-lock (see control-lock frame)." },
                "remoteControl": { "type": "boolean", "description": "true when this server build handles the control-request/control-grant/control-revoke family (protoVersion 2)." }
              }
            },
            "forceRelay":    { "type": "boolean", "description": "Per-tenant default for iceTransportPolicy='relay'. OR the URL ?relay= flag with this and any localStorage preference." },
            "admitted":      { "type": "boolean", "description": "true when the welcome arrives after knock-and-admit (as opposed to direct join)." },
            "protoVersion":  { "type": "string", "description": "Highest signaling protocol revision the server can speak. Currently '2'." }
          },
          "required": [ "type", "peerId", "peers", "iceServers", "mode" ]
        }
      },
      "peer_joined": {
        "name": "peer-joined",
        "title": "peer-joined",
        "summary": "Broadcast to every peer already in the room when a new peer's join is accepted.",
        "payload": {
          "type": "object",
          "properties": {
            "type": { "const": "peer-joined" },
            "peer": { "$ref": "#/components/schemas/PeerRef" }
          },
          "required": [ "type", "peer" ]
        }
      },
      "peer_left": {
        "name": "peer-left",
        "title": "peer-left",
        "summary": "Broadcast to remaining peers when a peer's WebSocket closes for any reason.",
        "payload": {
          "type": "object",
          "properties": {
            "type":   { "const": "peer-left" },
            "peerId": { "$ref": "#/components/schemas/PeerId" }
          },
          "required": [ "type", "peerId" ]
        }
      },
      "signal": {
        "name": "signal",
        "title": "signal",
        "summary": "Peer-to-peer relay. When a peer is the sender, 'to' names the recipient. When the server delivers, 'from' names the original sender; the 'to' field is stripped. The server does not inspect payload beyond routing.",
        "payload": {
          "type": "object",
          "properties": {
            "type":    { "const": "signal" },
            "to":      { "$ref": "#/components/schemas/PeerId" },
            "from":    { "$ref": "#/components/schemas/PeerId" },
            "payload": { "$ref": "#/components/schemas/SignalPayload" }
          },
          "required": [ "type", "payload" ]
        }
      },
      "leave": {
        "name": "leave",
        "title": "leave",
        "summary": "Voluntary leave. Optional. Closing the WebSocket has the same effect but leaves this frame explicit for the local audit trail.",
        "payload": {
          "type": "object",
          "properties": {
            "type": { "const": "leave" }
          },
          "required": [ "type" ]
        }
      },
      "ping": {
        "name": "ping",
        "title": "ping",
        "summary": "Application-level keepalive. Distinct from RFC 6455 ping frames; those are handled by IIS.",
        "payload": {
          "type": "object",
          "properties": { "type": { "const": "ping" } },
          "required": [ "type" ]
        }
      },
      "pong": {
        "name": "pong",
        "title": "pong",
        "summary": "Server's response to an application ping.",
        "payload": {
          "type": "object",
          "properties": { "type": { "const": "pong" } },
          "required": [ "type" ]
        }
      },
      "dial": {
        "name": "dial",
        "title": "dial",
        "summary": "Ask the room to place an outbound SIP call whose media joins as another peer. Wallet gate, whitelist, ACL and trunk configuration all apply; the local SIP bridge does the actual dial.",
        "payload": {
          "type": "object",
          "properties": {
            "type":   { "const": "dial" },
            "number": { "type": "string", "description": "E.164 (e.g. '+49301234567'), a registered SIP username on this tenant, a configured virtual number, or an alias of the form 'n_<12hex>'." },
            "trunk":  { "type": "string", "description": "Optional operator-chosen trunk slug from /signal.ashx?trunks." }
          },
          "required": [ "type", "number" ]
        }
      },
      "dial_result": {
        "name": "dial-result",
        "title": "dial-result",
        "summary": "Server's answer to a dial frame, echoing back what the caller supplied (or the alias when one was used).",
        "payload": {
          "type": "object",
          "properties": {
            "type":    { "const": "dial-result" },
            "ok":      { "type": "boolean" },
            "error":   { "type": [ "string", "null" ] },
            "number":  { "type": "string" },
            "aliased": { "type": "boolean" }
          },
          "required": [ "type", "ok" ]
        }
      },
      "ring": {
        "name": "ring",
        "title": "ring",
        "summary": "Inbound browser-to-browser or SIP-to-browser call arriving on an /office.html tab. Sent to every socket registered for the callee user.",
        "payload": {
          "type": "object",
          "properties": {
            "type":     { "const": "ring" },
            "ringId":   { "type": "string" },
            "from":     { "type": "string", "description": "Caller display or E.164." },
            "room":     { "$ref": "#/components/schemas/RoomCode" },
            "user":     { "type": "string", "description": "Callee user id." },
            "meta":     { "type": [ "object", "null" ] }
          },
          "required": [ "type", "ringId", "from", "room" ]
        }
      },
      "ring_cleared": {
        "name": "ring-cleared",
        "title": "ring-cleared",
        "summary": "Cancel or supersede an outstanding ring.",
        "payload": {
          "type": "object",
          "properties": {
            "type":   { "const": "ring-cleared" },
            "ringId": { "type": "string" },
            "reason": { "type": "string", "enum": [ "caller-hung-up", "answered-elsewhere", "caller-disconnected", "timeout", "server-clear" ] }
          },
          "required": [ "type", "ringId" ]
        }
      },
      "kicked": {
        "name": "kicked",
        "title": "kicked",
        "summary": "The server is closing this peer's connection. A close frame follows.",
        "payload": {
          "type": "object",
          "properties": {
            "type":   { "const": "kicked" },
            "reason": { "type": "string", "enum": [ "room-full", "moderator-kick", "policy-violation", "server-shutdown", "duplicate-connection" ] },
            "detail": { "type": [ "string", "null" ] }
          },
          "required": [ "type", "reason" ]
        }
      },
      "error": {
        "name": "error",
        "title": "error",
        "summary": "Recoverable error. The connection stays open; the client should not assume any state changed on the server.",
        "payload": {
          "type": "object",
          "properties": {
            "type":    { "const": "error" },
            "message": { "type": "string" },
            "code":    { "type": [ "string", "null" ], "description": "Well-known codes: bad-json, join-required, room-required, room-full, already-joined, not-allowed, message-too-large, rate-limited (also emitted when a control-request exceeds 3/min for one peer), sdp-required, publisher-required, peer-not-found (control-* or identity-attest-req against a peer that is not in the room), control-locked (control-request while moderator has room-wide lock set)." }
          },
          "required": [ "type", "message" ]
        }
      },
      "lock":         { "name": "lock",   "title": "lock",   "summary": "Moderator locks the room. Late joiners will knock.", "payload": { "type": "object", "properties": { "type": { "const": "lock"   } }, "required": [ "type" ] } },
      "unlock":       { "name": "unlock", "title": "unlock", "summary": "Moderator unlocks the room.",                        "payload": { "type": "object", "properties": { "type": { "const": "unlock" } }, "required": [ "type" ] } },
      "room_locked":   { "name": "room-locked",   "title": "room-locked",   "summary": "Broadcast when the room is locked by a moderator.",
        "payload": { "type": "object", "properties": { "type": { "const": "room-locked"   }, "by": { "type": "string" } }, "required": [ "type" ] } },
      "room_unlocked": { "name": "room-unlocked", "title": "room-unlocked", "summary": "Broadcast when the room is unlocked.",
        "payload": { "type": "object", "properties": { "type": { "const": "room-unlocked" }, "by": { "type": "string" } }, "required": [ "type" ] } },
      "knock":         { "name": "knock", "title": "knock", "summary": "Broadcast to peers already in a locked room when someone knocks.",
        "payload": { "type": "object", "properties": { "type": { "const": "knock" }, "peerId": { "$ref": "#/components/schemas/PeerId" }, "name": { "$ref": "#/components/schemas/DisplayName" }, "verified": { "type": "boolean" } }, "required": [ "type", "peerId", "name" ] } },
      "knock_pending": { "name": "knock-pending", "title": "knock-pending", "summary": "Sent to the knocker while waiting for admit or deny.",
        "payload": { "type": "object", "properties": { "type": { "const": "knock-pending" }, "peerId": { "$ref": "#/components/schemas/PeerId" } }, "required": [ "type", "peerId" ] } },
      "admit":         { "name": "admit", "title": "admit", "summary": "Moderator admits a knocker. peerId is the knocker's.",
        "payload": { "type": "object", "properties": { "type": { "const": "admit" }, "peerId": { "$ref": "#/components/schemas/PeerId" } }, "required": [ "type", "peerId" ] } },
      "deny":          { "name": "deny",  "title": "deny",  "summary": "Moderator denies a knocker. The knocker's socket is closed.",
        "payload": { "type": "object", "properties": { "type": { "const": "deny"  }, "peerId": { "$ref": "#/components/schemas/PeerId" } }, "required": [ "type", "peerId" ] } },
      "denied":        { "name": "denied", "title": "denied", "summary": "Sent to a denied knocker just before their socket is closed.",
        "payload": { "type": "object", "properties": { "type": { "const": "denied" } }, "required": [ "type" ] } },
      "mode_changed": {
        "name": "mode-changed",
        "title": "mode-changed",
        "summary": "Broadcast when the room switches between mesh and SFU (auto-promote, admin pin, or bandwidth-degraded trigger). Peers should promote/tear down their PeerConnections accordingly.",
        "payload": {
          "type": "object",
          "properties": {
            "type":   { "const": "mode-changed" },
            "mode":   { "type": "string", "enum": [ "mesh", "sfu" ] },
            "reason": { "type": "string" }
          },
          "required": [ "type", "mode" ]
        }
      },
      "sfu_offer": {
        "name": "sfu-offer",
        "title": "sfu-offer",
        "summary": "Client-to-server publish offer. Client sends its WebRTC offer; server forwards to the bridge /sfu/offer.",
        "payload": { "type": "object", "properties": { "type": { "const": "sfu-offer" }, "sdp": { "type": "string" } }, "required": [ "type", "sdp" ] }
      },
      "sfu_answer": {
        "name": "sfu-answer",
        "title": "sfu-answer",
        "summary": "Server's SFU answer for a published stream.",
        "payload": { "type": "object", "properties": { "type": { "const": "sfu-answer" }, "sdp": { "type": "string" }, "phase": { "type": "string" } }, "required": [ "type", "sdp" ] }
      },
      "sfu_offer_failed": {
        "name": "sfu-offer-failed",
        "title": "sfu-offer-failed",
        "summary": "SFU offer failed at the bridge. Fall back to mesh or retry.",
        "payload": {
          "type": "object",
          "properties": {
            "type":       { "const": "sfu-offer-failed" },
            "code":       { "type": "string" },
            "error":      { "type": "string" },
            "phase":      { "type": "string" },
            "httpStatus": { "type": "integer" }
          },
          "required": [ "type", "code" ]
        }
      },
      "sfu_subscribe_offer": {
        "name": "sfu-subscribe-offer",
        "title": "sfu-subscribe-offer",
        "summary": "Client asks to subscribe to a specific publisher's stream through the SFU.",
        "payload": {
          "type": "object",
          "properties": {
            "type":            { "const": "sfu-subscribe-offer" },
            "publisherPeerId": { "$ref": "#/components/schemas/PeerId" },
            "sdp":             { "type": "string" }
          },
          "required": [ "type", "publisherPeerId", "sdp" ]
        }
      },
      "sfu_subscribe_answer": {
        "name": "sfu-subscribe-answer",
        "title": "sfu-subscribe-answer",
        "summary": "SFU answer for a subscribe.",
        "payload": {
          "type": "object",
          "properties": {
            "type":            { "const": "sfu-subscribe-answer" },
            "publisherPeerId": { "$ref": "#/components/schemas/PeerId" },
            "sdp":             { "type": "string" }
          },
          "required": [ "type", "publisherPeerId", "sdp" ]
        }
      },
      "sfu_subscribe_failed": {
        "name": "sfu-subscribe-failed",
        "title": "sfu-subscribe-failed",
        "summary": "Subscribe failed at the bridge.",
        "payload": {
          "type": "object",
          "properties": {
            "type":            { "const": "sfu-subscribe-failed" },
            "publisherPeerId": { "$ref": "#/components/schemas/PeerId" },
            "code":            { "type": "string" },
            "error":           { "type": "string" }
          },
          "required": [ "type", "publisherPeerId", "code" ]
        }
      },
      "bandwidth_report": {
        "name": "bandwidth-report",
        "title": "bandwidth-report",
        "summary": "Client-side network telemetry snapshot. Feeds Trigger A (bandwidth-driven mesh -> SFU auto-promote).",
        "payload": {
          "type": "object",
          "properties": {
            "type":                       { "const": "bandwidth-report" },
            "availableOutgoingBitrate":   { "type": "number" },
            "packetsSent":                { "type": "number" },
            "packetsLost":                { "type": "number" },
            "roundTripTimeMs":            { "type": "number" },
            "windowSec":                  { "type": "number" }
          },
          "required": [ "type" ]
        }
      },
      "dup_ip_warning": {
        "name": "dup-ip-warning",
        "title": "dup-ip-warning",
        "summary": "Sent to every peer in the room sharing the joiner's source IP. Diagnostic; the client may show a 'you have another tab open' banner.",
        "payload": {
          "type": "object",
          "properties": {
            "type":  { "const": "dup-ip-warning" },
            "count": { "type": "integer" },
            "ip":    { "type": "string" },
            "names": { "type": "array", "items": { "type": "string" } }
          },
          "required": [ "type", "count" ]
        }
      },
      "control_request": {
        "name": "control-request",
        "title": "control-request",
        "summary": "Helper asks the target to be granted screen and input control. The server routes to 'to', server-stamps 'from', 'verifiedIdentity', 'verified' and 'name', and fires the control.requested webhook. The target's user surfaces this in the local consent dialog; nothing else happens until control-grant.",
        "payload": {
          "type": "object",
          "properties": {
            "type":               { "const": "control-request" },
            "to":                 { "$ref": "#/components/schemas/PeerId" },
            "from":               { "$ref": "#/components/schemas/PeerId", "description": "Server-stamped on delivery. Ignored if the client sets it." },
            "name":               { "$ref": "#/components/schemas/DisplayName", "description": "Server-stamped: helper's display name." },
            "verified":           { "type": "boolean", "description": "Server-stamped: true when the helper authenticated." },
            "verifiedIdentity":   { "type": [ "string", "null" ], "description": "Server-stamped: helper's OIDC sub. null when the helper is anonymous." },
            "purpose":            { "type": "string", "maxLength": 200, "description": "Human-readable reason shown in the consent dialog." },
            "requestedFeatures":  { "type": "array", "items": { "$ref": "#/components/schemas/ControlFeatureFlag" } },
            "requestId":          { "type": "string", "description": "Helper-picked identifier so the response can be correlated." },
            "attest":             { "type": [ "string", "null" ], "description": "Server-stamped on delivery: identity attestation JWS (HS256, 5-min lifetime) binding the helper's peerId to name and verifiedIdentity. Consent dialogs verify locally with the tenant secret or via /signal.ashx?attest-verify. null when no attest secret is configured on the tenant; a target that receives null MUST treat the helper as unverified regardless of the `verified` flag." },
            "attestExpiresUtc":   { "type": [ "string", "null" ], "format": "date-time", "description": "Expiry of the attached attest (matches the JWS exp claim). Present only when attest is non-null." }
          },
          "required": [ "type", "to", "requestedFeatures" ]
        }
      },
      "control_grant": {
        "name": "control-grant",
        "title": "control-grant",
        "summary": "Target user approved. Carries the session id and the actually granted feature set (may be a subset of what was requested).",
        "payload": {
          "type": "object",
          "properties": {
            "type":            { "const": "control-grant" },
            "to":              { "$ref": "#/components/schemas/PeerId", "description": "The helper's peer id." },
            "from":            { "$ref": "#/components/schemas/PeerId", "description": "Server-stamped." },
            "requestId":       { "type": "string" },
            "sessionId":       { "$ref": "#/components/schemas/ControlSessionId" },
            "grantedFeatures": { "type": "array", "items": { "$ref": "#/components/schemas/ControlFeatureFlag" } },
            "expiresUtc":      { "type": "string", "format": "date-time", "description": "Grant expiry. The target's local watchdog also auto-revokes on lock, disconnect, meeting close, and 10 minutes without input." },
            "dataChannelLabel": { "type": "string", "const": "codeb-control-v1", "description": "The RTCDataChannel label both sides open on their PeerConnection to carry control events. Fixed to 'codeb-control-v1'." },
            "hbIntervalMs":     { "type": "integer", "minimum": 500, "maximum": 30000, "default": 5000, "description": "Target-picked heartbeat cadence for the helper's `hb` messages on the data channel. Missing three consecutive heartbeats at this cadence triggers an auto-revoke with reason 'timeout'. Server clamps to [500, 30000] ms." }
          },
          "required": [ "type", "to", "sessionId", "grantedFeatures", "expiresUtc" ]
        }
      },
      "control_lock_changed": {
        "name": "control-lock-changed",
        "title": "control-lock-changed",
        "summary": "Server-broadcast update when the room-wide control-lock is toggled by a moderator via the control-lock frame. Every peer in the room receives one; there is no per-peer opt-out. Consumers may repaint the 'Request control' UI accordingly.",
        "payload": {
          "type": "object",
          "properties": {
            "type":   { "const": "control-lock-changed" },
            "locked": { "type": "boolean" },
            "by":     { "type": "string", "description": "Display name of the moderator that flipped the lock." }
          },
          "required": [ "type", "locked" ]
        }
      },
      "control_revoke": {
        "name": "control-revoke",
        "title": "control-revoke",
        "summary": "Either side ends a control session. The server fires the control.revoked webhook. **Exactly one of `sessionId` or `requestId` must be present.** Use `sessionId` for an ongoing session that has been granted. Use `requestId` when denying a request BEFORE it was granted (there is no session id to reference yet). Receivers MUST also check that the frame's server-stamped `from` matches the current session's other peer before acting on a `sessionId`-form revoke — otherwise a peer that learned the session id through another channel could end the session (DoS).",
        "payload": {
          "type": "object",
          "properties": {
            "type":       { "const": "control-revoke" },
            "to":         { "$ref": "#/components/schemas/PeerId" },
            "from":       { "$ref": "#/components/schemas/PeerId", "description": "Server-stamped." },
            "sessionId":  { "$ref": "#/components/schemas/ControlSessionId", "description": "The active session id. Present for revocations of a granted session." },
            "requestId":  { "type": "string", "description": "The requestId from the corresponding control-request. Present for pre-grant denials, where no session id exists yet." },
            "reason":     { "type": "string", "enum": [ "user-stopped", "timeout", "target-locked", "target-disconnected", "helper-disconnected", "policy", "emergency" ] }
          },
          "required": [ "type", "to", "reason" ],
          "oneOf": [
            { "required": [ "sessionId" ] },
            { "required": [ "requestId" ] }
          ]
        }
      },
      "control_lock": {
        "name": "control-lock",
        "title": "control-lock",
        "summary": "Moderator toggles a room-wide ban on control-request. When on, the server rejects new control-request frames with error code 'control-locked' and does not deliver them.",
        "payload": {
          "type": "object",
          "properties": {
            "type":     { "const": "control-lock" },
            "locked":   { "type": "boolean" }
          },
          "required": [ "type", "locked" ]
        }
      },
      "identity_attest_req": {
        "name": "identity-attest-req",
        "title": "identity-attest-req",
        "summary": "Client asks the server for a signed identity attestation for a specific peer. Consent dialogs use it to bind the helper's peerId to a verified identity, so a later imposter with the same display name cannot slip in.",
        "payload": {
          "type": "object",
          "properties": {
            "type":     { "const": "identity-attest-req" },
            "peerId":   { "$ref": "#/components/schemas/PeerId" }
          },
          "required": [ "type", "peerId" ]
        }
      },
      "identity_attest": {
        "name": "identity-attest",
        "title": "identity-attest",
        "summary": "Server-signed attestation. 'attest' is a compact JWS (HS256) with header {alg:HS256,typ:JWT} and payload {peerId,name,verified,verifiedIdentity,room,tenant,exp} signed with the tenant's WebPhone:IdentityAttestSecret (falls back to WebPhone:AdminSharedSecret). The consent dialog validates by calling GET /signal.ashx?attest-verify with the compact token.",
        "payload": {
          "type": "object",
          "properties": {
            "type":             { "const": "identity-attest" },
            "peerId":           { "$ref": "#/components/schemas/PeerId" },
            "name":             { "$ref": "#/components/schemas/DisplayName" },
            "verified":         { "type": "boolean" },
            "verifiedIdentity": { "type": [ "string", "null" ] },
            "attest":           { "type": "string", "description": "Compact JWS. Consumers may either treat this as opaque and verify by callback, or verify locally with the shared secret." },
            "issuedUtc":        { "type": "string", "format": "date-time" },
            "expiresUtc":       { "type": "string", "format": "date-time" }
          },
          "required": [ "type", "peerId", "attest", "expiresUtc" ]
        }
      },
      "dc_screen_info": {
        "name": "screen-info",
        "title": "screen-info",
        "summary": "Target -> helper. Sent immediately after the data channel opens and again whenever the shared surface changes (share swapped, monitor reconfigured, DPI change, window moved between monitors). Tells the helper what the pixels it is looking at actually are on the target's system, so `pointer.nx`/`ny` land where the user thinks they are pointing.",
        "payload": {
          "type": "object",
          "properties": {
            "s":           { "$ref": "#/components/schemas/ControlSessionId" },
            "seq":         { "type": "integer", "minimum": 0 },
            "t":           { "const": "screen-info" },
            "surface":     { "type": "string", "enum": [ "monitor", "window", "browser" ] },
            "surfaceRect": { "type": "object", "required": [ "x","y","w","h" ], "properties": {
              "x": { "type": "integer" }, "y": { "type": "integer" },
              "w": { "type": "integer" }, "h": { "type": "integer" }
            }, "description": "The shared surface as it lives on the target's virtual desktop (Windows: can start at negative x,y for secondary monitors)." },
            "virtual":     { "type": "object", "required": [ "x","y","w","h" ], "properties": {
              "x": { "type": "integer" }, "y": { "type": "integer" },
              "w": { "type": "integer" }, "h": { "type": "integer" }
            }, "description": "The union rect of all monitors on the target." },
            "monitors":    { "type": "array", "items": { "type": "object", "required": [ "id","x","y","w","h" ], "properties": {
              "id":      { "type": "string" },
              "x":       { "type": "integer" }, "y":       { "type": "integer" },
              "w":       { "type": "integer" }, "h":       { "type": "integer" },
              "scale":   { "type": "number", "minimum": 0.5, "maximum": 4.0 },
              "primary": { "type": "boolean" }
            } } }
          },
          "required": [ "s", "seq", "t", "surface", "surfaceRect" ]
        }
      },
      "dc_pointer": {
        "name": "pointer",
        "title": "pointer",
        "summary": "Helper -> target. **Position only.** Button state and wheel deltas travel in their own events; a `pointer` frame never changes button state. Coordinates are normalized to the current shared surface: `nx` and `ny` both in [0, 1] with (0,0) at the surface's top-left. The target maps to desktop coordinates using the most recent `screen-info`.",
        "payload": {
          "type": "object",
          "properties": {
            "s":   { "$ref": "#/components/schemas/ControlSessionId" },
            "seq": { "type": "integer", "minimum": 0 },
            "t":   { "const": "pointer" },
            "nx":  { "type": "number", "minimum": 0.0, "maximum": 1.0 },
            "ny":  { "type": "number", "minimum": 0.0, "maximum": 1.0 }
          },
          "required": [ "s", "seq", "t", "nx", "ny" ]
        }
      },
      "dc_button": {
        "name": "button",
        "title": "button",
        "summary": "Helper -> target. Authoritative mouse-button state change. `code`: 0 left, 1 middle, 2 right, 3 back, 4 forward.",
        "payload": {
          "type": "object",
          "properties": {
            "s":    { "$ref": "#/components/schemas/ControlSessionId" },
            "seq":  { "type": "integer", "minimum": 0 },
            "t":    { "const": "button" },
            "code": { "type": "integer", "minimum": 0, "maximum": 4 },
            "down": { "type": "boolean" }
          },
          "required": [ "s", "seq", "t", "code", "down" ]
        }
      },
      "dc_wheel": {
        "name": "wheel",
        "title": "wheel",
        "summary": "Helper -> target. Authoritative wheel delta. Because browser `WheelEvent.deltaX/Y` values vary by device, OS, and zoom (Chromium reports pixels ~100/notch scaled by DPI; Firefox reports lines; touchpads are continuous), the sender MUST include `deltaMode` so the target can normalise. The target implementation converts to platform-native scroll ticks; RDPLauncher, for example, treats one notch as 120 native units (Windows' WHEEL_DELTA).",
        "payload": {
          "type": "object",
          "properties": {
            "s":         { "$ref": "#/components/schemas/ControlSessionId" },
            "seq":       { "type": "integer", "minimum": 0 },
            "t":         { "const": "wheel" },
            "dx":        { "type": "number", "description": "Horizontal delta in the unit named by `deltaMode`." },
            "dy":        { "type": "number", "description": "Vertical delta in the unit named by `deltaMode`. Positive is scroll-down (matches WheelEvent.deltaY)." },
            "deltaMode": { "type": "integer", "enum": [ 0, 1, 2 ], "description": "Unit of `dx`/`dy`, as per the DOM WheelEvent.deltaMode constants: 0 = DOM_DELTA_PIXEL (default when field is absent), 1 = DOM_DELTA_LINE, 2 = DOM_DELTA_PAGE." }
          },
          "required": [ "s", "seq", "t" ]
        }
      },
      "dc_key": {
        "name": "key",
        "title": "key",
        "summary": "Helper -> target. Keyboard key press or release. `code` is the W3C UIEvents KeyboardEvent.code value (physical key). Targets may silently refuse specific keys or combinations (see /remote_control_api.html safety guarantees); refusal is not an error.",
        "payload": {
          "type": "object",
          "properties": {
            "s":       { "$ref": "#/components/schemas/ControlSessionId" },
            "seq":     { "type": "integer", "minimum": 0 },
            "t":       { "const": "key" },
            "code":    { "type": "string" },
            "keyCode": { "type": "integer" },
            "down":    { "type": "boolean" },
            "repeat":  { "type": "boolean" },
            "mods":    { "type": "object", "properties": {
              "ctrl":  { "type": "boolean" }, "shift": { "type": "boolean" },
              "alt":   { "type": "boolean" }, "meta":  { "type": "boolean" }
            } }
          },
          "required": [ "s", "seq", "t", "code", "down" ]
        }
      },
      "dc_clip_read":  {
        "name": "clip-read", "title": "clip-read",
        "summary": "Helper -> target. Request the target's current clipboard contents. Requires the `clipboard-read` feature to have been granted.",
        "payload": { "type": "object", "properties": {
          "s": { "$ref": "#/components/schemas/ControlSessionId" }, "seq": { "type": "integer" }, "t": { "const": "clip-read" }
        }, "required": [ "s", "seq", "t" ] }
      },
      "dc_clip_write": {
        "name": "clip-write", "title": "clip-write",
        "summary": "Helper -> target. Overwrite the target's clipboard with `text`. Requires the `clipboard-write` feature.",
        "payload": { "type": "object", "properties": {
          "s": { "$ref": "#/components/schemas/ControlSessionId" }, "seq": { "type": "integer" }, "t": { "const": "clip-write" },
          "text": { "type": "string" }
        }, "required": [ "s", "seq", "t", "text" ] }
      },
      "dc_clip":       {
        "name": "clip", "title": "clip",
        "summary": "Target -> helper. Response to a `clip-read`.",
        "payload": { "type": "object", "properties": {
          "s": { "$ref": "#/components/schemas/ControlSessionId" }, "seq": { "type": "integer" }, "t": { "const": "clip" },
          "text": { "type": "string" }
        }, "required": [ "s", "seq", "t", "text" ] }
      },
      "dc_file_offer": {
        "name": "file-offer", "title": "file-offer",
        "summary": "Helper -> target. Announce an incoming file. `file-transfer` feature is the gate for both `file-offer` and `file-chunk`. Target's local UI shows the offer and either accepts (starts consuming `file-chunk`) or ignores.",
        "payload": { "type": "object", "properties": {
          "s": { "$ref": "#/components/schemas/ControlSessionId" }, "seq": { "type": "integer" }, "t": { "const": "file-offer" },
          "id":     { "type": "string" },
          "name":   { "type": "string" },
          "size":   { "type": "integer", "minimum": 0 },
          "sha256": { "type": "string" }
        }, "required": [ "s", "seq", "t", "id", "name", "size" ] }
      },
      "dc_file_chunk": {
        "name": "file-chunk", "title": "file-chunk",
        "summary": "Helper -> target. Chunk of an offered file. `b` is base64. Chunk size <= 16 KiB. Chunks are also allowed on a dedicated `codeb-file-v1` channel to avoid head-of-line-blocking pointer/keyboard.",
        "payload": { "type": "object", "properties": {
          "s": { "$ref": "#/components/schemas/ControlSessionId" }, "seq": { "type": "integer" }, "t": { "const": "file-chunk" },
          "id": { "type": "string" }, "n": { "type": "integer" }, "b": { "type": "string" }
        }, "required": [ "s", "seq", "t", "id", "n", "b" ] }
      },
      "dc_hb": {
        "name": "hb", "title": "hb",
        "summary": "Either direction, at the cadence carried in `control-grant.hbIntervalMs`. `monoMs` is the sender's monotonic clock. Missing 3 consecutive `hb` triggers an auto-revoke with reason `timeout`.",
        "payload": { "type": "object", "properties": {
          "s": { "$ref": "#/components/schemas/ControlSessionId" }, "seq": { "type": "integer" }, "t": { "const": "hb" },
          "monoMs": { "type": "integer", "minimum": 0 }
        }, "required": [ "s", "seq", "t" ] }
      },
      "dc_end": {
        "name": "end", "title": "end",
        "summary": "Either direction. Cooperative close of the control session. Semantically equivalent to a WS `control-revoke`; the peer that sends `end` also sends `control-revoke` on the WS so the audit webhook fires. On receipt, and on any transport close (WS or DC), the target MUST release every key and mouse button that has an outstanding down event with no matching up event — nothing may remain 'stuck'.",
        "payload": { "type": "object", "properties": {
          "s": { "$ref": "#/components/schemas/ControlSessionId" }, "seq": { "type": "integer" }, "t": { "const": "end" },
          "reason": { "type": "string", "enum": [ "user-stopped", "timeout", "target-locked", "target-disconnected", "helper-disconnected", "policy", "emergency" ] }
        }, "required": [ "s", "seq", "t", "reason" ] }
      }
    }
  }
}
