Wiki
REST API
Справочник HTTP-эндпоинтов сервера uVersion: аутентификация, репозитории, файлы, блокировки, комментарии, список наблюдения, доска производства, сборки, администрирование.
Сервер uVersion предоставляет JSON HTTP API. Вызовы аутентифицируются
JWT (JSON Web Token), то есть сессионным токеном, который сервер выдаёт вам
при входе и который вы затем возвращаете при каждом запросе. Эта страница документирует
эндпоинты, используемые десктопным клиентом, плагинами редактора и CLI uversion.
Вы можете вызывать их напрямую, чтобы интегрировать uVersion в собственный внутренний инструментарий
(самодельная панель, скрипты аудита, вебхуки и т. д.).
Она не охватывает весь API: существует несколько семейств маршрутов, которые здесь не описаны. Список находится в разделе Незатронутые области.
Соглашения
Базовый URL
Каждый задокументированный путь задаётся относительно URL вашего экземпляра. В примерах используется
https://uversion.mygamestudio.com. Замените его на свой.
Обязательные заголовки
| Заголовок | Значение |
|---|---|
Authorization | Bearer <jwt> на каждом маршруте /api/*, кроме /api/auth/* и /api/server-info (и /health) |
Content-Type | application/json для POST/PUT с телом JSON. application/octet-stream для бинарных загрузок (файлы сборок). |
Accept | рекомендуется application/json (сервер по умолчанию возвращает JSON) |
Два формата ответа, и нужно знать, какой вы читаете
У сервера не один формат ответа, а два, и путать их оказывается самой дорогой ошибкой для того, кто начинает интеграцию.
1. Аутентифицированные маршруты (все /api/*, кроме /api/auth/*)
отвечают конвертом:
{
"success": true,
"data": { /* payload */ }
}
В случае ошибки те же маршруты отвечают:
{
"success": false,
"error": "No write permission on this repository"
}
Поле success присутствует всегда. data присутствует при успехе,
error при неудаче, никогда оба вместе.
2. Маршруты аутентификации (/api/auth/login,
/refresh, /validate, /logout, /register,
/change-password) не используют этот конверт. Они возвращают
голый объект, без success и data:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": 12, "username": "alice", ... },
"must_change_password": false
}
А их ошибки представляют собой объект с единственным полем, без success:
{ "error": "Invalid credentials" }
Практическое следствие: на /api/auth/login токен читается из .token,
а не из .data.token. Скрипт, запрашивающий .data.token, получает
null без какой-либо видимой ошибки.
Наконец, GET /api/files/{repo_id}/content не возвращает ни того, ни другого: это
сырое двоичное содержимое файла, как есть.
Постраничная разбивка
Общего соглашения о постраничной разбивке нет: у каждого маршрута своё, либо
его нет вовсе. Не предполагайте ни limit, ни total, ни has_more.
| Маршрут | Принимаемые параметры | Форма ответа |
|---|---|---|
GET /api/files/{repo_id}/history |
limit (по умолчанию 50), offset (по умолчанию 0), path |
Плоский массив коммитов. Нет total, нет has_more: вы достигли конца, когда массив содержит меньше элементов, чем limit. |
GET /api/repositories |
только include_inactive (и он учитывается лишь для суперадминистратора) |
Плоский массив. Ни limit, ни offset не читаются. |
GET /api/locks/{repo_id}/status |
Нет | Плоский массив всех блокировок репозитория. |
GET /api/files/{repo_id}/snapshot |
commit_hash, обязательно |
Плоский массив файлов. |
GET /api/admin/audit |
page и per_page, не limit/offset, плюс фильтры (user_id, action, entity_type, from, to) |
Только для суперадминистратора. Хорошо иллюстрирует отсутствие общего соглашения: это единственный маршрут, который разбивает по номеру страницы. |
Для любого не перечисленного здесь маршрута считайте, что он возвращает весь свой результат за один вызов.
Ограничение частоты
Общего ограничения частоты нет: проект Unreal насчитывает тысячи файлов, и пакетные операции (получение, снятие блокировок) постоянно срабатывали бы на троттлинг. Поэтому вы можете вызывать API в большом объёме.
Ограничен лишь один маршрут, /api/auth/login:
5 неудачных попыток на имя пользователя каждые 15 минут. Успешные
входы не учитываются.
Отзыв токенов
Каждый сессионный токен несёт поле tv (token version), которое отражает столбец
users.token_version в базе. Сервер сравнивает оба при каждом запросе.
Вызов POST /api/auth/logout, сброс пароля администратором
или деактивация учётной записи увеличивают это значение, что делает
мгновенно недействительными все существующие токены этого пользователя, на всех его
машинах.
Curl
Чтобы вызывать API из терминала. Обратите внимание на .token: ответ
/api/auth/login является голым объектом, нет никакого .data, через который нужно пройти.
TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}' | jq -r '.token')
# А вот аутентифицированные маршруты как раз используют конверт:
curl -s -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories | jq '.data'
Аутентификация
Напоминание: ни один маршрут в этом разделе не использует конверт
{"success", "data"}. Они возвращают голый объект, и их ошибки имеют вид
{"error": "..."}.
POST /api/auth/login
Аутентифицирует пользователя и возвращает сессионный токен, действительный 30 дней.
Body:
{
"username": "alice",
"password": "secret"
}
Response 200:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead"
},
"must_change_password": false
}
Нет поля expires_at: срок действия читается из самого
токена или выводится из конфигурации сервера (по умолчанию 30 дней).
Не игнорируйте must_change_password. Это поле равно true,
когда учётная запись всё ещё работает на временном пароле: том, что установщик сгенерировал
для начальной учётной записи администратора, или том, что администратор только что задал при
сбросе. Клиент, который его не смотрит, оставляет пользователя на этом временном пароле
бессрочно. Ожидаемое поведение состоит в том, чтобы немедленно перенаправить на
POST /api/auth/change-password до любого другого действия.
Ошибки:
401: неверные учётные данные или деактивированная учётная запись429: достигнуто 5 неудачных попыток для этого имени пользователя в окне 15 минут
Сервер всегда выполняет проверку пароля Argon2id, в том числе против фиктивного хеша, когда учётной записи не существует. Поэтому ответ занимает одинаковое время в обоих случаях, что не даёт угадать существование учётной записи, замеряя время запросов.
POST /api/auth/refresh
Обновляет сессионный токен без повторного ввода пароля.
Заголовки: Authorization: Bearer <текущий_токен>. Без тела.
Response 200: точно та же форма, что у /login
(token, user, must_change_password), с новым токеном.
Ошибки:
401: недействительный или просроченный токен, деактивированная учётная запись, или устаревшийtoken_version
POST /api/auth/logout
Делает недействительными все токены текущего пользователя, на всех его машинах,
увеличивая users.token_version. Ему придётся войти заново повсюду: десктопный клиент,
плагин редактора и CLI включительно.
Response 200: { "logged_out": true }.
POST /api/auth/validate
Проверяет, что токен ещё действителен, и возвращает пользователя, которому он соответствует. Проверка
касается также is_active и token_version, поэтому отозванный токен
отклоняется, даже если он ещё не истёк.
Внимание к телу: это не объект, это голая строка JSON, то есть токен, окружённый двойными кавычками.
curl -X POST https://uversion.mygamestudio.com/api/auth/validate \
-H "Content-Type: application/json" \
-d '"eyJhbGciOiJIUzI1NiIs..."'
Response 200: голый объект пользователя.
{
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead"
}
Нет ни valid, ни expires_at, ни refreshed_token:
действительность читается из HTTP-кода (200 или 401), а обновление идёт через
/api/auth/refresh, никогда через этот маршрут.
POST /api/auth/register
Этот маршрут по умолчанию отказывает. Открытая регистрация отключена, если только
оператор явно не включил её в конфигурации сервера; иначе ответ будет
403 с {"error": "Open registration is disabled; contact your administrator"}.
В обычной работе учётные записи создаются через администрирование:
POST /api/admin/users, или вкладка «Пользователи» панели администрирования. Не
стройте интеграцию, которая зависит от /register.
POST /api/auth/change-password
Меняет пароль текущего пользователя. Увеличивает token_version, что
делает недействительными все прежние токены, включая тот, что только что послужил для вызова.
Репозитории
GET /api/repositories
Перечисляет репозитории, доступные текущему пользователю, отфильтрованные по таблице прав. Пользователь,
у которого нет ни одного правила прав на репозиторий, его не видит. Роли
admin и lead видят все репозитории.
Query-параметры: один-единственный, include_inactive (булев, по умолчанию
false), и он учитывается лишь для суперадминистратора. Нет
ни limit, ни offset: маршрут возвращает весь список.
Response 200:
{
"success": true,
"data": [
{
"id": 1,
"name": "hero-rpg",
"description": "Main RPG project",
"storage_path": "/var/lib/uversion/data/hero-rpg",
"is_active": true,
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-05-15T08:30:00Z"
},
{
"id": 2,
"name": "shared-assets",
"description": "Shared asset library",
"storage_path": "/var/lib/uversion/data/shared-assets",
"is_active": true,
"created_at": "2026-02-01T09:00:00Z",
"updated_at": "2026-05-02T11:12:00Z"
}
]
}
Это единственные возвращаемые поля. В частности, нет ни owner, ни
current_revision, ни file_count, ни size_bytes, ни
last_commit_at: у репозитория нет владельца в смысле API, а
объёмные данные получаются через маршруты статистики администрирования.
GET /api/repositories/{repo_id}
Детали репозитория. Та же форма объекта, что в списке, с теми же полями.
Ошибки:
403: нет доступа к этому репозиторию404: репозиторий не существует
POST /api/repositories
Создаёт репозиторий. Только для суперадминистратора, то есть для роли
admin и только для неё. Это не возможность: проверка касается напрямую
роли, поэтому project_admin или lead получает 403. В
продукте нет никакой возможности create_repos.
Body:
{
"name": "new-project",
"description": "Необязательное описание"
}
Проверка:
name: от 1 до 255 символов. Сервер не накладывает никакого ограничения на набор символов. Путь хранения на диске выводится из имени путём его нормализации.description: необязательно.
Уникальность касается только имени, в масштабе сервера. Нет понятия владельца, а значит, и уникальности «по владельцу».
Ошибки:
400: пустое имя или свыше 255 символов403: у вызывающего нет ролиadmin409: репозиторий уже носит это имя
Файлы
POST /api/files/{repo_id}/upload-chunks
Отправляет целый файл, а не список кусков. Имя маршрута вводит в заблуждение: файл на блоки (chunks) режет сервер, а не вы. Вызывающему никогда не нужно делать это разбиение самому.
Идентичные блоки, распознаваемые по их хешу SHA-256, дедуплицируются: блок, уже присутствующий на сервере, не сохраняется второй раз, из какого бы файла или репозитория он ни пришёл. Именно это делает так, что большой двоичный файл, изменённый по краю, почти ничего не стоит в дисковом пространстве.
Body: объект с единственным полем, содержащий полный файл в кодировке base64.
{
"data": "<весь файл в кодировке base64>"
}
Любая другая форма, в частности массив chunks, отклоняется сервером.
Response 200:
{
"success": true,
"data": {
"chunks": [
{ "hash": "abc123...", "offset": 0, "size": 1048576, "compressed_size": 423152 },
{ "hash": "def456...", "offset": 1048576, "size": 2097152, "compressed_size": 891204 }
],
"chunks_stored": 1,
"chunks_deduplicated": 1
}
}
Массив chunks следует переиспользовать как есть, включая полные объекты,
в следующем за ним POST /commit. chunks_stored считает блоки, реально
записанные на диск, а chunks_deduplicated считает те, что уже существовали.
Право: требуется запись в репозиторий, иначе 403.
Ограничения: тело запроса ограничено 1 ГБ (настраивается через
security.max_body_size_files). Файл больше этого предела не может
пройти по этому маршруту.
POST /api/files/{repo_id}/commit
Создаёт атомарный коммит из списка файлов и их блоков. Либо проходят все файлы, либо ни один.
action: add, modify и delete
Не added, не modified, не deleted.
Это не деталь формы. Сервер буквально проверяет
action == "delete" и обрабатывает всё остальное как добавление или
изменение. Отправить "deleted", стало быть, не удаляет ровным счётом ничего: файл уходит в
ветку записи, с пустым массивом chunks, и сервер регистрирует
ревизию с пустым содержимым. Никакой ошибки не возбуждается. Файл остаётся на месте, его последняя
версия перезаписывается пустотой, и потеря становится видна лишь при следующем sync
кого-то другого.
Body:
{
"message": "Updated main level + hero pose pass",
"commit_hash": "необязательно: чтобы объединить несколько партий под одним коммитом",
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"action": "modify",
"chunks": [
{ "hash": "abc123...", "offset": 0, "size": 1048576, "compressed_size": 423152 },
{ "hash": "def456...", "offset": 1048576, "size": 2097152, "compressed_size": 891204 }
]
},
{
"path": "Content/Characters/NewVillain.uasset",
"action": "add",
"chunks": [
{ "hash": "fed789...", "offset": 0, "size": 524288, "compressed_size": 201004 }
]
},
{
"path": "Content/OldAsset.uasset",
"action": "delete",
"chunks": []
}
]
}
chunks не является необязательным, в том числе на delete.
Поле должно присутствовать: его пропуск проваливает десериализацию всего запроса. Для
удаления отправьте пустой массив.
Массив содержит объекты, а не строки: переиспользуйте без изменений
записи {hash, offset, size, compressed_size}, возвращённые
/upload-chunks. Каждый hash должен быть шестнадцатеричным дайджестом из 64
символов, иначе весь коммит отклоняется с 400.
Поле commit_hash на корневом уровне необязательно. Оно служит, чтобы несколько
последовательных вызовов несли один и тот же коммит, что десктопный клиент делает, когда режет большую
загрузку на партии. При пропуске сервер вычисляет его сам.
Response 200:
{
"success": true,
"data": {
"commit_hash": "7f3a9b1c2d3e4f...",
"files_committed": 3,
"revisions": [
{ "path": "Content/Maps/MainLevel.umap", "revision_number": 12 },
{ "path": "Content/Characters/NewVillain.uasset", "revision_number": 1 },
{ "path": "Content/OldAsset.uasset", "revision_number": 8 }
]
}
}
Это единственные возвращаемые поля. Нет ни files_changed, ни
bytes_uploaded, ни bytes_deduped, ни revision. Заметьте, что
revision_number является счётчиком по файлу, а не номером версии
репозитория: коммит идентифицирует commit_hash, и именно его нужно
хранить, чтобы ссылаться на состояние.
Ошибки:
400: некорректное тело, недействительный дайджест блока (ожидается: 64 шестнадцатеричных символа), отсутствующее полеchunks403: нет права записи на один из путей409: блокировка, удерживаемая кем-то другим, на изменённом файле, или конкурентный коммит на том же файле
GET /api/files/{repo_id}/snapshot
Возвращает состояние репозитория, каким оно было в момент данного коммита: список присутствующих файлов, с их номером ревизии и размером. Используется десктопным клиентом для клонирования и для принудительной синхронизации.
Query-параметры:
-
commit_hash: обязательно. Это хеш коммита, служащий точкой отсчёта. Без этого параметра запрос отклоняется, и не существует значения по умолчанию «последнее состояние».
Нет параметра revision. Снимок запрашивается по хешу
коммита, никогда по номеру. Неизвестный коммит отвечает 404.
Response 200: плоский массив, без обёртывающего объекта.
{
"success": true,
"data": [
{
"path": "Content/Maps/MainLevel.umap",
"revision_number": 12,
"file_size": 84934656
},
{
"path": "Content/Characters/Hero.uasset",
"revision_number": 3,
"file_size": 5242880
}
]
}
Ответ не содержит списков блоков. Чтобы получить содержимое, пройдите
через GET /api/files/{repo_id}/content, который пересобирает файл на стороне сервера.
GET /api/files/{repo_id}/content
Скачивает содержимое файла на данной ревизии (сервер пересобирает чанки). Используется клоном и sync.
Query-параметры:
path: путь файла (относительно корня repo)revision(необязательно): номер ревизии. По умолчанию: последняя.
Response 200: двоичное содержимое файла.
GET /api/files/{repo_id}/history
История коммитов репозитория, от самого недавнего к самому старому.
Query-параметры:
limit: число коммитов, по умолчанию 50offset: по умолчанию 0path(необязательно): оставляет только коммиты, затронувшие этот файл
Это единственные три читаемых параметра. Нет ни author, ни
since: неизвестный параметр молча игнорируется, что даёт
правдоподобный, но неотфильтрованный ответ. Фильтруйте по автору или по дате на стороне вызывающего.
Response 200: плоский массив коммитов.
{
"success": true,
"data": [
{
"commit_hash": "7f3a9b1c...",
"message": "Fixed lighting in main level",
"author": "alice",
"created_at": "2026-05-15T08:30:00Z",
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"revision_number": 12,
"file_size": 84934656
}
]
}
]
}
Нет ни total, ни has_more, ни повтора limit и
offset. Чтобы обойти всю историю, увеличивайте offset, пока не
получите меньше элементов, чем limit.
Каждый коммит несёт напрямую список затронутых файлов. Запись, ревизия которой является удалением, помечается как таковая и не имеет содержимого для скачивания.
Инкрементальный sync
Эндпоинты, используемые десктопным клиентом для эффективной синхронизации:
GET /api/files/{repo_id}/sync: файлы, изменённые с некоторой ревизии, для дельта-sync.GET /api/files/{repo_id}/deletions: файлы, удалённые на стороне сервера, чтобы распространить удаления локально.GET /api/files/{repo_id}/list: список файлов repo.POST /api/files/{repo_id}/checkout: получает блокировки и готовит редактирование.
Блокировки
Блокировка держится, пока не будет явно снята: через checkin, через revert, или через принудительное снятие администратором. Никакого автоматического истечения нет, ни через час, ни через месяц.
Поле expires_at существует единственно потому, что соответствующий столбец в базе
не принимает пустого значения. Сервер пишет туда сторожевое значение в сто лет: блокировка,
взятая сегодня, показывает срок где-то около 2126 года. Ничего не стройте на этом
поле и не показывайте эту дату пользователю.
Стало быть, heartbeat ничего не продлевает. Это сигнал наблюдения, единственная цель которого: показывать администраторам, какие блокировки ещё активно используются.
POST /api/locks/{repo_id}/acquire
Получает блокировки на список путей. Получения независимы: список
acquired содержит успехи, список failed содержит неудачи с их причиной.
Body:
{
"paths": [
"Content/Maps/MainLevel.umap",
"Content/Characters/Hero.uasset"
]
}
Response 200:
{
"success": true,
"data": {
"acquired": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"file_id": 12345,
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"expires_at": "2126-05-15T14:30:00Z"
}
],
"failed": [
{
"path": "Content/Characters/Hero.uasset",
"reason": "File is locked",
"locked_by": "bob"
}
]
}
}
Срок 2126 года в этом примере не является опечаткой: это сторожевое значение, описанное выше. Блокировка постоянна.
Причины неудачи. Поле reason представляет собой английскую фразу,
предназначенную для отображения, а не стабильный код. Не пишите логику, которая сравнивает эту
строку, и не «парсите» её префикс: она может быть переформулирована от версии к версии.
Значения, производимые в настоящее время:
reason | Значение | locked_by |
|---|---|---|
File is locked | Кто-то другой уже держит блокировку | Имя человека |
No write permission | Нет права записи на этом пути | null |
Failed to create lock | Установка блокировки провалилась в базе | null |
Database error | Ошибка базы данных на этом пути | null |
Недействительный путь (абсолютный, содержащий .., пустой, свыше 4096 символов или несущий
нулевой байт) не производит записи в failed: он проваливает весь
запрос.
POST /api/locks/{repo_id}/release
Снимает блокировки, которые вы держите. Body:
{
"paths": ["Content/Maps/MainLevel.umap"]
}
На этом маршруте нет поля force. Добавлять его бесполезно:
сервер игнорирует поля, которых не знает, запрос удаётся, и чужая блокировка
остаётся на месте. Интегратор, рассчитывающий на это, полагает, что освободил файл, а на деле нет.
Чтобы снять чужую блокировку, единственным маршрутом является
POST /api/admin/locks/{lock_id}/force-release. Он зарезервирован за администрированием
соответствующего репозитория, и операция вносится в журнал аудита. Он принимает идентификатор блокировки,
который вы получаете через GET /api/locks/{repo_id}/status.
POST /api/locks/{repo_id}/heartbeat
Обновляет метку времени активности ваших блокировок. Это ничего не продлевает, поскольку ничто не истекает: это сигнал наблюдения, позволяющий администратору отличить ещё используемую блокировку от забытой.
Body: массив JSON путей, напрямую, без обёртывающего объекта.
["Content/Maps/MainLevel.umap", "Content/Characters/Hero.uasset"]
Недействительные пути игнорируются по отдельности, вместо того чтобы провалить всю партию, чтобы устаревший остаток отслеживания не блокировал остальные.
GET /api/locks/{repo_id}/status
Перечисляет все блокировки репозитория, с именем файла и именем человека, который её держит.
Query-параметры: нет. Нет ни фильтра user, ни limit,
ни offset. Маршрут возвращает всю совокупность блокировок репозитория, для фильтрации на стороне вызывающего.
Response 200: плоский массив, без обёртывающего объекта и без total.
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"file_id": 12345,
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"expires_at": "2126-05-15T14:30:00Z"
}
]
}
Поле id нужно передать в
POST /api/admin/locks/{lock_id}/force-release.
Комментарии & ревью
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Перечисляет комментарии коммита, в виде тредов (родитель → дети).
Response 200:
{
"success": true,
"data": [
{
"id": 42,
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"author_id": 12,
"author_username": "alice",
"body": "LGTM, ship it",
"parent_id": null,
"created_at": "2026-05-15T15:00:00Z",
"replies": [
{
"id": 43,
"author_username": "bob",
"body": "Thanks!",
"parent_id": 42,
"created_at": "2026-05-15T15:05:00Z"
}
]
}
]
}
POST /api/comments/{repo_id}/comments
Создаёт комментарий. file_path необязателен (комментарий к коммиту vs. к файлу). parent_id, чтобы ответить в существующий тред.
Body:
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
Отправляет ревью на коммит. Требуется возможность approve_changes.
Body:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
Принимаемый status: approved, changes_requested, pending.
Список наблюдения
GET /api/watchlist/{repo_id}/watchlist
Перечисляет паттерны наблюдения текущего пользователя на этом репозитории.
Response 200:
{
"success": true,
"data": [
{
"id": 1,
"user_id": 12,
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"],
"created_at": "2026-04-10T10:00:00Z"
}
]
}
POST /api/watchlist/{repo_id}/watchlist
Добавляет паттерн наблюдения. Body:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
Возможные события: commit (коммит изменил совпадающий файл),
lock (была получена блокировка), review (было опубликовано ревью).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
Убирает паттерн наблюдения. Возвращает { "success": true }.
Доска производства (tasks)
Старый API modification-requests был убран: доска производства его поглощает
(запросы становятся карточками, origin='request'). Эндпоинты вложены под
/api/tasks/{repo_id}.
| Маршрут | Описание |
|---|---|
GET /api/tasks/{repo_id}/board | Полная доска: колонки + карточки |
POST /api/tasks/{repo_id}/tasks | Создаёт карточку |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | Обновляет / удаляет карточку |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | Назначает пользователей на карточку |
GET /api/tasks/{repo_id}/assignable-users | Список назначаемых пользователей |
POST /api/tasks/{repo_id}/columns | Настраивает колонки доски |
Также доступные подмаршруты: комментарии к карточке, ссылки на ассеты и коммиты, и вложения
(с обложкой). Настройка колонок требует возможности manage_board (admin / lead).
Сборки
POST /api/builds/{repo_id}/upload?hash=<sha256>
Загружает файл сборки в <storage_path>/builds/<hash>.
Сервер сверяет SHA-256, переданный в query-параметре, с полученным содержимым. Если хеш уже
существует на стороне сервера, немедленно возвращает 200 без повторного копирования (дедупликация на стороне хранилища сборок).
Заголовки: Content-Type: application/octet-stream.
Body: сырой двоичный (без обёртки JSON).
Ограничения: 8 ГБ на файл.
Ошибки:
400: query-параметр hash отсутствует или некорректен, или вычисленный SHA-256 != предоставленному hash413: файл > 8 ГБ
POST /api/builds/{repo_id}/publish
Регистрирует manifest сборки после загрузки всех её файлов. Требуется возможность publish_builds.
Body:
{
"version": "0.1.5-nightly",
"config": "Development",
"platform": "Win64",
"executable_path": "HeroRPG/Binaries/Win64/HeroRPG.exe",
"release_notes": "Nightly build of main branch, 2026-05-15",
"files": [
{ "path": "HeroRPG.exe", "hash": "abc123...", "size_bytes": 12345678 },
{ "path": "HeroRPG/Content/Paks/pak0.pak", "hash": "def456...", "size_bytes": 2147483648 }
]
}
GET /api/builds/{repo_id}
Перечисляет опубликованные сборки этого проекта.
Доступ к сборкам даётся проект за проектом. Возможности
download_builds недостаточно: будучи общесерверной, она лишь говорит, что
учётная запись не является простым зрителем. Сверх того нужен либо доступ к репозиторию, либо явное
разрешение на сборки этого проекта, предоставленное его администратором через
/api/admin/repositories/{repo_id}/build-access. Администратор проекта по умолчанию
имеет доступ к тем, что он администрирует, а суперадминистратор имеет доступ ко всем.
Response 200:
{
"success": true,
"data": [
{
"id": 5,
"version": "0.1.5-nightly",
"config": "Development",
"platform": "Win64",
"published_by": "ci-nightly",
"published_at": "2026-05-15T03:00:00Z",
"size_bytes": 2159829326,
"file_count": 142,
"release_notes": "Nightly build of main branch, 2026-05-15"
}
]
}
GET /api/builds/{repo_id}/{build_id}/file
Скачивает файл сборки. Требуется возможность download_builds. Manifest сборки доступен через GET /api/builds/{repo_id}/{build_id}/manifest.
Администрирование
Что на самом деле защищает эти маршруты
Вопреки тому, чего можно было бы ожидать, маршруты /api/admin/* не
защищены каждый одной возможностью. Они проходят через одну из четырёх проверок, которые почти все
касаются роли. capability представляет собой именованное право, привязанное к роли;
важный момент в том, что оно
действует на всём сервере, ибо не несёт никакой ссылки на репозиторий. Поэтому
оно никогда не может послужить, чтобы ограничить кого-то одним проектом.
| Проверка | Проходит для | Для чего служит |
|---|---|---|
| Суперадминистратор | Роль admin, и только она |
Всё, что действует для всего сервера: учётные записи, группы, журнал аудита, глобальная статистика, лицензия, обновление сервера. |
| Администратор этого репозитория | admin, или project_admin, администрирующий именно этот репозиторий |
Всё, что присуще одному проекту: права, правила проверки, вебхуки, доступ к сборкам, принудительное снятие блокировки, сборка мусора. |
| Общесерверная возможность | Любая роль, обладающая именованной возможностью | Несколько сквозных маршрутов. Внимание: возможность действует на всех репозиториях, такова её природа. project_admin, не обладающий ни одной, допускается вместо этого лишь на репозиториях, которые он администрирует. |
| Только допуск | admin или project_admin |
Впускает, но ничего не разрешает. Маршрут, который её применяет, должен затем сам ограничить свои результаты репозиториями, администрируемыми вызывающим. |
Иначе говоря: возможности manage_users и manage_permissions
существуют лишь на бумаге. Они действительно создаются в базе в момент установки,
но ни одна строка кода их не запрашивает. Предоставить их роли не меняет ровным счётом ничего.
Не пишите интеграцию, предполагающую, что учётная запись не-admin сможет управлять
пользователями, потому что ей дали manage_users: она получит 403.
| Маршрут | Реальная проверка | Описание |
|---|---|---|
GET /api/admin/users | Только допуск, затем фильтрация | project_admin видит лишь учётные записи своего периметра |
POST /api/admin/users | Только допуск | Создаёт учётную запись |
PUT/DELETE /api/admin/users/{id} | Суперадминистратор | Обновляет или удаляет учётную запись |
POST /api/admin/users/{id}/reset-password | Суперадминистратор | Сбрасывает пароль и отзывает все токены учётной записи |
GET/POST /api/admin/groups | Суперадминистратор | Группы глобальны, их нельзя делегировать по проекту |
GET/POST /api/admin/permissions/{repo_id} | Администратор этого репозитория | Правила прав по паттерну пути, предоставленные группе или пользователю |
GET/POST /api/admin/repositories/{repo_id}/rules | Администратор этого репозитория | Правила проверки, применяемые перед отправкой |
GET/POST /api/admin/repositories/{repo_id}/webhooks | Администратор этого репозитория | Discord, Slack, Teams, или обобщённый вебхук |
GET /api/admin/locks | Только допуск, затем фильтрация | Блокировки, ограниченные администрируемыми репозиториями |
POST /api/admin/locks/{lock_id}/force-release | Администратор этого репозитория | Снимает чужую блокировку. Вносится в журнал аудита |
GET /api/admin/audit | Суперадминистратор | Журнал аудита, с фильтрацией и постраничной разбивкой |
GET /api/admin/stats | Суперадминистратор | Объём хранилища, коэффициент дедупликации, рост |
GET /api/admin/licence | Суперадминистратор | Состояние лицензии и потреблённые места |
GET /api/admin/repositories/{repo_id}/gc/preview | Администратор этого репозитория | Моделирует сборку мусора, ничего не удаляя |
POST /api/admin/repositories/{repo_id}/gc | Администратор этого репозитория | Запускает сборку мусора |
Полные подробности о ролях, их рангах и о том, что каждый может делать, находятся на странице, посвящённой ролям и правам.
Разделы API, не охваченные этой страницей
Следующие семейства маршрутов существуют, смонтированы и обслуживаются сервером, но здесь не описаны. Если ваша интеграция в них нуждается, самое надёжное сегодня: наблюдать за вызовами, которые делает десктопный клиент, или написать нам.
| Префикс | Что он охватывает |
|---|---|
/api/advisor/* | Project Health: аудит проекта Unreal, его находки, оценка по столпам и сортировка элементов для игнорирования. |
/api/distribution/* | Распространение между проектами: связи между исходным и целевым репозиторием, публикация файлов из одного проекта в другой, история. |
/api/binaries/* | Предкомпилированные бинарники редактора, привязанные к коммиту, по модели UnrealGameSync. |
/api/watchlist/* | За пределами паттернов наблюдения, описанных выше: уведомления и входящие. |
/api/profile/* | Профиль текущего пользователя. |
/api/admin/repositories/{repo_id}/admins | Кто администрирует репозиторий: назначение и снятие администраторов проекта. |
/api/admin/repositories/{repo_id}/build-access | Кто может скачивать сборки этого проекта, предоставляется проект за проектом. |
/api/admin/licence | Состояние лицензии и потреблённые места. |
/api/admin/server/* | Обновление сервера из панели администрирования. |
Здоровье
GET /health
Неаутентифицированный эндпоинт, возвращающий 200 OK, если сервер может обратиться к базе.
Используется для health-check балансировщика нагрузки или мониторинга (Prometheus blackbox, Datadog synthetic и т. д.).
Response 200: OK (text/plain).
Response 503: если база недостижима.
GET /api/server-info
Публичный неаутентифицированный эндпоинт, возвращающий метаданные сервера (версия и т. д.).
Как и /health, он не защищён middleware аутентификации.
Формат ошибок
В случае ошибки аутентифицированный маршрут отвечает { "success": false, "error": "..." },
а маршрут /api/auth/* отвечает { "error": "..." }, в обоих случаях
с подходящим HTTP-кодом.
Поле error представляет собой сообщение, предназначенное человеку, а не код.
Не существует никакого каталога стабильных кодов ошибок, ни в теле, ни в заголовке. Поэтому не
стройте логику на его содержимом и не разбирайте его префикс: эти фразы
переформулируются от версии к версии, а строковый тест, ломающийся молча, хуже, чем полное отсутствие
теста.
HTTP-код является единственным, на чём можно ветвить поведение. Таблица ниже даёт его прочтение.
| HTTP | Значение | Когда |
|---|---|---|
| 400 | Bad request | Недействительный параметр, некорректный JSON, слишком большое тело (до жёсткого предела 413) |
| 401 | Not authenticated | Токен отсутствует, просрочен, недействительная подпись, или несовпадение token_version |
| 403 | Permission denied | Отсутствует возможность, или нет права на path / repo |
| 404 | Resource not found | Repo / файл / commit / user не существует или недоступен |
| 409 | Conflict | Блокировка уже удерживается, конкурентный commit, нарушено ограничение unique |
| 413 | Payload too large | Тело сверх лимита (1 ГБ на /files, 8 ГБ на /builds/upload) |
| 429 | Too many requests | Только на /auth/login (ограничение частоты по пользователю) |
| 500 | Internal server error | Ошибка DB, ошибка IO. Логируется на стороне сервера; приложите timestamp, если сообщаете о баге. |
| 503 | Service unavailable | База недостижима (health check) или выполняется миграция |