{
  "openapi": "3.0.3",
  "info": {
    "title": "XMVP API",
    "version": "1.0.0",
    "description": "Публичное API сервиса «XMVP»: комнаты для совместного кода, файлов и видео.\n\n## Принципы\n\n* Все пути чистые и статичные. Идентификаторы комнат, фразы, пароли и любые чувствительные данные передаются только в JSON теле запроса, никогда в пути или query.\n* Все эндпоинты принимают POST с телом application/json (кроме GET /openapi.json).\n* Авторизация: заголовок `Authorization: Bearer <токен>`.\n* Токен аккаунта (`xa_...`) постоянный, выдаётся и сбрасывается в личном кабинете или через `/auth/session`.\n* Временный сессионный токен (`xs_...`) выдаётся анониму через `/auth/anon`, живёт 12 часов со скользящим продлением и умирает при перезапуске сервера.\n\n## Лимиты\n\n* 5 запросов в секунду на токен.\n* 3 ГБ трафика в сутки на токен (тела HTTP запросов и ответов плюс файловый трафик по WebSocket).\n* При превышении приходит 429 с кодом `rate_limit` или `daily_limit`.\n\n## Реалтайм: WebSocket /api/v1/ws\n\nВесь живой обмен (совместный редактор, файлы, видео, git внутри комнаты) идёт по WebSocket на чистом пути `/api/v1/ws`. После подключения сервер шлёт `{\"t\":\"hello\"}`, дальше клиент обязан привязать соединение к токену сообщением `{\"t\":\"sess\",\"token\":\"<токен>\"}` и получить `{\"t\":\"sess_ok\"}`. Вход в комнату: `{\"t\":\"join\",\"room\":\"<id>\"}` (плюс `roomPass`, если комната под паролем). Бинарные кадры: `[1 байт тип][полезная нагрузка]`, типы: 1 синхронизация Yjs, 2 awareness, 16 чанк выгрузки файла, 17 чанк скачивания. Текстовые команды: room_state, room_checkpass, join, claim, dl, room_zip, up_begin, up_end, file_del, git_commit, git_log, git_restore, ping."
  },
  "servers": [
    {
      "url": "/api/v1"
    }
  ],
  "tags": [
    {
      "name": "meta",
      "description": "Публичные параметры инстанса"
    },
    {
      "name": "auth",
      "description": "Токены и вход"
    },
    {
      "name": "account",
      "description": "Аккаунт"
    },
    {
      "name": "rooms",
      "description": "Комнаты"
    },
    {
      "name": "versions",
      "description": "Версии (git) комнат"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Токен аккаунта xa_... или временный сессионный токен xs_..."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "rate_limit"
          },
          "message": {
            "type": "string",
            "example": "Слишком часто: лимит 5 запросов в секунду"
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "day_bytes": {
            "type": "integer",
            "description": "Израсходовано за сутки, байт"
          },
          "day_limit_bytes": {
            "type": "integer",
            "example": 3221225472
          },
          "rps_limit": {
            "type": "integer",
            "example": 5
          }
        }
      },
      "Room": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "x7f2k9m3q"
          },
          "created": {
            "type": "integer",
            "description": "unix мс"
          },
          "files": {
            "type": "integer"
          },
          "docs": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "people": {
            "type": "integer"
          },
          "locked": {
            "type": "boolean",
            "description": "комната под паролем"
          }
        }
      },
      "Visited": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "alive": {
            "type": "boolean"
          },
          "owner": {
            "type": "string",
            "nullable": true
          },
          "locked": {
            "type": "boolean"
          },
          "people": {
            "type": "integer"
          },
          "at": {
            "type": "integer",
            "description": "когда заходил, unix мс"
          }
        }
      },
      "Commit": {
        "type": "object",
        "properties": {
          "hash": {
            "type": "string"
          },
          "author": {
            "type": "string"
          },
          "date": {
            "type": "string"
          },
          "msg": {
            "type": "string"
          }
        }
      },
      "AuthOk": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "example": "xa_9f2c..."
          },
          "kind": {
            "type": "string",
            "enum": [
              "account",
              "anon"
            ]
          },
          "nick": {
            "type": "string",
            "nullable": true
          },
          "plan": {
            "type": "string",
            "example": "free"
          },
          "features": {
            "type": "object"
          }
        }
      }
    },
    "responses": {
      "Err400": {
        "description": "Некорректный запрос",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Err401": {
        "description": "Нет или протух токен / неверная фраза",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Err403": {
        "description": "Недостаточно прав или тарифа",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Err404": {
        "description": "Не найдено или не твоя комната",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Err429": {
        "description": "Лимит 5 запросов в секунду или 3 ГБ трафика в сутки",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/openapi.json": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "Эта спецификация",
        "responses": {
          "200": {
            "description": "OpenAPI документ"
          }
        }
      }
    },
    "/meta": {
      "post": {
        "tags": [
          "meta"
        ],
        "summary": "Параметры инстанса",
        "description": "Публичные параметры: версия, ключ капчи (null, если выключена), ссылка на исходный код, лимиты и время жизни пустой комнаты. Токен не нужен.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Параметры",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": {
                      "type": "string"
                    },
                    "version": {
                      "type": "string"
                    },
                    "captcha_sitekey": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "github": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "limits": {
                      "type": "object",
                      "properties": {
                        "rps": {
                          "type": "integer"
                        },
                        "day_bytes": {
                          "type": "integer"
                        },
                        "body_bytes": {
                          "type": "integer"
                        },
                        "max_file_bytes": {
                          "type": "integer"
                        }
                      }
                    },
                    "room_ttl_minutes": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/auth/anon": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Временный сессионный токен без аккаунта",
        "description": "Выдаёт гостевой токен xs_... на 12 часов (скользящее продление при активности). С ним можно подключаться к WebSocket и заходить во временные комнаты.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Токен выдан",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthOk"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Err429"
          }
        }
      }
    },
    "/auth/register": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Регистрация аккаунта",
        "description": "Создаёт аккаунт, возвращает секретную фразу BIP39 из 12 слов (единственный ключ входа) и постоянный токен API.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nick",
                  "terms"
                ],
                "properties": {
                  "nick": {
                    "type": "string",
                    "example": "алиса"
                  },
                  "terms": {
                    "type": "boolean",
                    "description": "согласие с условиями"
                  },
                  "captcha": {
                    "type": "string",
                    "description": "Токен Яндекс SmartCaptcha (обязателен, если капча включена: см. /meta)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Аккаунт создан",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/AuthOk"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "phrase": {
                          "type": "string",
                          "example": "барс модуль жук ..."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Err400"
          },
          "403": {
            "$ref": "#/components/responses/Err403"
          },
          "429": {
            "$ref": "#/components/responses/Err429"
          }
        }
      }
    },
    "/auth/session": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Вход по секретной фразе",
        "description": "Фраза передаётся только в теле запроса. Возвращает постоянный токен API аккаунта.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phrase"
                ],
                "properties": {
                  "phrase": {
                    "type": "string",
                    "example": "барс модуль жук рысь ..."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Вход выполнен",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthOk"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err401"
          },
          "429": {
            "$ref": "#/components/responses/Err429"
          }
        }
      }
    },
    "/auth/whoami": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Кто я и мои лимиты",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Данные токена",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "type": "string"
                    },
                    "nick": {
                      "type": "string",
                      "nullable": true
                    },
                    "plan": {
                      "type": "string"
                    },
                    "usage": {
                      "$ref": "#/components/schemas/Usage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err401"
          },
          "429": {
            "$ref": "#/components/responses/Err429"
          }
        }
      }
    },
    "/token/reset": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Сбросить токен API аккаунта",
        "description": "Старый токен сразу перестаёт работать. Чувствительная операция: если на инстансе включена капча, в теле нужен свежий токен капчи.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "captcha": {
                    "type": "string",
                    "description": "Токен Яндекс SmartCaptcha (обязателен, если капча включена: см. /meta)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Новый токен",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err401"
          },
          "403": {
            "description": "Капча не пройдена (captcha_required)"
          }
        }
      }
    },
    "/account/info": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Профиль, комнаты, посещённые, токен и расход лимитов",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Профиль",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "nick": {
                      "type": "string"
                    },
                    "created": {
                      "type": "integer"
                    },
                    "plan": {
                      "type": "string"
                    },
                    "rooms": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Room"
                      }
                    },
                    "visited": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Visited"
                      }
                    },
                    "api": {
                      "type": "object",
                      "properties": {
                        "token": {
                          "type": "string"
                        },
                        "usage": {
                          "$ref": "#/components/schemas/Usage"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err401"
          },
          "403": {
            "$ref": "#/components/responses/Err403"
          }
        }
      }
    },
    "/account/phrase_reset": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Выдать новую секретную фразу",
        "description": "Старая фраза сразу перестаёт работать. Токен API при этом не меняется.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Новая фраза",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "phrase": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err401"
          }
        }
      }
    },
    "/account/delete": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Удалить аккаунт и все его комнаты",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Удалено",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err401"
          }
        }
      }
    },
    "/rooms/list": {
      "post": {
        "tags": [
          "rooms"
        ],
        "summary": "Мои комнаты и история посещённых",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Списки",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rooms": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Room"
                      }
                    },
                    "visited": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Visited"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err401"
          }
        }
      }
    },
    "/rooms/create": {
      "post": {
        "tags": [
          "rooms"
        ],
        "summary": "Создать постоянную комнату",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Создана",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "rooms": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Room"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Err403"
          }
        }
      }
    },
    "/rooms/delete": {
      "post": {
        "tags": [
          "rooms"
        ],
        "summary": "Удалить свою комнату",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Удалена"
          },
          "404": {
            "$ref": "#/components/responses/Err404"
          }
        }
      }
    },
    "/rooms/password": {
      "post": {
        "tags": [
          "rooms"
        ],
        "summary": "Поставить, сменить или убрать пароль комнаты",
        "description": "Пустой pass снимает пароль. Пароль передаётся только в теле.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "pass": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Готово",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "locked": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Err404"
          }
        }
      }
    },
    "/rooms/rekey": {
      "post": {
        "tags": [
          "rooms"
        ],
        "summary": "Сбросить ссылку комнаты (новый id)",
        "description": "Старая ссылка перестаёт работать. Доступно, когда комната пуста.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Новый id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "old": {
                      "type": "string"
                    },
                    "id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Err404"
          },
          "409": {
            "description": "В комнате кто-то есть",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/rooms/state": {
      "post": {
        "tags": [
          "rooms"
        ],
        "summary": "Сводка комнаты перед входом",
        "description": "Жива ли комната, есть ли пароль, сколько людей. Доступно любому токену.",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Сводка",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Visited"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "own": {
                          "type": "boolean"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Err401"
          }
        }
      }
    },
    "/versions/log": {
      "post": {
        "tags": [
          "versions"
        ],
        "summary": "История версий комнаты",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Список коммитов",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "log": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Commit"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Err404"
          }
        }
      }
    },
    "/versions/files": {
      "post": {
        "tags": [
          "versions"
        ],
        "summary": "Файлы, затронутые версией",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "hash"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "hash": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Изменения",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "files": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "s": {
                            "type": "string",
                            "example": "M"
                          },
                          "p": {
                            "type": "string",
                            "example": "src/main.py"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Err404"
          }
        }
      }
    },
    "/versions/snapshot": {
      "post": {
        "tags": [
          "versions"
        ],
        "summary": "Зафиксировать снимок комнаты",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string",
                    "example": "первая версия"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Снимок создан, свежая история в ответе"
          },
          "404": {
            "$ref": "#/components/responses/Err404"
          },
          "409": {
            "description": "Нет изменений с прошлой версии",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/versions/restore": {
      "post": {
        "tags": [
          "versions"
        ],
        "summary": "Откатить комнату к версии",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "hash"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "hash": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Откат выполнен"
          },
          "404": {
            "$ref": "#/components/responses/Err404"
          }
        }
      }
    }
  }
}