openapi: 3.0.3

info:
  title: Documentation API v2
  version: "1.0"
  description: |
    REST API сервиса **documentation-api-v2** (`pdm/documentation-api-v2`) —
    Go-сервис домена «documentations» (v2): управление дисками, документами,
    бандлами, data source, страницами, публичными ссылками, workflow-обработкой
    и загрузкой файлов (в т.ч. multipart).

    Сервис написан на Go (**Fiber v2**, `github.com/gofiber/fiber/v2`). HTTP-сервер
    собирается в `internal/api/httpserver/server.go`. Спецификация ниже получена
    конвертацией сгенерированной `swag`-схемы (`docs/swagger.yaml`, Swagger 2.0)
    в OpenAPI 3.0.3 и нормализацией путей под реальные маршруты Fiber.

    Роутинг состоит из двух групп (`server.go`, `App.Run`):

    - публичный API — префикс `/api/v1`;
    - внутренний API — префикс `/internal/v1` (для вызовов внутри кластера).

    Интерактивная документация Swagger UI доступна по `/swagger/*`.
    Служебный эндпоинт проб k8s — `GET /ping` (вне групп, без аутентификации).

    ### Аутентификация
    JWT-middleware (`pkg/middleware/jwt_auth`) подключён **только к группе
    `/api/v1`**. Токен передаётся заголовком `Authorization: Bearer <jwt>`
    либо query-параметром `?auth_jwt=<jwt>` (`JWTToCtx`). Поддерживаются два
    режима (`jwtauth.New`):

    1. **Zitadel** — если передан заголовок `Identity: Bearer <jwt>`, полезная
       нагрузка (`urn:zitadel:iam:user:metadata`) берётся из identity-токена
       без проверки подписи (доверие обеспечивает Istio).
    2. **sarex-backend** — если заголовка `Identity` нет, подпись основного
       токена проверяется публичным RSA-ключом `PUBLIC_KEY` (PKIX).

    Для маршрутов публичных/временных ссылок (`/api/v1/public/*` и
    `download_type=temporary`) токен проверяется HMAC-секретом
    `DOCUMENT_PUBLIC_LINK_JWT_SECRET`.

    Эндпоинты `/internal/v1/*` middleware аутентификации на уровне приложения
    **не проходят** — доступ ограничивается сетевым слоем кластера. (В исходной
    swag-схеме параметр `Authorization` у них помечен как обязательный —
    это артефакт аннотаций, а не рантайм-поведение.)

    ### Пагинация
    Явной пагинации у списочных ответов нет: коллекции возвращаются целиком
    (массивом или объектом-обёрткой, напр. `GetDiskListResponse.disks`,
    `ServiceAccountsResponse.service_account`). Часть выборок ограничивается
    параметрами запроса (`company_id`, `extend`, `upload_path`).

    ### Обработка ошибок
    Ошибки возвращаются как JSON `application/json` со схемой `AppError` —
    `{ message, error_code }` (`internal/api/apperrors`). Код `error_code`
    определяет HTTP-статус (`apperrors/error_response.go`):

    | `error_code` | Константа | HTTP-статус |
    | --- | --- | --- |
    | `PDM-0000` | SystemErrorCode | 500 |
    | `PDM-0001` | ErrNotFoundCode | 404 |
    | `PDM-0002` | ErrNoAuthCode | (ошибки авторизации → 401) |
    | `PDM-0003` | ErrNoAccessCode | 403 |
    | `PDM-0004` | ErrInvalidCode | 400 |
    | `PDM-0005` | ErrGoneCode | 410 |

    ### Замечания (расхождения кода/схемы)
    - Ошибки аутентификации middleware отдаёт статусом **401** (в swag-схеме
      эти ответы не описаны — там только `400`/`404`/`500`).
    - Ошибка «истёкшая публичная ссылка» маппится на **`410 Gone`**
      (`ErrGoneCode`), а не на `404`.
    - Путь загрузки части файла содержит двойной слэш —
      `POST /api/v1/uploads//multipart/{upload_id}/{part_num}` (так в схеме и
      маршруте).
    - `api_server` задаёт очень большой `BodyLimit` (5 ТБ) для загрузки файлов.

  contact:
    name: Sarex
    url: https://gitlab/pdm/documentation-api-v2

