asyncapi: 2.6.0
info:
  title: GoGuardian Extension Realtime API
  version: 4.0.7422.1
  description: |
    Realtime (Pusher) interface of the GoGuardian student Chrome extension,
    v4.0.7422.1, reconstructed from the minified bundles. The extension talks
    to a private Pusher deployment: cluster "goguardian", production app key
    4e83de4fd19694be0821. The client (pusher-js, vendored as webpack modules)
    is configured wssOnly (forceTLS + enabledTransports ["ws","wss"]), retries
    channel subscriptions up to 10 times with ExponentialBackoff
    (1 s → 1024 s, factor 2, jitter 0.5), permanently gives up on HTTP 403/400
    from the auth endpoint, and repairs channel subscriptions every 30 s
    (background/src/475-pusher-client.js, src/987-pusher-env.js).

    Channel authentication: every private-/presence- channel subscription
    triggers POST https://sakura.goguardian.com/api/v1/auth/ext with header
    Authorization: <localStorage.compRand> and params channel_name, socket_id,
    version, protocolVersion=1, capabilities (comma-joined device-capability
    bit list), clientType="extension", os, liveStateVersion=6. The full
    request/response is documented in the companion OpenAPI spec
    (openapi.goguardian.yaml).

    A SECOND Pusher client exists for the "sender experiment"
    (background/src/1714-sender-module.js): identical key/cluster/auth, but
    wsHost/httpHost are overridden to sender-gateway.goguardian.com (staging:
    stg-sender-gateway.goguardian.com). It mirrors the same
    private-subaccount / private-account / presence-session channels, acks
    shadows, and receives client-state-set copies.

    PERSPECTIVE NOTE: Pusher "client events" (client-*) are extension-to-
    extension messages relayed through Pusher — the teacher/admin dashboard
    publishes, the student extension subscribes, and vice versa. In this spec
    "publish" means the student extension emits the event; "subscribe" means
    it listens for the event (usually sent by an admin dashboard).

    HONEST SCOPE NOTE: there is NO GoGuardian-domain WebSocket in this
    codebase. The only wss:// endpoints contacted are Twilio's
    (wss://global.vss.twilio.com/signaling — SDK-owned, used for the
    ggt-call.<callId> video rooms) and Pusher's own; neither is documented as
    channels here.

servers:
  pusher:
    url: wss://ws-goguardian.pusher.com:443/app/4e83de4fd19694be0821
    protocol: wss
    description: |
      Main Pusher WebSocket. pusher-js constructs this URL from the cluster
      ("goguardian") and app key, appending query params such as
      protocol=7&client=js&version=<pusher-js version>&flash=false.
  sender-gateway:
    url: wss://sender-gateway.goguardian.com:443/app/4e83de4fd19694be0821
    protocol: wss
    description: |
      Sender-experiment client only (src/1714-sender-module.js). Staging
      variant: stg-sender-gateway.goguardian.com. Same key and auth endpoint
      as the main deployment.

