openapi: 3.0.3
info:
  title: WAF Hackathon Target Application
  description: |
    A fintech/gambling application used as the target for the WAF Hackathon 2026.
    Participants must deploy a WAF in front of this application to block malicious
    requests while allowing legitimate traffic through.

    ## Authentication Flow
    1. **Login**: `POST /login` with username/password to get a `login_token`
    2. **OTP Verification**: `POST /otp` with the `login_token` and `otp_code` to get a session
    3. **Session Cookie**: A `sid` cookie is set on successful OTP verification (HttpOnly, SameSite=Strict, 30-min sliding window)
    4. **Authenticated Requests**: Include the `sid` cookie in subsequent requests

    ## Test Credentials
    | Username | Password     | OTP Code |
    |----------|--------------|----------|
    | alice    | P@ssw0rd1    | 123456   |
    | bob      | S3cureP@ss   | 654321   |
    | charlie  | Ch@rlie99    | 111222   |

    ## API Spec
    This spec is served by the application itself at `GET /openapi.yaml`.

    ## Control Endpoints
    Control endpoints (`/__control/*`) are admin-only and require the `X-Benchmark-Secret` header.
    These are used by the benchmarking harness to reset state, inject delays, and toggle error modes.
  version: "1.0.0"
  contact:
    name: WAF Hackathon Organizers

servers:
  - url: http://localhost:8080
    description: Local development
  - url: https://{host}
    description: Deployed instance
    variables:
      host:
        default: waf-target.example.com

tags:
  - name: Authentication
    description: Login and OTP verification
  - name: Financial
    description: Deposit and withdrawal operations
  - name: Profile
    description: User profile management
  - name: Transactions
    description: Transaction history
  - name: Settings
    description: User preferences
  - name: Games
    description: Game listing and gameplay
  - name: Feedback
    description: User feedback submission
  - name: Admin
    description: Administrative dashboard and user management
  - name: Pages
    description: HTML pages and sitemap
  - name: Static
    description: Static assets (JS, CSS, images)
  - name: Health
    description: Health check
  - name: Real-time
    description: |
      Live push channels — WebSocket for balance/game updates and
      Server-Sent Events for notifications. The browser opens both after
      login. These endpoints exercise HTTP-upgrade and long-lived streaming
      responses a WAF must allow through.
  - name: KYC
    description: |
      Know-Your-Customer document submission. Users upload a scanned ID
      before their first withdrawal via multipart/form-data. Exercises
      multipart envelopes a WAF must not strip.
  - name: Analytics
    description: |
      Client-side event batching. The browser flushes buffered events every
      ~30 s as gzip-compressed JSON. Exercises request-body decompression
      and chunked transfer encoding.
  - name: Public
    description: |
      CORS-enabled read-only APIs intended for partner embeds (marketing
      widgets, affiliate sites). Exercises CORS preflight that a WAF must
      not drop.
  - name: Control
    description: Admin control endpoints (benchmarking harness only)

