openapi: 3.1.0
info:
  title: Bigscreen Cloud API (non-admin)
  version: '0'
  summary: VR-client, website, and media-server endpoints on cloud_api.
  description: |
    ## ⚠️ This is the NON-ADMIN reference

    Endpoints exposed by [`cloud/cloud_api/cloud_api.ts`](../../cloud/cloud_api/cloud_api.ts)
    for end-users, the website, and media-server instances. This spec deliberately **does not**
    cover `/admin/*` — those are documented separately at
    [Bigscreen Cloud Admin API](./scalar/cloud.html). The two surfaces use different bearer
    keys and target different audiences:

    | Surface | Bearer key | Audience | Spec |
    |---------|-----------|----------|------|
    | **Non-admin** (this spec) | `cloud-api`, `media-server-api`, `hyperbeam-webhook` | VR clients, website, media servers, Hyperbeam | here |
    | **Admin** | `cloud-admin-api` | Arda, internal tools | [cloud-admin-api](./scalar/cloud.html) |

    Mixing the two — calling a `/admin/*` route with a `cloud-api` bearer, or a `/social/*`
    route with the `cloud-admin-api` bearer — is a 401. The keys are issued separately.

    ## Auth handlers

    `cloud_api` registers four non-admin auth handlers, each backing a distinct bearer-key
    identity:

    | Handler | `apiName` | Used by | Path patterns |
    |---------|-----------|---------|---------------|
    | `standardAuthHandler` | `cloud-api` | VR clients, website | most paths — `/room*`, `/social/*`, `/screen*`, `/activity*`, `/lobby/*`, `/media/*`, `/channel*`, `/connection`, `/schemas`, `/report/*` |
    | `mediaServerAuthHandler` | `media-server-api` | media_server_next instances | `/api/media-server/*` |
    | `websiteAuthHandler` | `website-cloud-api` | website-only flows | a subset of `/apps/browser/*` |
    | `hyperbeamAuthHandler` | `hyperbeam-webhook` | Hyperbeam webhooks | `/hyperbeam/webhook/*` |

    ## Auth at a glance

    Most paths require **two** headers via the standard handler:

    | Header | Value |
    |--------|-------|
    | `Authorization` | `Bearer <cloud-api key>` |
    | `x-access-token` | A JWT obtained from the public auth API |

    Media-server and Hyperbeam paths use their own bearer keys and do **not** require an
    access token. See each operation's `security:` block for the exact scheme.

    ## Spec layout

    Source is split per logical module under `docs/openapi/cloud-public-modules/*.yaml`,
    joined by `$ref` from this root file. Bundle with:

    ```bash
    npx -y @redocly/cli@latest bundle docs/openapi/cloud-api.yaml -o docs/openapi/cloud-api.bundled.yaml
    ```
servers:
  - url: http://localhost:3002
    description: Local dev (started by `yarn dev`).
  - url: https://cloud-api.internal.bigscreenvr.com
    description: Production (network-restricted).
tags:
  - name: Meta
    description: Connection probe and schema enumerations — used to bootstrap clients.
  - name: Rooms
    description: |
      Room create / join / update / delete plus per-room data, seat assignment,
      and user kick. Powers the multiplayer lobby and room-management surfaces.
  - name: Channels
    description: Bigscreen-curated channel groupings — list, fetch one, join.
  - name: Joining
    description: Quick-join entry points — media showings, the default lobby.
  - name: Screens
    description: Screen CRUD — create, list, get, update, attach to / detach from rooms, manage media sessions on a screen.
  - name: Activities
    description: Shared activity sessions (games, co-watching) — CRUD and listing.
  - name: Social
    description: |
      Social graph for end-users — profile lookup, history, friends, blocks,
      notifications, search. Mostly read-only from this surface; mutations are
      gated by the user's own profile state.
  - name: Reports
    description: User and room reports — file complaints surfaced to moderation tools.
  - name: Browser sharing
    description: |
      Hyperbeam-backed shared-browser activities. Unity writes settings; the
      website reads. Two different bearer schemes (standard + website).
  - name: Hyperbeam
    description: Webhook callbacks from the Hyperbeam access-control system. Authenticated by the `hyperbeam-webhook` bearer.
  - name: Media Server
    description: |
      HTTP-side bridge between media servers and cloud_api. Servers register,
      fetch the current manifest, and exchange messages over HTTP because some
      servers live outside AWS and can't reach SQS directly.