channels:
  private-subaccount.{subAccountId}:
    description: |
      Primary teacher/admin → student command channel, keyed by the
      enrolled sub-account. Both the main client and the sender-experiment
      client subscribe (src/1048-pusher-module.js:183, src/1714:141).
    parameters:
      subAccountId:
        schema:
          type: integer
        description: Numeric sub-account id (register.subAccountId).
    subscribe:
      summary: Teacher/admin shadow and live-control events
      operationId: subaccountSubscribe
      messages:
        - $ref: '#/components/messages/Shadow'
        - $ref: '#/components/messages/ForceLiveState'
        - $ref: '#/components/messages/RequestScreenshotResolutionIncrease'
        - $ref: '#/components/messages/AnnotateScreenRequest'
        - $ref: '#/components/messages/SetChatState'
        - $ref: '#/components/messages/SetOffTaskAlertsState'
        - $ref: '#/components/messages/RequestEntityFlushRateIncrease'
        - $ref: '#/components/messages/SmartMonitoring'
    publish:
      summary: Student acks and call state
      operationId: subaccountPublish
      messages:
        - $ref: '#/components/messages/ShadowApplied'
        - $ref: '#/components/messages/ForceInitialPayload'
        - $ref: '#/components/messages/StateSet'
        - $ref: '#/components/messages/AnnotateScreenResponse'
        - $ref: '#/components/messages/CallState'
        - $ref: '#/components/messages/CallRinging'
  private-account.{accountId}:
    description: |
      Account-wide admin event channel (src/1048:218, src/1714:171).
    parameters:
      accountId:
        schema:
          type: integer
        description: Numeric account id.
    subscribe:
      summary: Account-level admin events
      operationId: accountSubscribe
      messages:
        - $ref: '#/components/messages/AdminPoliciesUpdated'
  presence-session.{sessionId}:
    description: |
      Presence channel per classroom session; member presence drives whether
      live-state screenshots/state are sent at all (no present admins = no
      state sends) (src/1048:131, src/1714:130).
    parameters:
      sessionId:
        schema:
          type: integer
        description: Classroom session id.
    subscribe:
      summary: Presence membership events
      operationId: sessionSubscribe
      messages:
        - $ref: '#/components/messages/MemberAdded'
        - $ref: '#/components/messages/MemberRemoved'
  private-session-instrumentation.student.{accountId}:
    description: |
      Per-student session-instrumentation channel; the student reports which
      sessions received its live-state payload. The code keys this channel
      by accountId — NOT studentId (src/1048:143-144; older READMEs that say
      studentId are wrong).
    parameters:
      accountId:
        schema:
          type: integer
        description: The student's account id (not student id).
    publish:
      summary: Live-state delivery receipts
      operationId: instrumentationPublish
      messages:
        - $ref: '#/components/messages/LiveStateSent'
  presence-call.{callId}:
    description: |
      Presence channel per teacher video/voice call (src/1048:171-174).
    parameters:
      callId:
        schema:
          type: integer
        description: Call id.
    subscribe:
      summary: Call lifecycle events
      operationId: callSubscribe
      messages:
        - $ref: '#/components/messages/MemberAdded'
        - $ref: '#/components/messages/MemberRemoved'
        - $ref: '#/components/messages/CallArchived'
        - $ref: '#/components/messages/CallHangup'
  presence-student.{accountId}-session.{sessionId}:
    description: |
      Classroom chat channel (raw/1692.js:188). Both directions: the
      extension's own chat messages and acknowledgments, plus incoming
      teacher messages and peer acknowledgments.
    parameters:
      accountId:
        schema:
          type: integer
        description: Student account id.
      sessionId:
        schema:
          type: integer
        description: Classroom session id.
    publish:
      summary: Outgoing chat
      operationId: chatPublish
      messages:
        - $ref: '#/components/messages/ChatMessage'
        - $ref: '#/components/messages/ChatAcknowledgment'
    subscribe:
      summary: Incoming chat
      operationId: chatSubscribe
      messages:
        - $ref: '#/components/messages/ChatMessage'
        - $ref: '#/components/messages/ChatAcknowledgment'
  private-mlp-subaccount.{subAccountId}:
    description: |
      ML-pipeline mode channel (sender experiment / mlPipeline feature) —
      carries asynchronous beacon ML results back to the device
      (src/1714:96; src/1691:50-51, 109-145).
    parameters:
      subAccountId:
        schema:
          type: integer
        description: Numeric sub-account id.
    subscribe:
      summary: Beacon ML results
      operationId: mlpSubscribe
      messages:
        - $ref: '#/components/messages/BeaconSearch'
        - $ref: '#/components/messages/BeaconThreatsAndViolence'