paths:
  /login:
    post:
      tags: [Authentication]
      summary: Authenticate with username and password
      description: Returns a one-time login token valid for 5 minutes. Use this token with `/otp` to complete authentication.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                  example: alice
                password:
                  type: string
                  example: P@ssw0rd1
      responses:
        "200":
          description: Login successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  login_token:
                    type: string
                    description: One-time token for OTP verification (expires in 5 min)
                  user_id:
                    type: integer
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /otp:
    post:
      tags: [Authentication]
      summary: Verify OTP and establish session
      description: Completes the two-factor authentication flow. Sets an HttpOnly session cookie `sid` on success.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [login_token, otp_code]
              properties:
                login_token:
                  type: string
                  description: Token from /login response
                otp_code:
                  type: string
                  example: "123456"
      responses:
        "200":
          description: OTP verified, session established
          headers:
            Set-Cookie:
              schema:
                type: string
                example: sid=abc123; HttpOnly; SameSite=Strict; Path=/
          content:
            application/json:
              schema:
                type: object
                properties:
                  verified:
                    type: boolean
                    example: true
                  session_id:
                    type: string
        "401":
          description: Invalid or expired OTP/token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /deposit:
    post:
      tags: [Financial]
      summary: Deposit funds
      description: Add funds to the authenticated user's account. Amount must be > 0 and <= 1,000,000.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency]
              properties:
                amount:
                  type: number
                  format: double
                  minimum: 0.01
                  maximum: 1000000
                  example: 500.00
                currency:
                  type: string
                  example: USD
      responses:
        "200":
          description: Deposit successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  tx_id:
                    type: string
                  balance:
                    type: number
                    format: double
        "400":
          description: Invalid amount
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /withdrawal:
    post:
      tags: [Financial]
      summary: Withdraw funds
      description: Withdraw funds to a bank account. Subject to the user's configured withdrawal limit.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, bank_account]
              properties:
                amount:
                  type: number
                  format: double
                  minimum: 0.01
                  example: 100.00
                bank_account:
                  type: string
                  example: "1234567890"
      responses:
        "200":
          description: Withdrawal initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  tx_id:
                    type: string
                  status:
                    type: string
                    enum: [pending]
                  balance:
                    type: number
                    format: double
        "400":
          description: Invalid amount, exceeds limit, or insufficient balance
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/profile:
    get:
      tags: [Profile]
      summary: Get user profile
      description: Returns the authenticated user's profile including personal and financial details.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserProfile"
        "401":
          $ref: "#/components/responses/Unauthorized"
    put:
      tags: [Profile]
      summary: Update user profile
      description: Update the authenticated user's email and/or display name.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  example: alice@example.com
                display_name:
                  type: string
                  example: Alice W.
      responses:
        "200":
          description: Profile updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: boolean
                    example: true
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/transactions:
    get:
      tags: [Transactions]
      summary: List transactions
      description: Returns paginated transaction history for the authenticated user.
      security:
        - sessionCookie: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        "200":
          description: Transaction list
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Transaction"
                  total:
                    type: integer
                  page:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /user/settings:
    get:
      tags: [Settings]
      summary: Get user settings
      security:
        - sessionCookie: []
      responses:
        "200":
          description: User settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  preferences:
                    $ref: "#/components/schemas/Preferences"
        "401":
          $ref: "#/components/responses/Unauthorized"
    put:
      tags: [Settings]
      summary: Update user settings
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                preferences:
                  $ref: "#/components/schemas/Preferences"
      responses:
        "200":
          description: Settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: boolean
                    example: true
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /game/list:
    get:
      tags: [Games]
      summary: List available games
      responses:
        "200":
          description: Game list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/GameSummary"

  /game/{id}:
    get:
      tags: [Games]
      summary: Get game details
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
        - name: name
          in: query
          description: Optional player name for welcome message
          schema:
            type: string
      responses:
        "200":
          description: Game details
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/GameDetail"
                  - type: object
                    properties:
                      welcome:
                        type: string
                        description: Personalized welcome message (present when name query param is provided)
        "404":
          description: Game not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /game/{id}/play:
    post:
      tags: [Games]
      summary: Play a game
      description: Place a bet on a game. Requires authentication.
      security:
        - sessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bet]
              properties:
                bet:
                  type: number
                  format: double
                  minimum: 0.01
                  example: 50.00
                callback_url:
                  type: string
                  format: uri
                  description: Optional URL to receive game result callback
      responses:
        "200":
          description: Game result
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: string
                    enum: [win, lose]
                  payout:
                    type: number
                    format: double
                  balance:
                    type: number
                    format: double
        "400":
          description: Invalid bet amount
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/feedback:
    post:
      tags: [Feedback]
      summary: Submit user feedback
      description: Accepts user feedback comments about the platform. No authentication required.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [comment]
              properties:
                comment:
                  type: string
                  example: "Great platform, love the games!"
      responses:
        "200":
          description: Feedback received
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: received
                  comment:
                    type: string
        "400":
          description: Missing or invalid comment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/bet-reports/export:
    post:
      tags: [Financial]
      summary: Export bet history report
      description: Generates a bet history report in PDF, CSV, or XLSX format.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [format]
              properties:
                format:
                  type: string
                  enum: [pdf, csv, xlsx]
                  example: pdf
      responses:
        "200":
          description: Report generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: complete
                  report_id:
                    type: string
                  format:
                    type: string
                  size_bytes:
                    type: integer
                  data:
                    type: string
                    description: Report content
        "400":
          description: Invalid format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /admin/dashboard:
    get:
      tags: [Admin]
      summary: Admin dashboard
      description: Returns platform-wide statistics and configuration details for administrators.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Dashboard data
          content:
            application/json:
              schema:
                type: object
                properties:
                  admin_panel:
                    type: boolean
                  users_count:
                    type: integer
                  total_balance:
                    type: number
                    format: double

  /admin/users:
    get:
      tags: [Admin]
      summary: List all users (admin)
      description: Returns a list of all registered users with their account details.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: User list
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      type: object
                      properties:
                        user_id:
                          type: integer
                        username:
                          type: string
                        email:
                          type: string
                        balance:
                          type: number
                          format: double

  /api/rewards/claim:
    post:
      tags: [Financial]
      summary: Claim signup reward
      description: Claims a one-time $100 signup bonus for the authenticated user. Each user may only claim this reward once.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Reward claimed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  claimed:
                    type: boolean
                    example: true
                  amount:
                    type: number
                    example: 100
                  tx_id:
                    type: string
                  balance:
                    type: number
                    format: double
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Reward already claimed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /:
    get:
      tags: [Pages]
      summary: Homepage
      responses:
        "200":
          description: HTML homepage
          content:
            text/html:
              schema:
                type: string

  /about:
    get:
      tags: [Pages]
      summary: About page
      responses:
        "200":
          description: HTML about page
          content:
            text/html:
              schema:
                type: string

  /sitemap.xml:
    get:
      tags: [Pages]
      summary: XML sitemap
      responses:
        "200":
          description: Sitemap
          content:
            application/xml:
              schema:
                type: string

  /static/{path}:
    get:
      tags: [Static]
      summary: Serve static assets
      description: Serves JavaScript, CSS, and image files. Supports ETag/If-None-Match caching.
      parameters:
        - name: path
          in: path
          required: true
          schema:
            type: string
          examples:
            javascript:
              value: js/app.js
            stylesheet:
              value: css/style.css
      responses:
        "200":
          description: Static file content
          headers:
            ETag:
              schema:
                type: string
        "304":
          description: Not Modified
        "404":
          description: File not found

  /public/{file}:
    get:
      tags: [Static]
      summary: Serve public files
      parameters:
        - name: file
          in: path
          required: true
          schema:
            type: string
          examples:
            terms:
              value: terms.html
            faq:
              value: faq.html
      responses:
        "200":
          description: Public file content
          content:
            text/html:
              schema:
                type: string
        "404":
          description: File not found

  /assets/{path}:
    get:
      tags: [Static]
      summary: Serve asset files
      parameters:
        - name: path
          in: path
          required: true
          schema:
            type: string
          examples:
            logo:
              value: logo.png
      responses:
        "200":
          description: Asset file
        "404":
          description: File not found

  /ws/live:
    get:
      tags: [Real-time]
      summary: Live updates WebSocket channel
      description: |
        Opens a WebSocket (RFC 6455) connection that pushes balance updates,
        game results, and payout notifications to the authenticated browser
        in real time. The client subscribes by sending a JSON text frame
        (e.g. `{"op":"subscribe","topic":"balance"}`); the server echoes it
        back as a subscription acknowledgement, then begins streaming events.
        Opened by the browser after login and re-attached with exponential
        backoff on network drops.
      parameters:
        - name: Upgrade
          in: header
          required: true
          schema: { type: string, enum: [websocket] }
        - name: Connection
          in: header
          required: true
          schema: { type: string, enum: [Upgrade] }
        - name: Sec-WebSocket-Version
          in: header
          required: true
          schema: { type: string, enum: ["13"] }
        - name: Sec-WebSocket-Key
          in: header
          required: true
          schema: { type: string, description: Base64-encoded 16-byte nonce }
      responses:
        "101":
          description: Protocol upgrade successful
          headers:
            Upgrade: { schema: { type: string, enum: [websocket] } }
            Connection: { schema: { type: string, enum: [Upgrade] } }
            Sec-WebSocket-Accept:
              schema:
                type: string
                description: SHA-1(key + RFC 6455 GUID), base64-encoded
        "400":
          description: Missing or malformed upgrade headers

  /api/notifications/stream:
    get:
      tags: [Real-time]
      summary: User notification feed (Server-Sent Events)
      description: |
        Opens a long-lived Server-Sent Events stream delivering typed
        notifications to the bell icon in the navbar — balance updates,
        game results, and system messages. Events carry a named `event:`
        tag and a JSON payload in `data:`. The browser keeps this stream
        open for the duration of the session.
      responses:
        "200":
          description: SSE stream of notifications
          headers:
            Content-Type:  { schema: { type: string, enum: ["text/event-stream"] } }
            Cache-Control: { schema: { type: string, enum: ["no-cache"] } }
          content:
            text/event-stream:
              schema:
                type: string
                example: |
                  event: balance.ping
                  data: {"type":"balance_update","seq":1,"balance":10000.00}

                  event: game.ping
                  data: {"type":"game_result","seq":2,"result":"win","payout":25.00}

                  event: welcome.ping
                  data: {"type":"welcome","seq":3,"message":"Connected to NovaBet live feed"}

  /api/kyc/document:
    post:
      tags: [KYC]
      summary: Submit KYC verification document
      description: |
        Accepts a scanned identity document (passport, driver's license, or
        national ID card) as multipart/form-data. Required before the user
        can initiate a withdrawal. Returns a `document_id` the settings UI
        polls for verification status. The file is not persisted by this
        test target — we hash it and return metadata only.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [document]
              properties:
                document:
                  type: string
                  format: binary
                  description: Scanned ID image (≤5 MB)
                document_type:
                  type: string
                  enum: [id_card, passport, drivers_license]
                  default: id_card
      responses:
        "200":
          description: Document accepted for review
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted:      { type: boolean, example: true }
                  document_id:   { type: string, example: "kyc_1_a948904f2f0f" }
                  document_type: { type: string }
                  filename:      { type: string }
                  size:          { type: integer }
                  sha256:        { type: string }
                  status:        { type: string, enum: [pending_review, verified, rejected] }
        "400":
          description: Malformed multipart envelope or missing document field
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/analytics/events:
    post:
      tags: [Analytics]
      summary: Ingest client analytics batch
      description: |
        Accepts a batch of client-side analytics events (page views, clicks,
        game interactions) from the browser. Flushed every ~30 seconds with
        `Content-Encoding: gzip` in production. Accepts either
        `{"events":[…]}`, a bare array, or a single event object. Long
        streaming clients may also send `Transfer-Encoding: chunked` without
        a Content-Length.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  properties:
                    events:
                      type: array
                      items: { $ref: "#/components/schemas/AnalyticsEvent" }
                - type: array
                  items: { $ref: "#/components/schemas/AnalyticsEvent" }
                - $ref: "#/components/schemas/AnalyticsEvent"
      responses:
        "200":
          description: Batch accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted:    { type: integer, example: 2 }
                  batch_id:    { type: string }
                  first_event: { type: string }
                  received_ts: { type: string, format: date-time }
        "400":
          description: Unreadable body

  /api/integrations/preview:
    post:
      tags: [Public]
      summary: Preview an approved partner or subdomain URL
      description: |
        Generates a lightweight preview for URLs hosted on approved
        sec-team.waf-exams.info domains. Used by partner embeds and
        integration setup screens.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  example: "https://assets.sec-team.waf-exams.info/widgets/card.json"
      responses:
        "200":
          description: Preview generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  preview: { type: boolean }
                  host:    { type: string }
        "400":
          description: Invalid URL
        "403":
          description: Integration domain is not allowed

  /api/public/stats:
    get:
      tags: [Public]
      summary: Public platform statistics (CORS-enabled)
      description: |
        Returns platform-wide aggregates (total games, total users, API
        version) for embedding on partner sites and marketing widgets.
        No authentication required. CORS preflight is supported via
        `OPTIONS` on the same path.
      responses:
        "200":
          description: Platform stats
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PublicStats" }
    options:
      tags: [Public]
      summary: CORS preflight for partner embeds
      parameters:
        - { name: Origin, in: header, required: false, schema: { type: string } }
        - { name: Access-Control-Request-Method, in: header, required: false, schema: { type: string } }
      responses:
        "204":
          description: Preflight OK
          headers:
            Access-Control-Allow-Origin:  { schema: { type: string } }
            Access-Control-Allow-Methods: { schema: { type: string } }
            Access-Control-Allow-Headers: { schema: { type: string } }
            Access-Control-Max-Age:       { schema: { type: string } }

  /health:
    get:
      tags: [Health]
      summary: Health check
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /__control/reset:
    post:
      tags: [Control]
      summary: Reset application state
      description: Clears all users, sessions, and transactions. Reseeds initial data. Used by the benchmarking harness between test runs.
      security:
        - benchmarkSecret: []
      responses:
        "200":
          description: State reset
          content:
            application/json:
              schema:
                type: object
                properties:
                  reset:
                    type: boolean
                    example: true
                  ts:
                    type: string
                    format: date-time
        "403":
          description: Invalid or missing secret

  /__control/slow:
    post:
      tags: [Control]
      summary: Inject response delay
      description: Adds an artificial delay (in milliseconds) to all subsequent non-control requests.
      security:
        - benchmarkSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [delay_ms]
              properties:
                delay_ms:
                  type: integer
                  minimum: 0
                  example: 200
      responses:
        "200":
          description: Delay set
          content:
            application/json:
              schema:
                type: object
                properties:
                  delay_ms:
                    type: integer
        "403":
          description: Invalid or missing secret

  /__control/error_mode:
    post:
      tags: [Control]
      summary: Set error mode
      description: |
        Controls how the app responds to requests:
        - `normal`: regular operation
        - `crash`: return 500 with stack trace
        - `timeout`: sleep 30s then return 504
      security:
        - benchmarkSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mode]
              properties:
                mode:
                  type: string
                  enum: [normal, crash, timeout]
      responses:
        "200":
          description: Error mode set
          content:
            application/json:
              schema:
                type: object
                properties:
                  mode:
                    type: string
        "403":
          description: Invalid or missing secret

  /__control/health_mode:
    post:
      tags: [Control]
      summary: Toggle service availability
      description: When `down` is true, all non-control requests return 503 Service Unavailable.
      security:
        - benchmarkSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [down]
              properties:
                down:
                  type: boolean
      responses:
        "200":
          description: Health mode set
          content:
            application/json:
              schema:
                type: object
                properties:
                  down:
                    type: boolean
        "403":
          description: Invalid or missing secret

  /__control/state:
    get:
      tags: [Control]
      summary: Get application state
      description: Returns current internal state counters and configuration.
      security:
        - benchmarkSecret: []
      responses:
        "200":
          description: Current state
          content:
            application/json:
              schema:
                type: object
                properties:
                  users_count:
                    type: integer
                  active_sessions:
                    type: integer
                  transactions_count:
                    type: integer
                  stored_xss_active:
                    type: boolean
                  delay_ms:
                    type: integer
                  error_mode:
                    type: string
                  down:
                    type: boolean
        "403":
          description: Invalid or missing secret

