openapi: 3.0.3

info:
  title: PDM API
  version: "0.1.0"
  description: |
    REST API сервиса **pdm** (`pdm/pdm`, образ `pdmv2`) — шлюз/агрегатор над
    Postgres и множеством внутренних сервисов Sarex: документации, ресурсы и
    разрешения, замечания, вложения, состояния/рабочие области, подписки,
    атрибуты (EAV), инспекции, релизы, заметки, чертежи, BIM, трансмитталы,
    системный лог.

    Сервис написан на Go (**Fiber v2**). Приложение собирается в
    `internal/controller/http/v1.Setup` (`internal/controller/http/v1/router.go`),
    точка входа — `cmd/httpserver/main.go`. Роутинг делится на группы:

    - публичный API — префикс `/api` с версиями `v1`/`v2`/`v3`/`v4`
      (`group.Group("v1")` и т.д.);
    - внутренний API — префикс `/internal` (healthcheck, pprof, служебные
      ручки), без аутентификации на уровне приложения.

    Готовой спецификации (swagger) в репозитории нет — данный документ
    восстановлен из роутеров `internal/controller/http/**/router.go`.
    Сервер слушает порт `8080` (хардкод в `internal/app/http/httpserver.go`).

    ### Аутентификация
    Аутентификация применяется middleware `pkg/httpserver/middleware/auth.go` ко
    всем путям с префиксом `/api`. Токен передаётся заголовком
    `Authorization: Bearer <jwt>`. Поддерживаются два режима:

    1. **sarex-backend** (по умолчанию) — если заголовка `Identity` нет, подпись
       основного JWT проверяется публичным ключом из `PUBLIC_KEY` (PKIX). Из
       claims извлекаются `user_id`, `is_superuser`, `company_ids`,
       `service_accounts`, `permissions` и т.д.
    2. **Zitadel** — если передан дополнительный заголовок
       `Identity: Bearer <jwt>`, полезная нагрузка берётся из этого токена
       (`urn:zitadel:iam:user:metadata`); основной токен кладётся как
       `access_token`. Подпись identity-токена приложением не проверяется
       (`ParseUnverified`) — доверие обеспечивается сетевым слоем.

    При отсутствии заголовка `Authorization`, неверной схеме (не `Bearer`) или
    ошибке разбора токена возвращается **401 Unauthorized** (пустое тело).
    Пути `/internal/*` аутентификации на уровне приложения не требуют.

    ### Пагинация
    Списочные эндпоинты используют пагинацию через query-параметры `limit` и
    `offset` (напр. `internal/controller/http/v1/document/downloaded.go`,
    `flat.go`). Единого конверта ответа нет — формат зависит от эндпоинта.

    ### Обработка ошибок
    Ошибки бизнес-слоя оборачиваются в `AppError`
    (`internal/app_errors/errors.go`) и сериализуются как JSON
    `{ "message": "...", "error_code": "GW-XXXX" }`. HTTP-статус выбирается по
    `error_code` в `internal/app_errors/middleware.go`:

    | error_code | Статус | Значение |
    | --- | --- | --- |
    | `GW-0001` | 404 | Ресурс не найден |
    | `GW-0002` | 401 | Не аутентифицирован |
    | `GW-0003` | 403 | Нет доступа |
    | `GW-0004` | 400 | Некорректный запрос |
    | `GW-0014` | 400 | Ошибка валидации состояния |
    | `GW-0000` | 500 | Системная ошибка |

    Не все роутеры используют обёртку `middleware.ErrorHandler` — часть
    обработчиков возвращает ошибки/статусы напрямую, поэтому формат ответа об
    ошибке может отличаться от `AppError`.

    ### Замечания (расхождения кода)
    - Пути с сегментом `*` (напр. `/api/v1/disks/*/documents`,
      `/api/v1/documents/*/attributes`, `/api/v1/targets/*/remarks/*`) — это
      «жадные» wildcard-сегменты Fiber, захватывающие путь документа/цели
      целиком (включая `/`). В спецификации они представлены параметром пути.
    - Часть эндпоинтов завершается слэшем (`/`), часть — нет; поведение
      определяется определениями групп во Fiber.
    - v0-роутер (`internal/controller/http/v0`, на `labstack/echo`) в рантайме
      не подключён.

  contact:
    name: pdm
    url: https://gitlab.com/sarex-team/pdm

