openapi: 3.1.0
info:
  title: Querycop Admin API
  version: 0.1.0
  description: |
    Querycop is an AI-powered SQL firewall / DB access proxy. This file is the
    canonical machine-readable description of the admin HTTP API exposed by
    `cmd/querycop` (the proxy binary).

    This file is the source of truth for the route surface. CI runs
    `go run ./tools/check_openapi_routes` which AST-parses
    `pkg/api/handler.go` and diffs the registered routes against this spec.
    Any drift fails CI. Add or remove routes here at the same time you edit
    `RegisterRoutes`.

    Authentication: most endpoints require Bearer auth via
    `Authorization: Bearer <ADMIN_API_KEY>` (or an OIDC-issued session
    cookie). A handful of endpoints are intentionally unauthenticated:
    health probes, metrics scrape, dashboard SPA, OIDC bootstrap, license
    status, and the Slack signature-verified webhook.

    Schemas are intentionally minimal in this revision: the goal of this spec
    is route coverage and CI drift detection, not full request/response
    contract definition. Detailed schemas will be added in a follow-up.
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0

servers:
  - url: http://localhost:8080
    description: Local development

tags:
  - name: infra
    description: Liveness, readiness, metrics
  - name: dashboard
    description: SPA dashboard and SPA fallback routing
  - name: auth
    description: API-key + OIDC browser login
  - name: approval
    description: Pending query approval workflow
  - name: license
    description: License status (Pro/Trial/Community)
  - name: audit
    description: Audit log read-only access
  - name: policy
    description: RBAC policy management
  - name: review
    description: Standalone SQL review and dry-run simulation
  - name: masking
    description: Column masking rules
  - name: breakglass
    description: Emergency-access activation
  - name: slack
    description: Slack interactive callbacks
  - name: sessions
    description: Recorded session playback
  - name: jit
    description: Just-in-time access requests
  - name: ws
    description: WebSocket event stream

