openapi: 3.1.0
info:
  title: CloudyBot API (public subset)
  description: >
    First-pass OpenAPI for user-facing HTTP routes. Excludes admin, internal lean-agent,
    and webhooks. Align with `docs/API-REFERENCE.md` and `docs/API-INVENTORY.md`.
  version: "1.0.0"
  license:
    name: Proprietary
    url: https://cloudybot.ai/terms

servers:
  - url: http://127.0.0.1:3000
    description: Default local API
  - url: https://cloudybot.ai
    description: Production

tags:
  - name: Health
    description: Liveness and readiness
  - name: Auth
    description: Signup, login, session
  - name: Threads
    description: Conversation threads
  - name: Chat
    description: Main chat completion
  - name: Usage
    description: Plan and task meters
  - name: Referral
    description: Referral program

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Session token from `POST /api/auth/signup` or `POST /api/auth/login`.

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          examples:
            - Authentication required

    HealthOk:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          const: true
        rssMb:
          type: integer
          description: Optional — present in live `api/server.js` response.
        heapUsedMb:
          type: integer
        eventLoopDelayMs:
          type: integer

    HealthReadyOk:
      type: object
      required: [ok, ready]
      properties:
        ok:
          type: boolean
          const: true
        ready:
          type: boolean
          const: true

    HealthReadyFail:
      type: object
      properties:
        ok:
          type: boolean
          const: false
        ready:
          type: boolean
          const: false
        error:
          type: string

    SignupRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
        referralCode:
          type: string
          description: Optional 8 hex chars; takes precedence over `ref`.
          pattern: '^[a-f0-9]{8}$'
        ref:
          type: string
          description: Legacy alias for `referralCode`.

    SignupSessionResponse:
      type: object
      required: [token, userId, email]
      properties:
        token:
          type: string
        userId:
          type: string
        email:
          type: string

    SignupPendingResponse:
      type: object
      required: [pending, email, emailDeliveryFailed]
      properties:
        pending:
          type: boolean
          const: true
        email:
          type: string
        emailDeliveryFailed:
          type: boolean

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
        password:
          type: string

    LoginResponse:
      type: object
      required: [token, userId, email]
      properties:
        token:
          type: string
        userId:
          type: string
        email:
          type: string

    LogoutResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          const: true

    MeResponse:
      type: object
      required: [userId, email, googleOnly]
      properties:
        userId:
          type: string
        email:
          type: string
        googleOnly:
          type: boolean

    ThreadSummary:
      type: object
      required: [id, title, createdAt, updatedAt, preview, thread_type, employee_id]
      properties:
        id:
          type: string
          example: thr_a1b2c3d4e5f67890
        title:
          type: string
        createdAt:
          type: string
          description: SQLite datetime string from server.
        updatedAt:
          type: string
        preview:
          type: string
        thread_type:
          type: string
          description: Typically `chat` or `employee` (from `threads.thread_type`).
        employee_id:
          anyOf:
            - type: string
            - type: "null"

    ThreadListResponse:
      type: object
      required: [threads]
      properties:
        threads:
          type: array
          items:
            $ref: '#/components/schemas/ThreadSummary'

    ThreadCreateRequest:
      type: object
      properties:
        title:
          type: string
          maxLength: 100
          default: New Chat

    ThreadCreateResponse:
      type: object
      required: [id, title]
      properties:
        id:
          type: string
        title:
          type: string

    ThreadRenameRequest:
      type: object
      required: [title]
      properties:
        title:
          type: string
          maxLength: 100

    ThreadRenameResponse:
      type: object
      required: [ok, id, title]
      properties:
        ok:
          type: boolean
          const: true
        id:
          type: string
        title:
          type: string

    ThreadDeleteResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          const: true

    ChatRequest:
      type: object
      required: [message, threadId]
      properties:
        message:
          type: string
        threadId:
          type: string
        model:
          type: string
        attachmentFileIds:
          type: array
          items:
            type: string
        fastMode:
          type: boolean
        userId:
          type: string
          description: >
            Optional; must equal session user if sent. Mismatch returns 400.

    ChatUsage:
      type: object
      properties:
        input_tokens:
          type: integer
        output_tokens:
          type: integer
        total_tokens:
          type: integer
        remaining:
          type: integer
        remainingTasks:
          type: integer
        taskCap:
          type: integer

    ChatJsonResponse:
      type: object
      required: [reply]
      properties:
        reply:
          type: string
        usage:
          $ref: '#/components/schemas/ChatUsage'

    UsageResponse:
      type: object
      description: >
        Spread of `getUsage()` plus extra fields from `GET /usage/:userId` handler
        in `api/server.js`.
      required:
        - used
        - remaining
        - cap
        - usedTasks
        - remainingTasks
        - taskCap
        - tasksRemaining
        - plan
        - webSearchesRemaining
        - webSearchesCap
        - imagesRemaining
        - imagesCap
        - videosRemaining
        - videosCap
        - helpersActive
        - helpersCap
      properties:
        used:
          type: integer
        remaining:
          type: integer
        cap:
          type: integer
        usedTasks:
          type: integer
        remainingTasks:
          type: integer
        taskCap:
          type: integer
        tasksRemaining:
          type: integer
        plan:
          type: string
          examples: [free, base, growth, pro, max]
        webSearchesRemaining:
          type: integer
        webSearchesCap:
          type: integer
        imagesRemaining:
          type: integer
        imagesCap:
          type: integer
        videosRemaining:
          type: integer
        videosCap:
          type: integer
        helpersActive:
          type: integer
        helpersCap:
          type: integer

    ReferralResponse:
      type: object
      required:
        - referralCode
        - referralLink
        - referredUsers
        - referralBonusTasks
        - referralRewardsEnabled
      properties:
        referralCode:
          type: string
          pattern: '^[a-f0-9]{8}$'
        referralLink:
          type: string
          format: uri
        referredUsers:
          type: integer
          minimum: 0
        referralBonusTasks:
          type: integer
        referralRewardsEnabled:
          type: boolean

  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            default:
              value:
                error: title required

    Unauthorized:
      description: Missing or invalid Bearer token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            default:
              value:
                error: Not authenticated

    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    TooManyRequests:
      description: Rate limit or quota exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    InternalError:
      description: Server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