servers:
  - url: http://api-v2-service.documentations
    description: Stage (ClusterIP, порт 80 → 8080)
  - url: http://api-v2-service.documentations-preprod
    description: Preprod (ClusterIP)
  - url: http://api-v2-service.documentations-prod
    description: Production (ClusterIP)
  - url: http://localhost:8000
    description: Локальный запуск (API_ADDRESS)

tags:
  - name: document
    description: Документы (создание, изменение, перемещение, скачивание)
  - name: disk
    description: Диски и их проекты/сервис-аккаунты
  - name: bundle
    description: Бандлы и загрузка файлов
  - name: page
    description: Страницы data source
  - name: upload
    description: Multipart-загрузка
  - name: workflow
    description: Workflow-обработка и вебхуки
  - name: workspace
    description: Рабочие области
  - name: permission
    description: Типы прав доступа
  - name: data_source
    description: Data source (внутренние проверки)

security:
  - bearerAuth: []

paths:
  # ==========================================================================
  # Documents (public /api/v1)
  # ==========================================================================
  /api/v1/documents:
    post:
      tags: [document]
      summary: create document
      description: Create document with bundle and data_sources.
      operationId: createDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDocumentRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/documents/types:
    get:
      tags: [document]
      summary: types
      description: Descriptions of possible documents.
      operationId: getDocumentTypes
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BundleTypeStruct'

  /api/v1/documents/{document_id}:
    get:
      tags: [document]
      summary: get document
      description: >-
        Get document by document_id and optional "extend". If "extend" is
        "bundles", returns bundles with all data_sources and pages.
      operationId: getDocument
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 }
        - { name: extend, in: query, required: false, schema: { type: string }, example: bundles }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }
    patch:
      tags: [document]
      summary: change document
      description: Change document's name.
      operationId: changeDocument
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PatchDocument' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }
    delete:
      tags: [document]
      summary: delete document
      description: Delete document (soft delete).
      operationId: deleteDocument
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/documents/{document_id}/update-path:
    patch:
      tags: [document]
      summary: move document to another folder
      description: Move document to another folder.
      operationId: updateDocumentPath
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PatchDocumentPath' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/documents/{document_id}/move_bundles:
    patch:
      tags: [document]
      summary: move bundles
      description: >-
        Move bundles to document. The source document is marked as deleted if it
        has no bundles left after the move.
      operationId: moveBundles
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: string }, example: "1" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MoveBundleRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Bundle' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/documents/{document_id}/add_bundle:
    post:
      tags: [document]
      summary: add bundle to document
      description: Add created and uploaded bundle to an existing document.
      operationId: addBundleToDocument
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AddBundleRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Bundle' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/documents/{document_id}/ancestors:
    get:
      tags: [document]
      summary: get ancestors of documents
      operationId: getDocumentAncestors
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Document' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/documents/{document_id}/download:
    get:
      tags: [document]
      summary: get url for download document
      operationId: getDocumentDownloadURL
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DownloadURLResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/documents/{document_ids}/company:
    get:
      tags: [document]
      summary: get company_ids by document_ids
      description: Get map of documents and companies.
      operationId: getDocumentsCompanyMap
      parameters:
        - { name: document_ids, in: path, required: true, schema: { type: string }, example: "1,2,3,4,5" }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DocumentCompanyMap' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  # ==========================================================================
  # Disks
  # ==========================================================================
  /api/v1/disks:
    get:
      tags: [disk]
      summary: get disks
      description: >-
        Get disks. company_id requests disks of a specific company; by default
        the user gets disks of all their companies.
      operationId: getDisks
      parameters:
        - { name: company_id, in: query, required: true, schema: { type: integer }, example: 1 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GetDiskListResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }
    post:
      tags: [disk]
      summary: create disk
      description: Create disk. Requires admin permissions in Django.
      operationId: createDisk
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateDiskRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Disk' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }
    delete:
      tags: [disk]
      summary: delete disk
      description: >-
        Soft delete of a disk (marks the disk, not the document, as deleted).
        Requires admin permissions in Django. disk_id passed as path in the
        underlying route.
      operationId: deleteDisk
      parameters:
        - { name: disk_id, in: path, required: true, schema: { type: string }, example: 2e3bda68-09bb-4bc0-b3c3-984117256c8b }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/disks/{disk_id}/projects:
    get:
      tags: [disk]
      summary: get projects
      description: Get projects by disk_id.
      operationId: getDiskProjects
      parameters:
        - { name: disk_id, in: path, required: true, schema: { type: string }, example: 2e3bda68-09bb-4bc0-b3c3-984117256c8b }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GetDiskListResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/disks/{disk_id}/service_accounts:
    get:
      tags: [disk]
      summary: get service accounts
      description: Get service accounts by disk_id.
      operationId: getDiskServiceAccounts
      parameters:
        - { name: disk_id, in: path, required: true, schema: { type: string }, example: 2e3bda68-09bb-4bc0-b3c3-984117256c8b }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceAccountsResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  # ==========================================================================
  # Bundles
  # ==========================================================================
  /api/v1/bundles/{bundle_id}/bucket_name:
    get:
      tags: [bundle]
      summary: get bucket name
      description: Get bucket name by bundle_id.
      operationId: getBucketName
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BucketNameResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/bundles/{bundle_id}/download:
    get:
      tags: [bundle]
      summary: get url for download bundle
      operationId: getBundleDownloadURL
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DownloadURLResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/bundles/{bundle_id}/{bundle_key}/download:
    get:
      tags: [bundle]
      summary: get url for download bundle (data_source)
      description: Get url for download of a specific data_source of a bundle.
      operationId: getBundleDataSourceDownloadURL
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
        - { name: bundle_key, in: path, required: true, schema: { type: string }, example: las }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DownloadURLResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/bundles/{bundle_id}/{bundle_key}/upload_multipart:
    post:
      tags: [bundle]
      summary: initial multipart upload
      description: Initialize a multipart upload for chunked file upload to storage.
      operationId: initMultipartUpload
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
        - { name: bundle_key, in: path, required: true, schema: { type: string }, example: potree }
        - { name: upload_path, in: query, required: false, schema: { type: string }, example: r/1/1/2 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MultipartUpload' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/bundles/{bundle_id}/{bundle_key}/upload_single:
    post:
      tags: [bundle]
      summary: upload single file
      description: >-
        Upload a single file to storage for a specific bundle. Bundle and
        DataSource must already exist.
      operationId: uploadSingleFile
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
        - { name: bundle_key, in: path, required: true, schema: { type: string }, example: potree }
        - { name: upload_path, in: query, required: false, schema: { type: string }, example: data/r/r.bin }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/bundles/{bundle_id}/upload_finish:
    post:
      tags: [bundle]
      summary: finish upload
      description: >-
        Finish upload: change bundle status from "created" to "uploaded", update
        data_source file sizes, check for files in storage.
      operationId: finishUpload
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Bundle' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  # ==========================================================================
  # Pages
  # ==========================================================================
  /api/v1/pages:
    post:
      tags: [page]
      summary: create page
      description: Creates a page for a specified data_source_id.
      operationId: createPage
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreatePageRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Page' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/pages/{data_source}/{page_key}/download:
    get:
      tags: [page]
      summary: get download url page
      operationId: getPageDownloadURL
      parameters:
        - { name: data_source, in: path, required: true, schema: { type: string }, example: "1" }
        - { name: page_key, in: path, required: true, schema: { type: string }, example: "2" }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DownloadURLResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  # ==========================================================================
  # Permissions
  # ==========================================================================
  /api/v1/permissions:
    get:
      tags: [permission]
      summary: permission types
      operationId: getPermissionTypes
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/PermissionType' }

  # ==========================================================================
  # Uploads (multipart parts)
  # ==========================================================================
  /api/v1/uploads//multipart/{upload_id}/{part_num}:
    post:
      tags: [upload]
      summary: upload multipart upload part
      description: >-
        Upload a part of a file to storage. NB: the route contains a double
        slash after `uploads` (as registered).
      operationId: uploadMultipartPart
      parameters:
        - { name: upload_id, in: path, required: true, schema: { type: string } }
        - { name: part_num, in: path, required: true, schema: { type: integer }, example: 1 }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
              required: [file]
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/uploads/multipart/{upload_id}/abort:
    post:
      tags: [upload]
      summary: abort multipart upload
      operationId: abortMultipartUpload
      parameters:
        - { name: upload_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/uploads/multipart/{upload_id}/complete:
    post:
      tags: [upload]
      summary: complete multipart upload
      description: >-
        Complete a multipart upload. When all parts are uploaded, marks parts in
        storage and database as "completed".
      operationId: completeMultipartUpload
      parameters:
        - { name: upload_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  # ==========================================================================
  # Workflows
  # ==========================================================================
  /api/v1/workflows/{workflow_id}:
    get:
      tags: [workflow]
      summary: get workflow
      operationId: getWorkflow
      parameters:
        - { name: workflow_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Workflow' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }
    post:
      tags: [workflow]
      summary: webhook
      description: >-
        Determines and updates workflow status (and bundle workflow status). On
        success both are set to "done"; if bundle has no document_id, status is
        set to "No document".
      operationId: workflowWebhook
      parameters:
        - { name: workflow_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  # ==========================================================================
  # Workspaces
  # ==========================================================================
  /api/v1/workspaces:
    post:
      tags: [workspace]
      summary: create workspace
      description: Creates a workspace document that may include other documents.
      operationId: createWorkspace
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateWorkspaceRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /api/v1/workspaces/{ws_id}:
    get:
      tags: [workspace]
      summary: get workspace document
      description: Get document with workspace type.
      operationId: getWorkspace
      parameters:
        - { name: ws_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  # ==========================================================================
  # Internal (/internal/v1) — без app-аутентификации, только внутри кластера
  # ==========================================================================
  /internal/v1/bundles/{bundle_id}:
    get:
      tags: [bundle]
      summary: get internal bundle
      description: Get internal bundle by bundle_id.
      operationId: getInternalBundle
      security: []
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Bundle' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /internal/v1/bundles/{bundle_id}/workflow:
    post:
      tags: [bundle]
      summary: add workflow
      description: Add workflow to bundle.
      operationId: addBundleWorkflow
      security: []
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Workflow' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /internal/v1/documents/{document_id}:
    delete:
      tags: [document]
      summary: internal delete document
      description: Internal delete document (soft delete).
      operationId: internalDeleteDocument
      security: []
      parameters:
        - { name: document_id, in: path, required: true, schema: { type: integer }, example: 999 }
      responses:
        '200': { description: OK }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

  /internal/v1/is_folder/bundles/{bundle_id}/{bundle_key}:
    get:
      tags: [data_source]
      summary: is data_source folder handler
      description: Returns whether the data_source is a folder or not.
      operationId: isDataSourceFolder
      security: []
      parameters:
        - { name: bundle_id, in: path, required: true, schema: { type: string } }
        - { name: bundle_key, in: path, required: true, schema: { type: string }, example: potree }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IsDataSourceFolderResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/ServerError' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        JWT в заголовке `Authorization: Bearer <jwt>` или в query-параметре
        `auth_jwt`. Режим sarex-backend проверяется RSA-ключом `PUBLIC_KEY`;
        режим Zitadel — по заголовку `Identity`.

  responses:
    BadRequest:
      description: Bad Request
      content:
        application/json:
          schema: { $ref: '#/components/schemas/AppError' }
    NotFound:
      description: Not Found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/AppError' }
    ServerError:
      description: Internal Server Error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/AppError' }

  schemas:
    ErrCodeType:
      type: string
      enum: [PDM-0000, PDM-0001, PDM-0002, PDM-0003, PDM-0004, PDM-0005]
      description: >-
        SystemErrorCode / ErrNotFoundCode / ErrNoAuthCode / ErrNoAccessCode /
        ErrInvalidCode / ErrGoneCode

    AppError:
      type: object
      properties:
        message: { type: string }
        error_code: { $ref: '#/components/schemas/ErrCodeType' }

    AddBundleRequest:
      type: object
      required: [bundle_id]
      properties:
        bundle_id: { type: string }

    BucketNameResponse:
      type: object
      properties:
        bucket_name: { type: string }

    Bundle:
      type: object
      properties:
        attributes: { type: object, additionalProperties: true }
        author: { $ref: '#/components/schemas/User' }
        bim_id: { type: integer }
        bundle_copied_from: { type: string }
        created_at: { type: string }
        data_sources:
          type: array
          items: { $ref: '#/components/schemas/DataSource' }
        date: { type: string, description: optional field for django integration }
        document_id: { type: integer }
        has_digital_signature: { type: boolean }
        has_qr_code: { type: boolean }
        has_stamp: { type: boolean }
        id: { type: string }
        size: { type: integer }
        type: { $ref: '#/components/schemas/BundleType' }
        upload_status: { $ref: '#/components/schemas/BundleUploadStatus' }
        workflow: { $ref: '#/components/schemas/Workflow' }
        workflow_status: { $ref: '#/components/schemas/BundleWorkflowStatus' }

    BundleKey:
      type: string
      enum:
        - las
        - potree
        - e57
        - clouds
        - panoramas_json
        - panoramas
        - glb
        - original
        - tiles
        - tif
        - tif_dem
        - metadata
        - colored-relief-tif
        - topographic_tiles
        - geojson
        - ap_rasterized_las
        - ap_rasterized_potree
        - ab_las
        - ab_potree
        - deviation_json
        - bim
        - bim_optimized
        - bim_optimizedGz
        - bim_metadata
        - bim_metadataGz
        - bim_metadata_static
        - bim_metadata_staticGz
        - diff_json
        - ifc
        - nwd
        - rvt
        - nwc
        - abap_json
        - cloud_ooc
        - debug_abap_json
        - pdf
        - p7s
        - xlsx
        - dxf
        - docx
        - dwg

    BundleKeyField:
      type: object
      properties:
        allowed_extensions:
          type: array
          items: { type: string }
        can_be_downloaded_by_user: { type: boolean }
        can_be_uploaded_by_user: { type: boolean }
        file: { type: boolean }
        folder: { type: boolean }
        key: { $ref: '#/components/schemas/BundleKey' }
        required: { type: boolean }
        title: { type: string }

    BundleType:
      type: string
      enum:
        - cloud
        - bim
        - bimv2
        - surface
        - other_files
        - deviation
        - dem
        - orthophoto
        - dxf
        - ksg
        - docx
        - dwg
        - pdf
        - xlsx
        - abap
        - c2c
        - c2s

    BundleTypeStruct:
      type: object
      properties:
        fields:
          type: array
          items: { $ref: '#/components/schemas/BundleKeyField' }
        name: { $ref: '#/components/schemas/BundleType' }
        title: { type: string }

    BundleUploadStatus:
      type: string
      enum: [created, uploaded]

    BundleWorkflowStatus:
      type: string
      enum: [created, skipped, done, errored]

    CreateDiskRequest:
      type: object
      required: [admin_ids, company_id, storage_type]
      properties:
        admin_ids:
          type: array
          minItems: 1
          items: { type: integer }
        company_id: { type: integer }
        storage_type:
          type: string
          maxLength: 50
          enum: [sarex]

    CreateDocumentRequest:
      type: object
      required: [disk_id, is_folder, name, parent_id]
      properties:
        bundle_id: { type: string }
        disk_id: { type: string }
        is_folder: { type: boolean }
        is_project: { type: boolean }
        name: { type: string, maxLength: 250 }
        parent_id: { type: integer }
        target_id: { type: integer }

    CreatePageRequest:
      type: object
      properties:
        data_source: { type: string }
        id: { type: string }
        page_order: { type: integer }
        thumbnail: { type: string }

    CreateWorkspaceRequest:
      type: object
      required: [disk_id, documents, name, parent_id]
      properties:
        disk_id: { type: string }
        documents:
          type: array
          items: { type: integer }
        name: { type: string, maxLength: 250 }
        parent_id: { type: integer }

    DataSource:
      type: object
      properties:
        file_name: { type: string }
        format: { $ref: '#/components/schemas/DataSourceFormat' }
        id: { type: string }
        key: { $ref: '#/components/schemas/BundleKey' }
        pages:
          type: array
          items: { $ref: '#/components/schemas/Page' }
        size: { type: integer }
        type: { $ref: '#/components/schemas/DataSourceUploadType' }

    DataSourceFormat:
      type: string
      enum: [glb, json, gz, s3d]

    DataSourceUploadType:
      type: string
      enum: [file, folder]

    DisableButton:
      type: string
      enum: [workspace, delete, rename, create, project, download, history, add_doc_to_ws, del_doc_from_ws]

    Disk:
      type: object
      properties:
        admin_ids:
          type: array
          items: { type: integer }
        bucket_name: { type: string }
        company_id: { type: integer }
        created_at: { type: string }
        disable_button:
          type: array
          items: { $ref: '#/components/schemas/DisableButton' }
        document_id: { type: integer }
        id: { type: string }
        name: { type: string }
        permissions_editable: { type: boolean }
        storage_type: { type: string }

    Document:
      type: object
      properties:
        author: { $ref: '#/components/schemas/User' }
        bundles:
          type: array
          items: { $ref: '#/components/schemas/Bundle' }
        created_at: { type: string }
        dashboard_id: { type: integer }
        disable_button:
          type: array
          items: { $ref: '#/components/schemas/DisableButton' }
        disk_id: { type: string }
        document_copied_from: { type: integer }
        files:
          type: array
          items: { type: integer }
        folders:
          type: array
          items: { type: integer }
        has_digital_signature: { type: boolean }
        has_public_link: { type: boolean }
        has_qr_code: { type: boolean }
        has_stamp: { type: boolean }
        id: { type: integer }
        is_folder: { type: boolean }
        name: { type: string }
        path: { type: string }
        project_id: { type: integer }
        public_link: { type: string }
        public_link_available: { type: boolean }
        public_link_expiration_time: { type: string }
        public_link_id: { type: string }
        type: { $ref: '#/components/schemas/DocumentType' }
        workspace_id: { type: string }

    DocumentCompanyMap:
      type: object
      properties:
        result:
          type: object
          additionalProperties: { type: integer }

    DocumentType:
      type: string
      enum:
        - workspace
        - dashboard
        - cloud
        - bim
        - bimv2
        - project
        - folder
        - root
        - surface
        - pdf
        - xlsx
        - orthophoto
        - dem
        - dxf
        - ksg
        - docx
        - dwg

    DownloadURLResponse:
      type: object
      properties:
        download_url: { type: string }

    GetDiskListResponse:
      type: object
      properties:
        disks:
          type: array
          items: { $ref: '#/components/schemas/Disk' }

    IsDataSourceFolderResponse:
      type: object
      properties:
        result: { type: boolean }

    MoveBundleRequest:
      type: object
      required: [bundle_ids]
      properties:
        bundle_ids:
          type: array
          items: { type: string }

    MultipartUpload:
      type: object
      properties:
        upload_id: { type: string }

    Page:
      type: object
      properties:
        id: { type: string }
        page_order: { type: integer }
        thumbnail: { type: string }

    PatchDocument:
      type: object
      required: [name]
      properties:
        name: { type: string }

    PatchDocumentPath:
      type: object
      required: [parent_id]
      properties:
        parent_id: { type: integer }

    PermissionType:
      type: object
      properties:
        label: { type: string }
        value: { type: string }

    ServiceAccount:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        type: { type: string }
        username: { type: string }

    ServiceAccountsResponse:
      type: object
      properties:
        service_account:
          type: array
          items: { $ref: '#/components/schemas/ServiceAccount' }

    User:
      type: object
      properties:
        companies:
          type: array
          items: { type: integer }
        departments:
          type: array
          items: { $ref: '#/components/schemas/UserDepartment' }
        first_name: { type: string }
        id: { type: integer }
        is_superuser: { type: boolean }
        last_name: { type: string }
        positions:
          type: array
          items: { $ref: '#/components/schemas/UserPosition' }
        service_account_id: { type: string }
        username: { type: string }

    UserDepartment:
      type: object
      properties:
        id: { type: integer }
        service_account_id: { type: string }

    UserPosition:
      type: object
      properties:
        id: { type: integer }
        service_account_id: { type: string }

    Workflow:
      type: object
      properties:
        company_id: { type: integer }
        created_at: { type: string }
        id: { type: string }
        name: { type: string }
        state: { type: string }
        task_runs:
          type: array
          items: { type: object, additionalProperties: true }
        tasks:
          type: array
          items: { type: object, additionalProperties: true }
        updated_at: { type: string }
        valid_until: { type: string }
