openapi: 3.1.0
info:
  title: obserae REST API
  version: "1.0.0"
  license:
    name: See EULA.txt
  description: |
    Machine-readable description of the obserae HTTP API. Every path under
    `/api/…` returns JSON; the human-friendly guide with `curl` examples is in
    [rest-api.md](rest-api.md).

    ## Authentication
    All endpoints require an API token sent as a Bearer credential
    (`Authorization: Bearer obs_…`), except the two public probes `GET /healthz`
    and `GET /api/protocols`. Tokens are minted in the GUI (Identity & Access →
    Users → + New token) or via `obserae-cli user token-add`, and inherit their
    owner's permissions. Bearer clients are exempt from CSRF and MFA.

    ## Permissions
    Each operation states the permission its caller must hold in `x-permission`
    (and in the description). A caller missing it gets `403`. `x-permission: ""`
    means any authenticated token is enough.

    ## WebSockets
    Three real-time streams are **not** modelled here (OpenAPI does not describe
    WebSocket handshakes): `GET /ws/health` (any authenticated),
    `GET /ws/alerts` (`alerts:read`) and `GET /ws/carto` (`cartography:read`).
    See rest-api.md.

    obserae is in **alpha** — endpoints and payloads may change between releases.

servers:
  - url: http://127.0.0.1:8080
    description: Default local bind (web.bind)

security:
  - bearerAuth: []

tags:
  - name: Health
    description: Liveness probe and the machine-facing status snapshot.
  - name: NFQL
    description: Execute NFQL queries and manage saved queries.
  - name: Cartography
    description: Read and edit the network map (hosts, networks, groups, services).
  - name: Rules
    description: Flow-matrix rules — model and inspect allowed connectivity.
  - name: Alerting
    description: Detection rules and importable rule sets.
  - name: Anomaly
    description: Statistical anomaly rules and their learned baselines.
  - name: Alerts
    description: Read and acknowledge detection alerts.
  - name: Sessions
    description: Read session aggregations.
  - name: Enrichment
    description: Enrichment sources and flow exporters.
  - name: Devices
    description: OPNsense device connectors.
  - name: Outputs
    description: Alert delivery outputs (webhooks, syslog, chat…).
  - name: Lifecycle
    description: Storage, retention and backup operations.
  - name: Config I/O
    description: Export and import the YAML config bundle and master-key material.
  - name: Audit log
    description: Query and verify the tamper-evident audit journal.
  - name: Identity
    description: Users, groups, tokens and the LDAP/OIDC/login-security config.
  - name: Account
    description: Self-service MFA and password management for the current user.