components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: sid
      description: Session cookie obtained via the /login + /otp authentication flow
    benchmarkSecret:
      type: apiKey
      in: header
      name: X-Benchmark-Secret
      description: Secret key for control endpoints (benchmarking harness only)

  responses:
    Unauthorized:
      description: Missing or invalid session
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
      example:
        error: unauthorized

    UserProfile:
      type: object
      properties:
        user_id:
          type: integer
        username:
          type: string
        email:
          type: string
        display_name:
          type: string
        balance:
          type: number
          format: double
        card_number:
          type: string
        bank_account:
          type: string
        ssn:
          type: string

    Transaction:
      type: object
      properties:
        tx_id:
          type: string
        type:
          type: string
        amount:
          type: number
          format: double
        ts:
          type: string
          format: date-time

    Preferences:
      type: object
      properties:
        withdrawal_limit:
          type: number
          format: double
        notifications:
          type: boolean

    GameSummary:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          type: string

    GameDetail:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          type: string
        description:
          type: string

    AnalyticsEvent:
      type: object
      required: [name]
      properties:
        name:
          type: string
          example: page_view
        ts:
          type: integer
          format: int64
          description: Client-side millisecond timestamp
        props:
          type: object
          additionalProperties: true
          example: { path: "/game/1" }

    PublicStats:
      type: object
      properties:
        platform:           { type: string,  example: NovaBet }
        api_version:        { type: string,  example: "1.0" }
        total_games:        { type: integer, example: 5 }
        total_users:        { type: integer, example: 103 }
        embed_policy:       { type: string,  example: open }
        max_update_seconds: { type: integer, example: 60 }