paths:
  /health:
    get:
      operationId: getHealth
      tags: [Health]
      summary: Liveness
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthOk'
              example:
                ok: true
                rssMb: 120
                heapUsedMb: 45
        "429":
          description: Rate limited by an upstream proxy or WAF (not from core handler).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /health/ready:
    get:
      operationId: getHealthReady
      tags: [Health]
      summary: Readiness (database)
      security: []
      responses:
        "200":
          description: Database reachable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthReadyOk'
              example:
                ok: true
                ready: true
        "503":
          description: Database not ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthReadyFail'
              example:
                ok: false
                ready: false
                error: SQLITE_CANTOPEN
        "429":
          description: Rate limited by an upstream proxy or WAF (not from core handler).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/auth/signup:
    post:
      operationId: postAuthSignup
      tags: [Auth]
      summary: Create account
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignupRequest'
            example:
              email: newuser@example.com
              password: "hunter2!!"
      responses:
        "200":
          description: Session issued or email verification pending
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SignupSessionResponse'
                  - $ref: '#/components/schemas/SignupPendingResponse'
              examples:
                session:
                  value:
                    token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                    userId: user_01HZY...
                    email: newuser@example.com
                pending:
                  value:
                    pending: true
                    email: newuser@example.com
                    emailDeliveryFailed: false
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          description: IP blocked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Access denied. Contact support.

  /api/auth/login:
    post:
      operationId: postAuthLogin
      tags: [Auth]
      summary: Login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            example:
              email: user@example.com
              password: correcthorsebatterystaple
      responses:
        "200":
          description: Session token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          description: IP blocked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/auth/logout:
    post:
      operationId: postAuthLogout
      tags: [Auth]
      summary: Invalidate current session (Bearer optional)
      description: >
        Send `Authorization: Bearer <token>` to revoke that session. If the header is omitted,
        the server still returns `{ "ok": true }` (see `api/server.js`).
      security: []
      responses:
        "200":
          description: Logged out (always returns ok even if token missing)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogoutResponse'
        "429":
          description: Rate limited by an upstream proxy or WAF.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/auth/me:
    get:
      operationId: getAuthMe
      tags: [Auth]
      summary: Current user
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Authenticated user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
              example:
                userId: user_01HZYABCDEF
                email: user@example.com
                googleOnly: false
        "401":
          $ref: '#/components/responses/Unauthorized'

  /api/threads:
    get:
      operationId: listThreads
      tags: [Threads]
      summary: List threads
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Thread list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadListResponse'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "500":
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createThread
      tags: [Threads]
      summary: Create thread
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadCreateRequest'
            example:
              title: Support ticket
      responses:
        "200":
          description: Created thread id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadCreateResponse'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "500":
          $ref: '#/components/responses/InternalError'

  /api/threads/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      operationId: renameThread
      tags: [Threads]
      summary: Rename thread
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadRenameRequest'
            example:
              title: Renamed chat
      responses:
        "200":
          description: Renamed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadRenameResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "500":
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteThread
      tags: [Threads]
      summary: Delete thread
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadDeleteResponse'
        "400":
          description: Cannot delete last thread
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Cannot delete the last thread. Create a new one first.
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "500":
          $ref: '#/components/responses/InternalError'

  /chat:
    post:
      operationId: postChat
      tags: [Chat]
      summary: Send chat message
      description: >
        Response may be `application/json` or `text/event-stream` (SSE) depending on server mode.
        Errors include moderation, context limits, rate limits, and task caps per `docs/API-REFERENCE.md`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatRequest'
            example:
              threadId: thr_a1b2c3d4e5f67890
              message: Summarize the last message in one sentence.
      responses:
        "200":
          description: Assistant reply (JSON path; SSE not fully schema'd here)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatJsonResponse'
              example:
                reply: Hello! How can I help?
                usage:
                  input_tokens: 10
                  output_tokens: 20
                  total_tokens: 30
                  remaining: 250
                  remainingTasks: 250
                  taskCap: 300
            text/event-stream:
              schema:
                type: string
                description: >
                  SSE stream of assistant tokens; not a single JSON object.
                  Clients should use EventSource or fetch streaming.
        "400":
          $ref: '#/components/responses/BadRequest'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "429":
          $ref: '#/components/responses/TooManyRequests'
        "500":
          $ref: '#/components/responses/InternalError'

  /usage/me:
    get:
      operationId: getUsageMe
      tags: [Usage]
      summary: Usage and caps for the current user
      description: >
        Equivalent to `GET /usage/{userId}` when `userId` is `me` or matches the session.
        This path is documented explicitly for clients; the server also accepts other ids for admins only.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Plan usage snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
              example:
                used: 12
                remaining: 288
                cap: 300
                usedTasks: 12
                remainingTasks: 288
                taskCap: 300
                tasksRemaining: 288
                plan: growth
                webSearchesRemaining: 40
                webSearchesCap: 50
                imagesRemaining: 5
                imagesCap: 10
                videosRemaining: 1
                videosCap: 2
                helpersActive: 0
                helpersCap: 2
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'

  /api/user/referral:
    get:
      operationId: getUserReferral
      tags: [Referral]
      summary: Referral program payload
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Referral link and stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralResponse'
              example:
                referralCode: abcdef12
                referralLink: https://cloudybot.ai/?ref=abcdef12
                referredUsers: 0
                referralBonusTasks: 5
                referralRewardsEnabled: true
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          description: Could not assign or read referral_code (rare)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