components:
  messages:
    ShadowApplied:
      name: client-shadow-applied
      title: Shadow applied acknowledgment
      description: |
        Sent by the student extension after applying a shadow's state, on
        both the main and sender-experiment clients. If the trigger is
        refused, retried every 5 s.
      payload:
        type: object
        required: [subaccountId, shadowId, applied]
        properties:
          subaccountId:
            type: integer
          shadowId:
            type: integer
          applied:
            type: boolean
            enum: [true]
      x-evidence: src/1048:493-497; src/1714:229-234
    ForceInitialPayload:
      name: client-force-initial-payload
      title: Force initial payload
      description: |
        Empty payload. Fired 4 s after subscribing to private-subaccount if
        no shadow has arrived yet (5 s retry), to force the admin dashboard
        to resend state.
      payload:
        type: object
        additionalProperties: false
      x-evidence: src/1048:503-504; src/1714:240-241
    StateSet:
      name: client-state-set
      title: Live state update
      description: |
        The live-state payload (screenshots URL, tabs, lock state, idle...),
        sent on the main client trimmed to 20480 bytes and on the
        sender-experiment client untrimmed. Sent every 3 s (debounced) while
        monitoring is active and at least one admin is present.
      payload:
        $ref: '#/components/schemas/LiveState'
      x-evidence: src/1715:369 (trimmed), src/1715:410 (sender, untrimmed)
    LiveStateSent:
      name: client-live-state-sent
      title: Live-state delivery receipt
      description: |
        Reports which instrumented sessions were sent the live-state payload,
        keyed by the state's uuid id.
      payload:
        type: object
        required: [id, sessionIds]
        properties:
          id:
            type: string
            description: uuid.v4() of the sent state payload.
          sessionIds:
            type: array
            items:
              type: integer
      x-evidence: src/1715:385-390
    AnnotateScreenResponse:
      name: client-annotate-screen-response
      title: Annotate-screen response
      payload:
        type: object
        required: [requestId, success]
        properties:
          requestId:
            type: string
          success:
            type: boolean
      x-evidence: src/1048:539-543
    ChatMessage:
      name: client-chat-message
      title: Classroom chat message
      description: |
        The redux ADD_MESSAGE payload. The receiver parseInts studentId.
        Content is the message text; type is e.g. "message".
      payload:
        type: object
        required: [studentId, sessionId, messageId, content, type]
        properties:
          studentId:
            type: integer
          sessionId:
            type: integer
          messageId:
            type: string
          content:
            type: string
          type:
            type: string
            description: e.g. "message"
      x-evidence: raw/1704.js:6-18 (send); raw/1692.js:185-194 (receive); raw/1709.js:91-109
    ChatAcknowledgment:
      name: client-chat-acknowledgment
      title: Chat acknowledgment
      payload:
        type: object
        required: [eventType, messageIds, sessionId, studentId, timestamp]
        properties:
          eventType:
            type: string
            enum: ["client-chat-acknowledgment"]
          messageIds:
            type: array
            items:
              type: string
          sessionId:
            type: integer
          studentId:
            type: integer
          timestamp:
            type: integer
            description: Date.now() epoch millis.
      x-evidence: raw/1704.js:20-39
    CallState:
      name: client-call-{state}
      title: Outbound call state event
      description: |
        Call lifecycle state, mapped from the call state machine to the
        event name: joined → client-call-answered; declined →
        client-call-declined; declined_reason → client-call-declined-reason
        (adds data.reason); left → client-call-hangup; consent_pending →
        client-call-consent-pending; permission_pending →
        client-call-permission-pending; consent_declined →
        client-call-consent-declined; error → client-call-error; join_pending
        → client-call-join-pending; recording_consent_pending →
        client-call-recording-consent-pending; recording_consent_changed →
        client-call-recording-consent-changed.
      payload:
        type: object
        required: [participantId]
        properties:
          participantId:
            type: string
          reason:
            type: string
            description: Only for client-call-declined-reason.
          data:
            type: object
            description: Additional state fields for error states.
      x-evidence: raw/1051.js:60-73, 239-244
    CallRinging:
      name: client-call-ringing
      title: Call ringing
      payload:
        type: object
        required: [participantId]
        properties:
          participantId:
            type: string
      x-evidence: raw/1051.js:350-353
    Shadow:
      name: client-shadow
      title: Shadow state
      description: |
        The teacher/admin "shadow": the desired device state (lock, chat
        enablement, tab commands, lesson plan, active calls, check-ins). The
        extension applies it and acks via client-shadow-applied.
      payload:
        $ref: '#/components/schemas/ShadowPayload'
      x-evidence: src/1048:326-336, 436-486
    ForceLiveState:
      name: client-force-live-state
      title: Force live state resend
      description: Empty payload; both clients resend the current state.
      payload:
        type: object
        additionalProperties: false
      x-evidence: src/1048:198-200; src/1714:154-156
    RequestScreenshotResolutionIncrease:
      name: client-request-screenshot-resolution-increase
      title: Screenshot resolution increase request
      description: |
        Admin asks the device to raise screenshot resolution (live-state
        v4+: 1600px instead of 500px) for a bounded duration.
      payload:
        type: object
        required: [duration]
        properties:
          duration:
            type: integer
            description: Milliseconds, capped at 60000 server/client-side.
      x-evidence: src/1048:201-204, 506-515
    AnnotateScreenRequest:
      name: client-annotate-screen-request
      title: Annotate screen request
      description: |
        Teacher draws an annotation dot on the student's screen; hash
        identifies which screenshot the coordinates apply to.
      payload:
        type: object
        required: [requestId, hash, xPercent, yPercent, diameter, color, opacity, durationMs]
        properties:
          requestId:
            type: string
          hash:
            type: string
            description: md5 hash of the referenced screenshot.
          xPercent:
            type: number
          yPercent:
            type: number
          diameter:
            type: number
          color:
            type: string
            description: "#rrggbb"
          opacity:
            type: number
          durationMs:
            type: integer
      x-evidence: src/1048:545-556
    SetChatState:
      name: client-set-chat-state
      title: Set chat state
      payload:
        type: object
        required: [sessionId, payload]
        properties:
          sessionId:
            type: integer
          payload:
            type: object
            required: [command]
            properties:
              command:
                type: string
                enum: ["enable_chat", "disable_chat"]
      x-evidence: src/1048:338-348
    SetOffTaskAlertsState:
      name: client-set-off-task-alerts-state
      title: Set off-task alerts state
      payload:
        type: object
        required: [sessionId, payload]
        properties:
          sessionId:
            type: integer
          payload:
            type: object
            required: [enabled]
            properties:
              enabled:
                type: boolean
      x-evidence: src/1048:353-358
    RequestEntityFlushRateIncrease:
      name: client-request-entity-flush-rate-increase
      title: Entity flush rate increase request
      description: |
        Admin-only (verified via Pusher event metadata user_id prefix
        "admin:"): raise the surveillance entity flush rate to the fast
        interval (1 s) for a bounded duration.
      payload:
        type: object
        required: [duration]
        properties:
          duration:
            type: integer
            description: Milliseconds, capped at 60000.
      x-evidence: src/1048:516-531
    SmartMonitoring:
      name: client-smart-monitoring
      title: Smart monitoring override
      description: |
        Off-task-alerts override event. PAYLOAD UNVERIFIED — the handler is
        bound in raw/1725.js:130-135 but its fields were not inspected.
      payload:
        type: object
        additionalProperties: true
      x-evidence: raw/1725.js:130-135
    MemberAdded:
      name: pusher:member_added
      title: Presence member added
      description: |
        Presence metadata for classroom sessions and calls. Admin members
        have info.type "admin"; the member id is "admin:<...>:<adminId>" and
        the extension reads the admin id via id.split(":")[2]. For calls,
        member.info carries type, aid, sid, callParticipantId.
      payload:
        type: object
        required: [id, info]
        properties:
          id:
            type: string
          info:
            type: object
            properties:
              type:
                type: string
                description: '"admin" for admins.'
              liveStateVersion:
                type: integer
              aid:
                type: string
              sid:
                type: string
              callParticipantId:
                type: string
      x-evidence: src/1048:241-254 (session), src/1048:281-317 (call)
    MemberRemoved:
      name: pusher:member_removed
      title: Presence member removed
      payload:
        type: object
        required: [id, info]
        properties:
          id:
            type: string
          info:
            type: object
            additionalProperties: true
      x-evidence: src/1048:241-254; src/1048:281-317
    AdminPoliciesUpdated:
      name: admin-policies-updated
      title: Admin policies updated
      description: |
        Signals that admin policies changed; the extension re-fetches
        GET https://snat.goguardian.com/api/v1/ext/policy.
      payload:
        type: object
        additionalProperties: true
      x-evidence: src/1048:222-225
    CallArchived:
      name: call-archived
      title: Call archived
      description: Server event on presence-call; removes the joined call.
      payload:
        type: object
        additionalProperties: true
      x-evidence: src/1048:281-317
    CallHangup:
      name: client-call-hangup
      title: Call hangup (received)
      description: Received on presence-call when the peer hangs up.
      payload:
        type: object
        additionalProperties: true
      x-evidence: src/1048:281-317
    BeaconSearch:
      name: beacon-search
      title: Beacon ML search result
      description: |
        Asynchronous self-harm (SSH) ML result delivered in MLP mode. The
        extension acknowledges via POST ml-endpoint /device-notifications/v1/ack.
      payload:
        type: object
        required: [type, ack_id, data]
        properties:
          type:
            type: string
          ack_id:
            type: string
          data:
            type: object
            required: [seqId]
            properties:
              seqId:
                type: integer
                description: Surveillance entity seq id the result applies to.
      x-evidence: src/1691:50-51, 64-75, 109-145
    BeaconThreatsAndViolence:
      name: beacon-threats-and-violence
      title: Beacon ML threats-and-violence result
      description: Same envelope as beacon-search, TV category.
      payload:
        type: object
        required: [type, ack_id, data]
        properties:
          type:
            type: string
          ack_id:
            type: string
          data:
            type: object
            required: [seqId]
            properties:
              seqId:
                type: integer
      x-evidence: src/1691:50-51, 64-75, 109-145

  schemas:
    LiveState:
      type: object
      description: |
        The live-state payload sent every 3 s (task "sendStateData",
        debounced 250 ms / maxWait 1 s; deduped unless unchanged within 20 s
        for liveStateVersion >= 2; skipped entirely when no admins are
        present). When the encoded size exceeds PAYLOAD_MAX_SIZE = 20480
        bytes, the payload is trimmed: tabs are sorted by URL length
        descending, titles are truncated at 128 chars with "...", URLs are
        byte-safe truncated (module 1718), and inactive tabs are dropped
        entirely until it fits.
      required: [accountId, subaccountId, orgId, monitoringRestricted, imageUrl, locked, resolvedConflict, os, winAppConnected, idle, time, id]
      properties:
        accountId:
          type: integer
        subaccountId:
          type: integer
        orgId:
          type: integer
        monitoringRestricted:
          type: boolean
          description: "true when admin.monitorEnabled is false."
        imageUrl:
          type: string
          description: "Uploaded /v1/snapshots URL (empty when restricted)."
        locked:
          type: boolean
        lessonPlanId:
          type: integer
        lessonPlanSessionId:
          type: integer
        lessonPlanAdminId:
          type: integer
        lessonPlanAdminName:
          type: string
        resolvedConflict:
          type: boolean
        apps:
          type: array
          description: Windows native-app list (empty off Windows).
          items:
            type: object
            additionalProperties: true
        tabs:
          type: object
          description: "Map keyed by string tabId."
          additionalProperties:
            type: object
            required: [id, title, url, active, faviconUrl, lastActiveMs, isFocused]
            properties:
              id:
                type: integer
              title:
                type: string
              url:
                type: string
              active:
                type: boolean
              faviconUrl:
                type: string
              lastActiveMs:
                type: integer
              isFocused:
                type: boolean
              videos:
                type: array
                description: "YouTube video metadata; item shape unverified."
                items:
                  type: object
                  additionalProperties: true
        taskiness:
          type: object
          description: "offTaskAlerts.taskiness[activeTabId] or {}."
          additionalProperties: true
        os:
          type: string
          description: platform.getPlatform() string.
        winAppConnected:
          type: boolean
          description: Windows native messaging port connected.
        idle:
          type: object
          required: [currentState, durationMs]
          properties:
            currentState:
              type: string
            durationMs:
              type: integer
        time:
          type: integer
          description: Math.floor(Date.now()/1000) — epoch seconds.
        id:
          type: string
          description: uuid.v4().
      x-evidence: src/1715:221-279, 281-318 (trim), 365-368; src/336:25
    ShadowPayload:
      type: object
      description: |
        The admin shadow: desired device state. Applied by the extension and
        acked with client-shadow-applied.
      required: [shadowId]
      properties:
        shadowId:
          type: integer
        chatEnabled:
          type: object
          description: "Session object gating chat; shape from reducer."
          additionalProperties: true
        locked:
          type: boolean
        lockMessage:
          type: string
        lockSessionId:
          type: integer
        tabCommands:
          type: array
          items:
            type: object
            required: [type, applied, shadowId]
            properties:
              type:
                type: string
                enum: [openTab, closeTab, focusTab, unfocusTab, closeApp, checkIn, setActiveTab]
              applied:
                type: boolean
              shadowId:
                type: integer
              expiresAt:
                type: integer
                description: Epoch seconds.
              url:
                type: string
                description: For openTab.
              tabId:
                type: integer
                description: For closeTab/setActiveTab.
              isFocused:
                type: boolean
                description: For openTab (focus-lock).
              lessonPlanId:
                type: integer
        focusSessionId:
          type: integer
        lessonPlanShadowId:
          type: integer
        lessonPlanId:
          type: integer
        lessonPlanSessionId:
          type: integer
        lessonPlanAdminId:
          type: integer
        lessonPlanAdminName:
          type: string
        resolvedConflict:
          type: boolean
        activeCalls:
          type: array
          description: |
            Active calls; observed fields: callId, callType ("1:1"|"group"|
            "presentation"), videoBackend ("twilio"), participantId,
            acknowledged, forceRinging. Full item schema unverified (reducer
            877 not read).
          items:
            type: object
            additionalProperties: true
        checkIns:
          type: array
          description: Check-in assignments; item schema unverified (reducer 834 not read).
          items:
            type: object
            additionalProperties: true
      x-evidence: src/1048:436-486; raw/1051.js (observed call fields)
