openapi: 3.1.0
info:
  title: ArcadeFactory API
  version: 0.3.2
  description: |
    Standard API for all ArcadeFactory games with server-authoritative scoring and replay verification.
    This spec is optimized for both human and agent clients.
servers:
  - url: https://api.arcadefactory.example
  - url: http://localhost:4000
tags:
  - name: system
  - name: docs
  - name: auth
  - name: account
  - name: api-keys
  - name: games
  - name: runs
  - name: leaderboard
  - name: appeals
  - name: admin
  - name: websocket
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: SessionOrApiKey
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
        details:
          type: object
          additionalProperties: true
      required: [error]
    Account:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        username:
          type: string
        age13Plus:
          type: boolean
        tosAcceptedAt:
          type: [string, "null"]
          format: date-time
        tosVersion:
          type: [string, "null"]
        usernameUpdatedAt:
          type: string
          format: date-time
        usernameRenameEligibleAt:
          type: string
          format: date-time
        trustScore:
          type: integer
          minimum: 0
          maximum: 100
      required:
        - id
        - email
        - username
        - age13Plus
        - tosAcceptedAt
        - tosVersion
        - usernameUpdatedAt
        - usernameRenameEligibleAt
        - trustScore
    AuthMagicRequest:
      type: object
      properties:
        email:
          type: string
          format: email
      required: [email]
    AuthMagicVerify:
      type: object
      properties:
        email:
          type: string
          format: email
        code:
          type: string
          pattern: "^[0-9]{6}$"
        username:
          type: string
          minLength: 3
          maxLength: 24
      required: [email, code]
    AuthAttest:
      type: object
      properties:
        age13Plus:
          type: boolean
          enum: [true]
        tosAccepted:
          type: boolean
          enum: [true]
        tosVersion:
          type: string
      required: [age13Plus, tosAccepted]
    ApiKey:
      type: object
      properties:
        id:
          type: string
          format: uuid
        label:
          type: string
        keyPrefix:
          type: string
        scopes:
          type: array
          items:
            type: string
            enum: [play, read]
        status:
          type: string
          enum: [active, suspended, revoked]
        suspendedReason:
          type: [string, "null"]
        suspendedAt:
          type: [string, "null"]
          format: date-time
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: [string, "null"]
          format: date-time
        rotatedAt:
          type: [string, "null"]
          format: date-time
        rotationDueAt:
          type: [string, "null"]
          format: date-time
        rotationRequired:
          type: boolean
      required:
        - id
        - label
        - keyPrefix
        - scopes
        - status
        - suspendedReason
        - suspendedAt
        - createdAt
        - lastUsedAt
        - rotatedAt
        - rotationDueAt
        - rotationRequired
    ApiKeyCreate:
      type: object
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 64
        scopes:
          type: array
          items:
            type: string
            enum: [play, read]
      required: [label, scopes]
    RunCreate:
      type: object
      properties:
        gameVersionId:
          type: string
          format: uuid
        source:
          type: string
          enum: [ui, api]
      required: [gameVersionId, source]
    ReplayEvent:
      type: object
      properties:
        tick:
          type: integer
          minimum: 0
        intent:
          type: object
          properties:
            type:
              type: string
            payload:
              type: object
              additionalProperties: true
          required: [type]
        stateHash:
          type: [string, "null"]
      required: [tick, intent]
    ReplayUpload:
      type: object
      properties:
        reportedScore:
          type: integer
          minimum: 0
        events:
          type: array
          items:
            $ref: "#/components/schemas/ReplayEvent"
      required: [reportedScore, events]
    RunSummary:
      type: object
      properties:
        runId:
          type: string
          format: uuid
        gameId:
          type: string
        gameVersionId:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, verified, flagged, rejected]
        source:
          type: string
          enum: [ui, api]
        score:
          type: [string, "null"]
        finalTick:
          type: integer
        disconnectDeadlineAt:
          type: [string, "null"]
          format: date-time
        createdAt:
          type: string
          format: date-time
        endedAt:
          type: [string, "null"]
          format: date-time
      required:
        - runId
        - gameId
        - gameVersionId
        - status
        - source
        - score
        - finalTick
        - disconnectDeadlineAt
        - createdAt
        - endedAt
    RunDiagnostics:
      type: object
      properties:
        runId:
          type: string
          format: uuid
        sampleCount:
          type: integer
          minimum: 0
        wsRttAvgMs:
          type: [number, "null"]
        wsRttP95Ms:
          type: [number, "null"]
        wsRttMaxMs:
          type: [number, "null"]
        wsJitterAvgMs:
          type: [number, "null"]
        queueDepthAvg:
          type: [number, "null"]
        queueDepthP95:
          type: [number, "null"]
        queueDepthMax:
          type: [integer, "null"]
        serverAheadAvgTicks:
          type: [number, "null"]
        serverAheadMaxTicks:
          type: [integer, "null"]
        tickDesyncAvgTicks:
          type: [number, "null"]
        tickDesyncMaxTicks:
          type: [integer, "null"]
        correctionEvents:
          type: [integer, "null"]
        correctionAvg:
          type: [number, "null"]
        correctionMax:
          type: [number, "null"]
        fpsAvg:
          type: [number, "null"]
        fpsP05:
          type: [number, "null"]
        longTaskCount:
          type: [integer, "null"]
        longTaskTotalMs:
          type: [number, "null"]
        longTaskMaxMs:
          type: [number, "null"]
        warningCounts:
          type: object
          additionalProperties: true
        diagnostics:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - runId
        - sampleCount
        - warningCounts
        - diagnostics
        - createdAt
        - updatedAt
    RunDiagnosticsUpload:
      type: object
      properties:
        version:
          type: integer
          enum: [1]
        startedAtMs:
          type: number
        finishedAtMs:
          type: number
        wsOpenedAtMs:
          type: number
        wsClosedAtMs:
          type: number
        gameId:
          type: string
        metrics:
          type: object
          additionalProperties: true
        client:
          type: object
          additionalProperties: true
        debug:
          type: object
          additionalProperties: true
      required: [version, metrics]
    LeaderboardEntry:
      type: object
      properties:
        rank:
          type: integer
        runId:
          type: string
          format: uuid
        score:
          type: string
        source:
          type: string
          enum: [ui, api]
        username:
          type: string
        verifiedAt:
          type: string
          format: date-time
        status:
          type: string
          enum: [verified, flagged]
        flagged:
          type: boolean
        flagReasonCode:
          type: [string, "null"]
        flagDetails:
          oneOf:
            - type: object
              additionalProperties: true
            - type: "null"
      required:
        - rank
        - runId
        - score
        - source
        - username
        - verifiedAt
        - status
        - flagged
        - flagReasonCode
        - flagDetails
