openapi: 3.1.0
info:
  title: FixVibe REST API
  version: "1.0.0"
  description: |
    Bearer-authenticated v1 API for FixVibe, the DAST scanner for AI-generated
    web apps. Tokens are issued from /account/api-tokens. See
    https://fixvibe.app/docs/api for prose docs.
  contact:
    email: support@fixvibe.app
  license:
    name: Proprietary
servers:
  - url: https://fixvibe.app
security:
  - bearerToken: []
components:
  securitySchemes:
    bearerToken:
      type: http
      scheme: bearer
      bearerFormat: fxv_<43 base64url>
  schemas:
    SeverityCounts:
      type: object
      properties:
        critical: { type: integer, minimum: 0 }
        high:     { type: integer, minimum: 0 }
        medium:   { type: integer, minimum: 0 }
        low:      { type: integer, minimum: 0 }
        info:     { type: integer, minimum: 0 }
    Scan:
      type: object
      required: [id, target_url, target_hostname, mode, status, created_at]
      properties:
        id:                { type: string, format: uuid }
        target_url:        { type: string, format: uri }
        target_hostname:   { type: string }
        mode:              { type: string, enum: [passive, active] }
        status:            { type: string, enum: [queued, running, completed, failed] }
        started_at:        { type: string, format: date-time, nullable: true }
        completed_at:      { type: string, format: date-time, nullable: true }
        findings_count:    { $ref: "#/components/schemas/SeverityCounts" }
        triggered_by:      { type: string, enum: [manual, api, schedule, monitor] }
        created_at:        { type: string, format: date-time }
    Finding:
      type: object
      required: [id, scan_id, check_id, severity, created_at]
      properties:
        id:          { type: string, format: uuid }
        scan_id:     { type: string, format: uuid }
        check_id:    { type: string, example: "secrets.js-bundle-sweep" }
        severity:    { type: string, enum: [critical, high, medium, low, info] }
        title:       { type: string, nullable: true }
        description: { type: string, nullable: true }
        evidence:    { type: object, additionalProperties: true, nullable: true }
        remediation: { type: string, nullable: true }
        cwe_id:      { type: string, nullable: true, example: "CWE-798" }
        created_at:  { type: string, format: date-time }
    Error:
      type: object
      required: [error]
      properties:
        error:                  { type: string }
        message:                { type: string }
        retry_after_seconds:    { type: integer }
        issues:                 { type: array, items: { type: object } }
        quota:                  { type: object, additionalProperties: true }
  responses:
    BadRequest:
      description: Invalid input.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Caller lacks permission for the requested resource.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource does not exist or is not visible to the caller.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Token-level rate limit (10/sec burst, 60/min steady) or quota exceeded.
      headers:
        retry-after:
          schema: { type: integer }
        x-ratelimit-limit:
          schema: { type: integer }
        x-ratelimit-remaining:
          schema: { type: integer }
        x-ratelimit-reset:
          schema: { type: integer, description: "Unix epoch (seconds) when the limit resets." }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
paths:
  /api/v1/scans:
    post:
      summary: Start a scan
      operationId: startScan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [target]
              properties:
                target:
                  type: string
                  description: URL or hostname. Bare hostnames are normalised to https://<host>.
                  example: "https://staging.example.com"
                mode:
                  type: string
                  enum: [passive, active]
                  default: passive
                  description: Active mode requires a paid plan plus verified-domain authorization from the dashboard.
                intensity:
                  type: string
                  enum: [safe, intrusive]
                  default: safe
                  description: Optional for active scans. The requested value must be allowed by the domain authorization.
      responses:
        "201":
          description: Scan enqueued.
          content:
            application/json:
              schema:
                type: object
                required: [id, status, target, mode]
                properties:
                  id:     { type: string, format: uuid }
                  status: { type: string, enum: [queued] }
                  target: { type: string }
                  mode:   { type: string, enum: [passive, active] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      summary: List your scans
      operationId: listScans
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: cursor
          in: query
          schema: { type: string }
          description: Opaque cursor returned in next_cursor.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  scans:       { type: array, items: { $ref: "#/components/schemas/Scan" } }
                  next_cursor: { type: string, nullable: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /api/v1/scans/{scanId}:
    get:
      summary: Get a scan
      operationId: getScan
      parameters:
        - name: scanId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: include_findings
          in: query
          description: When true, returns the full FullReport struct. Default false (per-category summary).
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  scan:   { $ref: "#/components/schemas/Scan" }
                  report: { type: object, additionalProperties: true, nullable: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /api/v1/findings:
    get:
      summary: List findings
      operationId: listFindings
      parameters:
        - name: severity
          in: query
          description: Comma-separated severity filter.
          schema: { type: string, example: "critical,high" }
        - name: check_id
          in: query
          schema: { type: string, example: "secrets.patterns" }
        - name: since
          in: query
          schema: { type: string, format: date-time }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 100 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  findings:    { type: array, items: { $ref: "#/components/schemas/Finding" } }
                  next_cursor: { type: string, nullable: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