paths:
  /connection:
    get:
      tags:
        - Meta
      operationId: checkConnectedUser
      summary: Connection probe
      description: |
        Returns whether the caller is connected and recognized by the cloud
        cluster. Used by VR clients on startup to verify the cluster knows
        about them before they try to join a room.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Connection state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  connected:
                    type: boolean
                  userSessionId:
                    type: string
                  bigscreenAccountId:
                    type: string
                  serverId:
                    type: string
                    description: cloud_api instance the caller is talking to.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
      x-access-policies:
        - BigscreenDefault
  /schemas:
    get:
      tags:
        - Meta
      operationId: getCloudPublicSchemas
      summary: Schema enumerations
      description: |
        Returns the room policy enums and other schema enumerations the VR
        client needs to render UIs (room visibility dropdowns, microphone
        policy, drawing visibility, etc.).
      security: []
      responses:
        '200':
          description: Schema enumerations.
          content:
            application/json:
              schema:
                type: object
                description: Each top-level key is an enum name; the value is the enum's member object.
                additionalProperties:
                  type: object
                  additionalProperties: true
  /rooms/latest:
    get:
      tags:
        - Rooms
      operationId: getPublicRooms
      summary: List recent public rooms
      description: |
        Returns a snapshot of the most-recently-active public rooms — what the
        "explore" surface in the VR client uses to populate its rail. Rate-limited.
      security:
        - cloudApiKey: []
      responses:
        '200':
          description: Recent public rooms.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PublicRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /rooms/join:
    post:
      tags:
        - Rooms
      operationId: joinRoomViaInvite
      summary: Join a room via invite code
      description: |
        Joins a room using an invite code (instead of a known `roomId`). The
        cluster resolves the invite code to a room and returns the join payload.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - inviteCode
              properties:
                inviteCode:
                  type: string
      responses:
        '200':
          description: Join payload — connection details for the assigned media server.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/schema'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Invite code did not resolve.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /room:
    post:
      tags:
        - Rooms
      operationId: createRoom
      summary: Create a room
      description: |
        Creates a new room (persistent or ad-hoc, public or private). The
        calling user becomes the owner. Returns the freshly-minted
        `PublicRoom` directly — the VR client establishes the
        media-server connection separately via `/room/{roomId}/join`.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                visibility:
                  type: string
                  description: '`RoomVisibility` — `Public`, `Private`, `FriendsOnly`.'
                category:
                  type: string
                environmentId:
                  type: string
                maxUsersPerInstance:
                  type: integer
                instancePolicy:
                  type: string
              additionalProperties: true
      responses:
        '200':
          description: Created room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /room/{roomId}:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
    get:
      tags:
        - Rooms
      operationId: getRoom
      summary: Fetch a room
      description: |
        Returns the live snapshot of a room. The caller must be in the room
        (or have an invite or be the owner) for private rooms; public rooms
        are visible to anyone authenticated.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Live room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller lacks visibility into this room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Rooms
      operationId: updateRoom
      summary: Update a room (owner-only)
      description: Mutates the room's settings. Restricted to the room owner / admins.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                visibility:
                  type: string
                category:
                  type: string
                environmentId:
                  type: string
                backgroundImageUrl:
                  type: string
                  format: uri
                microphonePolicy:
                  type: string
                movementSoundsPolicy:
                  type: string
                drawingVisibility:
                  type: string
                avatarNameVisibility:
                  type: string
                poseVisibility:
                  type: string
                toySpawnability:
                  type: string
                preferredUserType:
                  type: string
              additionalProperties: true
      responses:
        '200':
          description: Updated room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not the owner / admin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Rooms
      operationId: deleteRoom
      summary: Delete a room (owner-only)
      description: Removes a room. Restricted to the room owner.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Room deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not the owner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /rooms:
    get:
      tags:
        - Rooms
      operationId: getRoomsForAccount
      summary: Rooms the caller is in or owns
      description: Lists rooms the calling user owns or is currently in.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Caller's rooms.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PublicRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /room/{roomId}/join:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
    post:
      tags:
        - Rooms
      operationId: joinRoomById
      summary: Join a room by id
      description: |
        Joins a known room. Returns connection details for the assigned media
        server. The caller must be allowed in (public room, or has an invite,
        or owner / admin / friend depending on visibility).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                inviteCode:
                  type: string
                seatIndex:
                  type: integer
              additionalProperties: true
      responses:
        '200':
          description: Join payload.
          content:
            application/json:
              schema:
                type: object
                required:
                  - room
                  - connection
                properties:
                  room:
                    $ref: '#/components/schemas/PublicRoom'
                  connection:
                    type: object
                    description: Media-server connection details.
                    properties:
                      mediaServerId:
                        type: string
                      host:
                        type: string
                      port:
                        type: integer
                      token:
                        type: string
                      userSessionId:
                        type: string
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not allowed in this room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /room/{roomId}/data:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
    get:
      tags:
        - Rooms
      operationId: getRoomData
      summary: Read free-form room data
      description: |
        Reads the free-form key/value bag attached to a room — used by
        activities and games to share state across a session.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Key/value bag.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Rooms
      operationId: setRoomData
      summary: Write free-form room data
      description: |
        Writes one or more key/value pairs to the room's data bag. Per-key
        size limits and per-room total size limits apply server-side.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Updated key/value bag.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not in the room (or not allowed to mutate it).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Payload exceeds server-side limits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /room/{roomId}/data/{key}:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
      - in: path
        name: key
        required: true
        schema:
          type: string
    delete:
      tags:
        - Rooms
      operationId: deleteRoomData
      summary: Delete one key from room data
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Key removed.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No matching room or key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /room/{roomId}/seat/{seatIndex}:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
      - in: path
        name: seatIndex
        required: true
        schema:
          type: integer
    put:
      tags:
        - Rooms
      operationId: updateSeatIndex
      summary: Move the caller to a seat
      description: Reseats the calling user to a specific seat index.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Seat updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  roomId:
                    type: string
                  seatIndex:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Seat unavailable or caller not in the room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /room/{roomId}/users/{userSessionId}/kick:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
      - in: path
        name: userSessionId
        required: true
        schema:
          type: string
    get:
      tags:
        - Rooms
      operationId: kickUser
      summary: Kick another user (owner / mod-only)
      description: |
        Kicks another user from the room. Restricted to the room owner / mod
        list. The kicked user can rejoin (it's an eject in moderation terms);
        staff-side kicks come from the admin spec.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Kick acknowledged.
          content:
            application/json:
              schema:
                type: object
                properties:
                  kicked:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not the room's owner / mod.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No matching session in this room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /room/{roomId}/leave:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
    get:
      tags:
        - Rooms
      operationId: leaveRoom
      summary: Leave the current room
      description: Removes the caller from the room.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Left.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Caller was not in this room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channels/{version}/{name}:
    parameters:
      - in: path
        name: version
        required: true
        schema:
          type: string
        description: Channel-group schema version (`v1`, `v2`, …).
      - in: path
        name: name
        required: true
        schema:
          type: string
        description: Channel-group name.
    get:
      tags:
        - Channels
      operationId: getChannelGroup
      summary: Fetch a channel group
      description: Returns a Bigscreen-curated channel group — its channels, atlas textures, and metadata.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Channel group payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelGroup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No channel group with this name / version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channel/info/{channel_id}:
    parameters:
      - in: path
        name: channel_id
        required: true
        schema:
          type: string
    get:
      tags:
        - Channels
      operationId: getChannelInfo
      summary: Fetch a single channel
      description: Returns a single channel by id.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Channel record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No channel with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /channel/join:
    post:
      tags:
        - Channels
      operationId: joinChannel
      summary: Join a channel's live stream
      description: |
        Joins the live streaming room for a channel — used when the user
        taps a channel tile in the VR client. Returns a media-server connection.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - channelId
              properties:
                channelId:
                  type: string
                streamIndex:
                  type: integer
      responses:
        '200':
          description: Join payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  room:
                    $ref: '#/components/schemas/PublicRoom'
                  connection:
                    type: object
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not allowed in this channel (geo-restricted, etc.).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No channel with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /media/join:
    post:
      tags:
        - Joining
      operationId: joinMediaShowing
      summary: Join a shared media showing
      description: |
        Joins a shared media product session — the showing-specific entry
        point that pairs the caller with whoever else is watching the same
        product at the same synchronization tick. Returns a media-server
        connection plus the assigned room.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - bigMediaId
              properties:
                bigMediaId:
                  type: string
                  description: Public id of the media product to join.
                entitlementId:
                  type: string
                  description: Entitlement the caller is using to access the product, when applicable.
              additionalProperties: true
      responses:
        '200':
          description: Join payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  room:
                    $ref: '#/components/schemas/PublicRoom'
                  connection:
                    type: object
                    additionalProperties: true
                  showing:
                    type: object
                    additionalProperties: true
                    description: Active showing metadata (current playback time, sync tick, etc.).
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller has no entitlement to this product.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No matching product / showing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /lobby/join:
    post:
      tags:
        - Joining
      operationId: joinLobby
      summary: Join the default lobby
      description: |
        Joins the default Bigscreen lobby — the room VR clients drop into
        when they boot up without an explicit destination. Returns a
        media-server connection.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                preferredRegion:
                  type: string
                  description: AWS region preference for media-server selection.
              additionalProperties: true
      responses:
        '200':
          description: Lobby join payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  room:
                    $ref: '#/components/schemas/PublicRoom'
                  connection:
                    type: object
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /screen:
    post:
      tags:
        - Screens
      operationId: createScreen
      summary: Create a screen
      description: Creates a new screen owned by the calling account.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                type:
                  type: string
                settings:
                  type: object
                  additionalProperties: true
              additionalProperties: true
      responses:
        '200':
          description: Created screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Screen'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /screen/{screenId}:
    parameters:
      - in: path
        name: screenId
        required: true
        schema:
          type: string
    get:
      tags:
        - Screens
      operationId: getScreen
      summary: Fetch a screen
      description: Returns a screen owned by the calling account.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Screen record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Screen'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own this screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Screens
      operationId: updateScreen
      summary: Update a screen
      description: Mutates the screen's settings (owner only).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                settings:
                  type: object
                  additionalProperties: true
              additionalProperties: true
      responses:
        '200':
          description: Updated screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Screen'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own this screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Screens
      operationId: deleteScreen
      summary: Delete a screen
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Screen deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own this screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /screen/{screenId}/session:
    parameters:
      - in: path
        name: screenId
        required: true
        schema:
          type: string
    get:
      tags:
        - Screens
      operationId: getScreenMediaInfo
      summary: Active media session on a screen
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Media session info, or empty if nothing is playing.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: '`Screen.mediaSession` payload — see `Screen` schema.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Screens
      operationId: updateScreenMediaSession
      summary: Update / start the screen's media session
      description: Starts a new session on the screen, or mutates an active one (play / pause / seek).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mediaProductId:
                  type: string
                bigMediaId:
                  type: string
                playbackState:
                  type: string
                  enum:
                    - playing
                    - paused
                    - ended
                currentTimeMs:
                  type: integer
              additionalProperties: true
      responses:
        '200':
          description: Updated screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Screen'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own the screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Screens
      operationId: closeScreenMediaShowing
      summary: End the screen's media session
      description: Ends the active media session on a screen.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Session closed.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller does not own the screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No screen with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /screens:
    get:
      tags:
        - Screens
      operationId: getScreens
      summary: List the caller's screens
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Screens.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Screen'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /room/{roomId}/screen/{screenId}:
    parameters:
      - in: path
        name: roomId
        required: true
        schema:
          type: string
      - in: path
        name: screenId
        required: true
        schema:
          type: string
    put:
      tags:
        - Screens
      operationId: updateRoomScreen
      summary: Attach / update a screen on a room
      description: Attaches the screen to the room (or mutates the binding's settings).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                settings:
                  type: object
                  additionalProperties: true
              additionalProperties: true
      responses:
        '200':
          description: Updated screen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Screen'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not the room owner / screen owner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Room or screen not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Screens
      operationId: removeRoomScreen
      summary: Detach a screen from a room
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Detached.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not the room owner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Room or screen not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /activity:
    post:
      tags:
        - Activities
      operationId: createActivity
      summary: Create an activity
      description: Creates a shared activity (game, co-watch, etc.) owned by the caller.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
              properties:
                type:
                  type: string
                  description: Activity type identifier.
                name:
                  type: string
                description:
                  type: string
                roomId:
                  type: string
                metaData:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Created activity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Activity'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /activity/{activityId}:
    parameters:
      - in: path
        name: activityId
        required: true
        schema:
          type: string
    get:
      tags:
        - Activities
      operationId: getActivity
      summary: Fetch an activity
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Activity record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Activity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No activity with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Activities
      operationId: updateActivity
      summary: Update an activity (owner only)
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                state:
                  type: string
                metaData:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Updated activity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Activity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not the activity owner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No activity with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Activities
      operationId: deleteActivity
      summary: Delete an activity (owner only)
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Activity deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not the activity owner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No activity with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /activities:
    get:
      tags:
        - Activities
      operationId: getActivities
      summary: List the caller's activities
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Activities.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Activity'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /social/rooms:
    get:
      tags:
        - Social
      operationId: getRoomsWithQuery
      summary: Search the caller's social rooms
      description: Returns rooms involving the caller's friend graph (friends-only public rooms, etc.).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      parameters:
        - in: query
          name: query
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
      responses:
        '200':
          description: Rooms.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PublicRoom'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /social/history:
    get:
      tags:
        - Social
      operationId: getSocialHistoryForUser
      summary: Caller's social history
      description: |
        Returns the caller's recent social history — rooms joined, users
        met, activities engaged in. Items are the public projection
        (`HistoryItem.getPublicData()`).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      parameters:
        - in: query
          name: type
          description: Optional `HistoryItemType` filter.
          schema:
            $ref: '#/components/schemas/HistoryItemType'
        - in: query
          name: limit
          schema:
            type: integer
      responses:
        '200':
          description: History page.
          content:
            application/json:
              schema:
                type: object
                required:
                  - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/HistoryItem'
                  cursorId:
                    type: string
                    description: Continuation cursor — pass on the next request to get the next page.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /social/friends:
    get:
      tags:
        - Social
      operationId: getFriends
      summary: Caller's friends
      description: |
        Returns the caller's accepted `Friends` edges. Each item carries
        the public projection of the `SocialGraphItem` (`createdAt`,
        `ownerSocialId`, `targetSocialId`) plus an inlined
        `remoteSocialProfile` — the friend's `PublicSocialProfile`.
        Paginated.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Friends page.
          content:
            application/json:
              schema:
                type: object
                required:
                  - items
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        createdAt:
                          type: integer
                          description: Unix milliseconds.
                        ownerSocialId:
                          type: string
                        targetSocialId:
                          type: string
                        remoteSocialProfile:
                          $ref: '#/components/schemas/PublicSocialProfile'
                  cursorId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
  /social/blocked:
    get:
      tags:
        - Social
      operationId: getBlockedUsers
      summary: Caller's blocked users
      description: |
        Returns the users the caller has blocked. Each item carries the
        public projection of the `Blocked` `SocialGraphItem` plus the
        blocked user's `PublicSocialProfile`. Note the inverse —
        users who have blocked the caller — is intentionally not
        exposed. Paginated.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Blocked users page.
          content:
            application/json:
              schema:
                type: object
                required:
                  - items
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        createdAt:
                          type: integer
                          description: Unix milliseconds.
                        ownerSocialId:
                          type: string
                        targetSocialId:
                          type: string
                        remoteSocialProfile:
                          $ref: '#/components/schemas/PublicSocialProfile'
                  cursorId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
  /social/profile/{socialId}:
    parameters:
      - in: path
        name: socialId
        required: true
        schema:
          type: string
        description: |
          Target social id. The path is `{/:socialId}` in Express — when the
          segment is omitted, the caller's own profile is returned. The
          caller's own profile carries the **full** `SocialStats` in `stats`;
          other profiles carry the `PublicSocialStats` projection.
    get:
      tags:
        - Social
      operationId: getSocialProfile
      summary: Fetch a social profile
      description: |
        Returns a `PublicSocialProfile` by id. When called without a
        `socialId` segment, returns the caller's own profile (the path is
        registered with an optional segment).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Profile.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSocialProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No profile with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/profile/{socialId}/block:
    parameters:
      - in: path
        name: socialId
        required: true
        schema:
          type: string
    put:
      tags:
        - Social
      operationId: createBlock
      summary: Block another user
      description: |
        Creates a `Blocked` edge owned by the caller, targeting `socialId`.
        Idempotent — the edge id is a deterministic hash, so re-asserting
        upserts the same row. Returns the JSON-encoded string `"OK"` on
        success.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Block created. Body is the JSON-encoded string `"OK"`.
          content:
            application/json:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No profile with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/profile/{socialId}/unblock:
    parameters:
      - in: path
        name: socialId
        required: true
        schema:
          type: string
    put:
      tags:
        - Social
      operationId: deleteBlock
      summary: Unblock a user
      description: |
        Deletes the caller's `Blocked` edge against `socialId`. Returns
        the JSON-encoded string `"OK"` on success.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Block removed. Body is the JSON-encoded string `"OK"`.
          content:
            application/json:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No matching block.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/profile/{socialId}/remove_friend:
    parameters:
      - in: path
        name: socialId
        required: true
        schema:
          type: string
    get:
      tags:
        - Social
      operationId: removeFriend
      summary: Remove a friend
      description: |
        Removes both `Friends` edge rows (the friendship is mutual, so two
        rows exist — one per direction). Returns the JSON-encoded
        string `"OK"` on success.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Friend removed. Body is the JSON-encoded string `"OK"`.
          content:
            application/json:
              schema:
                type: string
                example: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No matching friend edge.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/notification:
    post:
      tags:
        - Social
      operationId: createSocialNotification
      summary: Send a notification (e.g. friend request)
      description: |
        Creates a notification destined for another user — typically a
        `FriendRequest` or `RoomInvite`. The cooldown layer may reject
        with `TooSoon` / `AlreadyExists` (see `NotificationCooldownAction`).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - recipientSocialId
                - notificationType
              properties:
                recipientSocialId:
                  type: string
                notificationType:
                  $ref: '#/components/schemas/NotificationType'
                title:
                  type: string
                body:
                  type: string
                metaData:
                  type: object
                  description: |
                    `NotificationMetaData` — `roomId` for
                    `RoomInvite` / `KnockKnock`, `version`
                    where relevant.
                  properties:
                    roomId:
                      type: string
                    version:
                      type: string
                  additionalProperties: true
      responses:
        '200':
          description: Created notification.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicNotification'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Recipient has blocked the caller (or other privacy guard).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Recipient not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/notifications:
    get:
      tags:
        - Social
      operationId: getSocialNotifications
      summary: Caller's notifications
      description: |
        Returns the caller's notifications as `PublicNotification` objects
        (sender resolved into a `senderSocialProfile`, no
        `bigscreenAccountId` fields).
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Notifications.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PublicNotification'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /social/notification/{notificationId}:
    parameters:
      - in: path
        name: notificationId
        required: true
        schema:
          type: string
    get:
      tags:
        - Social
      operationId: getSocialNotification
      summary: Fetch one notification
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Notification record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicNotification'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No notification with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Social
      operationId: deleteSocialNotification
      summary: Dismiss a notification
      description: |
        Sets `notificationStatus=Deleted`. The row is not hard-deleted —
        see the admin route for that.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Dismissed.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No notification with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/notification/{notificationId}/read:
    parameters:
      - in: path
        name: notificationId
        required: true
        schema:
          type: string
    put:
      tags:
        - Social
      operationId: readSocialNotification
      summary: Mark notification read
      description: Sets `notificationStatus=Read`.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Marked read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicNotification'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No notification with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/notification/{notificationId}/accept:
    parameters:
      - in: path
        name: notificationId
        required: true
        schema:
          type: string
    put:
      tags:
        - Social
      operationId: acceptSocialNotification
      summary: Accept a notification
      description: |
        Sets `notificationStatus=Accepted` and drives any side effect the
        `notificationType` implies — e.g. accepting a `FriendRequest`
        creates the `Friends` edges and triggers an
        `AcceptedFriendRequest` notification back to the original sender.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Accepted; updated notification.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicNotification'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No notification with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/notification/{notificationId}/decline:
    parameters:
      - in: path
        name: notificationId
        required: true
        schema:
          type: string
    put:
      tags:
        - Social
      operationId: declineSocialNotification
      summary: Decline a notification
      description: Sets `notificationStatus=Declined` without firing the accept-side effect.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Declined; updated notification.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicNotification'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No notification with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /social/search:
    post:
      tags:
        - Social
      operationId: searchSocialProfiles
      summary: Search social profiles
      description: Searches profiles by username / display name. Rate-limited.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - query
              properties:
                query:
                  type: string
                limit:
                  type: integer
      responses:
        '200':
          description: Matching profiles.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PublicSocialProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /report/user:
    post:
      tags:
        - Reports
      operationId: reportSocialProfile
      summary: Report a user
      description: |
        Files a report against another user. Mirrors the equivalent endpoint
        on the public auth API — both feed the same moderation queue.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - targetSocialId
                - reason
              properties:
                targetSocialId:
                  type: string
                targetBigscreenAccountId:
                  type: string
                reason:
                  type: string
                roomId:
                  type: string
                  description: The room the incident took place in (when applicable).
                details:
                  type: string
                  description: Reporter-supplied free text.
      responses:
        '200':
          description: Report filed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  reportId:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Target user not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /report/room:
    post:
      tags:
        - Reports
      operationId: reportRoom
      summary: Report a room
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - roomId
                - reason
              properties:
                roomId:
                  type: string
                reason:
                  type: string
                details:
                  type: string
      responses:
        '200':
          description: Report filed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  reportId:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No room with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /apps/browser/{activityId}:
    parameters:
      - in: path
        name: activityId
        required: true
        schema:
          type: string
    get:
      tags:
        - Browser sharing
      operationId: getSharedBrowserActivity
      summary: Read shared-browser activity (website)
      description: |
        Returns the current state of a shared-browser activity — the URL being
        shared, viewer count, etc. Authenticated by the **website** bearer
        (`website-cloud-api`); the website calls this to render the
        companion-screen UI without VR-client credentials.
      security:
        - websiteApiKey: []
          cloudAccessToken: []
      responses:
        '200':
          description: Shared-browser activity payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  activityId:
                    type: string
                  url:
                    type: string
                    format: uri
                  hyperbeamSessionUrl:
                    type: string
                    format: uri
                  viewerCount:
                    type: integer
                  ownerSocialId:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No activity with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Browser sharing
      operationId: updateSharedBrowserSettings
      summary: Update shared-browser settings (Unity)
      description: |
        Updates the shared-browser activity's settings — typically the URL the
        session navigates to. Authenticated by the standard VR-client bearer.
      security:
        - cloudApiKey: []
          cloudAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                settings:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Updated activity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Activity'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Caller is not the activity owner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No activity with this id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /hyperbeam/webhook/{activityId}:
    parameters:
      - in: path
        name: activityId
        required: true
        schema:
          type: string
    post:
      tags:
        - Hyperbeam
      operationId: hyperbeamCallback
      summary: Hyperbeam access-control callback
      description: |
        Webhook called by the Hyperbeam browser-sharing service to ask cloud_api
        whether a given user / session should be allowed into the shared
        browser. Authenticated by the `hyperbeam-webhook` bearer; never called
        by Bigscreen clients directly.

        Returns 200 with the access decision; non-200 responses are treated as
        denial by Hyperbeam.
      security:
        - hyperbeamApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Hyperbeam-defined webhook payload — see Hyperbeam's docs for the full shape.
              additionalProperties: true
      responses:
        '200':
          description: Access decision.
          content:
            application/json:
              schema:
                type: object
                properties:
                  allowed:
                    type: boolean
                  reason:
                    type: string
                    description: Reason the request was allowed / denied — surfaced to Hyperbeam logs.
                  userInfo:
                    type: object
                    description: Optional user metadata returned to the Hyperbeam session.
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Activity not found (denial).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/media-server/register:
    post:
      tags:
        - Media Server
      operationId: registerExternalMediaServer
      summary: Register a media server
      description: |
        Called by a `media_server_next` instance on startup to register itself
        with cloud_api. The server reports its IP, port range, region, instance
        metadata, etc.; cloud_api adds it to the active rotation.
      security:
        - mediaServerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
                - ipAddress
              properties:
                id:
                  type: string
                  description: Server-reported id.
                version:
                  type: string
                ipAddress:
                  type: string
                minPort:
                  type: integer
                maxPort:
                  type: integer
                isAccelerated:
                  type: boolean
                region:
                  type: string
                instanceId:
                  type: string
                instanceType:
                  type: string
              additionalProperties: true
      responses:
        '200':
          description: Registered media server.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaServer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Registration payload invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/media-server:
    get:
      tags:
        - Media Server
      operationId: getMediaServerInfo
      summary: Self-introspect (the server's own record)
      description: |
        Returns the cloud_api-side record for the calling media server —
        used by the server to confirm its registration is intact and to read
        cluster-managed fields (activation status, etc.). Identifies the
        caller via headers checked by `MediaServerApi.checkMediaServerHeaders`.
      security:
        - mediaServerApiKey: []
      responses:
        '200':
          description: The caller's media-server record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaServer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Caller is not registered. Re-call `/register`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/media-server/manifest:
    get:
      tags:
        - Media Server
      operationId: getMediaServerManifest
      summary: Current manifest of rooms the server should host
      description: |
        Returns the manifest of rooms cloud_api expects this server to be
        hosting. The server reconciles its local state against the manifest;
        divergences trigger heartbeat-driven follow-up via SQS.
      security:
        - mediaServerApiKey: []
      responses:
        '200':
          description: Manifest payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  mediaServerId:
                    type: string
                  generatedAt:
                    type: string
                    format: date-time
                  rooms:
                    type: array
                    items:
                      type: object
                      properties:
                        roomId:
                          type: string
                        roomInstanceId:
                          type: string
                        expectedOccupants:
                          type: integer
                      additionalProperties: true
                  count:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/media-server/messages:
    get:
      tags:
        - Media Server
      operationId: getMediaServerMessages
      summary: Long-poll for outbound messages
      description: |
        Long-polls for outbound commands from cloud_api to this media server
        (kicks, mutes, room-state updates). Servers that can reach SQS would
        normally consume the same messages there; this endpoint is a fallback
        for servers that can't.
      security:
        - mediaServerApiKey: []
      parameters:
        - in: query
          name: maxMessages
          schema:
            type: integer
            default: 10
        - in: query
          name: waitSeconds
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: Pending messages (possibly empty).
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: array
                    items:
                      type: object
                      properties:
                        messageId:
                          type: string
                        type:
                          type: string
                        payload:
                          type: object
                          additionalProperties: true
                        sentAt:
                          type: string
                          format: date-time
                  count:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/media-server/message:
    post:
      tags:
        - Media Server
      operationId: postMediaServerMessage
      summary: Inbound message from a media server
      description: |
        HTTP-side of the media-server → cloud-api message bus. Servers that
        can't post to SQS directly use this endpoint; cloud_api forwards the
        message into the BigscreenCloud queue for the in-process worker to
        consume. Heartbeats, peer state changes, and error reports flow this way.
      security:
        - mediaServerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
              properties:
                type:
                  type: string
                  description: Message class — `Heartbeat`, `PeerState`, `RoomEvent`, `Error`, etc.
                payload:
                  type: object
                  description: Type-specific payload.
                  additionalProperties: true
      responses:
        '200':
          description: Message accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted:
                    type: boolean
                  queuedTo:
                    type: string
                    description: SQS queue the message was forwarded to.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: Message could not be parsed or routed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  securitySchemes:
    cloudApiKey:
      type: http
      scheme: bearer
      bearerFormat: CloudApiKey-or-OAuthJWT
      description: |
        Bearer key for the `cloud-api` application. Most non-admin paths
        require this **plus** `x-access-token`.
    cloudAccessToken:
      type: apiKey
      in: header
      name: x-access-token
      description: |
        JWT for the end-user account the request acts on behalf of. Required
        alongside `cloudApiKey` for VR / website paths.
    mediaServerApiKey:
      type: http
      scheme: bearer
      description: |
        Bearer key for `media-server-api`. Used by `media_server_next`
        instances to register, fetch the manifest, and exchange messages.
        No per-user access token — servers identify themselves via headers
        checked by `MediaServerApi.checkMediaServerHeaders`.
    websiteApiKey:
      type: http
      scheme: bearer
      description: Bearer key for the `website-cloud-api` application. Used by website-specific browser-sharing reads.
    hyperbeamApiKey:
      type: http
      scheme: bearer
      description: Bearer key for `hyperbeam-webhook`. Used by Hyperbeam to call back into cloud_api during browser-sharing access checks.
  responses:
    Unauthorized:
      description: Missing or invalid bearer / access token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Authenticated but lacks the required policy or relationship.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: Rate-limited.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
        details:
          description: Optional structured details.
      example:
        error: Forbidden
    BigscreenAccountStatus:
      type: string
      description: |
        Account moderation state. Mirrors `BigscreenAccountStatus` in
        [`auth/Schemas.ts`](../../../../auth/Schemas.ts).
        - `OK` — normal.
        - `ReadOnly` — can sign in and view, but mutations are blocked.
        - `Banned` — sign-in blocked.
      enum:
        - OK
        - ReadOnly
        - Banned
      default: OK
      example: OK
    SocialStats:
      type: object
      description: |
        Aggregate counters carried inside a `SocialProfile` and returned from
        `/admin/account/{id}/social/stats`. Mirrors `SocialStats` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        The public-facing projection (`PublicSocialStats`) is a subset of these
        fields — see [`PublicSocialStats`](./PublicSocialStats.yaml).
      required:
        - friendsCount
        - followersCount
        - blockedCount
        - notificationsCount
        - unreadNotificationsCount
        - historyItemCount
        - lastOnlineDate
      properties:
        socialId:
          type: string
          description: Profile this stats record belongs to.
        notificationsCount:
          type: integer
          description: Total notifications stored for the profile.
        unreadNotificationsCount:
          type: integer
          description: Notifications still in `Unread` status.
        historyItemCount:
          type: integer
          description: Active rows in the social-history table for the profile.
        friendsCount:
          type: integer
          description: Active `Friends` edges.
        followersCount:
          type: integer
          description: Active `Following` edges where the profile is the target.
        blockedCount:
          type: integer
          description: Active `Blocked` edges owned by the profile.
        lastOnlineDate:
          type: integer
          description: Last time the profile was online, as a Unix timestamp in **seconds**.
      example:
        socialId: soc_01HZR12345ABC678DEF90GHJKM
        notificationsCount: 12
        unreadNotificationsCount: 3
        historyItemCount: 87
        friendsCount: 42
        followersCount: 18
        blockedCount: 1
        lastOnlineDate: 1762462440
    SocialProfile:
      type: object
      description: |
        Stored social profile for a Bigscreen account — the **admin** shape
        returned from `/admin/account/{bigscreenAccountId}/social/profile` and
        `/admin/account/{bigscreenAccountId}/social_profiles`. Mirrors
        `SocialProfile` (extends `DocumentDatabase.BaseItem`) in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).

        - `id` is the `socialId` (the class also exposes a `socialId` getter that
          returns `id` — both names refer to the same value).
        - `bigscreenAccountId` is admin-only; the public projection
          ([`PublicSocialProfile`](./PublicSocialProfile.yaml)) strips it.
        - There is at most one `SocialProfile` per Bigscreen account
          (`SocialProfileLookup` enforces uniqueness); the
          `/admin/account/{id}/social_profiles` endpoint returning an array is a
          debug surface for migrations / multi-handle situations.
      required:
        - id
        - bigscreenAccountId
        - createdAt
      properties:
        id:
          type: string
          description: |
            `socialId` — same value as the `socialId` getter on the class.
        createdAt:
          type: integer
          description: Profile creation, Unix timestamp in **milliseconds** (from `BaseItem`).
        updatedAt:
          type: integer
          description: Last write to the profile row, Unix timestamp in **milliseconds**.
        bigscreenAccountId:
          type: string
          description: |
            Underlying Bigscreen account this profile wraps. Admin-only —
            stripped by `getPublicData()`.
        accountCreatedAt:
          type: integer
          description: |
            Underlying Bigscreen account creation, Unix timestamp in
            **seconds**. Copied in via `updatePublicAccountData()`.
        bigAccountStatus:
          $ref: '#/components/schemas/BigscreenAccountStatus'
        isVerified:
          type: boolean
        username:
          type: string
        oculusId:
          type: string
        oculusUserId:
          type: string
        steamId:
          type: string
        smallImageUrl:
          type: string
          format: uri
        largeImageUrl:
          type: string
          format: uri
        badges:
          type: array
          items:
            type: string
        stats:
          $ref: '#/components/schemas/SocialStats'
      example:
        id: soc_01HZR12345ABC678DEF90GHJKM
        createdAt: 1761127240000
        updatedAt: 1762462440000
        bigscreenAccountId: acct_01HXJ4Q12345
        accountCreatedAt: 1761127200
        bigAccountStatus: OK
        isVerified: true
        username: alice
        oculusId: ''
        oculusUserId: ''
        steamId: '76561198012345678'
        smallImageUrl: https://cdn.bigscreenvr.com/avatars/alice_sm.png
        largeImageUrl: https://cdn.bigscreenvr.com/avatars/alice_lg.png
        badges:
          - beyond-early-supporter
        stats:
          socialId: soc_01HZR12345ABC678DEF90GHJKM
          notificationsCount: 12
          unreadNotificationsCount: 3
          historyItemCount: 87
          friendsCount: 42
          followersCount: 18
          blockedCount: 1
          lastOnlineDate: 1762462440
    Screen:
      type: object
      description: |
        Public-facing screen — `PublicScreen` from
        [`cloud/src/Schemas.ts`](../../../../cloud/src/Schemas.ts). Returned
        by every cloud-api screen endpoint that exposes screen state to a VR
        client (`/screen`, `/screen/{id}`, `/screens`,
        `/room/{roomId}/screens`). `localScreenId` is only populated for the
        screen's owner.
      required:
        - id
        - name
      properties:
        id:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        screenType:
          type: string
          description: '`ScreenType` — the screen''s role in its room (`Movie`, `Monitor`, etc.).'
        visibility:
          type: string
          description: '`ScreenVisibility` enum.'
        mediaServer:
          type: object
          description: '`PublicMediaServer` for the host serving this screen.'
          additionalProperties: true
        mediaPeer:
          type: object
          description: '`MediaPeer` payload — peer-connection metadata for the active publisher / subscriber.'
          additionalProperties: true
        sharedMediaSession:
          type: object
          description: '`SharedMediaSession` — currently-playing media on the screen.'
          additionalProperties: true
        legacyContentString:
          type: string
          description: |
            Pre-`SharedMediaSession` content descriptor. Old clients
            still parse this; new clients should prefer
            `sharedMediaSession`.
        ownerUserSessionId:
          type: string
        isMonitor:
          type: boolean
        isPresenting:
          type: boolean
        isSimulated:
          type: boolean
        localScreenId:
          type: string
          description: |
            The owner's local screen id — only included when the
            requesting user owns the screen.
        format:
          type: string
          description: '`ScreenFormat` (e.g. `2D`, `3D_SBS`, `3D_TB`).'
        aspectRatio:
          type: number
          format: float
        brightness:
          type: number
          format: float
        channel:
          description: Legacy channel descriptor when the screen is showing channel content.
          additionalProperties: true
        mediaProduct:
          description: Legacy media-product descriptor when the screen is showing a `MediaProduct`.
          additionalProperties: true
      example:
        id: scr_01HZP01234
        name: Main Screen
        createdAt: '2026-04-22T10:14:00.000Z'
        updatedAt: '2026-05-06T19:14:00.000Z'
        screenType: Movie
        visibility: Public
        mediaServer:
          id: ms-us-west-2-04
          url: wss://ms-us-west-2-04.bigscreencloud.com
        sharedMediaSession: {}
        legacyContentString: ''
        ownerUserSessionId: sess_01HZP12345
        isMonitor: false
        isPresenting: true
        isSimulated: false
        format: 2D
        aspectRatio: 1.778
        brightness: 1
    PublicRoom:
      type: object
      description: |
        Public projection of a room — `PublicRoom` from
        [`cloud/src/Schemas.ts`](../../../../cloud/src/Schemas.ts). The
        sanitised view returned to VR clients from cloud-api room endpoints
        (`/room`, `/room/{roomId}`, `/rooms`, etc.).

        Distinct from [`LiveRoom`](./LiveRoom.yaml) (the internal
        Redis-backed shape used by admin endpoints). The cloud_api builds a
        `PublicRoom` from a `LiveRoom` + the requesting user's context;
        `me` and `localScreenId`-flavored fields are personalised per
        request.
      required:
        - roomId
      properties:
        roomId:
          type: string
        inviteCode:
          type: string
        createdAt:
          type: integer
          description: Unix milliseconds — from the underlying `StoredRoom`.
        instanceCreatedAt:
          type: integer
          description: Unix milliseconds — when the live instance was bootstrapped.
        ownerSocialProfile:
          description: |
            `SocialProfile` of the room owner — unset for unowned rooms
            (lobbies, TV channels).
          oneOf:
            - $ref: '#/components/schemas/SocialProfile'
            - type: 'null'
        name:
          type: string
        description:
          type: string
        category:
          type: string
          description: '`RoomCategory` enum.'
        environmentId:
          type: string
        size:
          type: integer
        version:
          type: string
          description: App version associated with the room (Unity version of the creator).
        roomType:
          type: string
          description: '`RoomType` — `Room`, `Channel`, `Lobby`, etc.'
        visibility:
          type: string
          description: '`RoomVisibility` — `Public`, `Private`, `FriendsOnly`.'
        status:
          type: string
          description: '`LiveRoomStatus` — surfaced so clients can detect migrations or room errors.'
        adminUserIds:
          type: array
          items:
            type: string
        mediaServer:
          type: object
          description: '`PublicMediaServer` — sanitised media-server descriptor.'
          additionalProperties: true
        me:
          type: object
          description: |
            `LocalUser` — the requesting user's own user-state in this
            room. Only populated when the requesting user is in the room.
          additionalProperties: true
        remoteUsers:
          type: array
          description: '`RemoteUser[]` — public projection of the other participants.'
          items:
            type: object
            additionalProperties: true
        participants:
          type: integer
          description: Quick count of participants in the room.
        blockedSocialIds:
          type: array
          items:
            type: string
          description: Social ids the requesting user has blocked who are also in this room.
        screens:
          type: array
          description: Screens currently in the room (`PublicScreen[]`).
          items:
            $ref: '#/components/schemas/Screen'
        microphonePolicy:
          type: string
          description: '`RoomAdminPolicy.MicrophonePolicy`.'
        movementSoundsPolicy:
          type: string
          description: '`RoomAdminPolicy.MovementSoundsPolicy`.'
        drawingVisibility:
          type: string
          description: '`RoomAdminPolicy.DrawingVisibility`.'
        avatarNameVisibility:
          type: string
          description: '`RoomAdminPolicy.AvatarNameVisibility`.'
        poseVisibility:
          type: string
          description: '`RoomAdminPolicy.PoseVisibility`.'
        toySpawnability:
          type: string
          description: '`RoomAdminPolicy.ToySpawnability`.'
        preferredUserType:
          type: string
          description: '`RoomAdminPolicy.PreferredUserType`.'
        stagedSocialIds:
          type: array
          items:
            type: string
        modSocialIds:
          type: array
          items:
            type: string
      example:
        roomId: room:abc1234567
        inviteCode: BLUE-LION-9421
        createdAt: 1761127240000
        instanceCreatedAt: 1762462000000
        name: Movie Night
        description: ''
        category: Movies
        environmentId: env-cinema-2
        size: 12
        version: 1.42.0
        roomType: Room
        visibility: FriendsOnly
        status: OK
        adminUserIds:
          - sess_01HZP12345
        participants: 3
        blockedSocialIds: []
        screens: []
        stagedSocialIds: []
        modSocialIds: []
    schema:
      type: object
      required:
        - room
        - connection
      properties:
        room:
          $ref: '#/components/schemas/PublicRoom'
        connection:
          type: object
          description: Media-server connection details.
          properties:
            mediaServerId:
              type: string
            host:
              type: string
            port:
              type: integer
            token:
              type: string
            userSessionId:
              type: string
          additionalProperties: true
    Channel:
      type: object
      description: |
        Public channel — `LegacySchemas.PublicChannel` from
        [`api/src/LegacySchemas.ts`](../../../../api/src/LegacySchemas.ts).
        Returned by `/channel/{channelId}` and embedded inside
        `ChannelGroup.channels`. The full admin `Channel` (with `streams`,
        `queries`, `geo`, `ageRestriction`, etc.) is **not** what this surface
        returns — admin-only fields are stripped.
      required:
        - id
        - name
      properties:
        id:
          type: string
          description: Channel UUID.
        name:
          type: string
          description: URL-friendly slug.
        description:
          type: string
        title:
          type: string
          description: Display title.
        roomSettings:
          type: object
          description: Free-form per-channel room settings the VR app applies on join.
          additionalProperties: true
        texture:
          type: object
          description: '`Texture` instance — image asset metadata for the channel tile.'
          additionalProperties: true
      example:
        id: 01HXKV5G6PQRS8T9UVWXYZABCD
        name: live-events
        title: Live Events
        description: Live concerts, sports, and special broadcasts.
        roomSettings: {}
        texture: {}
    ChannelGroup:
      type: object
      description: |
        Public channel group — the response shape returned by
        `getPublicChannelGroup` / `getPublicChannelGroups` in
        [`api/src/Channels.ts`](../../../../api/src/Channels.ts). Wraps a
        set of `Channel`s with display metadata. The admin `ChannelGroup`
        (with `channelIds`, `createdAt`, etc.) is the storage shape; this
        is the resolved public projection.
      required:
        - name
        - channels
      properties:
        name:
          type: string
          description: URL-friendly slug.
        title:
          type: string
          description: Display title.
        imageUrl:
          type: string
          format: uri
          description: Optional thumbnail for the group tile.
        isPublished:
          type: boolean
          description: Whether the group is exposed to non-staff clients.
        channels:
          type: array
          items:
            $ref: '#/components/schemas/Channel'
        textures:
          type: array
          description: Generated atlas `Texture` records associated with the group.
          items:
            type: object
            additionalProperties: true
      example:
        name: featured-this-week
        title: Featured This Week
        imageUrl: https://dev-internal-files.bigscreencloud.com/channel-groups/featured.png
        isPublished: true
        channels:
          - id: 01HXKV5G6PQRS8T9UVWXYZABCD
            name: live-events
            title: Live Events
            description: Live concerts, sports, and special broadcasts.
            roomSettings: {}
            texture: {}
        textures: []
    Activity:
      type: object
      description: |
        Shared activity — `Activity` class from
        [`cloud/src/Activities/Schemas.ts`](../../../../cloud/src/Activities/Schemas.ts).
        A live instance of an `ActivityTemplate` attached to a room
        (YouTube, Hyperbeam browser, etc.). Each `Activity` belongs to one
        room, has one `owner` (the only user with admin permissions on it),
        and carries a host-managed `state` blob synced to the rest of the
        room.
      required:
        - id
        - owner
        - activityTemplateId
        - roomId
      properties:
        id:
          type: string
        createdAt:
          type: integer
          description: Unix milliseconds.
        updatedAt:
          type: integer
          description: Unix milliseconds.
        owner:
          description: The activity owner — `BigscreenUser` shape (loosely typed).
          type: object
          additionalProperties: true
        activityTemplateId:
          type: string
          description: ID of the `ActivityTemplate` this instance was created from.
        activitySnapshot:
          type: object
          description: |
            `PublicApp` — denormalised copy of the template at
            activity-creation time, so the activity survives template
            mutations.
          additionalProperties: true
        roomId:
          type: string
        version:
          type: string
          description: Activity version — tied to the room it's attached to.
        title:
          type: string
          description: |
            Currently-active title (e.g. for YouTube, the title of the
            video currently playing). Mutable.
        displayTitle:
          type: string
          description: Fixed display title (e.g. `"YouTube"`).
        screenShot:
          type: string
          format: uri
          description: URL of the latest screenshot from the host. Currently unused.
        state:
          description: |
            `ActivityState` — host-managed state blob, propagated to all
            users in the room. Free-form per `schema`.
          additionalProperties: true
        schema:
          type: string
          description: '`ActivitySchema` — `Unknown`, plus per-template variants.'
      example:
        id: act_01HZQ01234
        createdAt: 1762462440000
        updatedAt: 1762462500000
        owner:
          bigscreenAccountId: acct_01HXJ4Q12345
          userSessionId: sess_01HZP12345
        activityTemplateId: tmpl_youtube
        activitySnapshot: {}
        roomId: room:abc1234567
        version: 1.42.0
        title: How To Train Your Dragon (full movie)
        displayTitle: YouTube
        screenShot: ''
        state: {}
        schema: Unknown
    HistoryItemType:
      type: string
      description: |
        Class of social-history entry. Mirrors `HistoryItemType` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        - `Room` — caller visited / joined a room (the snapshot is the room).
        - `User` — caller encountered another user (the snapshot is that user's
          public profile, plus the room they were met in).
        - `Activity` — caller engaged with a shared activity (game, co-watch).
      enum:
        - Room
        - User
        - Activity
      example: User
    HistoryItemVisibility:
      type: string
      description: |
        Soft-delete state on a history entry. Mirrors `HistoryItemVisibility` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        `MarkedForDeletion` rows are filtered out of normal history responses but
        kept on disk until TTL expiry.
      enum:
        - Default
        - MarkedForDeletion
      default: Default
      example: Default
    PublicSocialStats:
      type: object
      description: |
        Public-safe projection of `SocialStats` — strips notification / history
        counts so it can be returned to other users. Mirrors `PublicSocialStats`
        in [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        A `PublicSocialProfile` carries either this or the full `SocialStats`,
        depending on whether the requester is viewing their own profile.
      required:
        - friendsCount
        - followersCount
        - lastOnlineDate
      properties:
        friendsCount:
          type: integer
        followersCount:
          type: integer
        lastOnlineDate:
          type: integer
          description: Last time the profile was online, as a Unix timestamp in **seconds**.
      example:
        friendsCount: 42
        followersCount: 18
        lastOnlineDate: 1762462440
    PublicSocialProfile:
      type: object
      description: |
        Public-safe projection of a `SocialProfile` — the shape returned to other
        users from `/social/profile/{socialId}` and friends-list / search routes.
        Mirrors `PublicSocialProfile` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        Strips moderation-only fields (`bigscreenAccountId`, the full `SocialStats`)
        in favour of `socialId` and `PublicSocialStats`.

        `stats` carries the **full** `SocialStats` when the caller is viewing
        their own profile, and `PublicSocialStats` otherwise.
      required:
        - socialId
        - username
      properties:
        socialId:
          type: string
        createdAt:
          type: integer
          description: Profile creation, Unix timestamp in **milliseconds**.
        updatedAt:
          type: integer
          description: Last write to the profile row, Unix timestamp in **milliseconds**.
        accountCreatedAt:
          type: integer
          description: Underlying Bigscreen account creation, Unix timestamp in **seconds**.
        bigAccountStatus:
          $ref: '#/components/schemas/BigscreenAccountStatus'
        isVerified:
          type: boolean
        username:
          type: string
        oculusId:
          type: string
        oculusUserId:
          type: string
        steamId:
          type: string
        smallImageUrl:
          type: string
          format: uri
        largeImageUrl:
          type: string
          format: uri
        badges:
          type: array
          items:
            type: string
        stats:
          oneOf:
            - $ref: '#/components/schemas/SocialStats'
            - $ref: '#/components/schemas/PublicSocialStats'
          description: Full `SocialStats` when viewing own profile; `PublicSocialStats` otherwise.
      example:
        socialId: soc_01HZR12345ABC678DEF90GHJKM
        createdAt: 1761127240000
        updatedAt: 1762462440000
        accountCreatedAt: 1761127200
        bigAccountStatus: OK
        isVerified: true
        username: alice
        oculusId: ''
        oculusUserId: ''
        steamId: '76561198012345678'
        smallImageUrl: https://cdn.bigscreenvr.com/avatars/alice_sm.png
        largeImageUrl: https://cdn.bigscreenvr.com/avatars/alice_lg.png
        badges:
          - beyond-early-supporter
        stats:
          friendsCount: 42
          followersCount: 18
          lastOnlineDate: 1762462440
    HistoryItem:
      type: object
      description: |
        Entry in a user's social history — a room they joined, a person they met,
        or an activity they engaged in. The shape here is the **public projection**
        returned from `getPublicData()` on `HistoryItem` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts) —
        the stored row also carries a `bigscreenAccountId` (owner) and a `ttl`
        that are not surfaced.

        Two snapshot fields are present depending on `historyItemType`:
        - `historyItemType=Room` — `room` is set (a `SimpleRoomSnapshot`),
          `socialProfileSnapshot` is unset.
        - `historyItemType=User` — both `room` and `socialProfileSnapshot` are set.
        - `historyItemType=Activity` — neither snapshot is guaranteed.
      required:
        - id
        - createdAt
        - historyItemType
        - historyVisibility
      properties:
        id:
          type: string
          description: |
            Stable hash of `(owner, target, room)` — used as the upsert key so
            re-encountering the same user in the same room does not duplicate
            history rows.
        createdAt:
          type: integer
          description: First-encounter time, Unix timestamp in **milliseconds**.
        historyItemType:
          $ref: '#/components/schemas/HistoryItemType'
        historyVisibility:
          $ref: '#/components/schemas/HistoryItemVisibility'
        roomId:
          type: string
          description: Room the encounter happened in (when applicable).
        room:
          type: object
          description: |
            `SimpleRoomSnapshot` of the room at the time of the encounter — a
            denormalized copy so the history entry survives room deletion.
          additionalProperties: true
        socialProfileSnapshot:
          description: |
            Snapshot of the encountered user's `PublicSocialProfile` at
            encounter time. Only present when `historyItemType=User`.
          oneOf:
            - $ref: '#/components/schemas/PublicSocialProfile'
            - type: 'null'
        metaData:
          type: object
          description: Free-form payload attached at write time.
          additionalProperties: true
      example:
        id: 0a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789
        createdAt: 1762462440000
        historyItemType: User
        historyVisibility: Default
        roomId: room:abc1234567
        room:
          roomId: room:abc1234567
          title: Movie Night
          visibility: FriendsOnly
        socialProfileSnapshot:
          socialId: soc_01HZR67890ABC678DEF90GHJKM
          username: bob
          smallImageUrl: https://cdn.bigscreenvr.com/avatars/bob_sm.png
        metaData: {}
    NotificationType:
      type: string
      description: |
        Class of social notification. Mirrors `NotificationType` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        Drives the side effect on accept (e.g. accepting a `FriendRequest`
        creates the friend edge) and the icon / wording in the client.
        - `Unknown` — placeholder; should not appear in stored notifications.
        - `FriendRequest` — pending friend invite. Accept creates a `Friends` edge.
        - `RoomInvite` — invite to join a specific room.
        - `KnockKnock` — request to enter a private room.
        - `Message` — user-to-user message.
        - `AdminMessage` — system / staff-issued message (no reply).
        - `MarketingMessage` — Bigscreen-issued promotional message.
        - `AcceptedFriendRequest` — confirmation surfaced to the original sender
          after the recipient accepted their `FriendRequest`.
      enum:
        - Unknown
        - FriendRequest
        - RoomInvite
        - KnockKnock
        - Message
        - AdminMessage
        - MarketingMessage
        - AcceptedFriendRequest
      example: FriendRequest
    NotificationStatus:
      type: string
      description: |
        Lifecycle of a notification. Mirrors `NotificationStatus` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        Stored under the field `notificationStatus` (the `status` name is reserved
        in DynamoDB query filter expressions).
      enum:
        - Unread
        - Read
        - Deleted
        - Accepted
        - Declined
      default: Unread
      example: Unread
    PublicNotification:
      type: object
      description: |
        Public-facing shape of a notification — what `/social/notifications` and
        `/social/notification/{id}` return to the recipient client. Mirrors
        `PublicNotification` in
        [`cloud/src/SocialAPI/Schemas.ts`](../../../../cloud/src/SocialAPI/Schemas.ts).
        Differs from the stored `Notification` in that the `senderBigscreenAccountId`
        has been resolved into a `senderSocialProfile`, and the row id is exposed
        as `notificationId` rather than `id`.
      required:
        - notificationId
        - createdAt
        - notificationType
        - notificationStatus
      properties:
        notificationId:
          type: string
        createdAt:
          type: integer
          description: Notification creation, Unix timestamp in **milliseconds**.
        senderSocialProfile:
          $ref: '#/components/schemas/PublicSocialProfile'
        notificationType:
          $ref: '#/components/schemas/NotificationType'
        notificationStatus:
          $ref: '#/components/schemas/NotificationStatus'
        title:
          type: string
        body:
          type: string
        metaData:
          type: object
          description: |
            `NotificationMetaData` — free-form per-`notificationType` payload.
            Common keys: `roomId` (for `RoomInvite` / `KnockKnock`), `version`.
          properties:
            roomId:
              type: string
            version:
              type: string
          additionalProperties: true
      example:
        notificationId: ntf_01HZT12345ABC678DEF90GHJKM
        createdAt: 1762462440000
        senderSocialProfile:
          socialId: soc_01HZR12345ABC678DEF90GHJKM
          username: alice
          smallImageUrl: https://cdn.bigscreenvr.com/avatars/alice_sm.png
          stats:
            friendsCount: 42
            followersCount: 18
            lastOnlineDate: 1762462440
        notificationType: FriendRequest
        notificationStatus: Unread
        title: alice wants to be your friend
        body: ''
        metaData: {}
    MediaServer:
      type: object
      description: |
        A media-server registration — one row per `media_server_next` instance that
        has registered with `cloud_api`. Mirrors `cloud/src/Schemas.MediaServer`.
        Identifies where the server lives, current load, last-heartbeat freshness,
        and which rooms it's hosting.

        `activationStatus` is the only field admins typically mutate via
        `PUT /admin/network/media-server/{id}` — everything else is reported by the
        server itself.
      required:
        - id
        - ipAddress
        - activationStatus
      properties:
        id:
          type: string
          description: Server-reported identifier.
        version:
          type: string
          description: Media-server build version (independent of cloud_api's app version).
        createdAt:
          type: string
          format: date-time
          description: When the server first registered.
        lastHeartbeat:
          type: string
          format: date-time
          description: Most recent heartbeat from the server. Stale heartbeats imply `Unresponsive` status.
        activationStatus:
          type: string
          description: '`MediaServerActivationStatus` value — `Active`, `Disabled`, etc. Admin-mutable.'
        ipAddress:
          type: string
          description: Public IP address.
        minPort:
          type: integer
          description: Lower bound of the UDP port range this server will use.
        maxPort:
          type: integer
          description: Upper bound of the UDP port range.
        isAccelerated:
          type: boolean
          description: Whether the server uses AWS Global Accelerator.
        region:
          type:
            - string
            - 'null'
          description: AWS region (when applicable).
        instanceId:
          type:
            - string
            - 'null'
          description: AWS EC2 instance id.
        instanceType:
          type:
            - string
            - 'null'
          description: AWS instance type (e.g. `t2.xlarge`).
        load:
          type: number
          description: Estimated load 0–100, server-reported.
        roomIds:
          type: array
          items:
            type: string
          description: Rooms currently hosted on this server.
      example:
        id: ms-us-west-2-04
        version: 1.42.3
        createdAt: '2026-04-22T10:14:00.000Z'
        lastHeartbeat: '2026-05-06T19:14:00.000Z'
        activationStatus: Active
        ipAddress: 203.0.113.40
        minPort: 32000
        maxPort: 33000
        isAccelerated: true
        region: us-west-2
        instanceId: i-0a1b2c3d4e5f6789
        instanceType: c6i.xlarge
        load: 28
        roomIds:
          - room:abc1234567
          - room:def8901234
