openapi: 3.1.0
info:
  title: WRNexus Managed CAPTCHA API
  version: 0.5.0
servers:
  - url: https://captcha.example.com
paths:
  /v1/challenges:
    post:
      summary: Create a public challenge
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [siteKey, action]
              properties:
                siteKey: { type: string }
                action: { type: string }
                type:
                  {
                    type: string,
                    enum:
                      [
                        number,
                        alpha,
                        alphanumeric,
                        calculation,
                        image,
                        honeypot,
                        timing,
                        not-robot,
                      ],
                  }
                presentation: { type: string, enum: [visual, audio, invisible] }
                difficulty: { type: string, enum: [easy, normal, hard] }
                disturbance: { type: integer, minimum: 25, maximum: 75, default: 50 }
                imageStyle:
                  type: string
                  default: random
                  enum:
                    [
                      random,
                      classic,
                      collision,
                      snow,
                      corrosion,
                      spiderweb,
                      cross-shadow,
                      split,
                      split2,
                      cut,
                      darts,
                      distortion,
                      stitch,
                      striped,
                      wave,
                      grid-noise,
                      scribble,
                      pixel,
                      broken-lines,
                    ]
                allowedStyles:
                  oneOf:
                    - { type: string, description: Comma-separated renderer names }
                    - type: array
                      items: { type: string }
                excludedStyles:
                  oneOf:
                    - { type: string, description: Comma-separated renderer names }
                    - type: array
                      items: { type: string }
                randomizeStyle: { type: boolean, default: false }
                locale: { type: string }
      responses:
        "201":
          description: Challenge created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Challenge" }
        "403": { description: Invalid site key or hostname }
        "429": { description: Project quota exceeded }
  /v1/solve:
    post:
      summary: Solve a public challenge and receive a temporary response token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [challengeId, action]
              properties:
                challengeId: { type: string }
                action: { type: string }
                answer: { oneOf: [{ type: string }, { type: number }] }
                selections: { type: array, items: { type: string } }
                honeypot: { type: string }
                timingToken: { type: string }
      responses:
        "200":
          description: Challenge solved
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Verification" }
        "400": { description: Challenge rejected }
  /v1/verify:
    post:
      summary: Verify and consume a response token on the application server
      security: [{ projectSecret: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [responseToken, action]
              properties:
                responseToken: { type: string }
                action: { type: string }
                hostname: { type: string }
                sessionId: { type: string }
      responses:
        "200":
          description: Token accepted and consumed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Verification" }
        "400": { description: Token invalid, expired, mismatched, or already used }
        "401": { description: Invalid project secret }
  /v1/projects:
    get:
      summary: List projects
      security: [{ adminToken: [] }]
      responses:
        "200": { description: Project list without secrets }
    post:
      summary: Create a project and return keys once
      security: [{ adminToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                allowedHostnames: { type: array, items: { type: string } }
                monthlyQuota: { type: integer, minimum: 1 }
      responses:
        "201": { description: Project and initial keys }
  /v1/projects/{id}/rotate-secret:
    post:
      summary: Rotate a project verification secret
      security: [{ adminToken: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: New secret returned once }
        "404": { description: Project not found }
components:
  securitySchemes:
    projectSecret:
      type: http
      scheme: bearer
    adminToken:
      type: http
      scheme: bearer
  schemas:
    Challenge:
      type: object
      required:
        [id, provider, type, presentation, action, prompt, createdAt, expiresAt, responseField]
      properties:
        id: { type: string }
        provider: { type: string }
        type: { type: string }
        presentation: { type: string }
        action: { type: string }
        prompt: { type: string }
        image: { type: string }
        audioUrl: { type: string }
        items: { type: array, items: { type: object } }
        createdAt: { type: integer }
        expiresAt: { type: integer }
        responseField: { type: string }
        metadata:
          type: object
          properties:
            difficulty: { type: string }
            disturbance: { type: integer }
            imageStyle: { type: string }
            requestedImageStyle: { type: string }
            imageStylePool:
              type: array
              items: { type: string }
    Verification:
      type: object
      required: [success, provider, action]
      properties:
        success: { type: boolean }
        provider: { type: string }
        action: { type: string }
        code: { type: string }
        message: { type: string }
        responseToken: { type: string }
        expiresAt: { type: integer }
