Wiki
REST API
Referência completa dos endpoints HTTP do servidor uVersion: auth, repos, files, locks, comments, watchlist, modification requests, builds, admin.
O servidor uVersion expõe uma API HTTP JSON autenticada por JWT. Esta página documenta a totalidade
dos endpoints usados pelo cliente desktop, pelos plugins de editor e pela CLI uversion.
Você pode chamá-los diretamente para integrar o uVersion ao seu tooling interno
(dashboard personalizado, scripts de auditoria, webhooks, etc.).
Convenções
URL base
Todos os caminhos documentados são relativos à URL da sua instância. Os exemplos usam
https://uversion.mygamestudio.com. Substitua pela sua.
Cabeçalhos obrigatórios
| Cabeçalho | Valor |
|---|---|
Authorization | Bearer <jwt> em todas as rotas /api/* exceto /api/auth/* e /api/server-info (e /health) |
Content-Type | application/json para os POST/PUT com body JSON. application/octet-stream para os uploads binários (build files). |
Accept | application/json recomendado (o servidor retorna JSON por padrão) |
Formato de resposta uniforme
Todas as rotas respondem com o seguinte envelope:
{
"success": true,
"data": { /* payload */ }
}
Em caso de erro:
{
"success": false,
"error": "Permission denied: capability 'manage_users' required"
}
O campo success está sempre presente e indica se a requisição teve êxito.
O campo data está presente em caso de sucesso. O campo error está presente em caso de falha.
Nunca ambos juntos.
Paginação
As rotas que retornam listas potencialmente longas aceitam limit e offset
como query params (?limit=20&offset=40). A resposta inclui total e has_more
para facilitar a iteração. Limit máximo: 100.
Rate limiting
O rate limiting global foi removido (ver CLAUDE.md, seção Security). Apenas /api/auth/login
tem rate limiting: 5 tentativas malsucedidas por username a cada 15 minutos. As tentativas
válidas não contam.
Versionamento de tokens
Cada JWT contém um campo tv (token version) que corresponde a users.token_version no DB.
O middleware de auth verifica esse valor a cada requisição. Quando o usuário faz POST /api/auth/logout,
quando um admin reseta a senha dele, ou quando um admin desativa a conta dele, o token_version é incrementado,
invalidando instantaneamente todos os tokens existentes desse usuário em todas as máquinas dele.
Curl
Para chamar a API a partir de um 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
Autenticação
POST /api/auth/login
Autentica um usuário e retorna um JWT de 30 dias.
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
}
}
}
Erros:
401: credenciais inválidas ou usuário inativo429: 5 tentativas malsucedidas atingidas para este username na janela de 15 min
O servidor executa sempre argon2id::verify_password (contra um hash fictício se o usuário não existir)
para neutralizar os ataques de temporização. A resposta leva o mesmo tempo existindo ou não o usuário.
POST /api/auth/refresh
Renova o JWT sem passar novamente pela senha.
Cabeçalhos: Authorization: Bearer <current_token>. Sem body.
Response 200: idêntica a /login com uma expiração adiada em 30 dias e um novo token.
Erros:
401: token atual inválido, expirado, outoken_versionincompatível
POST /api/auth/logout
Invalida todos os tokens do usuário atual (em todas as máquinas dele) incrementando
users.token_version. O usuário terá que fazer login novamente em todos os lugares.
Response 200: { "success": true }.
POST /api/auth/validate
Verifica se o token ainda é válido. Para os tokens a menos de 7 dias de expirar, o servidor inclui
um refreshed_token na resposta (auto-extensão). O cliente deve persisti-lo no lugar
do antigo.
Response 200:
{
"success": true,
"data": {
"valid": true,
"user_id": 12,
"expires_at": "2026-06-13T14:30:00Z",
"refreshed_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
POST /api/auth/register
Cria uma conta de usuário (rota pública, sem autenticação).
POST /api/auth/change-password
Altera a senha do usuário atual. Incrementa token_version, o que invalida todos os tokens antigos.
Repositórios
GET /api/repositories
Lista os repositórios acessíveis pelo usuário atual (filtrados via a tabela de permissões). Um usuário que não tem nenhum padrão de permissão sobre um repo não o vê.
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}
Detalhe de um repositório, incluindo as estatí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"
}
}
Erros:
403: sem acesso a este repo404: repo inexistente
POST /api/repositories
Cria um novo repositório. Capability create_repos obrigatória (admin por padrão).
Body:
{
"name": "new-project",
"description": "Optional description"
}
Validação:
- name: 1 a 64 caracteres, alfanumérico + hífens + underscores. Único por owner.
- description: 0 a 1024 caracteres. Opcional.
Erros:
400: nome inválido ou longo demais403: capabilitycreate_reposausente409: já existe um repo com este nome para este owner
Arquivos
POST /api/files/{repo_id}/upload-chunks
Faz upload de um batch de chunks binários. Os chunks idênticos (por SHA-256) são deduplicados automaticamente.
O servidor não re-armazena um chunk já presente. A resposta retorna para cada chunk seu hash + tamanho +
tamanho comprimido, para usar no POST /commit seguinte.
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 }
]
}
}
Limites: body máx. 1 GB (configurável via security.max_body_size_files). Para arquivos maiores, dividir em vários batches.
POST /api/files/{repo_id}/commit
Cria um commit atômico com uma lista de arquivos e seus chunks. Ou todos os arquivos passam, ou nenhum.
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"
}
]
}
Ações possíveis: added, modified, deleted.
Para added e modified, o array chunks contém os hashes retornados
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
}
}
Erros:
400: mensagem vazia, arquivo sem ação, chunks referenciados inexistentes403: sem a capabilitycheckin, ou sem permissão write sobre um dos arquivos409: bloqueio já mantido por outro usuário sobre um arquivo modificado, ou commit concorrente (race condition sobre o mesmo arquivo)
GET /api/files/{repo_id}/snapshot
Retorna o estado completo do repositório na revisão atual: todos os arquivos, suas revisões e seus chunks. Usado pelo cliente para as operações de clone e sync forçado.
Query params:
revision(opcional): snapshot em uma revisão passada. Padrão: 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
Faz download do conteúdo de um arquivo em uma revisão dada (o servidor remonta os chunks). Usado pelo clone e pelo sync.
Query params:
path: caminho do arquivo (relativo à raiz do repo)revision(opcional): número de revisão. Padrão: a última.
Response 200: o conteúdo binário do arquivo.
GET /api/files/{repo_id}/history
Histórico paginado dos commits do repositório.
Query params:
limit(1 a 100, padrão 20)offset(padrão 0)path(opcional): filtra por caminho de arquivo (commits que modificaram este arquivo)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 pelo cliente desktop para sincronizar de forma eficiente:
GET /api/files/{repo_id}/sync: arquivos alterados desde uma revisão, para um sync delta.GET /api/files/{repo_id}/deletions: arquivos removidos no lado do servidor, para propagar as remoções localmente.GET /api/files/{repo_id}/list: lista dos arquivos do repo.POST /api/files/{repo_id}/checkout: adquire os bloqueios e prepara a edição.
Bloqueios
POST /api/locks/{repo_id}/acquire
Adquire bloqueios sobre uma lista de caminhos. As aquisições são independentes: a lista acquired
contém os sucessos, a lista failed contém as falhas com o 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"
}
]
}
}
Os bloqueios expiram após 60 minutos sem heartbeat. O campo expires_at reflete
esse prazo. Chame /heartbeat periodicamente para renovar o bloqueio, ou /release
para liberá-lo.
Motivos de failed:
ALREADY_LOCKED: outro usuário mantém o bloqueio (os camposlock_holderelock_acquired_atsão preenchidos)PERMISSION_DENIED: sem permissão write sobre este caminhoINVALID_PATH: caminho malformado (absoluto, contém.., etc.)
POST /api/locks/{repo_id}/release
Libera bloqueios que você possui. Body:
{
"paths": ["Content/Maps/MainLevel.umap"],
"force": false
}
force: true requer a capability force_unlock. A ação é auditada.
POST /api/locks/{repo_id}/heartbeat
Atualiza last_heartbeat_at nos seus bloqueios. Recomendado a cada 5 minutos para os jobs de CI de longa duração
que querem que os admins vejam a atividade.
GET /api/locks/{repo_id}/status
Lista todos os bloqueios ativos do repositório, com joins sobre users e files para retornar diretamente os nomes.
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
}
}
Comentários e revisões
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Lista os comentários de um commit, em threads (pai → filhos).
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
Cria um comentário. file_path é opcional (comentário de commit vs. de arquivo). parent_id para responder a uma thread existente.
Body:
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
Envia uma revisão sobre um commit. Capability approve_changes obrigatória.
Body:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
Status aceito: approved, changes_requested, pending.
Lista de observação
GET /api/watchlist/{repo_id}/watchlist
Lista os padrões de watch do usuário atual neste repositório.
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
Adiciona um padrão de watch. Body:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
Os eventos possíveis: commit (um commit modificou um arquivo correspondente),
lock (um bloqueio foi adquirido), review (uma revisão foi postada).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
Remove um padrão de watch. Retorna { "success": true }.
Quadro de produção (tasks)
A antiga API modification-requests foi removida: o quadro de produção a absorve
(as solicitações se tornam cartões, origin='request'). Os endpoints estão aninhados sob
/api/tasks/{repo_id}.
| Rota | Descrição |
|---|---|
GET /api/tasks/{repo_id}/board | Quadro completo: colunas + cartões |
POST /api/tasks/{repo_id}/tasks | Cria um cartão |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | Atualiza / remove um cartão |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | Atribui usuários a um cartão |
GET /api/tasks/{repo_id}/assignable-users | Lista de usuários atribuíveis |
POST /api/tasks/{repo_id}/columns | Configura as colunas do quadro |
Sub-rotas também disponíveis: comentários de cartão, links para assets e commits, e anexos
(com imagem de capa). A configuração das colunas requer a capability
manage_board (admin / lead).
Builds
POST /api/builds/{repo_id}/upload?hash=<sha256>
Faz upload de um arquivo de build para <storage_path>/builds/<hash>.
O servidor verifica o SHA-256 fornecido no query param contra o conteúdo recebido. Se o hash já existir
no lado do servidor, retorna 200 imediatamente sem recopiar (dedup do lado do build storage).
Cabeçalhos: Content-Type: application/octet-stream.
Body: binário bruto (sem wrapping JSON).
Limites: 8 GB por arquivo.
Erros:
400: query param hash ausente ou malformado, ou SHA-256 calculado != hash fornecido413: arquivo > 8 GB
POST /api/builds/{repo_id}/publish
Registra o manifest de um build após o upload de todos os seus arquivos. Capability publish_builds obrigatória.
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 os builds publicados. Capability download_builds obrigatória para ver os links de download.
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
Faz download de um arquivo de um build. Capability download_builds obrigatória. O manifest de um build está disponível via GET /api/builds/{repo_id}/{build_id}/manifest.
Administração
Todas as rotas /api/admin/* exigem que o usuário tenha pelo menos uma capability de admin
(conforme o endpoint: manage_users, manage_permissions, manage_rules, etc.).
Documentação sucinta abaixo. A maioria segue o padrão REST padrão (GET/POST/PUT/DELETE).
| Rota | Capability | Descrição |
|---|---|---|
GET/POST /api/admin/users | manage_users | Lista / cria usuários |
PUT/DELETE /api/admin/users/{id} | manage_users | Atualiza / remove |
POST /api/admin/users/{id}/reset-password | manage_users | Reseta a senha, incrementa token_version |
GET/POST /api/admin/groups | manage_users | Gestão de grupos |
GET/POST /api/admin/permissions/{repo_id} | manage_permissions | Regras glob de permissões por caminho |
GET/POST /api/admin/repositories/{repo_id}/rules | manage_rules | Regras de validação pré-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 e paginado |
GET /api/admin/stats | view_all_activity | Storage metrics, dedup ratio, crescimento |
GET /api/admin/repositories/{repo_id}/gc/preview | admin | Preview do GC sem executar |
POST /api/admin/repositories/{repo_id}/gc | admin | Inicia o garbage collection |
POST /api/admin/locks/{id}/force-release | force_unlock | Força o desbloqueio de um bloqueio de terceiros, auditado |
Saúde
GET /health
Endpoint não autenticado que retorna 200 OK se o servidor conseguir acessar o banco de dados.
Usado para os health checks de load balancer ou de monitoramento (Prometheus blackbox, Datadog synthetic, etc.).
Response 200: OK (text/plain).
Response 503: se o banco de dados estiver inacessível.
GET /api/server-info
Endpoint público não autenticado que retorna os metadados do servidor (versão, etc.).
Como /health, ele não é protegido pelo middleware de auth.
Formato de erro
Em caso de erro, a resposta é { "success": false, "error": "..." } com um status HTTP apropriado.
O campo error é uma mensagem legível por humanos. Para os clientes que precisam de códigos
estáveis legíveis por máquina, faça o parse do prefixo (ex.: "Permission denied: ..." começa sempre por
"Permission denied").
| HTTP | Significado | Quando |
|---|---|---|
| 400 | Bad request | Parâmetro inválido, JSON malformado, body grande demais (antes do limite rígido 413) |
| 401 | Not authenticated | Token ausente, expirado, assinatura inválida, ou token_version incompatível |
| 403 | Permission denied | Capability ausente, ou sem permissão sobre o caminho / repo |
| 404 | Resource not found | Repo / arquivo / commit / usuário inexistente ou inacessível |
| 409 | Conflict | Bloqueio já mantido, commit concorrente, restrição unique violada |
| 413 | Payload too large | Body além do limite (1 GB em /files, 8 GB em /builds/upload) |
| 429 | Too many requests | Apenas em /auth/login (rate limiting por usuário) |
| 500 | Internal server error | Erro de DB, erro de IO. Registrado no servidor; anexe o timestamp se você reportar o bug. |
| 503 | Service unavailable | Banco de dados inacessível (health check) ou migração em andamento |