Wiki
REST API
Полный справочник HTTP-эндпоинтов сервера uVersion: auth, repos, files, locks, comments, watchlist, modification requests, builds, admin.
Сервер uVersion предоставляет JSON HTTP API с аутентификацией по JWT. Эта страница документирует все
эндпоинты, используемые десктопным клиентом, плагинами редактора и CLI uversion.
Вы можете вызывать их напрямую, чтобы интегрировать uVersion в ваш внутренний инструментарий
(кастомный дашборд, скрипты аудита, webhook и т. д.).
Соглашения
Базовый 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 для бинарных загрузок (build files). |
Accept | application/json рекомендуется (сервер по умолчанию возвращает JSON) |
Единый формат ответа
Все маршруты отвечают следующей оболочкой:
{
"success": true,
"data": { /* payload */ }
}
В случае ошибки:
{
"success": false,
"error": "Permission denied: capability 'manage_users' required"
}
Поле success присутствует всегда и указывает, была ли запрос успешным.
Поле data присутствует в случае успеха. Поле error присутствует в случае неудачи.
Никогда оба вместе.
Пагинация
Маршруты, возвращающие потенциально длинные списки, принимают limit и offset
в query-параметрах (?limit=20&offset=40). Ответ включает total и has_more
для облегчения итерации. Максимальный limit: 100.
Ограничение частоты запросов
Глобальное ограничение частоты запросов было удалено (см. CLAUDE.md, раздел Security). Только /api/auth/login
имеет ограничение: 5 неудачных попыток на username каждые 15 минут. Валидные попытки
не учитываются.
Версионирование токенов
Каждый JWT содержит поле tv (token version), которое соответствует users.token_version в БД.
Middleware аутентификации проверяет это значение при каждом запросе. Когда пользователь выполняет POST /api/auth/logout,
когда админ сбрасывает его пароль или когда админ деактивирует его аккаунт, token_version увеличивается,
мгновенно аннулируя все существующие токены этого пользователя на всех его машинах.
Curl
Чтобы вызвать API из терминала:
TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}' | jq -r '.data.token')
curl -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories
Аутентификация
POST /api/auth/login
Аутентифицирует пользователя и возвращает JWT на 30 дней.
Body:
{
"username": "alice",
"password": "secret"
}
Response 200:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_at": "2026-06-13T14:30:00Z",
"user": {
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead",
"is_active": true
}
}
}
Ошибки:
401: неверные учётные данные или неактивный пользователь429: достигнуто 5 неудачных попыток для этого username в окне 15 мин
Сервер всегда выполняет argon2id::verify_password (против фиктивного хеша, если пользователь не существует),
чтобы нейтрализовать атаки по времени. Ответ занимает одинаковое время, существует пользователь или нет.
POST /api/auth/refresh
Обновляет JWT без повторного ввода пароля.
Заголовки: Authorization: Bearer <current_token>. Без тела.
Response 200: идентичен /login с истечением, отодвинутым на 30 дней, и новым токеном.
Ошибки:
401: текущий токен недействителен, истёк или несоответствиеtoken_version
POST /api/auth/logout
Аннулирует все токены текущего пользователя (на всех его машинах), увеличивая
users.token_version. Пользователю придётся снова войти везде.
Response 200: { "success": true }.
POST /api/auth/validate
Проверяет, что токен ещё действителен. Для токенов, до истечения которых осталось менее 7 дней, сервер включает
refreshed_token в ответ (авто-продление). Клиент должен сохранить его вместо
старого.
Response 200:
{
"success": true,
"data": {
"valid": true,
"user_id": 12,
"expires_at": "2026-06-13T14:30:00Z",
"refreshed_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
POST /api/auth/register
Создаёт учётную запись пользователя (публичный маршрут, без аутентификации).
POST /api/auth/change-password
Меняет пароль текущего пользователя. Увеличивает token_version, что аннулирует все старые токены.
Репозитории
GET /api/repositories
Перечисляет репозитории, доступные текущему пользователю (отфильтрованные через таблицу прав). Пользователь, у которого нет ни одного паттерна прав на репозиторий, его не видит.
Query-параметры: limit, offset.
Response 200:
{
"success": true,
"data": [
{
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"created_at": "2026-01-15T10:00:00Z"
},
{
"id": 2,
"name": "shared-assets",
"owner": "acme",
"description": "Shared asset library",
"current_revision": "3a4b5c6d...",
"file_count": 412,
"size_bytes": 980000000,
"created_at": "2026-02-01T09:00:00Z"
}
]
}
GET /api/repositories/{repo_id}
Детали репозитория, включая агрегированную статистику.
Response 200:
{
"success": true,
"data": {
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"dedup_ratio": 4.2,
"created_at": "2026-01-15T10:00:00Z",
"last_commit_at": "2026-05-15T08:30:00Z"
}
}
Ошибки:
403: нет доступа к этому репозиторию404: репозиторий не существует
POST /api/repositories
Создаёт новый репозиторий. Требуется capability create_repos (по умолчанию admin).
Body:
{
"name": "new-project",
"description": "Optional description"
}
Валидация:
- name: от 1 до 64 символов, буквенно-цифровые + дефисы + подчёркивания. Уникально в пределах owner.
- description: от 0 до 1024 символов. Необязательно.
Ошибки:
400: недопустимое или слишком длинное имя403: отсутствует capabilitycreate_repos409: репозиторий с таким именем уже существует для этого owner
Файлы
POST /api/files/{repo_id}/upload-chunks
Загружает пакет бинарных chunk. Идентичные chunk (по SHA-256) дедуплицируются автоматически.
Сервер не сохраняет повторно chunk, который уже присутствует. Ответ возвращает для каждого chunk его хеш + размер +
сжатый размер, для использования в последующем POST /commit.
Body:
{
"chunks": [
{ "data": "<base64-encoded chunk bytes>" },
{ "data": "<base64-encoded chunk bytes>" }
]
}
Response 200:
{
"success": true,
"data": {
"chunks": [
{ "hash": "abc123...", "size": 1048576, "compressed_size": 423152 },
{ "hash": "def456...", "size": 2097152, "compressed_size": 891204 }
]
}
}
Ограничения: тело макс. 1 GB (настраивается через security.max_body_size_files). Для более крупных файлов разбивайте на несколько пакетов.
POST /api/files/{repo_id}/commit
Создаёт атомарный commit со списком файлов и их chunk. Либо все файлы проходят, либо ни один.
Body:
{
"message": "Updated main level + hero pose pass",
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"action": "modified",
"chunks": ["abc123...", "def456..."]
},
{
"path": "Content/Characters/NewVillain.uasset",
"action": "added",
"chunks": ["fed789..."]
},
{
"path": "Content/OldAsset.uasset",
"action": "deleted"
}
]
}
Возможные действия: added, modified, deleted.
Для added и modified массив chunks содержит хеши, возвращённые
/upload-chunks. Для deleted опустите chunks.
Response 200:
{
"success": true,
"data": {
"commit_hash": "7f3a9b1c2d3e4f...",
"files_changed": 3,
"bytes_uploaded": 88080384,
"bytes_deduped": 4194304,
"revision": 47
}
}
Ошибки:
400: пустое сообщение, файл без действия, ссылки на несуществующие chunk403: нет capabilitycheckinили нет права write на один из файлов409: блокировка уже удерживается другим пользователем на изменённом файле, или конкурентный commit (race condition на том же файле)
GET /api/files/{repo_id}/snapshot
Возвращает полное состояние репозитория на текущей ревизии: все файлы, их ревизии и их chunk. Используется клиентом для операций clone и принудительного sync.
Query-параметры:
revision(необязательно): snapshot на прошлой ревизии. По умолчанию: HEAD.
Response 200:
{
"success": true,
"data": {
"revision": 47,
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"revision": 12,
"size_bytes": 84934656,
"chunks": ["abc123...", "def456..."]
}
]
}
}
GET /api/files/{repo_id}/content
Скачивает содержимое файла на заданной ревизии (сервер пересобирает chunk). Используется clone и sync.
Query-параметры:
path: путь к файлу (относительно корня репозитория)revision(необязательно): номер ревизии. По умолчанию: последняя.
Response 200: бинарное содержимое файла.
GET /api/files/{repo_id}/history
Пагинированная история commit репозитория.
Query-параметры:
limit(от 1 до 100, по умолчанию 20)offset(по умолчанию 0)path(необязательно): фильтр по пути файла (commit, изменившие этот файл)author(необязательно): фильтр по usernamesince(необязательно): ISO 8601 timestamp
Response 200:
{
"success": true,
"data": {
"commits": [
{
"hash": "7f3a9b1c...",
"author": "alice",
"author_id": 12,
"date": "2026-05-15T08:30:00Z",
"message": "Fixed lighting in main level",
"files_changed": 1,
"bytes_uploaded": 84934656
}
],
"total": 423,
"limit": 20,
"offset": 0,
"has_more": true
}
}
Инкрементальный sync
Эндпоинты, используемые десктопным клиентом для эффективной синхронизации:
GET /api/files/{repo_id}/sync: файлы, изменённые с некоторой ревизии, для дельта-sync.GET /api/files/{repo_id}/deletions: файлы, удалённые на стороне сервера, для распространения удалений локально.GET /api/files/{repo_id}/list: список файлов репозитория.POST /api/files/{repo_id}/checkout: получает блокировки и подготавливает редактирование.
Блокировки
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",
"last_heartbeat_at": "2026-05-15T14:30:00Z",
"expires_at": "2026-05-15T15:30:00Z"
}
],
"failed": [
{
"path": "Content/Characters/Hero.uasset",
"reason": "ALREADY_LOCKED",
"lock_holder": "bob",
"lock_acquired_at": "2026-05-15T13:00:00Z"
}
]
}
}
Блокировки истекают через 60 минут без heartbeat. Поле expires_at отражает
этот срок. Вызывайте /heartbeat периодически, чтобы продлить блокировку, или /release,
чтобы освободить её.
Причины failed:
ALREADY_LOCKED: другой пользователь удерживает блокировку (поляlock_holderиlock_acquired_atзаполнены)PERMISSION_DENIED: нет права write на этот путьINVALID_PATH: некорректный путь (абсолютный, содержит..и т. д.)
POST /api/locks/{repo_id}/release
Освобождает блокировки, которыми вы владеете. Body:
{
"paths": ["Content/Maps/MainLevel.umap"],
"force": false
}
force: true требует capability force_unlock. Действие аудируется.
POST /api/locks/{repo_id}/heartbeat
Обновляет last_heartbeat_at на ваших блокировках. Рекомендуется каждые 5 минут для долго выполняющихся задач CI,
которые хотят, чтобы админы видели активность.
GET /api/locks/{repo_id}/status
Перечисляет все активные блокировки репозитория, с join по users и files для прямого возврата имён.
Query-параметры: user (фильтр по username), limit, offset.
Response 200:
{
"success": true,
"data": {
"locks": [
{
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"last_heartbeat_at": "2026-05-15T16:00:00Z"
}
],
"total": 1
}
}
Комментарии и ревью
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Перечисляет комментарии commit, в виде тредов (родитель → потомки).
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 необязателен (комментарий к commit 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
Отправляет ревью по commit. Требуется capability approve_changes.
Body:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
Принимаемый status: approved, changes_requested, pending.
Список наблюдения
GET /api/watchlist/{repo_id}/watchlist
Перечисляет паттерны watch текущего пользователя на этом репозитории.
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
Добавляет паттерн watch. Body:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
Возможные события: commit (commit изменил соответствующий файл),
lock (была получена блокировка), review (было опубликовано ревью).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
Удаляет паттерн watch. Возвращает { "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 | Настраивает колонки доски |
Также доступны подмаршруты: комментарии карточек, ссылки на assets и commit, и вложения
(с обложкой). Настройка колонок требует capability
manage_board (admin / lead).
Сборки
POST /api/builds/{repo_id}/upload?hash=<sha256>
Загружает файл сборки в <storage_path>/builds/<hash>.
Сервер проверяет SHA-256, переданный в query-параметре, по полученному содержимому. Если хеш уже существует
на стороне сервера, сразу возвращает 200 без повторного копирования (dedup на стороне build storage).
Заголовки: Content-Type: application/octet-stream.
Body: сырой бинарный (без JSON-обёртки).
Ограничения: 8 GB на файл.
Ошибки:
400: query-параметр hash отсутствует или некорректен, или вычисленный SHA-256 != переданному хешу413: файл > 8 GB
POST /api/builds/{repo_id}/publish
Регистрирует manifest сборки после загрузки всех её файлов. Требуется capability 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}
Перечисляет опубликованные сборки. Требуется capability download_builds, чтобы видеть ссылки на скачивание.
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
Скачивает файл сборки. Требуется capability download_builds. Manifest сборки доступен через GET /api/builds/{repo_id}/{build_id}/manifest.
Администрирование
Все маршруты /api/admin/* требуют, чтобы у пользователя была хотя бы одна admin-capability
(в зависимости от эндпоинта: manage_users, manage_permissions, manage_rules и т. д.).
Краткая документация ниже. Большинство следуют стандартному паттерну REST (GET/POST/PUT/DELETE).
| Маршрут | Capability | Описание |
|---|---|---|
GET/POST /api/admin/users | manage_users | Перечисляет / создаёт пользователей |
PUT/DELETE /api/admin/users/{id} | manage_users | Обновляет / удаляет |
POST /api/admin/users/{id}/reset-password | manage_users | Сбрасывает пароль, увеличивает token_version |
GET/POST /api/admin/groups | manage_users | Управление группами |
GET/POST /api/admin/permissions/{repo_id} | manage_permissions | Glob-правила прав по пути |
GET/POST /api/admin/repositories/{repo_id}/rules | manage_rules | Правила валидации перед checkin |
GET/POST /api/admin/repositories/{repo_id}/webhooks | manage_rules | Discord / Slack / Teams / custom |
GET /api/admin/audit | view_all_activity | Отфильтрованный и пагинированный audit log |
GET /api/admin/stats | view_all_activity | Storage metrics, dedup ratio, рост |
GET /api/admin/repositories/{repo_id}/gc/preview | admin | Предпросмотр GC без выполнения |
POST /api/admin/repositories/{repo_id}/gc | admin | Запускает garbage collection |
POST /api/admin/locks/{id}/force-release | force_unlock | Принудительно снимает чужую блокировку, аудируется |
Здоровье
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": "..." } с подходящим HTTP-статусом.
Поле error: сообщение, читаемое человеком. Для клиентов, которым нужны стабильные машиночитаемые
коды, парсите префикс (напр.: "Permission denied: ..." всегда начинается с
"Permission denied").
| HTTP | Значение | Когда |
|---|---|---|
| 400 | Bad request | Недопустимый параметр, некорректный JSON, слишком большое тело (до жёсткого лимита 413) |
| 401 | Not authenticated | Токен отсутствует, истёк, недействительная подпись, или несоответствие token_version |
| 403 | Permission denied | Отсутствует capability, или нет прав на путь / репозиторий |
| 404 | Resource not found | Репозиторий / файл / commit / пользователь не существует или недоступен |
| 409 | Conflict | Блокировка уже удерживается, конкурентный commit, нарушено ограничение unique |
| 413 | Payload too large | Тело сверх лимита (1 GB на /files, 8 GB на /builds/upload) |
| 429 | Too many requests | Только на /auth/login (ограничение частоты по пользователю) |
| 500 | Internal server error | Ошибка DB, ошибка IO. Логируется на стороне сервера; приложите timestamp, если сообщаете о баге. |
| 503 | Service unavailable | База данных недоступна (health check) или выполняется миграция |