{
  "asyncapi": "3.0.0",
  "info": {
    "title": "CodeB /signal.ashx WebSocket signaling",
    "version": "2026-09-28.r19",
    "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_cursor":      { "$ref": "#/components/messages/dc_cursor" },
        "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_cursor" },
        { "$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" },
          "client":   { "type": "string", "description": "R10 (2026-09-28): free-form client identifier this peer sent in `join.client` (e.g. 'AloahaRDPSystemTray/5.1.0.0 CodeBWebphone/...'). Server-clamped to 128 chars. UI may use it to gate capability-dependent buttons (e.g. hide 'Request control' unless the target's `client` starts with an approved RDPLauncher prefix)." },
          "hostEdition": { "type": "string", "enum": [ "free", "commercial" ], "default": "free", "description": "R17 Host-Edition (2026-09-28): self-declared edition of the native embedding host, always present. Values: 'free' (default; used for browsers without a host, older RDPLauncher builds, and hosts that declared 'free') or 'commercial'. Display / telemetry only; NEVER treated as proof of a licence." }
        },
        "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." },
            "hostEdition":   { "type": "string", "enum": [ "free", "commercial" ], "default": "free", "description": "R17 Host-Edition (2026-09-28): self-declared edition of the native embedding host, copied verbatim from the bridge `hello.edition` field. Whitelist-clamped to 'free' or 'commercial'; anything else / missing normalises to 'free'. Server relays it in welcome.peers[] / peer-joined.peer alongside `client` for display badges and telemetry only. NOT proof of a licence: a real licence check will ride on a separately signed licenseToken in a future spec (per /remote_control_api.html)." }
          },
          "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 R17's per-helper caps: 5/min per (helper,target) pair or 20/min per (helper,room)), 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. **R17 (2026-09-28):** an optional `accessCode` may be included; when the target host (e.g. RDPLauncher) has an access code configured and it matches, the host skips the consent dialog and grants directly (TeamViewer-style one-touch help). The server relays the code unchanged, validates its format, drops the field on invalid values, NEVER logs it (not in server logs, not in the control.requested webhook), and per-helper rate-limits control-request to 5/min per (helper, target) pair and 20/min per (helper, room) so a bad actor cannot brute-force codes or spam consent dialogs.",
        "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." },
            "accessCode":         { "type": "string", "pattern": "^[A-Za-z0-9 \\-]{1,32}$", "description": "R17 (2026-09-28): optional TeamViewer-style access code. Matches the code the target user configured in their RDP tray dialog (RDPLauncher). Format: 1..32 chars, subset [A-Za-z0-9 -] (spaces and dashes for readability; the host normalises them out). Server validates format and drops the field silently on mismatch. NEVER logged. A target host that advertised the `access-code` capability in its native-bridge hello will compare against its salted hash and skip the consent dialog on a match; a wrong or missing code falls back to the normal Allow/Deny dialog. Target hosts without the `access-code` cap ignore the field. Also carried into `room.html` via a `#rc=<code>` fragment on the invite link, which the page strips before load and holds only in memory." }
          },
          "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", "expired", "cancelled", "superseded" ], "description": "`cancelled` (R11+) is the helper's own cancel of a pending request -- carries `requestId`, not `sessionId`. `expired` (R9+) is either side's response to the grant's `expiresUtc` timer. `superseded` (R19+, 2026-09-28) is emitted by the TARGET when a preempting helper is granted control with a valid access code while the target already holds an active session with a previous helper -- carries `sessionId` (the OLD session's id) and is delivered to the old helper only." }
          },
          "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_cursor": {
        "name": "cursor",
        "title": "cursor",
        "summary": "Target -> helper. Announces the current shape of the mouse pointer under the target's cursor. R15+. Requires the target host to have advertised `cursor-shape` in the bridge hello. Sent on every change; repeats of the same custom cursor carry only `id`, the initial send carries the PNG. Helper applies it directly to the controlled video's inline `cursor:` CSS; it is not forwarded to any host in the helper direction. Display-only metadata (not feature-gated) but MUST be dropped on the target side when arriving from the helper direction (forgery guard).",
        "payload": {
          "type": "object",
          "properties": {
            "s":        { "$ref": "#/components/schemas/ControlSessionId" },
            "seq":      { "type": "integer", "minimum": 0 },
            "t":        { "const": "cursor" },
            "kind":     { "type": "string", "enum": [ "default", "text", "pointer", "wait", "progress", "crosshair", "move", "not-allowed", "help", "ew-resize", "ns-resize", "nwse-resize", "nesw-resize", "none", "custom" ], "description": "Standard CSS cursor name, `none` for hidden, or `custom` for a program-specific cursor delivered inline." },
            "id":       { "type": "string", "pattern": "^c\\d{1,6}$", "description": "Per-session id for custom cursors. Present only when kind==custom. Ids reset each session." },
            "hx":       { "type": "integer", "minimum": 0, "maximum": 127 },
            "hy":       { "type": "integer", "minimum": 0, "maximum": 127 },
            "w":        { "type": "integer", "minimum": 1, "maximum": 128 },
            "h":        { "type": "integer", "minimum": 1, "maximum": 128 },
            "png":      { "type": "string", "description": "Base64 PNG (<=64 KiB before decode). Sent only the FIRST time an id appears in a session; later cursor events with the same id carry only id." },
            "fallback": { "type": "string", "enum": [ "default", "text", "pointer", "wait", "progress", "crosshair", "move", "not-allowed", "help", "ew-resize", "ns-resize", "nwse-resize", "nesw-resize", "none" ], "description": "Standard cursor used if the PNG cannot be loaded." }
          },
          "required": [ "s", "seq", "t", "kind" ]
        }
      },
      "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", "expired", "superseded" ], "description": "`superseded` (R19+, 2026-09-28) is the DC-side companion of the WS `control-revoke` with the same reason -- the target's helper-facing side of a preempt takeover." }
        }, "required": [ "s", "seq", "t", "reason" ] }
      }
    }
  }
}
