openapi: 3.1.0
info:
  title: GoGuardian Extension REST API
  version: "4.0.7422.1"
  description: >-
    Complete REST/HTTP surface of the GoGuardian student-monitoring Chrome
    extension, reconstructed from its minified webpack bundles (v4.0.7422.1).
    Every fact below is verified against the reconstructed source tree:
    request/response field names, content types, schedules, retry policies and
    evidence citations come from the wire-level extraction pass (module
    numbers refer to files under source/background/src/ and
    source/background/raw-modules/).

    AUTH MODEL — the generic HTTP client (module 149, base
    https://extapi.goguardian.com) sets on every request: "Authorization" =
    the raw compRand token (NO "Bearer" prefix; the token is issued by
    POST /api/v1/ext/register and stored in localStorage.compRand), and
    "ExtensionVersion" = the running extension version. When present and
    parseable, a "Device-Id" header (localStorage.enterpriseDirectoryDeviceId)
    is also sent. A few hand-rolled clients (beacon predictors, lesson-plan,
    Twilio) send lowercase "authorization"/"extensionversion" variants.

    HOST OVERRIDE QUIRK — the server can repoint any path to a different host
    via an apiMap (delivered with privacy settings): ajax() checks
    apiMap[fullPathString] before falling back to the default base, keyed by
    the complete path string, not a prefix.

    ENCODING — module-149 POST/PUT calls that pass a plain data object are
    jQuery form-encoded (application/x-www-form-urlencoded) by default; only
    calls that explicitly set contentType send JSON. Binary pipelines gzip an
    Avro record (entity batch, statsd) or a protobuf message (roll-call,
    apps-exts) and post it as application/binary or application/octet-stream.

    HONEST CAVEAT — response schemas list only the fields the client code
    actually reads; servers return more. Entries marked "response not read"
    are fire-and-forget. Uncertainties from the extraction pass are called
    out inline.
servers:
  - url: https://extapi.goguardian.com
    description: >-
      Default base for the module-149 HTTP client. All paths on other hosts
      carry their own path-level servers.
security:
  - CompRand: []
    ExtensionVersion: []
tags:
  - name: Registration & Identity
    description: Bootstrap registration, account/user state, name/email identity.
  - name: Settings & Policy
    description: Privacy, admin policy, experiments, notifications, model sync.
  - name: Surveillance
    description: Entity batch (Avro/gzip), roll-call heartbeat, screenshots.
  - name: Telemetry
    description: StatsD metrics and log shipping (Avro / JSON to countvoncount).
  - name: Smart Alerts (X3)
    description: Self-harm content prediction and report pipeline (x3-*).
  - name: Beacon (crisis response)
    description: Fullpage/search predictors and beacon reports shown to students.
  - name: Classroom & Calls
    description: Chat messages, lesson plans, video-call participants.
  - name: Taskiness & Video
    description: Off-task scoring and YouTube video metadata.
  - name: Pusher Auth
    description: Channel authorization for the realtime Pusher connection.
  - name: Utilities
    description: Public IP, clock sync, native-agent liveness, diagnostics.
  - name: Block Pages
    description: Navigations to blocked.goguardian.com with signed context params.
  - name: S3 Upload
    description: Server-signed multipart uploads to an S3 bucket.
paths:
  /api/v1/ext/register:
    post:
      tags: [Registration & Identity]
      operationId: register
      summary: Exchange license-extension IDs for org identity and compRand token
      description: >-
        First call of the bootstrap. The extension lists installed extensions
        (chrome.management), filters for those named exactly "Chromium License"
        or "GoGuardian License", and POSTs their extension IDs as orgRands
        (also stored to localStorage.orgRand). The server maps them to the org
        and returns the register payload merged verbatim into state.register.
        Retried 1 s base delay, 8 attempts. Failure path: "No License, please
        install license extension." -> Register subsystem wipes
        localStorage.uuid and reloads the extension after 60 s.
      x-evidence: raw-modules/1403.js:23-51, raw-modules/1337.js:19-40, src/1519-register.js:93-101
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/RegisterRequest' }
      responses:
        "200":
          description: Register payload merged into state.register (reducer 1337).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RegisterResponse' }
  /api/v1/ext/version:
    put:
      tags: [Registration & Identity]
      operationId: reportVersion
      summary: Report a running version newer than the server's record
      description: >-
        Sent only when the running version compares greater than user.extVer
        from GET /api/v1/ext/user. Failure is logged and non-fatal.
      x-evidence: src/1521-user.js:149-154
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/VersionRequest' }
      responses:
        "200": { description: Response not read. }
  /api/v1/ext/bypass:
    get:
      tags: [Settings & Policy]
      operationId: validateBypassPassword
      summary: Validate a block-page bypass password
      description: >-
        Called by the background Bypass subsystem when the student submits the
        block-page bypass form (admin bundle UI opens a runtime port; the
        background performs the call). On success the server returns a bypass
        window (time in minutes; bypassEnd = now + 60*time) and the tab is
        navigated to the target URL. CORRECTION of the earlier markdown doc
        (source/docs/WEB-AND-REALTIME-APIs.md): the wire call is GET on the
        extapi default base, not POST on snat — raw/1606.js is authoritative.
      x-evidence: raw-modules/1606.js:30-47, raw-modules/689.js:63-65
      parameters:
        - name: url
          in: query
          required: true
          description: Target URL to unblock ("www." prefix stripped by the UI).
          schema: { type: string }
        - name: password
          in: query
          required: true
          description: Bypass password (UI port message field is "pw"; wire field is "password").
          schema: { type: string }
        - name: profiles
          in: query
          required: true
          description: The profiles array (window.profiles) from registration.
          schema: { type: array, items: { type: string } }
      responses:
        "200":
          description: Bypass grant. Failure answers "fail" via the UI port.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BypassResponse' }
  /api/v1/ext/nameandemail:
    post:
      tags: [Registration & Identity]
      operationId: createAccount
      summary: Create/identify the student account from a scraped identity
      description: >-
        Native fetch (not module 149) with form-encoded body built by
        jQuery.param. Headers: content-type application/x-www-form-urlencoded,
        authorization (compRand), extensionversion (lowercase). Retried 1 s
        base, 7 attempts. The identity "url" field is one of
        "chrome.identity", "windows.nativehost",
        "chrome.identity.windows.nativehost".
      x-evidence: raw-modules/986.js:63-85, raw-modules/986.js:192-223
      security:
        - CompRand: []
          ExtensionVersion: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/NameAndEmailRequest' }
      responses:
        "200":
          description: "Response fields read: accountId, profiles (kept on window.profiles)."
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateAccountResponse' }
  /api/v1/ext/updatename:
    put:
      tags: [Registration & Identity]
      operationId: updateName
      summary: Update the student's scraped display name
      x-evidence: raw-modules/986.js:195-199
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                name: { type: string }
              required: [name]
      responses:
        "200": { description: Response not read. }
  /api/v1/ext/googleprofile:
    put:
      tags: [Registration & Identity]
      operationId: updateGoogleProfile
      summary: Update the student's Google profile ID
      x-evidence: raw-modules/986.js:202-223
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                googleProfileId: { type: string }
              required: [googleProfileId]
      responses:
        "200": { description: Response not read. }
  /assets/scripts/ext/submitCoordsGuard.php:
    post:
      tags: [Surveillance]
      operationId: submitGeolocation
      summary: Hourly student geolocation upload (legacy PHP path)
      description: >-
        navigator.geolocation.getCurrentPosition with timeout 10 s and
        enableHighAccuracy; error callback is a no-op. Fired every
        GEOLOCATION_UPDATE_INTERVAL_MS = 3600000 (1 h), once immediately on
        enable; gated on admin.monitorEnabled AND a license set containing
        "admin". Fire-and-forget — response never read.
      x-evidence: raw-modules/1556.js:16-51, src/336-intervals.js:21
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/GeolocationRequest' }
      responses:
        "200": { description: Response not read (fire-and-forget). }
  /api/v1/ext/user:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Registration & Identity]
      operationId: getUser
      summary: Fetch user/account state
      description: >-
        Merged verbatim into state.user. Drives the OrgRand mismatch check
        (if server orgRand is not among the locally installed
        Chromium/GoGuardian License extension IDs, the extension wipes
        localStorage and reloads after 60 s) and the email mismatch check
        (emailOnFile vs chrome.identity email triggers the same wipe).
      x-evidence: raw-modules/1404.js:10-17, raw-modules/1338.js:28-47, src/1521-user.js:110-178
      responses:
        "200":
          description: User payload merged into state.user.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UserResponse' }
  /api/v2/ext/privacy:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getPrivacySettings
      summary: Fetch privacy settings (delivers the apiMap and feature gates)
      description: >-
        Polled every 1000000 ms (task "updatePrivacySettings"); response
        cached at chrome.storage.local key "local_data". The privacy response
        installs apiMap (full-path -> host overrides for the module-149
        client) on every refresh. HTTP 403 wipes localStorage and reloads the
        extension.
      x-evidence: raw-modules/689.js:101-107, src/1523-privacy.js:64-111
      parameters:
        - name: version
          in: query
          required: false
          description: Current privacy version (client-passed cache-buster).
          schema: { type: string }
      responses:
        "200":
          description: Privacy settings bundle.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrivacyResponse' }
  /api/v1/ext/policy:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getAdminPolicy
      summary: Fetch admin blocking policy (site filters)
      description: Polled every 900000 ms (15 min, GETPOLICY_INTERVAL_MS).
      x-evidence: raw-modules/852.js:99-105, src/336-intervals.js:22
      parameters:
        - name: version
          in: query
          required: false
          schema: { type: string }
        - name: timezone
          in: query
          required: false
          description: moment.tz.guess() result.
          schema: { type: string }
      responses:
        "200":
          description: Admin policy (site filters, categories, schedules). Shape not exhaustively reconstructed — schema is loose.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PolicyResponse' }
  /api/v1/ext/experiments:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getExperiments
      summary: Fetch experiment assignment lists
      description: Validated response; retried 1 s base, 10 attempts.
      x-evidence: raw-modules/1756.js:10-22
      responses:
        "200":
          description: Experiment assignment/exposure lists.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExperimentsResponse' }
  /api/v1/ext/notifications:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getNotifications
      summary: Poll for student-visible notifications
      description: Polled every 900000 ms (GET_NOTIFICATIONS_INTERVAL_MS).
      x-evidence: raw-modules/1772.js:10-14, src/336-intervals.js:33
      responses:
        "200":
          description: Notification list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationsResponse' }
  /api/v1/ext/chat-messages:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Classroom & Calls]
      operationId: getChatMessages
      summary: Fetch classroom chat history for a session
      description: Client parseInts each message's studentId from the response.
      x-evidence: raw-modules/1709.js:20-33
      parameters:
        - name: sessionId
          in: query
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Chat history.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChatMessagesResponse' }
  /api/v1/ext/diagnose:
    servers: [{ url: "https://snat.goguardian.com" }]
    post:
      tags: [Utilities]
      operationId: submitDiagnostics
      summary: Dump full extension state on checkup navigation
      description: >-
        Fired when a tab navigates to https://checkup.goguardian.com. Body is
        a JSON.stringify of the full redux state plus Pusher channel info.
      x-evidence: raw-modules/1530.js:34, raw-modules/1530.js:82-88
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/DiagnoseRequest' }
      responses:
        "200": { description: Response not read. }
  /api/v1/ext/deviceId:
    servers: [{ url: "https://snat.goguardian.com" }]
    post:
      tags: [Registration & Identity]
      operationId: registerDeviceId
      summary: Register the enterprise-directory device ID
      description: >-
        dataType "text"; retried 1 s base, 3 attempts. Sends deviceId, or a
        reason ("Not Provisioned") when the enterprise API did not supply one.
      x-evidence: raw-modules/1607.js:77-92
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/DeviceIdRequest' }
      responses:
        "200": { description: Response not read (dataType text). }
  /api/v1/ext/app-exts:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Surveillance]
      operationId: getAppExtsSnapshot
      summary: Fetch the server's snapshot of installed extensions/apps
      description: Used to diff against the local chrome.management list before syncing.
      x-evidence: raw-modules/1569.js:92-98
      responses:
        "200": { description: Installed id list; shape not reconstructed (raw 1569 diff logic). }
  /api/v1/ext/smart-filtering/settings:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getSmartFilteringSettings
      summary: Fetch smart-filtering configuration (hosted domains, proxy matchers)
      description: Response cached; used to configure proxy/direct-IP blocking.
      x-evidence: raw-modules/1641.js:92-98
      responses:
        "200": { description: Smart-filtering settings; shape not reconstructed. }
  /api/v1/ext/smart-filtering/models/explicit/{version}/model.json:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getExplicitImageModel
      summary: Fetch the TF.js explicit-image model for a version
      x-evidence: raw-modules/1625.js:180
      parameters:
        - name: version
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: TF.js model JSON. Consumed by the explicit-image-filter pipeline.
          content:
            application/json: {}
  /api/v1/ext/smart-filtering/models/explicit/{version}/metadata.json:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getExplicitImageModelMetadata
      summary: Fetch metadata for the explicit-image model version
      x-evidence: raw-modules/1625.js:243
      parameters:
        - name: version
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Model metadata JSON.
          content:
            application/json: {}
  /api/v1/ext/smart-filtering/models/proxies/version/current:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getProxiesModelCurrentVersion
      summary: Current keyword-model version for proxy detection
      x-evidence: raw-modules/1644.js:9
      responses:
        "200": { description: Version descriptor consumed by module 1644. }
  /api/v1/ext/smart-filtering/models/proxies/{version}/model.json:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getProxiesModel
      summary: Fetch proxy keyword model for a version
      description: Synced every 900000 ms.
      x-evidence: raw-modules/1644.js:26
      parameters:
        - name: version
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: Keyword model JSON. }
  /api/v1/ext/smart-filtering/models/threats/version/current:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getThreatsModelCurrentVersion
      summary: Current keyword-model version for threats detection
      x-evidence: raw-modules/1646.js:9
      responses:
        "200": { description: Version descriptor consumed by module 1646. }
  /api/v1/ext/smart-filtering/models/threats/{version}/model.json:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Settings & Policy]
      operationId: getThreatsModel
      summary: Fetch threats keyword model for a version
      x-evidence: raw-modules/1646.js:27
      parameters:
        - name: version
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: Keyword model JSON. }
  /api/v2/ext/lesson-plan/{id}:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Classroom & Calls]
      operationId: getLessonPlan
      summary: Fetch a lesson plan scene definition
      description: >-
        Hand-rolled client with lowercase authorization/extensionversion
        headers; backoff initial 1 s, max 10 s with jitter; retried only on
        network errors and 5xx.
      x-evidence: raw-modules/1047.js:83-130
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Lesson-plan scene JSON (restricted sites etc.); shape not reconstructed.
          content:
            application/json: {}
  /api/v2/ext/ggt-calls/{callId}/participants:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Classroom & Calls]
      operationId: getCallParticipants
      summary: Fetch participants of a teacher-student video call
      description: contentType application/json; retried 500 ms base, 5 attempts.
      x-evidence: raw-modules/1051.js:196-218
      parameters:
        - name: callId
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Call participant list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ParticipantsResponse' }
  /api/v2/ext/ggt-calls/{callId}/participant/{id}:
    servers: [{ url: "https://snat.goguardian.com" }]
    get:
      tags: [Classroom & Calls]
      operationId: getCallParticipant
      summary: Fetch a single call participant
      x-evidence: raw-modules/1051.js:196-218
      parameters:
        - name: callId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: One participant record.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Participant' }
  /v1/entitybatchpreauth:
    servers: [{ url: "https://quiddity.goguardian.com" }]
    post:
      tags: [Surveillance]
      operationId: submitEntityBatch
      summary: Main surveillance pipeline — gzipped Avro entity batches
      description: >-
        Headers set in beforeSend: Authorization (compRand || "Unknown"),
        ExtensionVersion (|| "Unknown"), Content-type application/binary,
        Content-encoding gzip. Body is a Blob([binary], type
        application/binary) where binary = pako.gzip(1-byte schema version +
        Avro-serialized EntityBatchPreAuth). Schema version = 13 (shipped as
        avro/resources/EntityBatchPreAuth.version). Flushed by TaskManager
        task "entities_flush" every 60000 ms (1000 ms fast interval during
        mlPipeline rate-increase); immediate flush on a new search entity when
        mlPipeline is enabled; final flush when monitoring disables. Gated on
        admin.monitorEnabled (or the dataCollectionAllEntities feature). On
        POST failure the pending keys are NOT marked flushed (only statsd
        admin.entity_submission.error) so they retry next flush; entities
        that fail Avro trial serialization are dropped and the rest ships.
        Internal pipeline name "kinesis".
      x-evidence: src/1741-flush-entities-thunk.js:83-139, src/1740-entities-flush.js:44-110, src/1736-entity-manager-v3.js, source/background/…/EntityBatchPreAuth.avsc
      requestBody:
        required: true
        content:
          application/binary:
            schema: { type: string, format: binary }
            description: >-
              gzip(0x0D || avro(EntityBatchPreAuth)). The decoded record
              shape is documented as the informational components
              EntityBatchPreAuthRecord / EntityPreAuth.
      responses:
        "200": { description: Response not read; success releases the batch keys. }
  /v1/roll-call:
    servers: [{ url: "https://waluigi.goguardian.com" }]
    post:
      tags: [Surveillance]
      operationId: rollCall
      summary: Protobuf attendance heartbeat
      description: >-
        Body: protobuf waluigi.RollCallRequest.create({...}).encode().finish()
        — field 1 extensionVersion (string, tag 0x0A), field 2 ip (uint64,
        tag 0x10; ipv4 packed as 256*(256*(256*a+b)+c)+d). Content-Type
        application/octet-stream. TaskManager task "rollCall" every 300000 ms
        (5 min) plus immediate fire on monitor enable. Gated on
        priv.settings.features.eventTransportSettings.roll_call existing and
        !rollCall.disabled AND admin.monitorEnabled. Retried 1 s base, 3
        attempts; failure statsd "luigi_roll_call.error". Client checks only
        response.ok; RPC return type waluigi.SuccessResponse.
      x-evidence: src/1541-attendance.js:48-88, raw-modules/989.js:64-101
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, format: binary }
            description: "protobuf RollCallRequest { extensionVersion: string (f1), ip: uint64 (f2) }"
      responses:
        "200": { description: waluigi.SuccessResponse; only response.ok checked. }
  /v1/create-apps-exts:
    servers: [{ url: "https://waluigi.goguardian.com" }]
    post:
      tags: [Surveillance]
      operationId: createAppsExts
      summary: Sync newly installed apps/extensions (protobuf)
      description: >-
        protobuf CreateAppsExtsRequest: f1 extensionVersion (string), f2 ip
        (uint64), f3 repeated AppsExtsPayload { extID f1, extName f2, extJSON
        f3, isApp f4, description f5, homepageURL f6, iconURL f7 }.
        Content-Type application/octet-stream, processData false. Diffed
        against GET /api/v1/ext/app-exts before sending.
      x-evidence: raw-modules/1569.js:150-162, raw-modules/989.js:270-298
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, format: binary }
            description: protobuf CreateAppsExtsRequest
      responses:
        "200": { description: Response not read. }
  /v1/delete-apps-exts:
    servers: [{ url: "https://waluigi.goguardian.com" }]
    post:
      tags: [Surveillance]
      operationId: deleteAppsExts
      summary: Sync removed apps/extensions (protobuf)
      description: >-
        protobuf DeleteAppsExtsRequest: f1 extensionVersion (string), f2 ip
        (uint64), f3 repeated ids (string). Sent at most every
        SEND_EXTENSIONS_RATE_MS = 3600000 ms (1 h).
      x-evidence: raw-modules/1569.js:169-186, raw-modules/989.js:634-660, src/336-intervals.js:24
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, format: binary }
            description: protobuf DeleteAppsExtsRequest
      responses:
        "200": { description: Response not read. }
  /v1/snapshots:
    servers: [{ url: "https://screenshots.goguardian.com" }]
    post:
      tags: [Surveillance]
      operationId: uploadSnapshot
      summary: Upload a live teacher-view screenshot (raw JPEG)
      description: >-
        opts { processData: false, contentType: "image/jpeg" }; body is the
        raw JPEG bytes after canvas resize to maxDimension (800 legacy;
        liveStateVersion >= 4: 500, or 1600 during resolution-increase) at
        quality 80. The response must echo the client md5 as hash or the
        client throws "hash mismatch!"; url is the teacher-facing link. md5
        dedupe: a NodeCache (stdTTL 30 s) keyed by md5 skips re-upload;
        expirations are debounced (500 ms) into a "snapshotsExpired" event.
      x-evidence: src/1716-image-handler.js:82-228, src/1715-live-state.js:199-201
      requestBody:
        required: true
        content:
          image/jpeg:
            schema: { type: string, format: binary }
            description: Raw JPEG bytes (md5 computed over these).
      responses:
        "200":
          description: Snapshot descriptor.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SnapshotResponse' }
  /v1/snapshots/{hash}:
    servers: [{ url: "https://screenshots.goguardian.com" }]
    put:
      tags: [Surveillance]
      operationId: keepSnapshotAlive
      summary: Snapshot TTL keepalive
      description: >-
        TaskManager task "image-handler-update-ttl" every 20000 ms (20 s);
        skipped when no snapshot is cached. Body is the literal string "{}".
      x-evidence: src/1716-image-handler.js:96-100, src/1716-image-handler.js:252-258
      parameters:
        - name: hash
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
            example: {}
      responses:
        "200": { description: Response not read. }
  /v1/activity-snapshots:
    servers: [{ url: "https://screenshots.goguardian.com" }]
    post:
      tags: [Surveillance]
      operationId: uploadActivitySnapshot
      summary: Upload a permanent activity screenshot (data URL)
      description: >-
        JPEG data-URL captured at quality 75 (snapHeavy(null, 75)); cached
        with TTL 960 s and shipped when its screenshotTime falls inside the
        entity flush window or its seqId is in a server-requested backfill
        (seqIdsToCollect).
      x-evidence: src/411-snapper.js:69-81, src/1736-entity-manager-v3.js:186-243
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ActivitySnapshotRequest' }
      responses:
        "200": { description: Response not read. }
  /v1/statsd:
    servers: [{ url: "https://countvoncount.goguardian.com" }]
    post:
      tags: [Telemetry]
      operationId: submitStatsdBatch
      summary: StatsD metrics batch (1-byte version + Avro, application/binary)
      description: >-
        Headers: Authorization (compRand || "Unknown"), ExtensionVersion,
        Content-type application/binary. Blob([versionByte ++
        avro.toBuffer(record)]) where versionByte = StatsDBatch.version = 1
        (avro/resources/StatsDBatch.version). Flushed by task "statsd_flush"
        every 30000 ms; buffer capped at 100 metrics (keeps newest); retried
        1 s base, 3 attempts, failure re-queues with statsd
        "statsd.submit_error". Metric names are prefixed "chromium_m.".
      x-evidence: src/182-statsd-client.js:42-115
      requestBody:
        required: true
        content:
          application/binary:
            schema: { type: string, format: binary }
            description: 0x01 || avro(StatsDBatchRecord)
      responses:
        "200": { description: Response not read. }
  /v2/logs:
    servers: [{ url: "https://countvoncount.goguardian.com" }]
    post:
      tags: [Telemetry]
      operationId: submitLogs
      summary: Log shipping (JSON batch; server-side Avro schema)
      description: >-
        contentType application/json, processData false; body JSON.stringify(
        { logs, submissionTime }). Flushed by task "logs_flush" every 30000
        ms, 100 records per batch; retried 1 s base, 3 attempts, failure
        re-queues with statsd "logs.submit_error". Client records carry
        severity / log / stack_trace (module-678 logger); every record is
        pre-tagged with account_id, extension_version, os, org_id,
        sub_account_id. Server deserializes with avro LogsBatch.Log {
        logMessage, level INFO|WARN|ERROR, timestampMs, errorName,
        errorMessage, errorStack } — the exact client->server key mapping is
        not visible in the extension (uncertainty N3 of the extraction).
      x-evidence: src/24-log-client.js:44-180
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LogsRequest' }
      responses:
        "200": { description: Response not read. }
  /v2/predictions:
    servers: [{ url: "https://x3-predictor.goguardian.com" }]
    post:
      tags: [Smart Alerts (X3)]
      operationId: predictX3V2
      summary: Keyword-list self-harm prediction (gzipped base64)
      description: >-
        contentType: false, timeout 10000 ms, headers Authorization +
        X-GG-X3EXTVER. Body: btoa(pako.gzip(JSON.stringify({words}))) — a
        base64 string of gzipped JSON with a single array field "words".
        Retried 3 attempts at 500 * 2^n ms. Response
        data.prediction.{category, probability}.
      x-evidence: raw-modules/1610.js:26-46
      requestBody:
        required: true
        content:
          text/plain:
            schema: { type: string }
            description: "btoa(gzip(JSON({words:[...]})))"
      responses:
        "200":
          description: Prediction result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  prediction: { $ref: '#/components/schemas/X3Prediction' }
  /v3/predictions:
    servers: [{ url: "https://x3-predictor.goguardian.com" }]
    post:
      tags: [Smart Alerts (X3)]
      operationId: predictX3V3
      summary: Full-text self-harm prediction (gzip binary)
      description: >-
        Headers Authorization + ExtensionVersion; body
        pako.gzip(JSON.stringify({fullText})). This endpoint is used ONLY
        when the X3 policy's disabledCategories includes "self-harm" (which
        disables it elsewhere). Response
        data.prediction.{category, probability, threshold{low,medium,high}}.
      x-evidence: raw-modules/1009.js:56-77
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, format: binary }
            description: gzip(JSON({fullText}))
      responses:
        "200":
          description: Prediction with thresholds.
          content:
            application/json:
              schema:
                type: object
                properties:
                  prediction:
                    type: object
                    properties:
                      category: { type: string }
                      probability: { type: number }
                      threshold:
                        type: object
                        properties:
                          low: { type: number }
                          medium: { type: number }
                          high: { type: number }
  /proxy/v1/predictions:
    servers: [{ url: "https://x3-predictor.goguardian.com" }]
    post:
      tags: [Smart Alerts (X3)]
      operationId: predictX3Proxy
      summary: Full-text prediction via CORS proxy (base64 deflate)
      description: >-
        contentType: false. Body: btoa(pako.deflate(JSON.stringify({
        fullText (capped at 30000 chars, truncated at the last space),
        requestID (ISO date with ms truncated to 19 chars, "T" replaced by
        "-", then "-" + url.slice(0,500))
        }))). Response data.{probability, threshold{very_high, high, medium}}.
      x-evidence: raw-modules/1009.js:32-92, raw-modules/1643.js:144-167
      requestBody:
        required: true
        content:
          text/plain:
            schema: { type: string }
            description: btoa(deflate(JSON({fullText, requestID})))
      responses:
        "200":
          description: Proxy prediction result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  probability: { type: number }
                  threshold:
                    type: object
                    properties:
                      very_high: { type: number }
                      high: { type: number }
                      medium: { type: number }
  /v1/reports:
    servers: [{ url: "https://x3-reporter.goguardian.com" }]
    post:
      tags: [Smart Alerts (X3)]
      operationId: createX3Report
      summary: Submit a self-harm alert report
      description: >-
        contentType "application/json; charset=utf-8", timeout 7000 ms,
        headers Authorization + X-GG-X3EXTVER. Retried 3 attempts at 500 *
        2^n ms. Response {reportId, contentDescriptor, screenshotDescriptor,
        imageDescriptor}.
      x-evidence: raw-modules/697.js:31-47
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/X3ReportRequest' }
      responses:
        "200":
          description: Report created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/X3ReportResponse' }
  /v1/beacon/reports:
    servers: [{ url: "https://x3-reporter.goguardian.com" }]
    post:
      tags: [Beacon (crisis response)]
      operationId: createBeaconReport
      summary: Submit a Beacon crisis report
      description: >-
        Hand-rolled fetch: lowercase headers authorization / x-gg-x3extver.
        JSON {url, timestamp, prediction, closedMessage: false, didMessage,
        seqId, model: "FULLPAGE_PREDICTOR" | "TV_SEARCH_PREDICTOR"}.
        Retried 1 s base, 3 attempts. Response {reportId,
        contentDescriptor, screenshotDescriptor}.
      x-evidence: src/1652-beacon-reporter.js:538-562
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BeaconReportRequest' }
      responses:
        "200":
          description: Beacon report created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BeaconReportResponse' }
  /v1/beacon/reports/{reportId}:
    servers: [{ url: "https://x3-reporter.goguardian.com" }]
    put:
      tags: [Beacon (crisis response)]
      operationId: closeBeaconReport
      summary: Mark a Beacon report's crisis message closed
      description: >-
        Lowercase headers. Body {closedMessage: true}. Retried 1 s base, 3
        attempts. Fired after the crisis help modal is dismissed.
      x-evidence: src/1652-beacon-reporter.js:436-459
      parameters:
        - name: reportId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [closedMessage]
              properties:
                closedMessage: { type: boolean, enum: [true] }
      responses:
        "200": { description: Response not read. }
  /v1/policy:
    servers: [{ url: "https://x3-policy-maker.goguardian.com" }]
    get:
      tags: [Smart Alerts (X3)]
      operationId: getX3Policy
      summary: Smart Alerts trigger policy
      description: >-
        timeout 7000 ms, retried 1 s base, 10 attempts. Response cached
        under key "triggers". Response fields read: triggers.messages[] /
        triggers.blocks[] (items: triggerId, name, message, threshold,
        category, ou, triggerSources, blockTarget), blockedCategories
        (mapped to disabledCategories), disabledImageFilteringSiteCategories.
      x-evidence: raw-modules/1621.js:100-153
      responses:
        "200":
          description: X3 trigger policy.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/X3Policy' }
  /v1/beacon/policy:
    servers: [{ url: "https://x3-policy-maker.goguardian.com" }]
    get:
      operationId: getBeaconPolicy
      tags: [Beacon (crisis response)]
      summary: Beacon deployment policy (15-minute refresh)
      description: >-
        Refreshed every 15 min, cached under key "beacon". HTTP 400 with
        error code 9 means "not deployed" and silently disables Beacon.
      x-evidence: raw-modules/1651.js:92-98
      responses:
        "200":
          description: Beacon policy (may be an empty deployment marker).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BeaconPolicy' }
        "400":
          description: Error code 9 = Beacon not deployed for this org.
  /v1/beacon/fullpage:
    servers: [{ url: "https://beacon-fullpage-predictor.goguardian.com" }]
    post:
      tags: [Beacon (crisis response)]
      operationId: predictBeaconFullpage
      summary: Full-page crisis scan
      description: >-
        Hand-rolled fetch with lowercase headers. Body is the content
        object produced by the Beacon miner with its url field set. Retried
        1 s base, 3 attempts. Response
        data.prediction.{phase, probability}.
      x-evidence: src/1652-beacon-reporter.js:203, src/1652-beacon-reporter.js:460-484
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, description: Miner content object with url set. }
      responses:
        "200":
          description: Fullpage prediction.
          content:
            application/json:
              schema:
                type: object
                properties:
                  prediction: { $ref: '#/components/schemas/BeaconPrediction' }
  /threats-and-violence/predict:
    servers: [{ url: "https://ml-endpoint.goguardian.com" }]
    post:
      tags: [Beacon (crisis response)]
      operationId: predictThreatsAndViolence
      summary: Threats-and-violence search-term prediction
      description: >-
        JSON {searchterm}. Response data.{phase, probability}. Used by the
        TV_SEARCH_PREDICTOR Beacon model on search terms.
      x-evidence: src/1652-beacon-reporter.js:660-684
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [searchterm]
              properties:
                searchterm: { type: string }
      responses:
        "200":
          description: TV prediction.
          content:
            application/json:
              schema:
                type: object
                properties:
                  phase: { type: string }
                  probability: { type: number }
  /device-notifications/v1/get-notifications:
    servers: [{ url: "https://x3-policy-maker.goguardian.com" }]
    get:
      tags: [Smart Alerts (X3)]
      operationId: getDeviceNotifications
      summary: Poll admin device notifications
      description: >-
        Retried 1 s base, 10 attempts. Response read for notifications
        destined to this device/session (processed through the
        student-notifications module).
      x-evidence: raw-modules/1043.js
      responses:
        "200":
          description: Pending notifications.
          content:
            application/json:
              schema: { type: object, description: Exact shape not reconstructed. }
  /device-notifications/v1/ack:
    servers: [{ url: "https://x3-policy-maker.goguardian.com" }]
    post:
      tags: [Smart Alerts (X3)]
      operationId: ackDeviceNotifications
      summary: Acknowledge device notifications
      description: >-
        JSON {ack_ids:[...]} of notification ids being acknowledged. Retried
        1 s base, 10 attempts.
      x-evidence: raw-modules/1043.js
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ack_ids]
              properties:
                ack_ids: { type: array, items: { type: string } }
      responses:
        "200": { description: Response not read. }
  /api/v1/ext/fullurl/categories:
    servers: [{ url: "https://panther.goguardian.com" }]
    get:
      tags: [Utilities]
      operationId: categorizeUrl
      summary: Server-side URL categorization
      description: >-
        Query parameter url (the full URL to categorize). Retried 1 s base,
        3 attempts. Response read as {categories:[{cats:[...]}]} (first
        categories entry used).
      x-evidence: raw-modules/859.js:12-30
      parameters:
        - name: url
          in: query
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Category lookup result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items:
                      type: object
                      properties:
                        cats: { type: array, items: { type: string } }
  /taskiness_score:
    servers: [{ url: "https://taskiness-preprocessor.goguardian.com" }]
    post:
      tags: [Taskiness & Video]
      operationId: scoreTaskiness
      summary: On-task scoring for a subject's full text
      description: >-
        URL carries a subject query parameter (e.g. ?subject=<subject>).
        Body is the plain-text fullText (content type text/plain).
        Response is a map subject -> {score} plus a timestamp.
      x-evidence: raw-modules/1725.js:267-299
      parameters:
        - name: subject
          in: query
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          text/plain:
            schema: { type: string }
            description: The full text to score.
      responses:
        "200":
          description: Taskiness scores keyed by subject.
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: object
                  properties:
                    score: { type: number }
                description: Plus a timestamp field alongside the subject keys.
  /v1/video:
    servers: [{ url: "https://play-button.goguardian.com" }]
    get:
      tags: [Taskiness & Video]
      operationId: getVideoMetadata
      summary: YouTube video metadata lookup
      description: >-
        Query parameter videoID. Response data.video is read (fields
        consumed: ID, ageRestricted, channelID, categoryID, plus others
        remapped into VideoMetadata).
      x-evidence: raw-modules/1602.js:15-38
      parameters:
        - name: videoID
          in: query
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Video metadata.
          content:
            application/json:
              schema:
                type: object
                properties:
                  video:
                    type: object
                    description: >-
                      Fields actually consumed: ID, ageRestricted, channelID,
                      categoryID (remapped into VideoMetadata).
                    properties:
                      ID: { type: string }
                      ageRestricted: { type: boolean }
                      channelID: { type: string }
                      categoryID: { type: string }
  /ip:
    servers: [{ url: "https://ip.goguardian.com" }]
    get:
      tags: [Utilities]
      operationId: getPublicIp
      summary: Public IPv4 discovery (no auth)
      description: >-
        security: [] — no compRand/ExtensionVersion headers are sent.
        Response is a dotted quad string. Refresh task every 60000 ms;
        timeout 5 s, retried 1 s base, 5 attempts; fallback "0.0.0.0".
      x-evidence: raw-modules/692.js:41-77
      security: []
      responses:
        "200":
          description: Dotted-quad IPv4 string.
          content:
            text/plain:
              schema: { type: string, example: "203.0.113.7" }
  /api/v1/auth/ext:
    servers: [{ url: "https://sakura.goguardian.com" }]
    post:
      tags: [Pusher Auth]
      operationId: pusherAuth
      summary: Pusher channel authentication
      description: >-
        Header Authorization (compRand). Query parameters: version,
        protocolVersion=1, capabilities (CSV of numeric capability bits —
        base [1,17,18,19,21] plus 2,20 on non-Windows and 3,4 on Windows),
        clientType=extension, os, liveStateVersion=6, plus the pusher
        channel_name and socket_id. Response {auth, channel_data} is
        ASSUMED — it is consumed inside the vendored pusher-js library, not
        visible to the extension code (uncertainty N1).
      x-evidence: src/475-pusher.js:98-165, src/987-auth-endpoint.js:113-121, src/1642-live-state.js:364-433
      parameters:
        - name: version
          in: query
          required: true
          schema: { type: string }
        - name: protocolVersion
          in: query
          required: true
          schema: { type: integer, enum: [1] }
        - name: capabilities
          in: query
          required: true
          schema: { type: string }
          description: CSV of numeric capability bits (1..22).
        - name: clientType
          in: query
          required: true
          schema: { type: string, enum: [extension] }
        - name: os
          in: query
          required: true
          schema: { type: string }
        - name: liveStateVersion
          in: query
          required: true
          schema: { type: integer, enum: [6] }
        - name: channel_name
          in: query
          required: true
          schema: { type: string }
        - name: socket_id
          in: query
          required: true
          schema: { type: string }
      responses:
        "200":
          description: >-
            Pusher auth payload (shape assumed, consumed by vendored
            pusher-js).
          content:
            application/json:
              schema:
                type: object
                properties:
                  auth: { type: string }
                  channel_data: { type: string }
  /v1/ext/sample:
    servers: [{ url: "https://tivan.goguardian.com" }]
    post:
      tags: [Surveillance]
      operationId: uploadSample
      summary: Evidence sample bundle upload (screenshot + JSON artifacts)
      description: >-
        JSON {snapshotIds:[...], data:[{filename ("screenshot.jpeg" |
        "content.json" | "prediction.json" | "metadata.json"), dataURI}]}.
        On failure the sampler enters a 15-minute cooldown before retrying.
      x-evidence: raw-modules/866.js:41-44
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SampleRequest' }
      responses:
        "200": { description: Response not read. }
  /:
    description: >-
      Shared root path: three distinct hosts serve "/" — the pre-signed S3
      evidence bucket (POST), www.google.com (HEAD, clock sync), and
      blocked.goguardian.com (GET, generic block page). Each operation
      carries its own servers override, which OpenAPI 3.1 permits at the
      operation level.
    post:
      servers:
        - url: "https://{bucket}.s3.amazonaws.com"
          variables:
            bucket: { description: Evidence upload bucket from the sample descriptor. }
      tags: [S3 Upload]
      operationId: s3MultipartUpload
      summary: S3 multipart evidence upload (pre-signed, no auth headers)
      description: >-
        security: [] — the auth is baked into the pre-signed form fields from
        the sample descriptor (descriptor.auth): Content-Type, key, policy,
        x-amz-algorithm, x-amz-credential, x-amz-date, x-amz-signature,
        plus the file field carrying the blob.
      x-evidence: raw-modules/1011.js:9-34
      security: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [key, policy, x-amz-algorithm, x-amz-credential, x-amz-date, x-amz-signature, file]
              properties:
                "Content-Type": { type: string }
                key: { type: string }
                policy: { type: string }
                x-amz-algorithm: { type: string }
                x-amz-credential: { type: string }
                x-amz-date: { type: string }
                x-amz-signature: { type: string }
                file:
                  type: string
                  format: binary
                  description: The evidence blob (screenshot or JSON artifact).
      responses:
        "200": { description: Response not read. }
    head:
      servers: [{ url: "https://www.google.com" }]
      tags: [Utilities]
      operationId: syncClockFromGoogle
      summary: Clock sync via Google Date header (no auth)
      description: >-
        security: [] — HEAD https://www.google.com/ with timeout 5 s. Reads
        only the Date response header to correct the extension clock; on
        failure the system clock is trusted. Response body never consumed.
      x-evidence: raw-modules/693.js:78-105
      security: []
      responses:
        "200":
          description: Only the Date header is read; body ignored.
          headers:
            Date: { schema: { type: string }, description: Server time used for clock sync. }
    get:
      servers:
        - { url: "https://blocked.goguardian.com" }
        - { url: "https://staging.blocked.goguardian.com" }
      tags: [Block Pages]
      operationId: showGenericBlockPage
      summary: Generic admin/teacher/parent block page (no auth)
      description: >-
        security: [] — opened as a tab (not XHR), so no auth headers. Path
        chosen by block reason: ADMIN_* / BLOCK_* / UNKNOWN reasons land
        here. Query ctx = base64(sorted URLSearchParams) with oi (orgID
        multiplied by 1234567), ou, st (sender tab type
        chromium-m|cip|na|shield|unknown), rs (reason enum string), sci,
        api, afi, pfi, x3rpi, tsi, tsfi, tsans (repeatable), v=1; required
        oi/ou/st/rs. Query sum = crc32(ctx) hex, padStart 8. The page
        validates by crc32 match, v < 8192, and oi positive and divisible
        by 1234567. Staging host is used when the extension build targets
        staging.
      x-evidence: src/409-block-url.js:66-125, src/409-block-url.js:139-231, src/409-block-url.js:283-305
      security: []
      parameters:
        - name: ctx
          in: query
          required: true
          schema: { type: string }
          description: base64 of the sorted, stringified URLSearchParams blob.
        - name: sum
          in: query
          required: true
          schema: { type: string }
          description: crc32(ctx) as zero-padded 8-char hex.
      responses:
        "200":
          description: HTML block page (navigated to, not parsed as API).
          content:
            text/html:
              schema: { type: string }
  /ping:
    servers: [{ url: "https://extension.goguardian.com" }]
    get:
      tags: [Utilities]
      operationId: nativeAgentPing
      summary: Native-agent watchdog ping (no auth)
      description: >-
        security: [] — no auth headers. Polled every 5 min while the native
        agent is healthy; expects HTTP 200 with body "pong". On failure the
        poll tightens to 10 s; 6 consecutive misses mark the native agent
        uninstalled, a body change triggers a reload of the agent, and the
        loop never runs on ChromeOS devices.
      x-evidence: src/1517-native-agent-watcher.js:47-100
      security: []
      responses:
        "200":
          description: Literally the string "pong".
          content:
            text/plain:
              schema: { type: string, enum: [pong] }
  /AccountChooser:
    servers: [{ url: "https://accounts.google.com" }]
    get:
      tags: [Utilities]
      operationId: scrapeGoogleAccountChooser
      summary: Google account name scrape (no auth, response not parsed)
      description: >-
        security: [] — fetches the Google account chooser page to read the
        signed-in Google account name from the DOM in a separate context.
        The HTTP response is not consumed as an API. A sibling scrape
        GET https://myaccount.google.com/ (also security: [], response not
        consumed) reads the account name the same way; it shares this
        operation's path key spelling "/" with three other hosts, so it
        cannot carry its own path item in this document.
      x-evidence: raw-modules/986.js:120
      security: []
      responses:
        "200": { description: HTML page; response not consumed as API data. }
  /parent/block.html:
    servers:
      - { url: "https://blocked.goguardian.com" }
      - { url: "https://staging.blocked.goguardian.com" }
    get:
      tags: [Block Pages]
      operationId: showParentBlockPage
      summary: Parent site-filter block page (no auth)
      description: >-
        security: [] — tab navigation, no auth headers. Reason mapping:
        PARENT_SITE_FILTER lands here. Same ctx/sum query contract as the
        generic block page.
      x-evidence: src/409-block-url.js:66-125, src/409-block-url.js:139-231
      security: []
      parameters:
        - name: ctx
          in: query
          required: true
          schema: { type: string }
          description: "base64(sorted URLSearchParams): oi (orgID*1234567), ou, st, rs=PARENT_SITE_FILTER, v=1, optional sci/api/afi/pfi."
        - name: sum
          in: query
          required: true
          schema: { type: string }
          description: crc32(ctx) hex padStart 8.
      responses:
        "200":
          description: HTML block page.
          content:
            text/html:
              schema: { type: string }
  /parent/pause.html:
    servers:
      - { url: "https://blocked.goguardian.com" }
      - { url: "https://staging.blocked.goguardian.com" }
    get:
      tags: [Block Pages]
      operationId: showParentPausePage
      summary: Parent pause / scheduled-pause page (no auth)
      description: >-
        security: [] — tab navigation. Reason mapping: PARENT_PAUSE and
        PARENT_SCHEDULED_PAUSE land here. Same ctx/sum contract.
      x-evidence: src/409-block-url.js:66-125, src/409-block-url.js:139-231
      security: []
      parameters:
        - name: ctx
          in: query
          required: true
          schema: { type: string }
          description: "base64(sorted URLSearchParams): oi, ou, st, rs=PARENT_PAUSE|PARENT_SCHEDULED_PAUSE, v=1."
        - name: sum
          in: query
          required: true
          schema: { type: string }
          description: crc32(ctx) hex padStart 8.
      responses:
        "200":
          description: HTML pause page.
          content:
            text/html:
              schema: { type: string }
  /teacher/block.html:
    servers:
      - { url: "https://blocked.goguardian.com" }
      - { url: "https://staging.blocked.goguardian.com" }
    get:
      tags: [Block Pages]
      operationId: showTeacherBlockPage
      summary: Teacher scene block page (no auth)
      description: >-
        security: [] — tab navigation. Reason mapping: TEACHER_SCENE lands
        here. Same ctx/sum contract, with tsi (teacherSceneId), tsfi
        (teacherSceneFilterId), and repeatable tsans in ctx.
      x-evidence: src/409-block-url.js:66-125, src/409-block-url.js:139-231
      security: []
      parameters:
        - name: ctx
          in: query
          required: true
          schema: { type: string }
          description: "base64(sorted URLSearchParams): oi, ou, st, rs=TEACHER_SCENE, tsi, tsfi, tsans (repeatable), v=1."
        - name: sum
          in: query
          required: true
          schema: { type: string }
          description: crc32(ctx) hex padStart 8.
      responses:
        "200":
          description: HTML block page.
          content:
            text/html:
              schema: { type: string }
  /x3/block.html:
    servers:
      - { url: "https://blocked.goguardian.com" }
      - { url: "https://staging.blocked.goguardian.com" }
    get:
      tags: [Block Pages]
      operationId: showX3BlockPage
      summary: Smart Alerts (X3) block page (no auth)
      description: >-
        security: [] — tab navigation. Reason mapping: X3_REPORT lands
        here. ctx additionally carries x3rpi (x3ReportPublicId).
      x-evidence: src/409-block-url.js:66-125, src/409-block-url.js:139-231
      security: []
      parameters:
        - name: ctx
          in: query
          required: true
          schema: { type: string }
          description: "base64(sorted URLSearchParams): oi, ou, st, rs=X3_REPORT, x3rpi, v=1."
        - name: sum
          in: query
          required: true
          schema: { type: string }
          description: crc32(ctx) hex padStart 8.
      responses:
        "200":
          description: HTML block page with crisis resources.
          content:
            text/html:
              schema: { type: string }
