Wiki
REST API
Referencia completa de los endpoints HTTP del servidor uVersion: auth, repos, files, locks, comments, watchlist, modification requests, builds, admin.
El servidor uVersion expone una API HTTP JSON autenticada por JWT. Esta página documenta la totalidad
de los endpoints usados por el cliente de escritorio, los plugins de editor y la CLI uversion.
Puedes llamarlos directamente para integrar uVersion en tu tooling interno
(dashboard personalizado, scripts de auditoría, webhooks, etc.).
Convenciones
URL base
Todas las rutas documentadas son relativas a la URL de tu instancia. Los ejemplos usan
https://uversion.mygamestudio.com. Reemplázala por la tuya.
Cabeceras requeridas
| Cabecera | Valor |
|---|---|
Authorization | Bearer <jwt> en todas las rutas /api/* excepto /api/auth/* y /api/server-info (y /health) |
Content-Type | application/json para los POST/PUT con body JSON. application/octet-stream para las subidas binarias (build files). |
Accept | application/json recomendado (el servidor devuelve JSON por defecto) |
Formato de respuesta uniforme
Todas las rutas responden con el siguiente envoltorio:
{
"success": true,
"data": { /* payload */ }
}
En caso de error:
{
"success": false,
"error": "Permission denied: capability 'manage_users' required"
}
El campo success siempre está presente e indica si la petición tuvo éxito.
El campo data está presente en caso de éxito. El campo error está presente en caso de fallo.
Nunca ambos a la vez.
Paginación
Las rutas que devuelven listas potencialmente largas aceptan limit y offset
como query params (?limit=20&offset=40). La respuesta incluye total y has_more
para facilitar la iteración. Limit máximo: 100.
Rate limiting
El rate limiting global se ha eliminado (ver CLAUDE.md, sección Security). Solo /api/auth/login
tiene rate limiting: 5 intentos fallidos por username cada 15 minutos. Los intentos
válidos no cuentan.
Versionado de tokens
Cada JWT contiene un campo tv (token version) que corresponde a users.token_version en la DB.
El middleware de auth verifica este valor en cada petición. Cuando el usuario hace POST /api/auth/logout,
cuando un admin resetea su password, o cuando un admin desactiva su cuenta, se incrementa token_version,
invalidando instantáneamente todos los tokens existentes de ese usuario en todas sus máquinas.
Curl
Para llamar a la API desde una terminal:
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
Autenticación
POST /api/auth/login
Autentica a un usuario y devuelve un JWT de 30 días.
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
}
}
}
Errores:
401: credenciales inválidas o usuario inactivo429: alcanzados 5 intentos fallidos para este username en la ventana de 15 min
El servidor ejecuta siempre argon2id::verify_password (contra un hash ficticio si el usuario no existe)
para neutralizar los ataques de temporización. La respuesta tarda lo mismo exista o no el usuario.
POST /api/auth/refresh
Renueva el JWT sin pasar de nuevo por el password.
Cabeceras: Authorization: Bearer <current_token>. Sin body.
Response 200: idéntica a /login con una expiración aplazada 30 días y un nuevo token.
Errores:
401: token actual inválido, expirado, otoken_versionno coincide
POST /api/auth/logout
Invalida todos los tokens del usuario actual (en todas sus máquinas) incrementando
users.token_version. El usuario tendrá que volver a iniciar sesión en todas partes.
Response 200: { "success": true }.
POST /api/auth/validate
Verifica que el token siga siendo válido. Para los tokens a menos de 7 días de expirar, el servidor incluye
un refreshed_token en la respuesta (auto-extensión). El cliente debe persistirlo en lugar
del antiguo.
Response 200:
{
"success": true,
"data": {
"valid": true,
"user_id": 12,
"expires_at": "2026-06-13T14:30:00Z",
"refreshed_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
POST /api/auth/register
Crea una cuenta de usuario (ruta pública, sin autenticación).
POST /api/auth/change-password
Cambia la contraseña del usuario actual. Incrementa token_version, lo que invalida todos los tokens antiguos.
Repositorios
GET /api/repositories
Lista los repositorios accesibles para el usuario actual (filtrados a través de la tabla de permisos). Un usuario que no tiene ningún patrón de permiso sobre un repo no lo ve.
Query params: 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}
Detalle de un repositorio, incluyendo las estadísticas agregadas.
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"
}
}
Errores:
403: sin acceso a este repo404: repo inexistente
POST /api/repositories
Crea un nuevo repositorio. Capability create_repos requerida (admin por defecto).
Body:
{
"name": "new-project",
"description": "Optional description"
}
Validación:
- name: 1 a 64 caracteres, alfanumérico + guiones + guiones bajos. Único por owner.
- description: 0 a 1024 caracteres. Opcional.
Errores:
400: nombre inválido o demasiado largo403: capabilitycreate_reposausente409: ya existe un repo con este nombre para este owner
Archivos
POST /api/files/{repo_id}/upload-chunks
Sube un batch de chunks binarios. Los chunks idénticos (por SHA-256) se deduplican automáticamente.
El servidor no vuelve a almacenar un chunk ya presente. La respuesta devuelve para cada chunk su hash + tamaño +
tamaño comprimido, para usar en el siguiente 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 }
]
}
}
Límites: body máx. 1 GB (configurable vía security.max_body_size_files). Para archivos más grandes, dividir en varios batches.
POST /api/files/{repo_id}/commit
Crea un commit atómico con una lista de archivos y sus chunks. O pasan todos los archivos, o ninguno.
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"
}
]
}
Acciones posibles: added, modified, deleted.
Para added y modified, el array chunks contiene los hashes devueltos
por /upload-chunks. Para deleted, omitir chunks.
Response 200:
{
"success": true,
"data": {
"commit_hash": "7f3a9b1c2d3e4f...",
"files_changed": 3,
"bytes_uploaded": 88080384,
"bytes_deduped": 4194304,
"revision": 47
}
}
Errores:
400: mensaje vacío, archivo sin acción, chunks referenciados inexistentes403: sin la capabilitycheckin, o sin permiso write sobre uno de los archivos409: bloqueo ya en manos de otro usuario sobre un archivo modificado, o commit concurrente (race condition sobre el mismo archivo)
GET /api/files/{repo_id}/snapshot
Devuelve el estado completo del repositorio en la revisión actual: todos los archivos, sus revisiones y sus chunks. Usado por el cliente para las operaciones de clone y sync forzado.
Query params:
revision(opcional): snapshot en una revisión pasada. Por defecto: 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
Descarga el contenido de un archivo en una revisión dada (el servidor reensambla los chunks). Usado por el clone y el sync.
Query params:
path: ruta del archivo (relativa a la raíz del repo)revision(opcional): número de revisión. Por defecto: la última.
Response 200: el contenido binario del archivo.
GET /api/files/{repo_id}/history
Historial paginado de los commits del repositorio.
Query params:
limit(1 a 100, por defecto 20)offset(por defecto 0)path(opcional): filtra por ruta de archivo (commits que han modificado este archivo)author(opcional): filtra por usernamesince(opcional): 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 incremental
Endpoints usados por el cliente de escritorio para sincronizar de forma eficiente:
GET /api/files/{repo_id}/sync: archivos cambiados desde una revisión, para un sync delta.GET /api/files/{repo_id}/deletions: archivos eliminados en el servidor, para propagar las eliminaciones en local.GET /api/files/{repo_id}/list: lista de los archivos del repo.POST /api/files/{repo_id}/checkout: adquiere los bloqueos y prepara la edición.
Bloqueos
POST /api/locks/{repo_id}/acquire
Adquiere bloqueos sobre una lista de rutas. Las adquisiciones son independientes: la lista acquired
contiene los éxitos, la lista failed contiene los fallos con su motivo.
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"
}
]
}
}
Los bloqueos expiran tras 60 minutos sin heartbeat. El campo expires_at refleja
este vencimiento. Llama a /heartbeat periódicamente para renovar el bloqueo, o a /release
para liberarlo.
Motivos de failed:
ALREADY_LOCKED: otro usuario tiene el bloqueo (los camposlock_holderylock_acquired_atestán poblados)PERMISSION_DENIED: sin permiso write sobre esta rutaINVALID_PATH: ruta malformada (absoluta, contiene.., etc.)
POST /api/locks/{repo_id}/release
Libera bloqueos que posees. Body:
{
"paths": ["Content/Maps/MainLevel.umap"],
"force": false
}
force: true requiere la capability force_unlock. La acción se audita.
POST /api/locks/{repo_id}/heartbeat
Actualiza last_heartbeat_at en tus bloqueos. Recomendado cada 5 minutos para los jobs de CI de larga duración
que quieren que los admins vean la actividad.
GET /api/locks/{repo_id}/status
Lista todos los bloqueos activos del repositorio, con joins sobre users y files para devolver directamente los nombres.
Query params: user (filtra por 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
}
}
Comentarios y revisiones
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Lista los comentarios de un commit, en hilos (padre → hijos).
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
Crea un comentario. file_path es opcional (comentario de commit vs. de archivo). parent_id para responder a un hilo existente.
Body:
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
Envía una revisión sobre un commit. Capability approve_changes requerida.
Body:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
Status aceptado: approved, changes_requested, pending.
Lista de seguimiento
GET /api/watchlist/{repo_id}/watchlist
Lista los patrones de watch del usuario actual en este repositorio.
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
Añade un patrón de watch. Body:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
Los eventos posibles: commit (un commit ha modificado un archivo que coincide),
lock (se ha adquirido un bloqueo), review (se ha publicado una revisión).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
Elimina un patrón de watch. Devuelve { "success": true }.
Tablero de producción (tasks)
La antigua API modification-requests se ha eliminado: el tablero de producción la absorbe
(las solicitudes se convierten en tarjetas, origin='request'). Los endpoints están anidados bajo
/api/tasks/{repo_id}.
| Ruta | Descripción |
|---|---|
GET /api/tasks/{repo_id}/board | Tablero completo: columnas + tarjetas |
POST /api/tasks/{repo_id}/tasks | Crea una tarjeta |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | Actualiza / elimina una tarjeta |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | Asigna usuarios a una tarjeta |
GET /api/tasks/{repo_id}/assignable-users | Lista de usuarios asignables |
POST /api/tasks/{repo_id}/columns | Configura las columnas del tablero |
Sub-rutas también disponibles: comentarios de tarjeta, enlaces a assets y commits, y archivos
adjuntos (con imagen de portada). La configuración de las columnas requiere la capability
manage_board (admin / lead).
Builds
POST /api/builds/{repo_id}/upload?hash=<sha256>
Sube un archivo de build a <storage_path>/builds/<hash>.
El servidor verifica el SHA-256 proporcionado en el query param contra el contenido recibido. Si el hash ya existe
en el servidor, devuelve 200 de inmediato sin volver a copiar (dedup del lado del build storage).
Cabeceras: Content-Type: application/octet-stream.
Body: binario en bruto (sin envoltura JSON).
Límites: 8 GB por archivo.
Errores:
400: query param hash ausente o malformado, o SHA-256 calculado != hash proporcionado413: archivo > 8 GB
POST /api/builds/{repo_id}/publish
Registra el manifest de un build tras subir todos sus archivos. Capability publish_builds requerida.
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}
Lista los builds publicados. Capability download_builds requerida para ver los enlaces de descarga.
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
Descarga un archivo de un build. Capability download_builds requerida. El manifest de un build está disponible vía GET /api/builds/{repo_id}/{build_id}/manifest.
Administración
Todas las rutas /api/admin/* requieren que el usuario tenga al menos una capability de admin
(según el endpoint: manage_users, manage_permissions, manage_rules, etc.).
Documentación sucinta a continuación. La mayoría siguen el patrón REST estándar (GET/POST/PUT/DELETE).
| Ruta | Capability | Descripción |
|---|---|---|
GET/POST /api/admin/users | manage_users | Lista / crea usuarios |
PUT/DELETE /api/admin/users/{id} | manage_users | Actualiza / elimina |
POST /api/admin/users/{id}/reset-password | manage_users | Resetea el password, incrementa token_version |
GET/POST /api/admin/groups | manage_users | Gestión de grupos |
GET/POST /api/admin/permissions/{repo_id} | manage_permissions | Reglas glob de permisos por ruta |
GET/POST /api/admin/repositories/{repo_id}/rules | manage_rules | Reglas de validación previas al checkin |
GET/POST /api/admin/repositories/{repo_id}/webhooks | manage_rules | Discord / Slack / Teams / custom |
GET /api/admin/audit | view_all_activity | Audit log filtrado y paginado |
GET /api/admin/stats | view_all_activity | Storage metrics, dedup ratio, crecimiento |
GET /api/admin/repositories/{repo_id}/gc/preview | admin | Preview del GC sin ejecutar |
POST /api/admin/repositories/{repo_id}/gc | admin | Lanza el garbage collection |
POST /api/admin/locks/{id}/force-release | force_unlock | Fuerza el desbloqueo de un bloqueo ajeno, auditado |
Estado
GET /health
Endpoint no autenticado que devuelve 200 OK si el servidor puede acceder a la base de datos.
Usado para los health checks de balanceadores de carga o de monitorización (Prometheus blackbox, Datadog synthetic, etc.).
Response 200: OK (text/plain).
Response 503: si la base de datos es inaccesible.
GET /api/server-info
Endpoint público no autenticado que devuelve los metadatos del servidor (versión, etc.).
Como /health, no está protegido por el middleware de auth.
Formato de error
En caso de error, la respuesta es { "success": false, "error": "..." } con un status HTTP apropiado.
El campo error es un mensaje legible para humanos. Para los clientes que necesitan códigos
estables legibles por máquina, parsea el prefijo (ej.: "Permission denied: ..." empieza siempre por
"Permission denied").
| HTTP | Significado | Cuándo |
|---|---|---|
| 400 | Bad request | Parámetro inválido, JSON malformado, body demasiado grande (antes del límite duro 413) |
| 401 | Not authenticated | Token ausente, expirado, firma inválida, o token_version no coincide |
| 403 | Permission denied | Capability ausente, o sin permiso sobre la ruta / repo |
| 404 | Resource not found | Repo / archivo / commit / usuario inexistente o inaccesible |
| 409 | Conflict | Bloqueo ya en manos de otro, commit concurrente, restricción unique violada |
| 413 | Payload too large | Body más allá del límite (1 GB en /files, 8 GB en /builds/upload) |
| 429 | Too many requests | Únicamente en /auth/login (rate limiting por usuario) |
| 500 | Internal server error | Error de DB, error de IO. Registrado en el servidor; adjunta el timestamp si reportas el bug. |
| 503 | Service unavailable | Base de datos inaccesible (health check) o migración en curso |