uVersion
Español
Descargar →

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

CabeceraValor
AuthorizationBearer <jwt> en todas las rutas /api/* excepto /api/auth/* y /api/server-info (y /health)
Content-Typeapplication/json para los POST/PUT con body JSON. application/octet-stream para las subidas binarias (build files).
Acceptapplication/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 inactivo
  • 429: 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, o token_version no 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 repo
  • 404: 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 largo
  • 403: capability create_repos ausente
  • 409: 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 inexistentes
  • 403: sin la capability checkin, o sin permiso write sobre uno de los archivos
  • 409: 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 username
  • since (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 campos lock_holder y lock_acquired_at están poblados)
  • PERMISSION_DENIED: sin permiso write sobre esta ruta
  • INVALID_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}.

RutaDescripción
GET /api/tasks/{repo_id}/boardTablero completo: columnas + tarjetas
POST /api/tasks/{repo_id}/tasksCrea una tarjeta
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}Actualiza / elimina una tarjeta
PUT /api/tasks/{repo_id}/tasks/{task_id}/assigneesAsigna usuarios a una tarjeta
GET /api/tasks/{repo_id}/assignable-usersLista de usuarios asignables
POST /api/tasks/{repo_id}/columnsConfigura 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 proporcionado
  • 413: 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).

RutaCapabilityDescripción
GET/POST /api/admin/usersmanage_usersLista / crea usuarios
PUT/DELETE /api/admin/users/{id}manage_usersActualiza / elimina
POST /api/admin/users/{id}/reset-passwordmanage_usersResetea el password, incrementa token_version
GET/POST /api/admin/groupsmanage_usersGestión de grupos
GET/POST /api/admin/permissions/{repo_id}manage_permissionsReglas glob de permisos por ruta
GET/POST /api/admin/repositories/{repo_id}/rulesmanage_rulesReglas de validación previas al checkin
GET/POST /api/admin/repositories/{repo_id}/webhooksmanage_rulesDiscord / Slack / Teams / custom
GET /api/admin/auditview_all_activityAudit log filtrado y paginado
GET /api/admin/statsview_all_activityStorage metrics, dedup ratio, crecimiento
GET /api/admin/repositories/{repo_id}/gc/previewadminPreview del GC sin ejecutar
POST /api/admin/repositories/{repo_id}/gcadminLanza el garbage collection
POST /api/admin/locks/{id}/force-releaseforce_unlockFuerza 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").

HTTPSignificadoCuándo
400Bad requestParámetro inválido, JSON malformado, body demasiado grande (antes del límite duro 413)
401Not authenticatedToken ausente, expirado, firma inválida, o token_version no coincide
403Permission deniedCapability ausente, o sin permiso sobre la ruta / repo
404Resource not foundRepo / archivo / commit / usuario inexistente o inaccesible
409ConflictBloqueo ya en manos de otro, commit concurrente, restricción unique violada
413Payload too largeBody más allá del límite (1 GB en /files, 8 GB en /builds/upload)
429Too many requestsÚnicamente en /auth/login (rate limiting por usuario)
500Internal server errorError de DB, error de IO. Registrado en el servidor; adjunta el timestamp si reportas el bug.
503Service unavailableBase de datos inaccesible (health check) o migración en curso