components:
  securitySchemes:
    CompRand:
      type: apiKey
      in: header
      name: Authorization
      description: >-
        Raw compRand token — NO "Bearer" prefix. Issued by
        POST /api/v1/ext/register, persisted at localStorage.compRand. Some
        hand-rolled fetches send it as the lowercase header "authorization".
    ExtensionVersion:
      type: apiKey
      in: header
      name: ExtensionVersion
      description: >-
        The extension's own version string. Hand-rolled fetches may send it
        lowercase ("extensionversion"); the X3 predictor pair uses
        X-GG-X3EXTVER instead.
  schemas:
    RegisterRequest:
      type: object
      required: [orgRands]
      properties:
        orgRands: { type: string }
      description: jQuery form body (application/x-www-form-urlencoded).
    RegisterResponse:
      type: object
      required: [compRand]
      properties:
        compRand: { type: string, description: Session token stored at localStorage.compRand. }
    VersionRequest:
      type: object
      required: [extVer]
      properties:
        extVer: { type: string }
    BypassResponse:
      type: object
      properties:
        success: { type: boolean }
        time: { type: number, description: Bypass window end (epoch ms). }
    NameAndEmailRequest:
      type: object
      required: [name, email]
      properties:
        name: { type: string }
        email: { type: string }
    CreateAccountResponse:
      type: object
      properties:
        message: { type: string }
        success: { type: boolean }
    GeolocationRequest:
      type: object
      required: [latitude, longitude]
      properties:
        latitude: { type: number }
        longitude: { type: number }
      description: submitCoordsGuard.php form body; fire-and-forget.
    UserResponse:
      type: object
      required: [orgRand, email]
      properties:
        orgRand: { type: string }
        email: { type: string }
        role: { type: string }
        orgId: { type: string }
        name: { type: string }
      description: Mismatch of orgRand/email against cached values wipes local state.
    NameAndEmailSelectors:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        email: { type: string }
    PrivacyResponse:
      type: object
      properties:
        apiMap:
          type: object
          additionalProperties: { type: string }
          description: >-
            Optional full-path -> host override map, installed with privacy
            settings; keyed by the complete path string (e.g.
            "/api/v1/ext/policy": "https://alt.goguardian.com").
    PolicyResponse:
      type: object
      description: >-
        Deliberately loose: the policy object is merged into settings
        wholesale. Known top-level groups include scene settings, YouTube
        category filters, smart-filtering config, off-task alerts, and
        feature flags.
    ExperimentsResponse:
      type: object
      properties:
        experiments: { type: object, additionalProperties: true }
    NotificationsResponse:
      type: object
      properties:
        notifications:
          type: array
          items: { type: object, additionalProperties: true }
    ChatMessagesResponse:
      type: object
      properties:
        messages:
          type: array
          items: { type: object, additionalProperties: true }
    DiagnoseRequest:
      type: object
      required: [data]
      properties:
        data:
          type: string
          description: JSON.stringify of a diagnostic state snapshot.
    DeviceIdRequest:
      type: object
      properties:
        deviceId: { type: string }
      description: Plain-text response (dataType text) with the resolved device id.
    ParticipantsResponse:
      type: object
      properties:
        participants:
          type: array
          items: { $ref: '#/components/schemas/Participant' }
    Participant:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        email: { type: string }
        joinedAt: { type: number }
    SnapshotResponse:
      type: object
      required: [hash, url]
      properties:
        hash: { type: string, description: Must equal the client's md5 or the client throws "hash mismatch!". }
        url: { type: string, description: Teacher-facing screenshot URL. }
    ActivitySnapshotRequest:
      type: object
      required: [dataUrl, seqId, orgId, subAccountId]
      properties:
        dataUrl: { type: string, description: JPEG data URL, quality 75. }
        seqId: { type: number }
        orgId: { type: string }
        subAccountId: { type: string }
    LogsRequest:
      type: object
      required: [logs, submissionTime]
      properties:
        logs:
          type: array
          items:
            type: object
            properties:
              severity: { type: string }
              log: { type: string }
              stack_trace: { type: string }
              account_id: { type: string }
              extension_version: { type: string }
              os: { type: string }
              org_id: { type: string }
              sub_account_id: { type: string }
        submissionTime: { type: number }
      description: Server re-serializes with Avro LogsBatch.Log (uncertainty N3).
    X3Prediction:
      type: object
      properties:
        category: { type: string }
        probability: { type: number }
    X3ReportRequest:
      type: object
      required: [url, title, timestamp, sources, prediction, triggerIds]
      properties:
        url: { type: string }
        title: { type: string }
        imageId: { type: string, nullable: true }
        timestamp: { type: number }
        sources: { type: array, items: { type: string, enum: [text, image] } }
        prediction:
          type: object
          properties:
            category: { type: string }
            confidence: { type: number }
        triggerIds: { type: array, items: { type: string } }
        keywords: { type: array, items: { type: string } }
    X3ReportResponse:
      type: object
      properties:
        reportId: { type: string }
        contentDescriptor: { type: string }
        screenshotDescriptor: { type: string }
        imageDescriptor: { type: string }
    BeaconReportRequest:
      type: object
      required: [url, timestamp, prediction, closedMessage, model]
      properties:
        url: { type: string }
        timestamp: { type: number }
        prediction: { type: object, additionalProperties: true }
        closedMessage: { type: boolean, enum: [false] }
        didMessage: { type: boolean }
        seqId: { type: number }
        model: { type: string, enum: [FULLPAGE_PREDICTOR, TV_SEARCH_PREDICTOR] }
    BeaconReportResponse:
      type: object
      properties:
        reportId: { type: string }
        contentDescriptor: { type: string }
        screenshotDescriptor: { type: string }
    X3Policy:
      type: object
      properties:
        triggers:
          type: object
          properties:
            messages:
              type: array
              items: { $ref: '#/components/schemas/X3Trigger' }
            blocks:
              type: array
              items: { $ref: '#/components/schemas/X3Trigger' }
        blockedCategories: { type: array, items: { type: string }, description: Mapped internally to disabledCategories. }
        disabledImageFilteringSiteCategories: { type: array, items: { type: string } }
    X3Trigger:
      type: object
      properties:
        triggerId: { type: string }
        name: { type: string }
        message: { type: string }
        threshold: { type: number }
        category: { type: string }
        ou: { type: string }
        triggerSources: { type: array, items: { type: string } }
        blockTarget: { type: string }
    BeaconPolicy:
      type: object
      description: >-
        Deployment/feature payload cached under key "beacon"; exact fields
        not reconstructed. HTTP 400 with code 9 means not deployed.
    BeaconPrediction:
      type: object
      properties:
        phase: { type: string }
        probability: { type: number }
    SampleRequest:
      type: object
      required: [snapshotIds, data]
      properties:
        snapshotIds: { type: array, items: { type: string } }
        data:
          type: array
          items:
            type: object
            required: [filename, dataURI]
            properties:
              filename: { type: string, enum: [screenshot.jpeg, content.json, prediction.json, metadata.json] }
              dataURI: { type: string }
    EntityBatchPreAuthRecord:
      type: object
      description: >-
        INFORMATIONAL — the decoded Avro record inside the gzipped
        application/binary body of POST /v1/entitybatchpreauth (schema
        version 13). See EntityPreAuth for the per-entity payload.
      required: [type, compRand, submissionTime, extensionVersion, payload]
      properties:
        type: { type: string, enum: [ENTITIES] }
        compRand: { type: string }
        submissionTime: { type: integer, format: int64 }
        extensionVersion: { type: string }
        userAgent: { type: string }
        ip: { type: integer, format: int64, description: IPv4 packed as 256*(256*(256*a+b)+c)+d. }
        deviceId: { type: [string, "null"] }
        payload: { type: array, items: { $ref: '#/components/schemas/EntityPreAuth' } }
    EntityPreAuth:
      type: object
      description: >-
        INFORMATIONAL — per-entity Avro record. FlaggedActivity, searches
        (deprecated), videos, docs, blockedSite, viewings, tabs are the
        substantive surveillance fields; keyFlags is always [] as of
        src/1736; sourceType defaults to 0.
      properties:
        seqId: { type: integer, format: int64 }
        url: { type: string }
        flaggedActivity:
          type: array
          items:
            type: object
            properties:
              isNew: { type: boolean }
              severity: { type: number }
              image: { type: [string, "null"] }
              categoryId: { type: integer }
              flags:
                type: array
                items:
                  type: object
                  properties:
                    term: { type: string }
                    count: { type: integer }
                    severity: { type: number }
                    calculatedSeverity: { type: number }
        keyFlags:
          type: array
          description: Always empty as of src/1736.
          items: { type: object }
        searches:
          type: array
          deprecated: true
          items:
            type: object
            properties:
              isNew: { type: boolean }
              searchTerms: { type: string }
        videos:
          type: array
          items:
            type: object
            properties:
              isNew: { type: boolean }
              title: { type: string }
              uploader: { type: string }
              categoryId: { type: integer }
              rating: { type: integer }
              description: { type: string }
              channelId: { type: string }
              category: { type: string }
              uri: { type: string }
              videoId: { type: string }
              type: { type: string, enum: [YOUTUBE] }
              blockMetadata:
                oneOf:
                  - type: "null"
                  - type: object
                    properties:
                      reason:
                        type: string
                        enum: [ADMIN_YOUTUBE_CATEGORY, ADMIN_YOUTUBE_CHANNEL, ADMIN_YOUTUBE_KEYWORD, ADMIN_YOUTUBE_VIDEO, ADMIN_YOUTUBE_AGE_RESTRICTION, ADMIN_YOUTUBE_SAFE_MODE, UNKNOWN]
                      adminPolicyId: { type: [integer, "null"], format: int64 }
                      adminFilterId: { type: [integer, "null"], format: int64 }
              adminPolicyOverrideVideoId: { type: [integer, "null"], format: int64 }
        docs:
          type: array
          items:
            type: object
            properties:
              isNew: { type: boolean }
              title: { type: string }
        faviconUrl: { type: [string, "null"] }
        title: { type: [string, "null"] }
        tabId: { type: [integer, "null"] }
        blockedSite:
          type: array
          items:
            type: object
            properties:
              isNew: { type: [boolean, "null"] }
              categoryId: { type: integer }
              originalUrl: { type: [string, "null"] }
              reason:
                oneOf:
                  - type: "null"
                  - type: string
                    enum: [BLOCK_WEB_PROXIES, BLOCK_DIRECT_IP_ACCESS, ADMIN_SITE_FILTER, ADMIN_SITE_CATEGORY_FILTER, ADMIN_SAFE_MODE, PARENT_SITE_FILTER, PARENT_PAUSE, PARENT_SCHEDULED_PAUSE, X3_REPORT, TEACHER_SCENE, UNKNOWN, BLOCK_CONSUMER_ACCOUNTS]
              adminPolicyId: { type: integer, format: int64 }
              adminFilterId: { type: integer, format: int64 }
              parentFilterId: { type: integer, format: int64 }
              x3ReportPublicId: { type: [string, "null"] }
              teacherSceneId: { type: [integer, "null"], format: int64 }
              teacherSceneFilterId: { type: [integer, "null"], format: int64 }
        viewings:
          type: array
          items:
            type: object
            properties:
              startTimeMs: { type: integer, format: int64 }
              endTimeMs: { type: integer, format: int64 }
              isDirty: { type: boolean }
              lastSubmissionTimeMs: { type: [integer, "null"], format: int64 }
        isNew: { type: [boolean, "null"] }
        classroomIds: { type: array, items: { type: integer } }
        classroomSessionIds: { type: array, items: { type: integer } }
        isLocked: { type: [boolean, "null"] }
        tabs:
          type: array
          items:
            type: object
            properties:
              url: { type: string }
              title: { type: string }
              faviconUrl: { type: string }
              isActive: { type: boolean }
              id: { type: integer }
        isProxy: { type: [boolean, "null"] }
        sourceType: { type: integer, default: 0 }
        tivanIds: { type: array, items: { type: integer, format: int64 } }
        beaconIds: { type: array, items: { type: integer, format: int64 } }
        offTaskScores:
          type: object
          additionalProperties: { type: number }
          description: Keyed "<classroomSessionId>-<timestamp>".
        dataScienceTags: { type: object, additionalProperties: { type: string } }
        clientSettings:
          oneOf:
            - type: "null"
            - type: object
              properties:
                admin: { type: [boolean, "null"] }
                teacher: { type: [boolean, "null"] }
        adminPolicyOverrideId: { type: [integer, "null"], format: int64 }
        x3ReportPublicIds: { type: array, items: { type: string } }
        containsImageBlock: { type: [boolean, "null"] }
