{
  "openapi": "3.0.3",
  "info": {
    "title": "GitWave API",
    "version": "1.0.0",
    "description": "Zero-knowledge DevOps gateway. Сервер хранит **только шифротекст** — шифрование и расшифровка происходят на стороне клиента (CLI, WASM в браузере, десктоп-приложение, аттестованный CI-раннер). Любой endpoint, отдающий `application/octet-stream`, возвращает зашифрованные байты; JSON-endpoint'ы отдают только метаданные (идентификаторы, счётчики, статусы). Подробное руководство по взаимодействию — на странице /docs.",
    "contact": { "name": "GitWave", "url": "https://gitwave.ru" }
  },
  "servers": [
    { "url": "https://gitwave.ru", "description": "Production (single origin)" }
  ],
  "tags": [
    { "name": "Health", "description": "Проверка живости (без авторизации)." },
    { "name": "Auth", "description": "Получение токена: Aris Drive (hands-free) или Aris ID (OIDC). Под единым доменом — /aris-drive и /aris-id." },
    { "name": "Repositories", "description": "Создание и перечисление репозиториев (метаданные)." },
    { "name": "Objects", "description": "Зашифрованные git-объекты (blob/tree/commit). Тело — шифротекст." },
    { "name": "Epochs", "description": "Обёрнутые ключи по эпохам (forward-secrecy ротация, CRYPTO §4)." },
    { "name": "Devices", "description": "Подписанные одобрения устройств (trusted-device, CRYPTO §6)." },
    { "name": "Refs", "description": "Ветки: имя → oid коммита (байты)." },
    { "name": "CI Runs", "description": "Статусы CI по коммитам (только pass/fail, без логов)." },
    { "name": "Monitor", "description": "Aris Monitor: приём ошибок прод-рантайма → авто-issue." },
    { "name": "Issues", "description": "Задачи (метаданные). Авто из Monitor или вручную." },
    { "name": "Docs", "description": "Ссылки на документы ArisDoc (ТЗ, договоры, заметки)." }
  ],
  "security": [ { "bearerAuth": [] } ],
  "paths": {
    "/healthz": {
      "get": {
        "tags": ["Health"], "summary": "Проверка живости", "security": [],
        "responses": { "200": { "description": "Сервис жив", "content": { "text/plain": { "schema": { "type": "string", "example": "ok" } } } } }
      }
    },
    "/aris-drive/agent/session": {
      "get": {
        "tags": ["Auth"], "summary": "Сессия Aris Drive (hands-free вход)", "security": [],
        "description": "Локальный агент Aris Drive выдаёт подписанный OIDC-токен (ES256) без ручного ввода. Используйте `token` как `Authorization: Bearer`.",
        "responses": { "200": { "description": "Токен и пользователь", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" }, "example": { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJyb21hbiI...", "user": "roman" } } } } }
      }
    },
    "/aris-drive/agent/keys/{repo}": {
      "get": {
        "tags": ["Auth"], "summary": "Device-ключ репозитория (для локальной расшифровки)", "security": [],
        "description": "Возвращает device-ключ (hex) для разворачивания ключа репозитория и расшифровки объектов на устройстве. Ключ не покидает доверенную зону — сервер его не хранит.",
        "parameters": [ { "$ref": "#/components/parameters/Repo" } ],
        "responses": {
          "200": { "description": "Device-ключ", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceKey" } } } },
          "404": { "description": "Ключа для репозитория нет" }
        }
      }
    },
    "/aris-id/authorize": {
      "get": {
        "tags": ["Auth"], "summary": "Aris ID — экран согласия (OIDC)", "security": [],
        "description": "Альтернатива hands-free входу: OIDC-флоу. Открывается в браузере; после согласия `/aris-id/callback` редиректит на `redirect_uri#access_token=<jwt>`.",
        "parameters": [
          { "name": "redirect_uri", "in": "query", "required": true, "schema": { "type": "string" }, "example": "https://gitwave.ru/app" },
          { "name": "state", "in": "query", "required": false, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "HTML-страница согласия", "content": { "text/html": {} } } }
      }
    },
    "/aris-id/callback": {
      "get": {
        "tags": ["Auth"], "summary": "Aris ID — выпуск токена и редирект", "security": [],
        "parameters": [
          { "name": "redirect_uri", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "sub", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Идентификатор пользователя Aris ID." },
          { "name": "state", "in": "query", "required": false, "schema": { "type": "string" } }
        ],
        "responses": { "303": { "description": "Редирект на redirect_uri с #access_token=<jwt> во фрагменте." } }
      }
    },
    "/v1/repos": {
      "get": {
        "tags": ["Repositories"], "summary": "Список репозиториев (метаданные)",
        "responses": {
          "200": { "description": "Сводка по каждому репозиторию", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RepoSummary" } }, "example": [ { "id": "demo", "objects": 4, "branches": 1, "devices": 0, "runs": 0, "open_issues": 0, "docs": 0, "ci": null, "head": "04163b2527ef01c7d5840afffae646e4bd4fd09d" } ] } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Repositories"], "summary": "Создать репозиторий",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateRepo" } } } },
        "responses": {
          "201": { "description": "Создан" },
          "409": { "description": "Репозиторий с таким id уже есть" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/repos/{repo_id}/objects": {
      "get": {
        "tags": ["Objects"], "summary": "Выгрузить все объекты (oid → hex шифротекст)",
        "description": "Массовая выгрузка для браузерного WASM-клиента: карта `oid → hex(ciphertext)`. Сервер ничего не расшифровывает.",
        "parameters": [ { "$ref": "#/components/parameters/RepoId" } ],
        "responses": {
          "200": { "description": "Карта объектов", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": { "type": "string", "description": "hex шифротекста" }, "example": { "540010b4ef...": "96c9b846a756217b..." } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/repos/{repo_id}/objects/{oid}": {
      "parameters": [ { "$ref": "#/components/parameters/RepoId" }, { "$ref": "#/components/parameters/Oid" } ],
      "put": {
        "tags": ["Objects"], "summary": "Загрузить зашифрованный объект",
        "description": "Тело — сырой шифротекст (AES-256-GCM). Клиент шифрует локально; сервер хранит как есть.",
        "requestBody": { "required": true, "content": { "application/octet-stream": { "schema": { "$ref": "#/components/schemas/Ciphertext" } } } },
        "responses": { "201": { "description": "Сохранён" }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      },
      "get": {
        "tags": ["Objects"], "summary": "Скачать зашифрованный объект",
        "responses": {
          "200": { "description": "Шифротекст объекта", "content": { "application/octet-stream": { "schema": { "$ref": "#/components/schemas/Ciphertext" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/repos/{repo_id}/epochs": {
      "get": {
        "tags": ["Epochs"], "summary": "Список эпох (по возрастанию)",
        "parameters": [ { "$ref": "#/components/parameters/RepoId" } ],
        "responses": { "200": { "description": "Идентификаторы эпох", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "integer", "format": "int64" }, "example": [0, 1, 2] } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/epochs/{epoch}": {
      "parameters": [ { "$ref": "#/components/parameters/RepoId" }, { "name": "epoch", "in": "path", "required": true, "schema": { "type": "integer", "format": "int64" } } ],
      "put": {
        "tags": ["Epochs"], "summary": "Сохранить обёрнутый ключ эпохи",
        "requestBody": { "required": true, "content": { "application/octet-stream": { "schema": { "$ref": "#/components/schemas/Ciphertext" } } } },
        "responses": { "201": { "description": "Сохранён" }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      },
      "get": {
        "tags": ["Epochs"], "summary": "Получить обёрнутый ключ эпохи",
        "responses": { "200": { "description": "Обёрнутый ключ (шифротекст)", "content": { "application/octet-stream": { "schema": { "$ref": "#/components/schemas/Ciphertext" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/devices": {
      "get": {
        "tags": ["Devices"], "summary": "Список устройств",
        "parameters": [ { "$ref": "#/components/parameters/RepoId" } ],
        "responses": { "200": { "description": "Идентификаторы устройств", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" }, "example": ["laptop-1", "ci-runner-3"] } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/devices/{device_id}": {
      "parameters": [ { "$ref": "#/components/parameters/RepoId" }, { "name": "device_id", "in": "path", "required": true, "schema": { "type": "string" } } ],
      "put": {
        "tags": ["Devices"], "summary": "Сохранить подписанное одобрение устройства",
        "description": "Тело — подписанный блоб одобрения (цепочка доверия устройств). Сервер хранит, не интерпретирует.",
        "requestBody": { "required": true, "content": { "application/octet-stream": { "schema": { "$ref": "#/components/schemas/Ciphertext" } } } },
        "responses": { "201": { "description": "Сохранено" }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      },
      "get": {
        "tags": ["Devices"], "summary": "Получить одобрение устройства",
        "responses": { "200": { "description": "Подписанный блоб", "content": { "application/octet-stream": { "schema": { "$ref": "#/components/schemas/Ciphertext" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/refs": {
      "get": {
        "tags": ["Refs"], "summary": "Список веток",
        "parameters": [ { "$ref": "#/components/parameters/RepoId" } ],
        "responses": { "200": { "description": "Имена веток", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" }, "example": ["main", "dev"] } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/refs/{name}": {
      "parameters": [ { "$ref": "#/components/parameters/RepoId" }, { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "example": "main" } ],
      "put": {
        "tags": ["Refs"], "summary": "Обновить ветку (указать на oid коммита)",
        "requestBody": { "required": true, "content": { "application/octet-stream": { "schema": { "type": "string", "description": "Байты oid коммита (hex-строка)" } } } },
        "responses": { "201": { "description": "Обновлено" }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      },
      "get": {
        "tags": ["Refs"], "summary": "Получить oid ветки",
        "responses": { "200": { "description": "oid коммита", "content": { "application/octet-stream": { "schema": { "type": "string" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/runs": {
      "get": {
        "tags": ["CI Runs"], "summary": "Список коммитов с CI-статусом",
        "parameters": [ { "$ref": "#/components/parameters/RepoId" } ],
        "responses": { "200": { "description": "oid коммитов", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/runs/{commit}": {
      "parameters": [ { "$ref": "#/components/parameters/RepoId" }, { "name": "commit", "in": "path", "required": true, "schema": { "type": "string" } } ],
      "put": {
        "tags": ["CI Runs"], "summary": "Записать статус CI коммита",
        "description": "Только метаданные результата — `{ \"ok\": true|false }`. Логи остаются на раннере и сюда не отправляются.",
        "requestBody": { "required": true, "content": { "application/octet-stream": { "schema": { "$ref": "#/components/schemas/RunStatus" } } } },
        "responses": { "201": { "description": "Записано" }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      },
      "get": {
        "tags": ["CI Runs"], "summary": "Получить статус CI коммита",
        "responses": { "200": { "description": "Запись статуса", "content": { "application/octet-stream": { "schema": { "$ref": "#/components/schemas/RunStatus" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/monitor/errors": {
      "post": {
        "tags": ["Monitor"], "summary": "Принять ошибку прод-рантайма (→ авто-issue)",
        "description": "Aris Monitor: на первую ошибку с данным `fingerprint` создаётся issue, на повторы — инкремент счётчика. Стек-трейс шифруется рантаймом и сюда не попадает.",
        "parameters": [ { "$ref": "#/components/parameters/RepoId" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorReport" } } } },
        "responses": { "200": { "description": "Подтверждение", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorAck" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/issues": {
      "parameters": [ { "$ref": "#/components/parameters/RepoId" } ],
      "get": {
        "tags": ["Issues"], "summary": "Список задач (новые первыми)",
        "responses": { "200": { "description": "Задачи", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Issue" } } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      },
      "post": {
        "tags": ["Issues"], "summary": "Создать задачу вручную",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NewIssue" } } } },
        "responses": { "200": { "description": "Создана", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Issue" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/issues/{id}": {
      "get": {
        "tags": ["Issues"], "summary": "Получить задачу",
        "parameters": [ { "$ref": "#/components/parameters/RepoId" }, { "$ref": "#/components/parameters/IssueId" } ],
        "responses": { "200": { "description": "Задача", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Issue" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/issues/{id}/close": {
      "post": {
        "tags": ["Issues"], "summary": "Закрыть задачу",
        "parameters": [ { "$ref": "#/components/parameters/RepoId" }, { "$ref": "#/components/parameters/IssueId" } ],
        "responses": { "200": { "description": "Закрыта", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Issue" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    },
    "/v1/repos/{repo_id}/docs": {
      "parameters": [ { "$ref": "#/components/parameters/RepoId" } ],
      "get": {
        "tags": ["Docs"], "summary": "Список ссылок ArisDoc",
        "responses": { "200": { "description": "Ссылки на документы", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/DocLink" } } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      },
      "post": {
        "tags": ["Docs"], "summary": "Привязать документ ArisDoc",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NewDoc" } } } },
        "responses": { "200": { "description": "Привязан", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DocLink" } } } }, "404": { "$ref": "#/components/responses/NotFound" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "OIDC-токен (ES256) из Aris Drive `/aris-drive/agent/session` или Aris ID. Заголовок: `Authorization: Bearer <jwt>`." }
    },
    "parameters": {
      "RepoId": { "name": "repo_id", "in": "path", "required": true, "schema": { "type": "string" }, "example": "demo" },
      "Repo": { "name": "repo", "in": "path", "required": true, "schema": { "type": "string" }, "example": "demo" },
      "Oid": { "name": "oid", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Идентификатор git-объекта (hex)." },
      "IssueId": { "name": "id", "in": "path", "required": true, "schema": { "type": "integer", "format": "int32" } }
    },
    "responses": {
      "Unauthorized": { "description": "Нет или неверный Bearer-токен" },
      "NotFound": { "description": "Репозиторий или ресурс не найден" }
    },
    "schemas": {
      "CreateRepo": { "type": "object", "required": ["repo_id"], "properties": { "repo_id": { "type": "string", "example": "demo" } } },
      "RepoSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "objects": { "type": "integer", "description": "число зашифрованных объектов" },
          "branches": { "type": "integer" },
          "devices": { "type": "integer" },
          "runs": { "type": "integer" },
          "open_issues": { "type": "integer" },
          "docs": { "type": "integer" },
          "ci": { "type": "boolean", "nullable": true, "description": "результат CI head-коммита (pass/fail) или null" },
          "head": { "type": "string", "nullable": true, "description": "oid head-коммита ветки main" }
        },
        "example": { "id": "demo", "objects": 4, "branches": 1, "devices": 0, "runs": 0, "open_issues": 0, "docs": 0, "ci": null, "head": "04163b2527ef01c7d5840afffae646e4bd4fd09d" }
      },
      "Ciphertext": { "type": "string", "format": "binary", "description": "Зашифрованные байты (AES-256-GCM). Сервер не может их прочитать." },
      "RunStatus": { "type": "object", "required": ["ok"], "properties": { "ok": { "type": "boolean", "description": "true — CI прошёл, false — упал" } } },
      "ErrorReport": {
        "type": "object", "required": ["commit", "fingerprint", "title"],
        "properties": {
          "commit": { "type": "string", "description": "oid сборки, где возникла ошибка" },
          "fingerprint": { "type": "string", "description": "ключ дедупликации повторяющихся ошибок" },
          "title": { "type": "string", "description": "класс ошибки (метаданные; без стека)" }
        },
        "example": { "commit": "04163b25", "fingerprint": "NPE@pay.rs:42", "title": "NullPointer in refunds" }
      },
      "ErrorAck": {
        "type": "object",
        "properties": { "issue_id": { "type": "integer" }, "created": { "type": "boolean", "description": "создан ли новый issue" }, "count": { "type": "integer", "description": "число повторов" } },
        "example": { "issue_id": 1, "created": true, "count": 1 }
      },
      "NewIssue": { "type": "object", "required": ["title"], "properties": { "title": { "type": "string" }, "commit": { "type": "string", "nullable": true } } },
      "Issue": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "title": { "type": "string" },
          "commit": { "type": "string" },
          "status": { "type": "string", "enum": ["open", "closed"] },
          "source": { "type": "string", "enum": ["monitor", "manual"] },
          "count": { "type": "integer" }
        },
        "example": { "id": 1, "title": "NullPointer in refunds", "commit": "04163b25", "status": "open", "source": "monitor", "count": 1 }
      },
      "NewDoc": { "type": "object", "required": ["title", "kind"], "properties": { "title": { "type": "string" }, "kind": { "type": "string", "enum": ["spec", "contract", "proposal", "note"] }, "link": { "type": "string", "nullable": true } } },
      "DocLink": {
        "type": "object",
        "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "kind": { "type": "string" }, "link": { "type": "string", "description": "id/URL документа в ArisDoc" } },
        "example": { "id": 1, "title": "ТЗ на платежи", "kind": "spec", "link": "arisdoc://acme/spec/142" }
      },
      "Session": {
        "type": "object",
        "properties": { "token": { "type": "string", "description": "JWT (ES256)" }, "user": { "type": "string" } },
        "example": { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9...", "user": "roman" }
      },
      "DeviceKey": {
        "type": "object",
        "properties": { "device_key": { "type": "string", "description": "device-ключ в hex" } },
        "example": { "device_key": "a4af1c85eeb9e9b1275611b8993295f2..." }
      }
    }
  }
}