paths:
  # --- Infrastructure ---
  /healthz:
    get:
      tags: [infra]
      summary: Liveness probe
      security: []
      responses:
        "200":
          description: Process is alive
          content:
            application/json: {}

  /readyz:
    get:
      tags: [infra]
      summary: Readiness probe (verifies backend DB TCP reachable)
      security: []
      responses:
        "200": { description: Ready }
        "503": { description: Not ready }

  /metrics:
    get:
      tags: [infra]
      summary: Prometheus metrics (text exposition)
      description: Only registered when a metrics collector is configured.
      security: []
      responses:
        "200":
          description: Prometheus 0.0.4 text format
          content:
            text/plain: {}

  # --- Dashboard / SPA ---
  /:
    get:
      tags: [dashboard]
      summary: Dashboard SPA root
      description: |
        Returns the embedded `index.html`. Any unmatched path also falls
        through this handler (SPA fallback).
      security: []
      responses:
        "200":
          description: HTML SPA
          content:
            text/html: {}

  # --- Auth ---
  /auth/login:
    post:
      tags: [auth]
      summary: API-key login (issues session cookie)
      security: []
      responses:
        "200": { description: Cookie session established }
        "401": { description: Bad API key }

  /auth/logout:
    post:
      tags: [auth]
      summary: Invalidate the current session cookie
      responses:
        "200": { description: Logged out }

  /auth/me:
    get:
      tags: [auth]
      summary: Return current authenticated subject
      responses:
        "200": { description: Identity payload }
        "401": { description: Unauthenticated }

  /auth/oidc/available:
    get:
      tags: [auth]
      summary: Boolean capability probe for OIDC login
      security: []
      responses:
        "200":
          description: '{"available": true|false}'
          content:
            application/json: {}

  /auth/oidc/login:
    get:
      tags: [auth]
      summary: Begin OIDC authorization-code flow (302 redirect)
      description: Only registered when OIDC is configured.
      security: []
      responses:
        "302": { description: Redirect to OIDC issuer }

  /auth/oidc/callback:
    get:
      tags: [auth]
      summary: OIDC authorization-code callback
      description: Only registered when OIDC is configured.
      security: []
      responses:
        "302": { description: Redirect to dashboard with session cookie }
        "400": { description: Invalid state or code }

  # --- Approval workflow ---
  /requests:
    get:
      tags: [approval]
      summary: List pending approval requests
      responses:
        "200": { description: Array of pending requests }

  /approve:
    post:
      tags: [approval]
      summary: Approve a pending query by id
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Approved }
        "400": { description: Missing id }
        "404": { description: Unknown id }

  /reject:
    post:
      tags: [approval]
      summary: Reject a pending query by id
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Rejected }
        "400": { description: Missing id }
        "404": { description: Unknown id }

  /explain:
    get:
      tags: [approval]
      summary: Get AI risk explanation for a pending request
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Risk explanation payload }
        "404": { description: Unknown id }

  # --- License ---
  /license:
    get:
      tags: [license]
      summary: Current license status (tier, trial, features, expiry)
      description: |
        Unauthenticated by design. The dashboard nudge and trial countdown
        UI consume this even before login. The response is mutated at
        runtime if the heartbeat client downgrades to Community after a
        revocation or offline-grace timeout.
      security: []
      responses:
        "200":
          description: License status JSON
          content:
            application/json: {}

  # --- Audit ---
  /audit:
    get:
      tags: [audit]
      summary: Tail audit log entries (read-only)
      responses:
        "200": { description: Audit JSONL entries }

  # --- Policy / RBAC ---
  /policies:
    get:
      tags: [policy]
      summary: Get current RBAC policy config
      responses:
        "200": { description: Policy JSON }
    put:
      tags: [policy]
      summary: Replace the RBAC policy config (hot reload)
      requestBody:
        required: true
        content:
          application/json: {}
      responses:
        "200": { description: Policy updated }
        "400": { description: Invalid policy JSON }
        "413": { description: Body too large (>1MB) }

  # --- WebSocket ---
  /ws:
    get:
      tags: [ws]
      summary: WebSocket event stream (HTTP Upgrade)
      description: |
        HTTP Upgrade to WebSocket. Auth is enforced via cookie session,
        `?token=<API_KEY>` query param, or OIDC session.
      parameters:
        - in: query
          name: token
          required: false
          schema: { type: string }
      responses:
        "101": { description: Switching Protocols (WebSocket) }
        "403": { description: Origin or auth rejected }

  # --- SQL Review / Simulate ---
  /api/v1/review:
    post:
      tags: [review]
      summary: Standalone SQL review (no DB execution)
      requestBody:
        required: true
        content:
          application/json: {}
      responses:
        "200": { description: Risk score + reasoning }
        "400": { description: Invalid request }

  /api/v1/simulate:
    post:
      tags: [review]
      summary: Side-effect-free policy simulator
      requestBody:
        required: true
        content:
          application/json: {}
      responses:
        "200": { description: Simulated decision }

  # --- Masking ---
  /masking/rules:
    get:
      tags: [masking]
      summary: List masking rules
      responses:
        "200": { description: Rules array }
    post:
      tags: [masking]
      summary: Add a masking rule
      requestBody:
        required: true
        content:
          application/json: {}
      responses:
        "201": { description: Rule added }
        "400": { description: Invalid rule }

  /masking/config:
    put:
      tags: [masking]
      summary: Replace the entire masking configuration
      requestBody:
        required: true
        content:
          application/json: {}
      responses:
        "200": { description: Config replaced }
        "400": { description: Invalid config }

  # --- Break-glass ---
  /breakglass/activate:
    post:
      tags: [breakglass]
      summary: Activate emergency-access window
      requestBody:
        required: true
        content:
          application/json: {}
      responses:
        "201": { description: Activated }
        "400": { description: Invalid request body }
        "403": { description: Token missing or wrong (when GATEKEEPER_BREAKGLASS_TOKEN set) }

  /breakglass/deactivate:
    post:
      tags: [breakglass]
      summary: Deactivate the active emergency-access window
      responses:
        "200": { description: Deactivated }

  /breakglass/status:
    get:
      tags: [breakglass]
      summary: Current emergency-access status
      responses:
        "200": { description: Active flag + metadata }

  # --- Slack ---
  /slack/interactions:
    post:
      tags: [slack]
      summary: Slack interactive component callback
      description: |
        Authenticated via Slack signing-secret HMAC. No bearer token used.
      security: []
      responses:
        "200": { description: Slack ack }
        "401": { description: Bad Slack signature }

  # --- Sessions ---
  /sessions:
    get:
      tags: [sessions]
      summary: List recorded sessions
      responses:
        "200": { description: Sessions array }

  /sessions/detail:
    get:
      tags: [sessions]
      summary: Read full recorded session by id
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Session detail }
        "404": { description: Unknown id }

  # --- JIT access ---
  /access/request:
    post:
      tags: [jit]
      summary: Submit a JIT access request
      requestBody:
        required: true
        content:
          application/json: {}
      responses:
        "201": { description: Request created }

  /access/requests:
    get:
      tags: [jit]
      summary: List pending JIT access requests
      responses:
        "200": { description: Pending requests }

  /access/approve:
    post:
      tags: [jit]
      summary: Approve a JIT access request
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Approved + temporary credentials issued }
        "404": { description: Unknown id }

  /access/reject:
    post:
      tags: [jit]
      summary: Reject a JIT access request
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Rejected }
        "404": { description: Unknown id }

  /access/active:
    get:
      tags: [jit]
      summary: List currently-active JIT sessions
      responses:
        "200": { description: Active sessions }

  /access/revoke:
    post:
      tags: [jit]
      summary: Revoke an active JIT session
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string }
      responses:
        "200": { description: Revoked }
        "404": { description: Unknown id }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer <ADMIN_API_KEY>`. Endpoints with `security: []`
        explicitly opt out (health, dashboard, metrics, OIDC bootstrap,
        /license, Slack callback).
    sessionCookie:
      type: apiKey
      in: cookie
      name: querycop_session
      description: Cookie session issued by /auth/login or OIDC callback.

security:
  - bearerAuth: []
  - sessionCookie: []