paths:
  # ---------------------------------------------------------------- Health
  /healthz:
    get:
      tags: [Health]
      summary: Liveness probe
      operationId: healthz
      description: Public, unauthenticated plain-text probe. Returns `ok`.
      security: []
      x-permission: ""
      responses:
        "200":
          description: Daemon is alive
          content:
            text/plain:
              schema: { type: string, example: ok }

  /api/status:
    get:
      tags: [Health]
      summary: Daemon status snapshot
      operationId: getStatus
      description: |
        Full machine-facing JSON snapshot — identical to `obserae-cli status`.
        Requires `monitoring:read`.
      x-permission: monitoring:read
      responses:
        "200":
          description: Status snapshot
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Status" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/protocols:
    get:
      tags: [Health]
      summary: IANA protocol table
      operationId: getProtocols
      description: Public IANA protocol-number → name table.
      security: []
      x-permission: ""
      responses:
        "200":
          description: Protocol table
          content:
            application/json:
              schema: { type: object, additionalProperties: true }

  # ------------------------------------------------------------------ NFQL
  /api/query:
    post:
      tags: [NFQL]
      summary: Execute an NFQL query
      operationId: executeQuery
      description: |
        Compile and execute an NFQL pipeline on the read-only pool (row-capped,
        timed). Requires `nfql:execute`. A compile error is returned as **200**
        with a populated `error` field, not an HTTP error.
      x-permission: nfql:execute
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ExecuteRequest" }
      responses:
        "200":
          description: Result rows, or a compile error in the `error` field
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ExecuteResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/query/columns:
    get:
      tags: [NFQL]
      summary: Compile a query and return its columns
      operationId: getQueryColumns
      description: Compile only (no execution). Requires `sessions:read`.
      x-permission: sessions:read
      parameters:
        - name: query
          in: query
          required: true
          schema: { type: string }
          description: The NFQL source to compile
      responses:
        "200":
          description: Output columns, or a compile error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ColumnsResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/query/schema:
    get:
      tags: [NFQL]
      summary: NFQL schema reference
      operationId: getQuerySchema
      description: Tables, columns, keywords and constants with descriptions. Requires `sessions:read`.
      x-permission: sessions:read
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/query/cookbook:
    get:
      tags: [NFQL]
      summary: Built-in example queries
      operationId: getQueryCookbook
      description: Requires `sessions:read`.
      x-permission: sessions:read
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/query/carto-refs:
    get:
      tags: [NFQL]
      summary: Cartography names usable in predicates
      operationId: getQueryCartoRefs
      description: Requires `sessions:read`.
      x-permission: sessions:read
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/query/resolve-ip:
    get:
      tags: [NFQL]
      summary: Resolve an IP to its cartography entity
      operationId: resolveIp
      description: Requires `sessions:read`.
      x-permission: sessions:read
      parameters:
        - name: ip
          in: query
          required: true
          schema: { type: string, format: ipv4 }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/queries:
    get:
      tags: [NFQL]
      summary: List saved queries
      operationId: listSavedQueries
      description: Requires `sessions:read`.
      x-permission: sessions:read
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [NFQL]
      summary: Create a saved query
      operationId: createSavedQuery
      description: Requires `nfql:execute`.
      x-permission: nfql:execute
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/queries/search:
    get:
      tags: [NFQL]
      summary: Search saved queries
      operationId: searchSavedQueries
      description: Requires `sessions:read`.
      x-permission: sessions:read
      parameters:
        - name: q
          in: query
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/queries/{id}:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [NFQL]
      summary: Read a saved query
      operationId: getSavedQuery
      description: Requires `sessions:read`.
      x-permission: sessions:read
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [NFQL]
      summary: Update a saved query
      operationId: updateSavedQuery
      description: Requires `nfql:execute`.
      x-permission: nfql:execute
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [NFQL]
      summary: Delete a saved query
      operationId: deleteSavedQuery
      description: Requires `nfql:execute`.
      x-permission: nfql:execute
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ----------------------------------------------------------- Cartography
  /api/carto/graph:
    get:
      tags: [Cartography]
      summary: Full cartography graph
      operationId: getCartoGraph
      description: Nodes, edges and groups. Requires `cartography:read`.
      x-permission: cartography:read
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/carto/layout:
    get:
      tags: [Cartography]
      summary: Node layout positions
      operationId: getCartoLayout
      x-permission: cartography:read
      description: Requires `cartography:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/carto/color:
    get:
      tags: [Cartography]
      summary: Group colours
      operationId: getCartoColor
      x-permission: cartography:read
      description: Requires `cartography:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/carto/icon:
    get:
      tags: [Cartography]
      summary: Service icon catalogue
      operationId: getCartoIcon
      x-permission: cartography:read
      description: Requires `cartography:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/carto/os-icon:
    get:
      tags: [Cartography]
      summary: OS icon catalogue
      operationId: getCartoOsIcon
      x-permission: cartography:read
      description: Requires `cartography:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/carto/search:
    get:
      tags: [Cartography]
      summary: Search hosts, networks and groups
      operationId: searchCarto
      x-permission: cartography:read
      description: Requires `cartography:read`.
      parameters:
        - name: q
          in: query
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/carto/auto-layout:
    post:
      tags: [Cartography]
      summary: Recompute the layout
      operationId: cartoAutoLayout
      x-permission: cartography:write
      description: Requires `cartography:write` and the cartography edit lock.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }

  /api/carto/networks:
    post:
      tags: [Cartography]
      summary: Create a network
      operationId: createNetwork
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/networks/{name}:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    patch:
      tags: [Cartography]
      summary: Update a network
      operationId: updateNetwork
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
    delete:
      tags: [Cartography]
      summary: Delete a network
      operationId: deleteNetwork
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }

  /api/carto/groups:
    post:
      tags: [Cartography]
      summary: Create a group
      operationId: createGroup
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/groups/{name}:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    patch:
      tags: [Cartography]
      summary: Update a group
      operationId: updateGroup
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
    delete:
      tags: [Cartography]
      summary: Delete a group
      operationId: deleteGroup
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }

  /api/carto/hosts:
    post:
      tags: [Cartography]
      summary: Create a host
      operationId: createHost
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/hosts/{name}:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    patch:
      tags: [Cartography]
      summary: Update a host
      operationId: updateHost
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
    delete:
      tags: [Cartography]
      summary: Delete a host
      operationId: deleteHost
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/hosts/{name}/clone:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    post:
      tags: [Cartography]
      summary: Clone a host
      operationId: cloneHost
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }

  /api/carto/hosts/{host}/interfaces:
    parameters: [ { $ref: "#/components/parameters/Host" } ]
    post:
      tags: [Cartography]
      summary: Add an interface to a host
      operationId: createInterface
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/hosts/{host}/interfaces/{name}:
    parameters:
      - $ref: "#/components/parameters/Host"
      - $ref: "#/components/parameters/Name"
    patch:
      tags: [Cartography]
      summary: Update an interface
      operationId: updateInterface
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
    delete:
      tags: [Cartography]
      summary: Delete an interface
      operationId: deleteInterface
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }

  /api/carto/hosts/{host}/services:
    parameters: [ { $ref: "#/components/parameters/Host" } ]
    post:
      tags: [Cartography]
      summary: Add a service to a host
      operationId: createService
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/hosts/{host}/services/{name}:
    parameters:
      - $ref: "#/components/parameters/Host"
      - $ref: "#/components/parameters/Name"
    patch:
      tags: [Cartography]
      summary: Update a service
      operationId: updateService
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
    delete:
      tags: [Cartography]
      summary: Delete a service
      operationId: deleteService
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }

  /api/carto/ip-discovery/{ip}/add:
    parameters: [ { $ref: "#/components/parameters/Ip" } ]
    post:
      tags: [Cartography]
      summary: Adopt a discovered IP as a host
      operationId: ipDiscoveryAdd
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/ip-discovery/{ip}/merge:
    parameters: [ { $ref: "#/components/parameters/Ip" } ]
    post:
      tags: [Cartography]
      summary: Merge a discovered IP into an existing host
      operationId: ipDiscoveryMerge
      x-permission: cartography:write
      description: Requires `cartography:write` and the edit lock.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }

  /api/carto/lock:
    get:
      tags: [Cartography]
      summary: Read the edit-lock state
      operationId: getCartoLock
      x-permission: cartography:read
      description: Requires `cartography:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Cartography]
      summary: Acquire the single-editor lock
      operationId: acquireCartoLock
      x-permission: cartography:write
      description: Requires `cartography:write`. Returns `423` if another admin holds it.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/lock/heartbeat:
    post:
      tags: [Cartography]
      summary: Renew the edit lock
      operationId: heartbeatCartoLock
      x-permission: cartography:write
      description: Requires `cartography:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "423": { $ref: "#/components/responses/Locked" }
  /api/carto/lock/release:
    post:
      tags: [Cartography]
      summary: Release the edit lock
      operationId: releaseCartoLock
      x-permission: cartography:write
      description: Requires `cartography:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ----------------------------------------------------------------- Rules
  /api/rules:
    get:
      tags: [Rules]
      summary: List flow-matrix rules
      operationId: listRules
      x-permission: rules:read
      description: Requires `rules:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Rules]
      summary: Create a rule
      operationId: createRule
      x-permission: rules:write
      description: Requires `rules:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/{name}:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    get:
      tags: [Rules]
      summary: Read a rule
      operationId: getRule
      x-permission: rules:read
      description: Requires `rules:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Rules]
      summary: Update a rule
      operationId: updateRule
      x-permission: rules:write
      description: Requires `rules:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Rules]
      summary: Delete a rule
      operationId: deleteRule
      x-permission: rules:write
      description: Requires `rules:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/{name}/enable:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    post:
      tags: [Rules]
      summary: Enable a rule
      operationId: enableRule
      x-permission: rules:write
      description: Requires `rules:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/{name}/disable:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    post:
      tags: [Rules]
      summary: Disable a rule
      operationId: disableRule
      x-permission: rules:write
      description: Requires `rules:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/bulk:
    post:
      tags: [Rules]
      summary: Bulk apply rule changes
      operationId: bulkRules
      x-permission: rules:write
      description: Requires `rules:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/preview:
    post:
      tags: [Rules]
      summary: Preview a rule (dry run)
      operationId: previewRule
      x-permission: rules:write
      description: Requires `rules:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/find-covering:
    post:
      tags: [Rules]
      summary: Find rules covering a flow
      operationId: findCoveringRules
      x-permission: rules:write
      description: Requires `rules:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/discover/accept:
    post:
      tags: [Rules]
      summary: Accept a discovered rule
      operationId: acceptDiscoveredRule
      x-permission: rules:write
      description: Requires `rules:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/search-entities:
    get:
      tags: [Rules]
      summary: Search entities for rule authoring
      operationId: searchRuleEntities
      x-permission: rules:read
      description: Requires `rules:read`.
      parameters: [ { name: q, in: query, schema: { type: string } } ]
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/search-services:
    get:
      tags: [Rules]
      summary: Search services for rule authoring
      operationId: searchRuleServices
      x-permission: rules:read
      description: Requires `rules:read`.
      parameters: [ { name: q, in: query, schema: { type: string } } ]
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rules/search-interfaces:
    get:
      tags: [Rules]
      summary: Search interfaces for rule authoring
      operationId: searchRuleInterfaces
      x-permission: rules:read
      description: Requires `rules:read`.
      parameters: [ { name: q, in: query, schema: { type: string } } ]
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # -------------------------------------------------------------- Alerting
  /api/alerting-rules:
    get:
      tags: [Alerting]
      summary: List detection rules
      operationId: listAlertingRules
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Alerting]
      summary: Create a detection rule
      operationId: createAlertingRule
      x-permission: alerting:write
      description: Requires `alerting:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/alerting-rules/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Alerting]
      summary: Read a detection rule
      operationId: getAlertingRule
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Alerting]
      summary: Update a detection rule
      operationId: updateAlertingRule
      x-permission: alerting:write
      description: Requires `alerting:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Alerting]
      summary: Delete a detection rule
      operationId: deleteAlertingRule
      x-permission: alerting:write
      description: Requires `alerting:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/alerting-rules/{id}/toggle:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Alerting]
      summary: Enable/disable a detection rule
      operationId: toggleAlertingRule
      x-permission: alerting:write
      description: Requires `alerting:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/alerting-rules/{id}/duplicate:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Alerting]
      summary: Duplicate a detection rule
      operationId: duplicateAlertingRule
      x-permission: alerting:write
      description: Requires `alerting:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/alerting-rules/{id}/runs:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Alerting]
      summary: Recent evaluations of a detection rule
      operationId: getAlertingRuleRuns
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/rulesets/vocabulary:
    get:
      tags: [Alerting]
      summary: Rule-set vocabulary
      operationId: getRulesetVocabulary
      x-permission: cartography:read
      description: Requires `cartography:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rulesets:
    get:
      tags: [Alerting]
      summary: List imported rule sets
      operationId: listRulesets
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rulesets/shipped:
    get:
      tags: [Alerting]
      summary: List shipped rule packs
      operationId: listShippedRulesets
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rulesets/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Alerting]
      summary: Read a rule set
      operationId: getRuleset
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Alerting]
      summary: Delete a rule set
      operationId: deleteRuleset
      x-permission: alerting:write
      description: Requires `alerting:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rulesets/validate:
    post:
      tags: [Alerting]
      summary: Validate a rule pack (dry run)
      operationId: validateRuleset
      x-permission: alerting:write
      description: Requires `alerting:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rulesets/import:
    post:
      tags: [Alerting]
      summary: Import a rule pack
      operationId: importRuleset
      x-permission: alerting:write
      description: Requires `alerting:write`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rulesets/{id}/toggle:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Alerting]
      summary: Enable/disable a rule set
      operationId: toggleRuleset
      x-permission: alerting:write
      description: Requires `alerting:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/rulesets/{id}/delete/preview:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Alerting]
      summary: Preview a rule-set deletion
      operationId: previewRulesetDelete
      x-permission: alerting:write
      description: Requires `alerting:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # --------------------------------------------------------------- Anomaly
  /api/anomaly/overview:
    get:
      tags: [Anomaly]
      summary: Anomaly detection overview
      operationId: getAnomalyOverview
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/anomaly/rules:
    get:
      tags: [Anomaly]
      summary: List anomaly rules
      operationId: listAnomalyRules
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/anomaly/rules/{id}/baselines:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Anomaly]
      summary: List learned baselines
      operationId: listAnomalyBaselines
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/anomaly/rules/{id}/baselines/{keyhash}:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/KeyHash"
    get:
      tags: [Anomaly]
      summary: Read one baseline
      operationId: getAnomalyBaseline
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/anomaly/rules/{id}/baselines/{keyhash}/series:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/KeyHash"
    get:
      tags: [Anomaly]
      summary: Baseline time series
      operationId: getAnomalyBaselineSeries
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/anomaly/rules/{id}/heatmap:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Anomaly]
      summary: Anomaly heatmap
      operationId: getAnomalyHeatmap
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/anomaly/rules/{id}/fires:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Anomaly]
      summary: Recent anomaly fires
      operationId: getAnomalyFires
      x-permission: alerting:read
      description: Requires `alerting:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/anomaly/rules/{id}/reset-baseline:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Anomaly]
      summary: Reset a rule's baseline
      operationId: resetAnomalyBaseline
      x-permission: alerting:write
      description: Requires `alerting:write`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ---------------------------------------------------------------- Alerts
  /api/alerts:
    get:
      tags: [Alerts]
      summary: List detection alerts
      operationId: listAlerts
      x-permission: alerts:read
      description: Requires `alerts:read`. Filters are passed as query parameters.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/alerts/{id}/status:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Alerts]
      summary: Change an alert's status
      operationId: setAlertStatus
      x-permission: alerts:ack
      description: Acknowledge or reopen. Requires `alerts:ack`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status: { type: string, example: acknowledged }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/alerts/delete:
    post:
      tags: [Alerts]
      summary: Bulk delete alerts
      operationId: bulkDeleteAlerts
      x-permission: alerts:ack
      description: Requires `alerts:ack`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/alerts/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    delete:
      tags: [Alerts]
      summary: Delete an alert
      operationId: deleteAlert
      x-permission: alerts:ack
      description: Requires `alerts:ack`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # -------------------------------------------------------------- Sessions
  /api/sessions/riverview:
    get:
      tags: [Sessions]
      summary: River-view session aggregation
      operationId: getRiverview
      x-permission: sessions:read
      description: Requires `sessions:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ------------------------------------------------------------ Enrichment
  /api/enrichment:
    get:
      tags: [Enrichment]
      summary: Read enrichment settings
      operationId: getEnrichment
      x-permission: sources:manage
      description: Requires `sources:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Enrichment]
      summary: Update enrichment settings
      operationId: updateEnrichment
      x-permission: sources:manage
      description: Requires `sources:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/enrichment/sources:
    get:
      tags: [Enrichment]
      summary: List enrichment sources
      operationId: listEnrichmentSources
      x-permission: sources:manage
      description: Requires `sources:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/enrichment/sources/{name}:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    patch:
      tags: [Enrichment]
      summary: Update an enrichment source
      operationId: updateEnrichmentSource
      x-permission: sources:manage
      description: Requires `sources:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/enrichment/sources/{name}/refresh:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    post:
      tags: [Enrichment]
      summary: Refresh an enrichment source now
      operationId: refreshEnrichmentSource
      x-permission: sources:manage
      description: Requires `sources:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/exporters:
    get:
      tags: [Enrichment]
      summary: List flow exporters
      operationId: listExporters
      x-permission: sources:manage
      description: Requires `sources:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/exporters/{ip}:
    parameters: [ { $ref: "#/components/parameters/Ip" } ]
    patch:
      tags: [Enrichment]
      summary: Update an exporter
      operationId: updateExporter
      x-permission: sources:manage
      description: Requires `sources:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Enrichment]
      summary: Forget an exporter
      operationId: deleteExporter
      x-permission: sources:manage
      description: Requires `sources:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/exporters/rescan:
    post:
      tags: [Enrichment]
      summary: Rescan for exporters
      operationId: rescanExporters
      x-permission: sources:manage
      description: Requires `sources:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # --------------------------------------------------------------- Devices
  /api/devices:
    get:
      tags: [Devices]
      summary: List devices
      operationId: listDevices
      x-permission: devices:manage
      description: Requires `devices:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Devices]
      summary: Add a device
      operationId: createDevice
      x-permission: devices:manage
      description: Requires `devices:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/devices/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Devices]
      summary: Read a device
      operationId: getDevice
      x-permission: devices:manage
      description: Requires `devices:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Devices]
      summary: Update a device
      operationId: updateDevice
      x-permission: devices:manage
      description: Requires `devices:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Devices]
      summary: Remove a device
      operationId: deleteDevice
      x-permission: devices:manage
      description: Requires `devices:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/devices/{id}/refresh:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Devices]
      summary: Poll a device now
      operationId: refreshDevice
      x-permission: devices:manage
      description: Requires `devices:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # --------------------------------------------------------------- Outputs
  /api/outputs:
    get:
      tags: [Outputs]
      summary: List outputs
      operationId: listOutputs
      x-permission: outputs:manage
      description: Requires `outputs:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Outputs]
      summary: Create an output
      operationId: createOutput
      x-permission: outputs:manage
      description: Requires `outputs:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/outputs/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Outputs]
      summary: Read an output
      operationId: getOutput
      x-permission: outputs:manage
      description: Requires `outputs:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Outputs]
      summary: Update an output
      operationId: updateOutput
      x-permission: outputs:manage
      description: Requires `outputs:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Outputs]
      summary: Delete an output
      operationId: deleteOutput
      x-permission: outputs:manage
      description: Requires `outputs:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/outputs/{id}/toggle:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Outputs]
      summary: Enable/disable an output
      operationId: toggleOutput
      x-permission: outputs:manage
      description: Requires `outputs:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/outputs/{id}/test:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Outputs]
      summary: Send a test event to an output
      operationId: testOutput
      x-permission: outputs:manage
      description: Requires `outputs:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/outputs/{id}/deliveries:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Outputs]
      summary: Recent delivery attempts
      operationId: getOutputDeliveries
      x-permission: outputs:manage
      description: Requires `outputs:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonArray" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ------------------------------------------------------------- Lifecycle
  /api/lifecycle/storage:
    get:
      tags: [Lifecycle]
      summary: Storage usage
      operationId: getStorage
      x-permission: system:manage
      description: Requires `system:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/retention:
    get:
      tags: [Lifecycle]
      summary: Read retention policy
      operationId: getRetention
      x-permission: system:manage
      description: Requires `system:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Lifecycle]
      summary: Set retention policy
      operationId: updateRetention
      x-permission: system:manage
      description: Requires `system:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/retention/run:
    post:
      tags: [Lifecycle]
      summary: Run retention now
      operationId: runRetention
      x-permission: system:manage
      description: Requires `system:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/backup:
    get:
      tags: [Lifecycle]
      summary: Read backup schedule
      operationId: getBackup
      x-permission: system:manage
      description: Requires `system:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [Lifecycle]
      summary: Set backup schedule
      operationId: updateBackup
      x-permission: system:manage
      description: Requires `system:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/backup/run:
    post:
      tags: [Lifecycle]
      summary: Run a backup now
      operationId: runBackup
      x-permission: system:manage
      description: Requires `system:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/backup/plan:
    post:
      tags: [Lifecycle]
      summary: Plan a backup
      operationId: planBackup
      x-permission: system:manage
      description: Requires `system:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/backup/restore:
    post:
      tags: [Lifecycle]
      summary: Restore from a backup
      operationId: restoreBackup
      x-permission: system:manage
      description: Requires `system:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/backup/restore/{id}/stream:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Lifecycle]
      summary: Stream restore progress (SSE)
      operationId: streamRestore
      x-permission: system:manage
      description: Server-Sent Events stream of restore progress. Requires `system:manage`.
      responses:
        "200":
          description: SSE stream
          content:
            text/event-stream:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/backup/timeline:
    get:
      tags: [Lifecycle]
      summary: Backup timeline
      operationId: getBackupTimeline
      x-permission: system:manage
      description: Requires `system:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/lifecycle/backup/next-action:
    get:
      tags: [Lifecycle]
      summary: Next scheduled backup action
      operationId: getBackupNextAction
      x-permission: system:manage
      description: Requires `system:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # -------------------------------------------------------------- Config I/O
  /api/config-io/export:
    get:
      tags: [Config I/O]
      summary: Export the config bundle
      operationId: exportConfig
      x-permission: config:export
      description: Requires `config:export`. Returns the consolidated YAML bundle.
      responses:
        "200":
          description: YAML config bundle
          content:
            application/yaml:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/config-io/validate:
    post:
      tags: [Config I/O]
      summary: Validate a config bundle
      operationId: validateConfig
      x-permission: config:import
      description: Requires `config:import`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/config-io/import:
    post:
      tags: [Config I/O]
      summary: Import a config bundle
      operationId: importConfig
      x-permission: config:import
      description: Requires `config:import`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/config-io/masterkey/export:
    get:
      tags: [Config I/O]
      summary: Export master-key material
      operationId: exportMasterkey
      x-permission: system:manage
      description: Requires `system:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/config-io/masterkey/import:
    post:
      tags: [Config I/O]
      summary: Import master-key material
      operationId: importMasterkey
      x-permission: system:manage
      description: Requires `system:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ------------------------------------------------------------- Audit log
  /api/auditlog:
    get:
      tags: [Audit log]
      summary: Query the audit journal
      operationId: getAuditlog
      x-permission: auditlog:read
      description: Requires `auditlog:read`. Filters passed as query parameters.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/auditlog/histogram:
    get:
      tags: [Audit log]
      summary: Audit histogram
      operationId: getAuditlogHistogram
      x-permission: auditlog:read
      description: Requires `auditlog:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/auditlog/facets:
    get:
      tags: [Audit log]
      summary: Audit facets
      operationId: getAuditlogFacets
      x-permission: auditlog:read
      description: Requires `auditlog:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/auditlog/verify:
    get:
      tags: [Audit log]
      summary: Verify the audit hash chain and seals
      operationId: verifyAuditlog
      x-permission: auditlog:read
      description: Requires `auditlog:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/auditlog/export:
    get:
      tags: [Audit log]
      summary: Export audit entries
      operationId: exportAuditlog
      x-permission: auditlog:read
      description: Requires `auditlog:read`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # -------------------------------------------------------------- Identity
  /api/users:
    post:
      tags: [Identity]
      summary: Create a user
      operationId: createUser
      x-permission: users:manage
      description: Requires `users:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/users/{username}:
    parameters: [ { $ref: "#/components/parameters/Username" } ]
    patch:
      tags: [Identity]
      summary: Update a user
      operationId: updateUser
      x-permission: users:manage
      description: Requires `users:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Identity]
      summary: Delete a user
      operationId: deleteUser
      x-permission: users:manage
      description: Requires `users:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/users/{username}/password:
    parameters: [ { $ref: "#/components/parameters/Username" } ]
    post:
      tags: [Identity]
      summary: Reset a user's password
      operationId: resetUserPassword
      x-permission: users:manage
      description: Requires `users:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/users/{username}/mfa/reset:
    parameters: [ { $ref: "#/components/parameters/Username" } ]
    post:
      tags: [Identity]
      summary: Reset a user's MFA
      operationId: resetUserMfa
      x-permission: users:manage
      description: Requires `users:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/groups:
    post:
      tags: [Identity]
      summary: Create a group
      operationId: createIamGroup
      x-permission: users:manage
      description: Requires `users:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/groups/{name}:
    parameters: [ { $ref: "#/components/parameters/Name" } ]
    patch:
      tags: [Identity]
      summary: Update a group
      operationId: updateIamGroup
      x-permission: users:manage
      description: Requires `users:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Identity]
      summary: Delete a group
      operationId: deleteIamGroup
      x-permission: users:manage
      description: Requires `users:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/tokens:
    post:
      tags: [Identity]
      summary: Mint an API token
      operationId: createToken
      x-permission: users:manage
      description: Requires `users:manage`. The secret is returned once.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/tokens/{id}/revoke:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Identity]
      summary: Revoke an API token
      operationId: revokeToken
      x-permission: users:manage
      description: Requires `users:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/tokens/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    delete:
      tags: [Identity]
      summary: Delete an API token
      operationId: deleteToken
      x-permission: users:manage
      description: Requires `users:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/ldap/config:
    get:
      tags: [Identity]
      summary: Read LDAP configuration
      operationId: getLdapConfig
      x-permission: auth:manage
      description: Requires `auth:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Identity]
      summary: Update LDAP configuration
      operationId: updateLdapConfig
      x-permission: auth:manage
      description: Requires `auth:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/ldap/test-connection:
    post:
      tags: [Identity]
      summary: Test the LDAP connection
      operationId: testLdap
      x-permission: auth:manage
      description: Requires `auth:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/oidc/config:
    get:
      tags: [Identity]
      summary: Read OIDC configuration
      operationId: getOidcConfig
      x-permission: auth:manage
      description: Requires `auth:manage`.
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Identity]
      summary: Update OIDC configuration
      operationId: updateOidcConfig
      x-permission: auth:manage
      description: Requires `auth:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/oidc/test-connection:
    post:
      tags: [Identity]
      summary: Test the OIDC connection
      operationId: testOidc
      x-permission: auth:manage
      description: Requires `auth:manage`.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/auth/login-security:
    get:
      tags: [Identity]
      summary: Read login-security policy
      operationId: getLoginSecurity
      x-permission: users:manage
      description: Requires `users:manage` (login-security lives on the admin Users page).
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Identity]
      summary: Update login-security policy
      operationId: updateLoginSecurity
      x-permission: users:manage
      description: Requires `users:manage` (login-security lives on the admin Users page).
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/auth/mfa-policy:
    put:
      tags: [Identity]
      summary: Update the MFA policy
      operationId: updateMfaPolicy
      x-permission: users:manage
      description: Requires `users:manage` (login-security lives on the admin Users page).
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # --------------------------------------------------------------- Account
  /api/account/mfa/begin:
    post:
      tags: [Account]
      summary: Begin MFA enrollment
      operationId: beginMfa
      x-permission: ""
      description: Any authenticated user (manages their own MFA).
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/account/mfa/qr:
    get:
      tags: [Account]
      summary: MFA enrollment QR code
      operationId: getMfaQr
      x-permission: ""
      description: Any authenticated user. Returns a PNG QR image.
      responses:
        "200":
          description: QR code
          content:
            image/png:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/account/mfa/activate:
    post:
      tags: [Account]
      summary: Activate MFA
      operationId: activateMfa
      x-permission: ""
      description: Any authenticated user.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/account/mfa/disable:
    post:
      tags: [Account]
      summary: Disable MFA
      operationId: disableMfa
      x-permission: ""
      description: Any authenticated user.
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/account/password:
    post:
      tags: [Account]
      summary: Change own password
      operationId: changeOwnPassword
      x-permission: ""
      description: Any authenticated local user (verifies the current password).
      requestBody: { $ref: "#/components/requestBodies/JsonObject" }
      responses:
        "200": { $ref: "#/components/responses/JsonObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: obs_token
      description: "An API token minted for a user (`Authorization: Bearer obs_…`)."

  parameters:
    Id:
      name: id
      in: path
      required: true
      schema: { type: string }
    Name:
      name: name
      in: path
      required: true
      schema: { type: string }
    Host:
      name: host
      in: path
      required: true
      schema: { type: string }
    Username:
      name: username
      in: path
      required: true
      schema: { type: string }
    Ip:
      name: ip
      in: path
      required: true
      schema: { type: string }
    KeyHash:
      name: keyhash
      in: path
      required: true
      schema: { type: string }

  requestBodies:
    JsonObject:
      required: true
      description: |
        A JSON object whose fields depend on the resource. See the domain page
        in the docs (or the Web GUI form) for the exact fields.
      content:
        application/json:
          schema: { type: object, additionalProperties: true }

  responses:
    JsonObject:
      description: A JSON object
      content:
        application/json:
          schema: { type: object, additionalProperties: true }
    JsonArray:
      description: A JSON array
      content:
        application/json:
          schema:
            type: array
            items: { type: object, additionalProperties: true }
    Unauthorized:
      description: Missing, unknown, revoked or expired token
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: unauthenticated }
    Forbidden:
      description: The token's user lacks the required permission
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: forbidden }
    Locked:
      description: Another admin holds the cartography edit lock
      content:
        application/json:
          schema: { $ref: "#/components/schemas/LockError" }
    BadRequest:
      description: Malformed request (missing field, invalid JSON)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
      required: [error]
    LockError:
      type: object
      properties:
        error: { type: string }
        holder: { type: string, description: Username currently holding the lock }
      required: [error]

    ExecuteRequest:
      type: object
      required: [source]
      properties:
        source:
          type: string
          description: The NFQL pipeline to compile and execute
          example: "FROM sessions | LAST 3600 | STATS COUNT(*) AS n BY server_port"
        args:
          type: array
          description: Positional `?` arguments; integers are bound as integers
          items: {}
        as_of:
          type: string
          format: date-time
          description: >
            Optional RFC3339 instant pinning the "now" of every relative window
            (the investigate-at-fire-time path). Empty = live now.
    ExecuteColumn:
      type: object
      properties:
        name: { type: string }
        type: { type: string, description: "DuckDB column type, e.g. INTEGER, BIGINT, VARCHAR" }
    ExecuteResponse:
      type: object
      properties:
        columns:
          type: array
          items: { $ref: "#/components/schemas/ExecuteColumn" }
        rows:
          type: array
          description: Row-major result; each row is an array aligned to `columns`
          items:
            type: array
            items: {}
        compile_ms: { type: integer, format: int64 }
        exec_ms: { type: integer, format: int64 }
        row_count: { type: integer }
        truncated: { type: boolean, description: True when the row cap was hit }
        error: { type: string, description: Populated (with columns/rows null) on a compile/exec error }
    ColumnsResponse:
      type: object
      properties:
        columns:
          type: array
          items: { $ref: "#/components/schemas/ExecuteColumn" }
        error: { type: string }

    AlertCounts:
      type: object
      properties:
        total: { type: integer, format: int64 }
        by_status:
          type: object
          additionalProperties: { type: integer, format: int64 }
        by_severity:
          type: object
          additionalProperties: { type: integer, format: int64 }
        last_1h: { type: integer, format: int64 }
        last_24h: { type: integer, format: int64 }
    StorageInfo:
      type: object
      properties:
        db_path: { type: string }
        db_size_bytes: { type: integer, format: int64 }
        disk_free_bytes: { type: integer, format: int64 }
        disk_total_bytes: { type: integer, format: int64 }
        buffer_dir: { type: string }
        buffer_dir_bytes: { type: integer, format: int64 }
        buffer_dir_files: { type: integer }
        backup_dir: { type: string }
        backup_dir_bytes: { type: integer, format: int64 }
        backup_dir_files: { type: integer }
    Status:
      type: object
      description: The daemon status snapshot returned by GET /api/status.
      properties:
        version: { type: string }
        commit: { type: string }
        started_at: { type: string, format: date-time }
        uptime_seconds: { type: integer, format: int64 }
        timestamp: { type: string, description: RFC3339 collection instant }
        data_version: { type: integer }
        data_version_target: { type: integer }
        flows:
          type: object
          properties:
            total: { type: integer, format: int64 }
            per_sec: { type: number, format: double }
        cartography:
          type: object
          properties:
            networks: { type: integer, format: int64 }
            hosts: { type: integer, format: int64 }
            services: { type: integer, format: int64 }
            groups: { type: integer, format: int64 }
        sessions:
          type: object
          properties:
            active: { type: integer, format: int64 }
            half_open: { type: integer, format: int64 }
            closed: { type: integer, format: int64 }
            open_live: { type: integer, format: int64 }
            open_max: { type: integer, format: int64 }
            fill_pct: { type: number, format: double }
            pressure: { type: string }
            evicted: { type: integer, format: int64 }
            dropped_closes: { type: integer, format: int64 }
            dropped_emits: { type: integer, format: int64 }
        enrich:
          type: object
          properties:
            lru_used: { type: integer, format: int64 }
            lru_cap: { type: integer, format: int64 }
        templates:
          type: object
          properties:
            received: { type: integer, format: int64 }
            packets_awaiting: { type: integer, format: int64 }
        rules:
          type: object
          properties:
            total: { type: integer, format: int64 }
            expansions: { type: integer, format: int64 }
            alert_rules_active: { type: integer, format: int64 }
        coverage:
          type: object
          properties:
            rate_1h: { type: number, format: double }
            violations_closed_1h: { type: integer, format: int64 }
        alerts: { $ref: "#/components/schemas/AlertCounts" }
        ingestion:
          type: object
          properties:
            files_written: { type: integer, format: int64 }
            records_ingested: { type: integer, format: int64 }
            last_flush_at: { type: string }
            avg_flush_ms: { type: number, format: double }
            max_flush_ms: { type: integer, format: int64 }
        storage: { $ref: "#/components/schemas/StorageInfo" }
        runtime:
          type: object
          properties:
            heap_inuse_bytes: { type: integer, format: int64 }
            sys_bytes: { type: integer, format: int64 }
            goroutines: { type: integer }
            udp_drops: { type: integer, format: int64 }
            matcher_cursor_lag_seconds: { type: integer, format: int64 }
        audit:
          type: object
          properties:
            integrity: { type: string }
            breaks: { type: integer }
        live_metrics_available:
          type: boolean
          description: False on a headless daemon (web.enabled=false) — producer-derived blocks stay zero
