openapi: 3.0.3
info:
  title: Soxway Proxy Checker API
  version: "1.0"
  description: |
    Bulk proxy checker: liveness, protocol, latency, anonymity (transparent /
    anonymous / elite), exit-IP + geo / ASN, residential / mobile / datacenter
    classification, site reachability and composite quality rating.

    Anonymous access is allowed for `/check`, `/jobs/{id}`, `/jobs/{id}/results`,
    `/archive` and `/rating`. Account endpoints (`/me`, `/keys`, `/jobs`)
    require either a session cookie or a `Authorization: Bearer <api_key>`
    header. API keys are issued in the account UI (`/account/api`).
servers:
  - url: http://localhost:8088
    description: Local
  - url: https://api.soxway.example
    description: Production
security:
  - bearerAuth: []
  - cookieAuth: []
  - {}
paths:
  /api/v1/check:
    post:
      summary: Submit a proxy-checking job
      description: |
        Returns a `job_id` immediately. Results stream via WebSocket
        (`/api/v1/jobs/{id}/stream`) and can also be fetched in bulk
        through `/api/v1/jobs/{id}/results`.
      tags: [check]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                proxies:
                  type: string
                  description: Newline / comma / semicolon separated proxies.
                url:
                  type: string
                  description: Alternative to `proxies` — fetch list from this URL.
                type:
                  type: string
                  enum: [fast, full]
                  default: full
                timeout:
                  type: integer
                  description: Per-proxy timeout, seconds.
                  minimum: 3
                  maximum: 60
                  default: 10
                private:
                  type: boolean
                  description: Exclude from the public archive.
                sites:
                  type: array
                  items: { type: string }
                  description: |
                    Hosts to probe through each alive proxy. Whitelisted
                    domains only (see `/api/v1/sites/whitelist`). Anything
                    else is silently dropped.
      responses:
        '200':
          description: Job accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CheckResponse' }
        '400':
          description: Invalid input
        '429':
          description: Rate limit exceeded

  /api/v1/jobs/{id}:
    get:
      tags: [check]
      summary: Job summary (status + progress counters)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '404':
          description: Not found

  /api/v1/jobs/{id}/results:
    get:
      tags: [check]
      summary: List per-proxy results
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
        - in: query
          name: alive
          schema: { type: boolean }
        - in: query
          name: country
          schema: { type: string }
        - in: query
          name: ip_type
          schema: { type: string, enum: [residential, mobile, datacenter] }
        - in: query
          name: protocol
          schema: { type: string, enum: [http, https, socks4, socks5] }
        - in: query
          name: anonymity
          schema: { type: string, enum: [elite, anonymous, transparent] }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 1000, default: 200 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/ProxyResult' }

  /api/v1/jobs/{id}/stream:
    get:
      tags: [check]
      summary: WebSocket stream of per-result events
      description: |
        Upgrade required. Each text frame is a JSON `ProxyResult` for one
        proxy as it completes.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '101':
          description: Switching Protocols

  /api/v1/archive:
    get:
      tags: [archive]
      summary: Daily public archive — list of dates
      parameters:
        - in: query
          name: limit
          schema: { type: integer, default: 90 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  lists:
                    type: array
                    items: { $ref: '#/components/schemas/ArchiveList' }

  /api/v1/archive/{date}:
    get:
      tags: [archive]
      summary: Public alive proxies observed on a date
      parameters:
        - in: path
          name: date
          required: true
          schema: { type: string, example: "2026-05-22" }
        - in: query
          name: country
          schema: { type: string }
        - in: query
          name: ip_type
          schema: { type: string, enum: [residential, mobile, datacenter] }
        - in: query
          name: protocol
          schema: { type: string, enum: [http, https, socks4, socks5] }
        - in: query
          name: limit
          schema: { type: integer, default: 200, maximum: 1000 }
        - in: query
          name: offset
          schema: { type: integer, default: 0 }
      responses:
        '200':
          description: OK
        '404':
          description: No archive for that date

  /api/v1/rating:
    get:
      tags: [archive]
      summary: Top public proxies by composite rating
      description: |
        Distinct (host,port,protocol) tuples ranked by their best rating
        across the recent archive window.
      parameters:
        - in: query
          name: country
          schema: { type: string }
        - in: query
          name: ip_type
          schema: { type: string, enum: [residential, mobile, datacenter] }
        - in: query
          name: protocol
          schema: { type: string, enum: [http, https, socks4, socks5] }
        - in: query
          name: days
          schema: { type: integer, default: 7, minimum: 1, maximum: 30 }
        - in: query
          name: limit
          schema: { type: integer, default: 100, maximum: 500 }
      responses:
        '200':
          description: OK

  /api/v1/auth/register:
    post:
      tags: [auth]
      summary: Create a new account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
      responses:
        '201':
          description: Created — Set-Cookie session attached
        '409':
          description: Email already registered

  /api/v1/auth/login:
    post:
      tags: [auth]
      summary: Sign in
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:    { type: string, format: email }
                password: { type: string }
      responses:
        '200': { description: OK — Set-Cookie session attached }
        '401': { description: Invalid credentials }

  /api/v1/auth/logout:
    post:
      tags: [auth]
      summary: Sign out (revokes the current session)
      responses:
        '204': { description: No Content }

  /api/v1/me:
    get:
      tags: [account]
      summary: Current authenticated user
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        '200': { description: OK }
        '401': { description: Unauthenticated }

  /api/v1/keys:
    get:
      tags: [account]
      summary: Active API key metadata for the current user
      security:
        - cookieAuth: []
      responses:
        '200': { description: OK }
        '401': { description: Unauthenticated }

  /api/v1/keys/rotate:
    post:
      tags: [account]
      summary: Generate a new API key (revokes the current one)
      description: |
        The plain `key` is included in the response **once** — store it
        immediately, it cannot be retrieved later. Only the prefix is
        persisted for display.
      security:
        - cookieAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:         { type: string, format: uuid }
                  prefix:     { type: string }
                  key:        { type: string, description: "Shown once." }
                  created_at: { type: string, format: date-time }

  /api/v1/jobs:
    get:
      tags: [account]
      summary: List your jobs
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - in: query
          name: limit
          schema: { type: integer, default: 50, maximum: 200 }
        - in: query
          name: offset
          schema: { type: integer, default: 0 }
      responses:
        '200':
          description: OK
        '401':
          description: Unauthenticated

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: api_key
    cookieAuth:
      type: apiKey
      in: cookie
      name: soxway_session
  schemas:
    CheckResponse:
      type: object
      properties:
        job_id:  { type: string, format: uuid }
        total:   { type: integer }
        invalid: { type: array, items: { type: string } }
    Job:
      type: object
      properties:
        id:           { type: string, format: uuid }
        type:         { type: string, enum: [fast, full] }
        status:       { type: string, enum: [queued, running, done, failed] }
        total:        { type: integer }
        checked:      { type: integer }
        alive:        { type: integer }
        is_private:   { type: boolean }
        timeout_sec:  { type: integer }
        created_at:   { type: string, format: date-time }
        updated_at:   { type: string, format: date-time }
    ProxyResult:
      type: object
      properties:
        raw:          { type: string }
        protocol:     { type: string, enum: [http, https, socks4, socks5, ""] }
        host:         { type: string }
        port:         { type: integer }
        alive:        { type: boolean }
        latency_ms:   { type: integer }
        anonymity:    { type: string, enum: [elite, anonymous, transparent, ""] }
        exit_ip:      { type: string }
        country:      { type: string }
        country_code: { type: string }
        region:       { type: string }
        city:         { type: string }
        asn:          { type: integer }
        isp:          { type: string }
        ip_type:      { type: string, enum: [residential, mobile, datacenter, ""] }
        error:        { type: string }
        rating:       { type: integer, description: "Composite quality 0..100" }
        grade:        { type: string, enum: [A, B, C, D, F, ""] }
        sites:
          type: object
          additionalProperties:
            type: object
            properties:
              code:    { type: integer }
              ms:      { type: integer }
              blocked: { type: boolean }
              err:     { type: string }
    ArchiveList:
      type: object
      properties:
        date:       { type: string, example: "2026-05-22" }
        total:      { type: integer }
        alive:      { type: integer }
        created_at: { type: string, format: date-time }