servers:
  - url: http://pdm-api.documentations:8080
    description: Stage (внутренний адрес в кластере, namespace documentations)
  - url: http://pdm-api.documentations-preprod:8080
    description: Preprod (внутренний адрес в кластере)
  - url: http://pdm-api.documentations-prod:8080
    description: Production (внутренний адрес в кластере)

security:
  - bearerAuth: []

tags:
  - name: documents
    description: Документы, диски, проекты, атрибуты (v1/v2/v3/v4)
  - name: resources
    description: Ресурсы и разрешения (v1/v2)
  - name: remarks
    description: Замечания по таргетам
  - name: attachments
    description: Вложения
  - name: states
    description: Состояния рабочих областей
  - name: subscriptions
    description: Подписки
  - name: system_log
    description: Системный лог
  - name: targets
    description: Таргеты
  - name: inspections
    description: Инспекции
  - name: users
    description: Пользователи
  - name: eav
    description: Атрибуты (EAV)
  - name: releases
    description: Релизы
  - name: notes
    description: Заметки
  - name: drawings
    description: Чертежи (сечения и экспорты)
  - name: internal
    description: Служебные эндпоинты (без аутентификации)

paths:
  # ---------------- v1: documents ----------------
  /api/v1/disks/{diskPath}/documents:
    get:
      tags: [documents]
      summary: Список документов по диску (v1)
      parameters:
        - $ref: '#/components/parameters/DiskPath'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/disks/{diskPath}/downloaded_documents:
    get:
      tags: [documents]
      summary: Список скачанных документов диска
      parameters:
        - $ref: '#/components/parameters/DiskPath'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/disks/{diskPath}/flat_documents:
    get:
      tags: [documents]
      summary: Плоский список документов диска
      parameters:
        - $ref: '#/components/parameters/DiskPath'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/disks/{diskId}/thumbnails:
    post:
      tags: [documents]
      summary: Превью документов диска
      parameters:
        - name: diskId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/projects/:
    get:
      tags: [documents]
      summary: Получить проект по ID документа
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents:
    post:
      tags: [documents]
      summary: Создать документ
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/{documentPath}/attributes:
    get:
      tags: [documents]
      summary: Атрибуты документа
      parameters:
        - $ref: '#/components/parameters/DocumentPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    put:
      tags: [documents]
      summary: Создать/обновить атрибуты документа
      parameters:
        - $ref: '#/components/parameters/DocumentPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/{documentPath}/subscription/:
    delete:
      tags: [documents]
      summary: Удалить подписку по ID документа
      parameters:
        - $ref: '#/components/parameters/DocumentPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/bin:
    get:
      tags: [documents]
      summary: Список удалённых документов (корзина)
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/approving_users:
    post:
      tags: [documents]
      summary: Согласующие пользователи
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/bundle_versions:
    post:
      tags: [documents]
      summary: Версии бандлов документов
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/ancestors:
    post:
      tags: [documents]
      summary: Предки документов
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/size:
    get:
      tags: [documents]
      summary: Размер папки
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/related_documents:
    get:
      tags: [documents]
      summary: Связанные документы
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [documents]
      summary: Создать связи документов
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/documents/related_documents/bulk_delete:
    post:
      tags: [documents]
      summary: Массовое удаление связей документов
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: remarks ----------------
  /api/v1/targets/{targetPath}/remarks:
    post:
      tags: [remarks]
      summary: Создать замечание для таргета
      parameters:
        - $ref: '#/components/parameters/TargetPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/targets/{targetPath}/remarks/{remarkPath}:
    get:
      tags: [remarks]
      summary: Получить замечание
      parameters:
        - $ref: '#/components/parameters/TargetPath'
        - $ref: '#/components/parameters/RemarkPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    patch:
      tags: [remarks]
      summary: Обновить замечание
      parameters:
        - $ref: '#/components/parameters/TargetPath'
        - $ref: '#/components/parameters/RemarkPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: resources ----------------
  /api/v1/resources/:
    get:
      tags: [resources]
      summary: Список ресурсов
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/resources/users-with-resources/:
    post:
      tags: [resources]
      summary: Пользователи с ресурсами
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/resources/permissions-bulk/:
    patch:
      tags: [resources]
      summary: Массовое обновление разрешений
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/resources/{uuid}/:
    get:
      tags: [resources]
      summary: Ресурс по UUID
      parameters:
        - name: uuid
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/resource-permissions/:
    get:
      tags: [resources]
      summary: Разрешения на ресурсы
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [resources]
      summary: Массовое создание разрешений на ресурс
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/resource-permissions/{id}/:
    delete:
      tags: [resources]
      summary: Удалить разрешение на ресурс
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/bulk_delete/resource-permissions/:
    post:
      tags: [resources]
      summary: Массовое удаление разрешений на ресурс
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/company-resource-permissions/:
    get:
      tags: [resources]
      summary: Разрешения компаний на ресурсы
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [resources]
      summary: Создать разрешение компании на ресурс
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/company-resource-permissions/{id}/:
    delete:
      tags: [resources]
      summary: Удалить разрешение компании на ресурс
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/resources-rpc/resource-by-document-id/{id}/:
    get:
      tags: [resources]
      summary: Ресурс по ID документа
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/resources-rpc/parent-document-by-resource-id/{uuid}/:
    get:
      tags: [resources]
      summary: Родительская папка проекта по UUID ресурса
      parameters:
        - name: uuid
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: attachments ----------------
  /api/v1/attachments:
    post:
      tags: [attachments]
      summary: Создать вложение
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [attachments]
      summary: Список вложений
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/attachments/{id}:
    get:
      tags: [attachments]
      summary: Вложение по ID
      parameters:
        - $ref: '#/components/parameters/IdPathPlain'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    delete:
      tags: [attachments]
      summary: Удалить вложение
      parameters:
        - $ref: '#/components/parameters/IdPathPlain'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: states (workspace) ----------------
  /api/v1/workspace/{ws_id}:
    post:
      tags: [states]
      summary: Создать состояние с вложением
      parameters:
        - $ref: '#/components/parameters/WsId'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [states]
      summary: Состояния с вложениями
      parameters:
        - $ref: '#/components/parameters/WsId'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/workspace/{ws_id}/dynamic_states:
    post:
      tags: [states]
      summary: Создать динамические состояния
      parameters:
        - $ref: '#/components/parameters/WsId'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [states]
      summary: Динамические состояния
      parameters:
        - $ref: '#/components/parameters/WsId'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/workspace/states/{id}:
    get:
      tags: [states]
      summary: Состояние по ID
      parameters:
        - $ref: '#/components/parameters/IdPathPlain'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/workspace/dynamic_states/{id}:
    get:
      tags: [states]
      summary: Динамическое состояние по ID
      parameters:
        - $ref: '#/components/parameters/IdPathPlain'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: subscriptions ----------------
  /api/v1/subscription/:
    post:
      tags: [subscriptions]
      summary: Создать подписку
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: system_log ----------------
  /api/v1/system_log/:
    get:
      tags: [system_log]
      summary: Отфильтрованный системный лог
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [system_log]
      summary: Записать в системный лог
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: targets ----------------
  /api/v1/targets/:
    get:
      tags: [targets]
      summary: Список таргетов
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/targets/{targetPath}/:
    get:
      tags: [targets]
      summary: Таргет по ID
      parameters:
        - $ref: '#/components/parameters/TargetPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: inspections ----------------
  /api/v1/inspections/:
    post:
      tags: [inspections]
      summary: Создать инспекцию
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [inspections]
      summary: Список инспекций
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/inspections/{id}:
    get:
      tags: [inspections]
      summary: Инспекция по ID
      parameters:
        - $ref: '#/components/parameters/IdInt'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    patch:
      tags: [inspections]
      summary: Обновить инспекцию
      parameters:
        - $ref: '#/components/parameters/IdInt'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    delete:
      tags: [inspections]
      summary: Удалить инспекцию
      parameters:
        - $ref: '#/components/parameters/IdInt'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/inspections/created_at/daterange:
    get:
      tags: [inspections]
      summary: Диапазон дат создания
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/inspections/inspection_date/daterange:
    get:
      tags: [inspections]
      summary: Диапазон дат инспекций
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/inspections/available_responsible_users:
    get:
      tags: [inspections]
      summary: Доступные ответственные пользователи
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/inspections/available_inspection_dates:
    get:
      tags: [inspections]
      summary: Доступные даты инспекций
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/inspections/export:
    get:
      tags: [inspections]
      summary: Экспорт инспекций
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: users / eav / releases / notes ----------------
  /api/v1/users/:
    get:
      tags: [users]
      summary: Пользователи по разрешению на ресурс
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/attribute/:
    get:
      tags: [eav]
      summary: Получить атрибуты (EAV)
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/releases/{id}:
    get:
      tags: [releases]
      summary: Список релизов по ID проекта
      parameters:
        - $ref: '#/components/parameters/IdInt'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/notes/{service_id}/{entity_id}/{instance_id}/:
    get:
      tags: [notes]
      summary: Заметки по сервису/сущности/инстансу
      parameters:
        - { name: service_id, in: path, required: true, schema: { type: string } }
        - { name: entity_id, in: path, required: true, schema: { type: string } }
        - { name: instance_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v1: drawings ----------------
  /api/v1/drawings/cross-sections:
    post:
      tags: [drawings]
      summary: Создать сечение
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [drawings]
      summary: Сечения по ID инстанса
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/drawings/cross-sections/{cross_section_id}/data:
    get:
      tags: [drawings]
      summary: Данные сечения
      parameters:
        - { name: cross_section_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/drawings/cross-sections/{cross_section_id}:
    delete:
      tags: [drawings]
      summary: Удалить сечение
      parameters:
        - { name: cross_section_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/drawings/exports:
    post:
      tags: [drawings]
      summary: Создать экспорт
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [drawings]
      summary: Список экспортов
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/drawings/exports/{export_id}:
    delete:
      tags: [drawings]
      summary: Удалить экспорт
      parameters:
        - { name: export_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v2 ----------------
  /api/v2/resources/:
    get:
      tags: [resources]
      summary: Расширенный список ресурсов (v2)
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v2/resources:
    post:
      tags: [resources]
      summary: Создать расширенный ресурс (v2)
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v2/resources/{id}:
    get:
      tags: [resources]
      summary: Расширенный ресурс по ID (v2)
      parameters:
        - $ref: '#/components/parameters/IdPathPlain'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    patch:
      tags: [resources]
      summary: Обновить расширенный ресурс (v2)
      parameters:
        - $ref: '#/components/parameters/IdPathPlain'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    delete:
      tags: [resources]
      summary: Удалить расширенный ресурс (v2)
      parameters:
        - $ref: '#/components/parameters/IdPathPlain'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v2/users/:
    get:
      tags: [users]
      summary: Пользователи по ресурсу (v2)
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v2/disks/{diskPath}/documents:
    get:
      tags: [documents]
      summary: Список документов по диску (v2)
      parameters:
        - $ref: '#/components/parameters/DiskPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- v3 / v4 ----------------
  /api/v3/disks/{diskPath}/documents:
    get:
      tags: [documents]
      summary: Упрощённый список документов по диску (v3)
      parameters:
        - $ref: '#/components/parameters/DiskPath'
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v4/disks/{disk_id}/documents:
    get:
      tags: [documents]
      summary: Поиск документов по диску (v4)
      parameters:
        - { name: disk_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Ok' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ---------------- internal ----------------
  /internal/v1/documents/approving_users:
    post:
      tags: [internal]
      summary: Согласующие пользователи (внутренний, без аутентификации)
      security: []
      responses:
        '200': { $ref: '#/components/responses/Ok' }
  /internal/healthz/startup:
    get:
      tags: [internal]
      summary: Startup-проба (БД, опционально Valkey)
      security: []
      responses:
        '200': { description: OK }
        '503': { description: Service Unavailable }
  /internal/healthz/live:
    get:
      tags: [internal]
      summary: Liveness-проба
      security: []
      responses:
        '200': { description: OK }
  /internal/healthz/ready:
    get:
      tags: [internal]
      summary: Readiness-проба
      security: []
      responses:
        '200': { description: OK }
        '503': { description: Service Unavailable }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT в заголовке `Authorization: Bearer <jwt>`. В режиме Zitadel
        дополнительно передаётся заголовок `Identity: Bearer <jwt>`.

  parameters:
    Limit:
      name: limit
      in: query
      required: false
      schema: { type: integer, format: int64, minimum: 0 }
      description: Размер страницы
    Offset:
      name: offset
      in: query
      required: false
      schema: { type: integer, format: int64, minimum: 0 }
      description: Смещение
    DiskPath:
      name: diskPath
      in: path
      required: true
      schema: { type: string }
      description: Жадный wildcard-сегмент Fiber (может содержать `/`) — путь/ID диска
    DocumentPath:
      name: documentPath
      in: path
      required: true
      schema: { type: string }
      description: Жадный wildcard-сегмент Fiber — путь/ID документа
    TargetPath:
      name: targetPath
      in: path
      required: true
      schema: { type: string }
      description: Жадный wildcard-сегмент Fiber — путь/ID таргета
    RemarkPath:
      name: remarkPath
      in: path
      required: true
      schema: { type: string }
      description: Жадный wildcard-сегмент Fiber — путь/ID замечания
    WsId:
      name: ws_id
      in: path
      required: true
      schema: { type: string }
      description: UUID рабочей области
    IdPath:
      name: id
      in: path
      required: true
      schema: { type: string }
    IdPathPlain:
      name: id
      in: path
      required: true
      schema: { type: string }
    IdInt:
      name: id
      in: path
      required: true
      schema: { type: integer }
      description: Числовой идентификатор (Fiber-ограничение `:id<int>`)

  responses:
    Ok:
      description: Успешный ответ (структура зависит от эндпоинта)
      content:
        application/json:
          schema: {}
    BadRequest:
      description: Некорректный запрос
      content:
        application/json:
          schema: { $ref: '#/components/schemas/AppError' }
    Unauthorized:
      description: Не аутентифицирован (пустое тело)
    Forbidden:
      description: Нет доступа
      content:
        application/json:
          schema: { $ref: '#/components/schemas/AppError' }
    NotFound:
      description: Ресурс не найден
      content:
        application/json:
          schema: { $ref: '#/components/schemas/AppError' }

  schemas:
    AppError:
      type: object
      description: Формат ошибки бизнес-слоя (`internal/app_errors/errors.go`)
      properties:
        message:
          type: string
          example: not found
        error_code:
          type: string
          description: Внутренний код ошибки (`GW-XXXX`)
          enum: [GW-0000, GW-0001, GW-0002, GW-0003, GW-0004, GW-0014]
          example: GW-0001
      required: [message, error_code]