security: []
paths:
  /health:
    get:
      tags: [system]
      summary: Liveness and DB reachability probe
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                required: [ok]
              examples:
                ok:
                  value:
                    ok: true
  /v1/openapi.yaml:
    get:
      tags: [docs]
      summary: Machine-readable API specification
      responses:
        "200":
          description: OpenAPI YAML
          content:
            application/yaml:
              schema:
                type: string
  /v1/docs:
    get:
      tags: [docs]
      summary: Human-readable API docs landing page
      responses:
        "200":
          description: HTML docs page
          content:
            text/html:
              schema:
                type: string
  /v1/auth/magic/request:
    post:
      tags: [auth]
      summary: Request one-time magic login code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuthMagicRequest"
            examples:
              email:
                value:
                  email: player@example.com
      responses:
        "200":
          description: Code issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  expiresInSec:
                    type: integer
                  devCode:
                    type: [string, "null"]
                required: [ok, expiresInSec]
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: Email delivery failed and fallback disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/auth/magic/verify:
    post:
      tags: [auth]
      summary: Verify magic code and mint a session token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuthMagicVerify"
            examples:
              verify:
                value:
                  email: player@example.com
                  code: "123456"
                  username: arcade_player
      responses:
        "200":
          description: Session issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  account:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      email:
                        type: string
                        format: email
                      username:
                        type: string
                    required: [id, email, username]
                required: [token, account]
        "401":
          description: Invalid or expired code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Attempt/rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/auth/google/config:
    get:
      tags: [auth]
      summary: Discover Google OAuth configuration status
      responses:
        "200":
          description: OAuth provider config state
          content:
            application/json:
              schema:
                type: object
                properties:
                  configured:
                    type: boolean
                  placeholder:
                    type: boolean
                  missingEnv:
                    type: array
                    items:
                      type: string
                  clientId:
                    type: [string, "null"]
                  redirectUri:
                    type: [string, "null"]
                  scopes:
                    type: array
                    items:
                      type: string
                required: [configured, placeholder, missingEnv, clientId, redirectUri, scopes]
  /v1/auth/google/start:
    get:
      tags: [auth]
      summary: Start Google OAuth flow
      description: |
        Use `intent=login` to sign in.
        Use `intent=link` to link Google identity to an existing session account.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: intent
          required: false
          schema:
            type: string
            enum: [login, link]
        - in: query
          name: redirect
          required: false
          schema:
            type: boolean
      responses:
        "200":
          description: Authorization URL payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  intent:
                    type: string
                    enum: [login, link]
                  authorizationUrl:
                    type: string
                  state:
                    type: string
                required: [intent, authorizationUrl, state]
        "302":
          description: Redirect response when `redirect=true`
        "401":
          description: Unauthorized when `intent=link` without session token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: OAuth not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/auth/google/callback:
    get:
      tags: [auth]
      summary: Google OAuth callback handler
      parameters:
        - in: query
          name: state
          required: true
          schema:
            type: string
        - in: query
          name: code
          required: false
          schema:
            type: string
        - in: query
          name: error
          required: false
          schema:
            type: string
        - in: query
          name: error_description
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Session issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  account:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      email:
                        type: string
                        format: email
                      username:
                        type: string
                    required: [id, email, username]
                  oauth:
                    type: object
                    properties:
                      provider:
                        type: string
                      sub:
                        type: string
                      emailVerified:
                        type: boolean
                    required: [provider, sub, emailVerified]
                required: [token, account, oauth]
        "400":
          description: Provider callback error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or expired OAuth state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Provider email not verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Identity/account conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: Provider token exchange/userinfo failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: OAuth not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/auth/me:
    get:
      tags: [auth]
      summary: Get current account profile
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Account profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  account:
                    $ref: "#/components/schemas/Account"
                required: [account]
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/auth/attest:
    post:
      tags: [auth, account]
      summary: Record 13+ and ToS attestation
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuthAttest"
            examples:
              attest:
                value:
                  age13Plus: true
                  tosAccepted: true
                  tosVersion: v1
      responses:
        "200":
          description: Attestation persisted
          content:
            application/json:
              schema:
                type: object
                properties:
                  attested:
                    type: boolean
                  account:
                    type: object
                    properties:
                      accountId:
                        type: string
                        format: uuid
                      age13Plus:
                        type: boolean
                      age13PlusAttestedAt:
                        type: [string, "null"]
                        format: date-time
                      tosAcceptedAt:
                        type: [string, "null"]
                        format: date-time
                      tosVersion:
                        type: [string, "null"]
                    required: [accountId, age13Plus, age13PlusAttestedAt, tosAcceptedAt, tosVersion]
                required: [attested, account]
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/account/username:
    patch:
      tags: [account]
      summary: Update username (30-day cooldown)
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username:
                  type: string
                  minLength: 3
                  maxLength: 24
              required: [username]
      responses:
        "200":
          description: Username update result
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: boolean
                  username:
                    type: string
                  renameEligibleAt:
                    type: string
                    format: date-time
                required: [updated, username, renameEligibleAt]
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rename cooldown active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/account/anomalies:
    get:
      tags: [account]
      summary: List account anomaly events
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
      responses:
        "200":
          description: Anomaly list
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      type: object
                      properties:
                        eventId:
                          type: string
                          format: uuid
                        runId:
                          type: [string, "null"]
                          format: uuid
                        apiKeyId:
                          type: [string, "null"]
                          format: uuid
                        reasonCode:
                          type: string
                        severity:
                          type: integer
                        trustDelta:
                          type: integer
                        details:
                          type: object
                          additionalProperties: true
                        createdAt:
                          type: string
                          format: date-time
                      required: [eventId, runId, apiKeyId, reasonCode, severity, trustDelta, details, createdAt]
                required: [events]
  /v1/api-keys:
    get:
      tags: [api-keys]
      summary: List API keys for session account
      security:
        - bearerAuth: []
      responses:
        "200":
          description: API key list
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKeys:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKey"
                required: [apiKeys]
    post:
      tags: [api-keys]
      summary: Create API key for bot use
      description: Requires account attestation (`/v1/auth/attest`) first.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApiKeyCreate"
            examples:
              default:
                value:
                  label: bot-main
                  scopes: [play, read]
      responses:
        "201":
          description: API key created (token returned once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKey:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      label:
                        type: string
                      keyPrefix:
                        type: string
                      scopes:
                        type: array
                        items:
                          type: string
                      createdAt:
                        type: string
                        format: date-time
                    required: [id, label, keyPrefix, scopes, createdAt]
                  token:
                    type: string
                required: [apiKey, token]
        "403":
          description: Session auth required or attestation required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Active key limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/api-keys/{apiKeyId}:
    delete:
      tags: [api-keys]
      summary: Revoke API key
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: apiKeyId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: API key revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: boolean
                  apiKeyId:
                    type: string
                    format: uuid
                required: [revoked, apiKeyId]
        "404":
          description: API key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/api-keys/{apiKeyId}/rotate:
    post:
      tags: [api-keys]
      summary: Rotate API key secret and re-activate key
      description: Requires account attestation (`/v1/auth/attest`) first.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: apiKeyId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: API key rotated (new token returned once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  rotated:
                    type: boolean
                  apiKey:
                    $ref: "#/components/schemas/ApiKey"
                  token:
                    type: string
                required: [rotated, apiKey, token]
        "404":
          description: API key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/games:
    get:
      tags: [games]
      summary: List active game versions
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Games list
          content:
            application/json:
              schema:
                type: object
                properties:
                  games:
                    type: array
                    items:
                      type: object
                      properties:
                        gameId:
                          type: string
                        displayName:
                          type: string
                        gameVersionId:
                          type: string
                          format: uuid
                        activeVersion:
                          type: string
                        tickRateHz:
                          type: integer
                      required: [gameId, displayName, gameVersionId, activeVersion, tickRateHz]
                required: [games]
        "403":
          description: API key missing `read` scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/games/{gameId}/capabilities:
    get:
      tags: [games]
      summary: Discover per-game API/intents/cost model
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: gameId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Per-game capabilities
          content:
            application/json:
              schema:
                type: object
                properties:
                  gameId:
                    type: string
                  gameVersionId:
                    type: string
                    format: uuid
                  version:
                    type: string
                  tickRateHz:
                    type: integer
                  maxRunDurationSec:
                    type: integer
                  transport:
                    type: object
                    properties:
                      rest:
                        type: boolean
                      websocket:
                        type: boolean
                      replayUploadFallback:
                        type: boolean
                        enum: [false]
                    required: [rest, websocket, replayUploadFallback]
                  intents:
                    type: array
                    items:
                      type: string
                  antiCheat:
                    type: object
                    additionalProperties: true
                required:
                  - gameId
                  - gameVersionId
                  - version
                  - tickRateHz
                  - maxRunDurationSec
                  - transport
                  - intents
                  - antiCheat
        "404":
          description: Game not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/capabilities:
    get:
      tags: [games, docs]
      summary: Global capabilities discovery endpoint for agents
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Global capabilities
          content:
            application/json:
              schema:
                type: object
                properties:
                  version:
                    type: string
                  transport:
                    type: object
                    additionalProperties: true
                  auth:
                    type: object
                    additionalProperties: true
                  games:
                    type: array
                    items:
                      type: object
                      properties:
                        gameId:
                          type: string
                        displayName:
                          type: string
                        gameVersionId:
                          type: string
                          format: uuid
                        version:
                          type: string
                        tickRateHz:
                          type: integer
                        maxRunDurationSec:
                          type: integer
                        intents:
                          type: array
                          items:
                            type: string
                      required:
                        - gameId
                        - displayName
                        - gameVersionId
                        - version
                        - tickRateHz
                        - maxRunDurationSec
                        - intents
                required: [version, transport, auth, games]
              examples:
                bot-discovery:
                  value:
                    version: v1
                    transport:
                      rest: true
                      websocket: true
                    auth:
                      bearer:
                        sessionTokenPrefix: afs_
                        apiKeyPrefix: afk_
                    games:
                      - gameId: snake
                        displayName: Snake
                        gameVersionId: 2fba1d89-76b2-4ec4-b6af-bf9d1620ec0e
                        version: snake@1.0.0
                        tickRateHz: 60
                        maxRunDurationSec: 1800
                        intents: [direction]
  /v1/runs:
    get:
      tags: [runs]
      summary: List runs owned by current account
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - in: query
          name: status
          required: false
          schema:
            type: string
            enum: [pending, verified, flagged, rejected]
      responses:
        "200":
          description: Runs list
          content:
            application/json:
              schema:
                type: object
                properties:
                  runs:
                    type: array
                    items:
                      $ref: "#/components/schemas/RunSummary"
                required: [runs]
    post:
      tags: [runs]
      summary: Start run and mint websocket run token
      description: |
        Requires account attestation. For `source=ui`, use session auth.
        For `source=api`, use API key auth with `play` scope.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RunCreate"
            examples:
              api:
                value:
                  gameVersionId: 2fba1d89-76b2-4ec4-b6af-bf9d1620ec0e
                  source: api
      responses:
        "201":
          description: Run created
          content:
            application/json:
              schema:
                type: object
                properties:
                  runId:
                    type: string
                    format: uuid
                  status:
                    type: string
                  gameVersionId:
                    type: string
                    format: uuid
                  seedCommit:
                    type: string
                  websocket:
                    type: object
                    properties:
                      url:
                        type: string
                      runToken:
                        type: string
                    required: [url, runToken]
                required: [runId, status, gameVersionId, seedCommit, websocket]
        "403":
          description: Wrong auth mode, missing scope, or missing attestation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Game version not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: API key active-run limits reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/runs/{runId}:
    get:
      tags: [runs]
      summary: Get run by id for owner account
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: runId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Run detail
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/RunSummary"
                  - type: object
                    properties:
                      seedCommit:
                        type: string
                      flags:
                        type: array
                        items:
                          type: object
                          properties:
                            reasonCode:
                              type: string
                            details:
                              type: object
                              additionalProperties: true
                          required: [reasonCode, details]
                      diagnostics:
                        oneOf:
                          - $ref: "#/components/schemas/RunDiagnostics"
                          - type: "null"
                    required: [seedCommit, flags, diagnostics]
        "404":
          description: Run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/runs/{runId}/diagnostics:
    post:
      tags: [runs]
      summary: Upload per-run client diagnostics
      description: Upload lightweight telemetry to help diagnose lag vs gameplay mistakes.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: runId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RunDiagnosticsUpload"
      responses:
        "202":
          description: Diagnostics accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted:
                    type: boolean
                  runId:
                    type: string
                    format: uuid
                required: [accepted, runId]
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing scope or run ownership mismatch
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Diagnostics table unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/runs/{runId}/replay-upload:
    post:
      tags: [runs]
      summary: Replay upload endpoint (disabled)
      description: Replay upload is disabled for ranked fairness. Use websocket run ingestion.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: runId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "410":
          description: Replay upload disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing scope or attestation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/leaderboards/{gameVersionId}:
    get:
      tags: [leaderboard]
      summary: Get leaderboard entries for a game version
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: gameVersionId
          required: true
          schema:
            type: string
            format: uuid
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - in: query
          name: includeFlagged
          required: false
          schema:
            type: boolean
        - in: query
          name: window
          required: false
          schema:
            type: string
            enum: [all, daily, weekly, monthly, seasonal]
      responses:
        "200":
          description: Leaderboard
          content:
            application/json:
              schema:
                type: object
                properties:
                  gameVersionId:
                    type: string
                    format: uuid
                  includeFlagged:
                    type: boolean
                  window:
                    type: string
                  entries:
                    type: array
                    items:
                      $ref: "#/components/schemas/LeaderboardEntry"
                required: [gameVersionId, includeFlagged, window, entries]
  /v1/appeals:
    get:
      tags: [appeals]
      summary: List appeals owned by authenticated session account
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - in: query
          name: status
          required: false
          schema:
            type: string
            enum: [open, closed]
      responses:
        "200":
          description: Appeals list
          content:
            application/json:
              schema:
                type: object
                properties:
                  appeals:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                required: [appeals]
    post:
      tags: [appeals]
      summary: Create appeal for your flagged run
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                runId:
                  type: string
                  format: uuid
                note:
                  type: string
                  minLength: 20
                  maxLength: 2000
              required: [runId, note]
      responses:
        "201":
          description: Appeal created
          content:
            application/json:
              schema:
                type: object
                properties:
                  appeal:
                    type: object
                    additionalProperties: true
                required: [appeal]
        "404":
          description: Run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Not appealable or already open
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/appeals/{appealId}/actions:
    get:
      tags: [appeals]
      summary: Get action history for one owned appeal
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: appealId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Appeal action history
          content:
            application/json:
              schema:
                type: object
                properties:
                  appealId:
                    type: string
                    format: uuid
                  actions:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                required: [appealId, actions]
  /v1/admin/appeals:
    get:
      tags: [admin]
      summary: List appeals for moderation queue (admin session only)
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - in: query
          name: status
          required: false
          schema:
            type: string
            enum: [open, closed]
      responses:
        "200":
          description: Appeals queue
          content:
            application/json:
              schema:
                type: object
                properties:
                  reviewer:
                    type: object
                    additionalProperties: true
                  appeals:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                required: [reviewer, appeals]
        "403":
          description: Admin required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/admin/appeals/{appealId}:
    patch:
      tags: [admin]
      summary: Close appeal with resolution note (admin session only)
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: appealId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [closed]
                resolutionNote:
                  type: string
                  minLength: 5
                  maxLength: 2000
              required: [status, resolutionNote]
      responses:
        "200":
          description: Appeal closed
          content:
            application/json:
              schema:
                type: object
                properties:
                  reviewer:
                    type: object
                    additionalProperties: true
                  appeal:
                    type: object
                    additionalProperties: true
                required: [reviewer, appeal]
        "403":
          description: Admin required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/admin/appeals/{appealId}/reopen:
    post:
      tags: [admin]
      summary: Reopen closed appeal (admin session only)
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: appealId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
                  minLength: 5
                  maxLength: 2000
              required: [note]
      responses:
        "200":
          description: Appeal reopened
          content:
            application/json:
              schema:
                type: object
                properties:
                  reviewer:
                    type: object
                    additionalProperties: true
                  appeal:
                    type: object
                    additionalProperties: true
                required: [reviewer, appeal]
  /v1/admin/appeals/{appealId}/actions:
    get:
      tags: [admin]
      summary: Appeal action history (admin session only)
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: appealId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Appeal actions
          content:
            application/json:
              schema:
                type: object
                properties:
                  reviewer:
                    type: object
                    additionalProperties: true
                  appealId:
                    type: string
                    format: uuid
                  actions:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                required: [reviewer, appealId, actions]
  /v1/admin/replay-retention:
    get:
      tags: [admin]
      summary: Replay-retention policy, latest pass, backlog, queue stats
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Replay retention observability
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /v1/admin/replay-retention/passes:
    get:
      tags: [admin]
      summary: Replay-retention pass history (admin session only)
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - in: query
          name: status
          required: false
          schema:
            type: string
            enum: [ok, error]
        - in: query
          name: trigger
          required: false
          schema:
            type: string
            enum: [startup, interval, manual]
      responses:
        "200":
          description: Replay retention pass list
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /v1/admin/replay-retention/run:
    post:
      tags: [admin]
      summary: Queue immediate replay-retention pass (admin session only)
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
                  minLength: 3
                  maxLength: 200
      responses:
        "202":
          description: Manual replay-retention pass queued
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /v1/ws/runs:
    get:
      tags: [websocket, runs]
      summary: WebSocket endpoint for live run intents
      description: |
        This endpoint upgrades to WebSocket and requires a `runToken` query param from `POST /v1/runs`.

        Client -> server frames:
        - `{"type":"intent","clientTick":12,"intent":{"type":"direction","payload":{"value":"left"}}}`
        - `{"type":"finish","reportedScore":120}`
        - `{"type":"ping","nonce":"abc123"}`

        Server -> client frames:
        - `hello`: initial tick and render state
        - `queued`: accepted intent queue depth
        - `state`: authoritative tick/state/score progression
        - `ack_finish`: finish accepted
        - `finished`: run finalized + verification queued
        - `error`: terminal or recoverable protocol errors
      x-websocket: true
      parameters:
        - in: query
          name: runToken
          required: true
          schema:
            type: string
      responses:
        "101":
          description: Switching protocols (WebSocket)
        "400":
          description: Invalid websocket request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
