uVersion
Português
Baixar →

Wiki

REST API

Referência dos endpoints HTTP do servidor uVersion: autenticação, repositórios, arquivos, bloqueios, comentários, lista de observação, quadro de produção, builds, administração.

O servidor uVersion expõe uma API HTTP JSON. As chamadas são autenticadas com um JWT (JSON Web Token), ou seja, o token de sessão que o servidor lhe entrega no momento do login e que você devolve depois em cada requisição. Esta página documenta os endpoints usados pelo cliente desktop, pelos plugins do editor e pela CLI uversion. Você pode chamá-los diretamente para integrar o uVersion ao seu ferramental interno (painel próprio, scripts de auditoria, webhooks, etc.).

Ela não cobre a totalidade da API: existem várias famílias de rotas que não são descritas aqui. A lista está na seção Áreas não cobertas.

Convenções

URL base

Todo caminho documentado é relativo à URL da sua instância. Os exemplos usam https://uversion.mygamestudio.com. Substitua pela sua.

Cabeçalhos obrigatórios

CabeçalhoValor
AuthorizationBearer <jwt> em toda rota /api/* exceto /api/auth/* e /api/server-info (e /health)
Content-Typeapplication/json para POST/PUT com corpo JSON. application/octet-stream para uploads binários (arquivos de build).
Acceptapplication/json recomendado (o servidor retorna JSON por padrão)

Dois formatos de resposta, e é preciso saber qual você está lendo

O servidor não tem um formato de resposta, mas dois, e confundi-los é o erro mais custoso para quem começa uma integração.

1. As rotas autenticadas (todo /api/* exceto /api/auth/*) respondem com um envelope:

{
  "success": true,
  "data": { /* payload */ }
}

Em caso de erro, essas mesmas rotas respondem:

{
  "success": false,
  "error": "No write permission on this repository"
}

O campo success está sempre presente. data está presente em caso de sucesso, error em caso de falha, nunca os dois juntos.

2. As rotas de autenticação (/api/auth/login, /refresh, /validate, /logout, /register, /change-password) não usam esse envelope. Elas retornam o objeto nu, sem success nem data:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": { "id": 12, "username": "alice", ... },
  "must_change_password": false
}

E seus erros são um objeto de um único campo, sem success:

{ "error": "Invalid credentials" }

Consequência prática: em /api/auth/login, o token é lido em .token e não em .data.token. Um script que consulta .data.token recebe null sem nenhum erro visível.

Por fim, GET /api/files/{repo_id}/content não retorna nem um nem outro: é o conteúdo binário do arquivo, tal como está.

Paginação

Não há convenção de paginação comum: cada rota tem a sua, ou não tem nenhuma. Não presuma nem limit, nem total, nem has_more.

RotaParâmetros aceitosForma da resposta
GET /api/files/{repo_id}/history limit (padrão 50), offset (padrão 0), path Array plano de commits. Sem total, sem has_more: você chegou ao fim quando o array contém menos elementos que limit.
GET /api/repositories apenas include_inactive (e ele só é respeitado para um superadministrador) Array plano. Nem limit nem offset são lidos.
GET /api/locks/{repo_id}/status Nenhum Array plano de todos os bloqueios do repositório.
GET /api/files/{repo_id}/snapshot commit_hash, obrigatório Array plano de arquivos.
GET /api/admin/audit page e per_page, não limit/offset, além de filtros (user_id, action, entity_type, from, to) Reservada ao superadministrador. Ilustra bem a ausência de convenção comum: é a única rota que pagina por número de página.

Para qualquer rota não listada aqui, considere que ela retorna a totalidade do seu resultado em uma única chamada.

Limitação de taxa

Não há limitação de taxa geral: um projeto Unreal conta com milhares de arquivos e as operações em lote (adquirir, liberar bloqueios) disparariam um throttle constantemente. Você pode, portanto, chamar a API em volume.

Uma única rota é limitada, /api/auth/login: 5 tentativas falhas por nome de usuário a cada 15 minutos. Os logins bem-sucedidos não são contados.

Revogação de tokens

Cada token de sessão carrega um campo tv (token version), que reflete a coluna users.token_version no banco. O servidor compara os dois em cada requisição. Uma chamada a POST /api/auth/logout, uma redefinição de senha por um administrador ou a desativação de uma conta incrementam esse valor, o que invalida instantaneamente todos os tokens existentes desse usuário, em todas as suas máquinas.

Curl

Para chamar a API a partir de um terminal. Note o .token: a resposta de /api/auth/login é o objeto nu, não há nenhum .data a atravessar.

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')

# As rotas autenticadas, por sua vez, usam sim o envelope:
curl -s -H "Authorization: Bearer $TOKEN" \
  https://uversion.mygamestudio.com/api/repositories | jq '.data'

Autenticação

Lembrete: nenhuma rota desta seção usa o envelope {"success", "data"}. Elas retornam o objeto nu, e seus erros têm a forma {"error": "..."}.

POST /api/auth/login

Autentica um usuário e retorna um token de sessão válido por 30 dias.

Body:

{
  "username": "alice",
  "password": "secret"
}

Response 200:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": 12,
    "username": "alice",
    "email": "alice@mygamestudio.com",
    "role": "lead"
  },
  "must_change_password": false
}

Não há campo expires_at: a duração de validade é lida no próprio token, ou deduzida da configuração do servidor (30 dias por padrão).

Não ignore must_change_password. Esse campo vale true quando a conta ainda roda com uma senha temporária: a que o instalador gerou para a conta de administrador inicial, ou a que um administrador acabou de definir numa redefinição. Um cliente que não o observa deixa o usuário nessa senha provisória indefinidamente. O comportamento esperado é redirecionar imediatamente para POST /api/auth/change-password antes de qualquer outra ação.

Erros:

  • 401: credenciais inválidas ou conta desativada
  • 429: 5 tentativas falhas atingidas para este nome de usuário na janela de 15 minutos

O servidor sempre executa a verificação Argon2id da senha, inclusive contra uma impressão fictícia quando a conta não existe. A resposta leva, portanto, o mesmo tempo nos dois casos, o que impede adivinhar a existência de uma conta cronometrando as requisições.

POST /api/auth/refresh

Renova o token de sessão sem passar novamente pela senha.

Headers: Authorization: Bearer <token_atual>. Sem body.

Response 200: exatamente a mesma forma que /login (token, user, must_change_password), com um token novo.

Erros:

  • 401: token inválido, expirado, conta desativada, ou token_version obsoleto

POST /api/auth/logout

Invalida todos os tokens do usuário atual, em todas as suas máquinas, incrementando users.token_version. Ele terá de fazer login novamente em todo lugar: cliente desktop, plugin do editor e CLI inclusos.

Response 200: { "logged_out": true }.

POST /api/auth/validate

Verifica se um token ainda é válido e retorna o usuário ao qual corresponde. A verificação abrange também is_active e token_version, então um token revogado é rejeitado mesmo que ainda não tenha expirado.

Atenção ao body: não é um objeto, é uma string JSON nua, ou seja, o token cercado por aspas duplas.

curl -X POST https://uversion.mygamestudio.com/api/auth/validate \
  -H "Content-Type: application/json" \
  -d '"eyJhbGciOiJIUzI1NiIs..."'

Response 200: um objeto de usuário nu.

{
  "id": 12,
  "username": "alice",
  "email": "alice@mygamestudio.com",
  "role": "lead"
}

Não há valid, nem expires_at, nem refreshed_token: a validade é lida no código HTTP (200 ou 401), e a renovação passa por /api/auth/refresh, nunca por esta rota.

POST /api/auth/register

Esta rota recusa por padrão. O registro aberto é desativado a menos que o operador o tenha ativado explicitamente na configuração do servidor; caso contrário a resposta é 403 com {"error": "Open registration is disabled; contact your administrator"}.

No funcionamento normal, as contas são criadas pela administração: POST /api/admin/users, ou a aba Usuários do painel de administração. Não construa uma integração que dependa de /register.

POST /api/auth/change-password

Altera a senha do usuário atual. Incrementa token_version, o que invalida todos os tokens anteriores, inclusive o que acabou de servir para fazer a chamada.

Repositórios

GET /api/repositories

Lista os repositórios acessíveis pelo usuário atual, filtrados pela tabela de permissões. Um usuário que não tem nenhuma regra de permissão sobre um repositório não o vê. Os papéis admin e lead veem todos os repositórios.

Query params: um só, include_inactive (booleano, padrão false), e ele só é respeitado para um superadministrador. Não há nem limit nem offset: a rota retorna toda a lista.

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"
    }
  ]
}

Esses são os únicos campos retornados. Em particular, não há nem owner, nem current_revision, nem file_count, nem size_bytes, nem last_commit_at: um repositório não tem proprietário no sentido da API, e as volumetrias se obtêm pelas rotas de estatísticas de administração.

GET /api/repositories/{repo_id}

Detalhe de um repositório. Mesma forma de objeto que na lista, com os mesmos campos.

Erros:

  • 403: sem acesso a este repositório
  • 404: repositório inexistente

POST /api/repositories

Cria um repositório. Reservado ao superadministrador, ou seja, ao papel admin e somente a ele. Não é uma capacidade: o controle recai diretamente sobre o papel, então um project_admin ou um lead recebe um 403. Não existe nenhuma capacidade create_repos no produto.

Body:

{
  "name": "new-project",
  "description": "Descrição opcional"
}

Validação:

  • name: 1 a 255 caracteres. O servidor não aplica nenhuma restrição de conjunto de caracteres. O caminho de armazenamento em disco é derivado do nome normalizando-o.
  • description: opcional.

A unicidade recai sobre o nome sozinho, na escala do servidor. Não há noção de proprietário, portanto não há unicidade «por proprietário».

Erros:

  • 400: nome vazio ou além de 255 caracteres
  • 403: o chamador não tem o papel admin
  • 409: um repositório já leva este nome

Arquivos

POST /api/files/{repo_id}/upload-chunks

Envia um arquivo inteiro, e não uma lista de pedaços. O nome da rota é enganoso: é o servidor quem divide o arquivo em blocos (os chunks), não você. Um chamador nunca precisa fazer essa divisão ele mesmo.

Os blocos idênticos, reconhecidos pela sua impressão SHA-256, são deduplicados: um bloco já presente no servidor não é armazenado uma segunda vez, seja qual for o arquivo ou o repositório de onde vem. É isso que faz com que um arquivo binário grande modificado na margem não custe quase nada em espaço de disco.

Body: um objeto de um único campo, contendo o arquivo completo codificado em base64.

{
  "data": "<o arquivo inteiro, codificado em base64>"
}

Qualquer outra forma, em particular um array chunks, é rejeitada pelo servidor.

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
  }
}

O array chunks deve ser retomado tal como está, objetos completos inclusos, no POST /commit que segue. chunks_stored conta os blocos realmente escritos em disco e chunks_deduplicated os que já existiam.

Permissão: a escrita sobre o repositório é exigida, caso contrário 403.

Limites: corpo de requisição limitado a 1 GB (ajustável por security.max_body_size_files). Um arquivo maior que esse limite não pode passar por esta rota.

POST /api/files/{repo_id}/commit

Cria um commit atômico a partir de uma lista de arquivos e seus blocos. Ou todos os arquivos passam, ou nenhum.

Os três únicos valores aceitos para action são add, modify e delete

Não added, não modified, não deleted.

Não é um detalhe de forma. O servidor testa literalmente action == "delete" e trata todo o resto como uma adição ou uma modificação. Enviar "deleted" não apaga, portanto, nada: o arquivo vai para o ramo de escrita, com um array chunks vazio, e o servidor registra uma revisão de conteúdo vazio. Nenhum erro é lançado. O arquivo permanece presente, sua última versão é sobrescrita pelo vazio, e a perda só se vê no próximo sync de outra pessoa.

Body:

{
  "message": "Updated main level + hero pose pass",
  "commit_hash": "opcional: para agrupar vários lotes sob um único commit",
  "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 não é opcional, inclusive num delete. O campo deve estar presente: omiti-lo faz falhar a desserialização de toda a requisição. Para uma exclusão, envie um array vazio.

O array contém objetos, não strings: retome sem modificar as entradas {hash, offset, size, compressed_size} retornadas por /upload-chunks. Cada hash deve ser uma impressão hexadecimal de 64 caracteres, sem o que o commit inteiro é recusado com 400.

O campo commit_hash na raiz é opcional. Ele serve para fazer várias chamadas sucessivas serem carregadas por um só e mesmo commit, o que o cliente desktop faz quando divide um envio volumoso em lotes. Omitido, o servidor calcula um.

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  }
    ]
  }
}

Esses são os únicos campos retornados. Não há nem files_changed, nem bytes_uploaded, nem bytes_deduped, nem revision. Note que revision_number é um contador por arquivo, não um número de versão do repositório: é commit_hash quem identifica o commit, e é ele que se deve conservar para referenciar um estado.

Erros:

  • 400: corpo malformado, impressão de bloco inválida (esperado: 64 caracteres hexadecimais), campo chunks ausente
  • 403: sem permissão de escrita sobre um dos caminhos
  • 409: bloqueio mantido por outra pessoa sobre um arquivo modificado, ou commit concorrente sobre o mesmo arquivo

GET /api/files/{repo_id}/snapshot

Retorna o estado do repositório tal como estava no momento de um commit dado: a lista de arquivos presentes, com seu número de revisão e seu tamanho. Usado pelo cliente desktop para o clone e para a sincronização forçada.

Query params:

  • commit_hash: obrigatório. É a impressão do commit que serve de ponto de referência. Sem este parâmetro a requisição é rejeitada, e não existe um valor padrão «último estado».

Não há parâmetro revision. O snapshot é pedido por impressão de commit, nunca por número. Um commit desconhecido responde 404.

Response 200: um array plano, sem objeto envolvente.

{
  "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
    }
  ]
}

A resposta não contém as listas de blocos. Para recuperar um conteúdo, passe por GET /api/files/{repo_id}/content, que remonta o arquivo do lado do servidor.

GET /api/files/{repo_id}/content

Baixa o 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 de commits do repositório, do mais recente ao mais antigo.

Query params:

  • limit: número de commits, padrão 50
  • offset: padrão 0
  • path (opcional): mantém apenas os commits que tocaram este arquivo

Esses são os três únicos parâmetros lidos. Não há nem author nem since: um parâmetro desconhecido é ignorado em silêncio, o que dá uma resposta plausível mas não filtrada. Filtre por autor ou por data do lado do chamador.

Response 200: um array plano de commits.

{
  "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
        }
      ]
    }
  ]
}

Não há nem total, nem has_more, nem lembrete de limit e offset. Para percorrer todo o histórico, incremente offset até receber menos elementos que limit.

Cada commit carrega diretamente a lista de arquivos tocados. Uma entrada cuja revisão é uma exclusão é marcada como tal e não tem conteúdo a baixar.

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 excluídos do lado do servidor, para propagar as exclusõ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

Os bloqueios nunca expiram

Um bloqueio é mantido até ser explicitamente liberado: por um checkin, por um revert, ou por um desbloqueio forçado de administrador. Não há nenhuma expiração automática, nem ao fim de uma hora, nem ao fim de um mês.

O campo expires_at existe unicamente porque a coluna correspondente no banco não aceita um valor vazio. O servidor escreve nele um valor sentinela de cem anos: um bloqueio tomado hoje mostra um vencimento situado por volta de 2126. Não construa nada sobre este campo, e não mostre esta data a um usuário.

O heartbeat não prolonga, portanto, nada. É um sinal de supervisão, cujo único fim é mostrar aos administradores quais bloqueios ainda são usados ativamente.

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 as falhas com seu 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",
        "expires_at": "2126-05-15T14:30:00Z"
      }
    ],
    "failed": [
      {
        "path": "Content/Characters/Hero.uasset",
        "reason": "File is locked",
        "locked_by": "bob"
      }
    ]
  }
}

O vencimento em 2126 deste exemplo não é um erro de digitação: é o valor sentinela descrito acima. O bloqueio é permanente.

Motivos de falha. O campo reason é uma frase em inglês destinada a ser exibida, não um código estável. Não escreva lógica que compare esta string, e não «parseie» o seu prefixo: ela pode ser reformulada de uma versão a outra. Os valores produzidos atualmente são:

reasonSignificadolocked_by
File is lockedOutra pessoa já detém o bloqueioO nome da pessoa
No write permissionSem direito de escrita sobre este caminhonull
Failed to create lockA colocação do bloqueio falhou no banconull
Database errorErro de banco de dados sobre este caminhonull

Um caminho inválido (absoluto, contendo .., vazio, além de 4096 caracteres ou portador de um byte nulo) não produz uma entrada em failed: ele faz falhar toda a requisição.

POST /api/locks/{repo_id}/release

Libera bloqueios que você detém. Body:

{
  "paths": ["Content/Maps/MainLevel.umap"]
}

Não há campo force nesta rota. Adicionar um não tem nenhum efeito: o servidor ignora os campos que não conhece, a requisição tem sucesso, e o bloqueio alheio permanece no lugar. Um integrador que conta com isso acredita ter liberado o arquivo quando não.

Para retirar o bloqueio de outra pessoa, a única rota é POST /api/admin/locks/{lock_id}/force-release. Ela é reservada à administração do repositório em questão, e a operação é inscrita no log de auditoria. Ela recebe o identificador do bloqueio, que você obtém por GET /api/locks/{repo_id}/status.

POST /api/locks/{repo_id}/heartbeat

Atualiza o carimbo de tempo de atividade dos seus bloqueios. Isso não prolonga nada, já que nada expira: é um sinal de supervisão, que permite a um administrador distinguir um bloqueio ainda usado de um bloqueio esquecido.

Body: um array JSON de caminhos, diretamente, sem objeto envolvente.

["Content/Maps/MainLevel.umap", "Content/Characters/Hero.uasset"]

Os caminhos inválidos são ignorados individualmente em vez de fazer falhar todo o lote, para que um resíduo de rastreamento obsoleto não bloqueie os outros.

GET /api/locks/{repo_id}/status

Lista todos os bloqueios do repositório, com o nome do arquivo e o da pessoa que o detém.

Query params: nenhum. Não há nem filtro user, nem limit, nem offset. A rota retorna a totalidade dos bloqueios do repositório, a filtrar do lado do chamador.

Response 200: um array plano, sem objeto envolvente nem 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"
    }
  ]
}

O campo id é o que deve ser passado a POST /api/admin/locks/{lock_id}/force-release.

Comentários & 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. Capacidade approve_changes exigida.

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 observação do usuário atual sobre este 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 observação. 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}

Retira um padrão de observação. Retorna { "success": true }.

Quadro de produção (tasks)

A antiga API modification-requests foi retirada: o quadro de produção a absorve (as solicitações viram cartões, origin='request'). Os endpoints estão aninhados sob /api/tasks/{repo_id}.

RotaDescrição
GET /api/tasks/{repo_id}/boardQuadro completo: colunas + cartões
POST /api/tasks/{repo_id}/tasksCria um cartão
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}Atualiza / exclui um cartão
PUT /api/tasks/{repo_id}/tasks/{task_id}/assigneesAtribui usuários a um cartão
GET /api/tasks/{repo_id}/assignable-usersLista dos usuários atribuíveis
POST /api/tasks/{repo_id}/columnsConfigura 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 capacidade manage_board (admin / lead).

Builds

POST /api/builds/{repo_id}/upload?hash=<sha256>

Sobe 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á existe do lado do servidor, retorna 200 imediatamente sem recopiar (dedup do lado do armazenamento de builds).

Headers: Content-Type: application/octet-stream.

Body: binário cru (sem envoltório JSON).

Limites: 8 GB por arquivo.

Erros:

  • 400: query param hash ausente ou malformado, ou SHA-256 calculado != hash fornecido
  • 413: arquivo > 8 GB

POST /api/builds/{repo_id}/publish

Registra o manifest de um build após o upload de todos os seus arquivos. Capacidade publish_builds exigida.

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 deste projeto.

O acesso aos builds é dado projeto por projeto. A capacidade download_builds não basta: sendo de servidor inteiro, ela só diz que a conta não é um simples espectador. É preciso além disso ou um acesso ao repositório, ou uma autorização explícita sobre os builds deste projeto, concedida pelo seu administrador via /api/admin/repositories/{repo_id}/build-access. Um administrador de projeto acessa de ofício os que administra, um superadministrador todos.

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

Baixa um arquivo de um build. Capacidade download_builds exigida. O manifest de um build está disponível via GET /api/builds/{repo_id}/{build_id}/manifest.

Administração

O que realmente protege estas rotas

Ao contrário do que se poderia esperar, as rotas /api/admin/* não são protegidas por uma capacidade cada uma. Elas passam por um de quatro controles, que quase todos recaem sobre o papel. Uma capability é um direito com nome vinculado a um papel; o ponto importante é que ela é válida em todo o servidor, pois não carrega nenhuma referência a um repositório. Ela não pode, portanto, jamais servir para confinar alguém a um projeto.

ControlePassa paraPara que serve
Superadministrador O papel admin, e somente ele Tudo o que vale para o servidor inteiro: contas, grupos, log de auditoria, estatísticas globais, licença, atualização do servidor.
Administrador deste repositório admin, ou um project_admin que administra este repositório preciso Tudo o que é próprio de um projeto: permissões, regras de validação, webhooks, acesso aos builds, desbloqueio forçado, coleta de lixo.
Capacidade de servidor inteiro Todo papel que detém a capacidade nomeada Algumas rotas transversais. Atenção: a capacidade vale sobre todos os repositórios, é a sua natureza. Um project_admin, que não detém nenhuma, é admitido em seu lugar apenas sobre os repositórios que administra.
Admissão sozinha admin ou project_admin Deixa entrar, mas não autoriza nada. A rota que a emprega deve depois restringir ela mesma os seus resultados aos repositórios administrados pelo chamador.

Dito de outro modo: as capacidades manage_users e manage_permissions só existem no papel. Elas são de fato criadas no banco no momento da instalação, mas nenhuma linha de código as consulta. Concedê-las a um papel não muda absolutamente nada. Não escreva uma integração que suponha que uma conta não-admin poderá gerenciar usuários porque lhe deram manage_users: ela receberá um 403.

RotaControle realDescrição
GET /api/admin/usersAdmissão sozinha, depois filtragemUm project_admin só vê as contas do seu perímetro
POST /api/admin/usersAdmissão sozinhaCria uma conta
PUT/DELETE /api/admin/users/{id}SuperadministradorAtualiza ou exclui uma conta
POST /api/admin/users/{id}/reset-passwordSuperadministradorRedefine a senha e revoga todos os tokens da conta
GET/POST /api/admin/groupsSuperadministradorOs grupos são globais, não podem ser delegados por projeto
GET/POST /api/admin/permissions/{repo_id}Administrador deste repositórioRegras de permissão por padrão de caminho, concedidas a um grupo ou a um usuário
GET/POST /api/admin/repositories/{repo_id}/rulesAdministrador deste repositórioRegras de validação aplicadas antes do envio
GET/POST /api/admin/repositories/{repo_id}/webhooksAdministrador deste repositórioDiscord, Slack, Teams, ou webhook genérico
GET /api/admin/locksAdmissão sozinha, depois filtragemBloqueios, restritos aos repositórios administrados
POST /api/admin/locks/{lock_id}/force-releaseAdministrador deste repositórioRetira o bloqueio de outra pessoa. Inscrito no log de auditoria
GET /api/admin/auditSuperadministradorLog de auditoria, filtrável e paginado
GET /api/admin/statsSuperadministradorVolumetria de armazenamento, taxa de deduplicação, crescimento
GET /api/admin/licenceSuperadministradorEstado da licença e assentos consumidos
GET /api/admin/repositories/{repo_id}/gc/previewAdministrador deste repositórioSimula a coleta de lixo sem excluir nada
POST /api/admin/repositories/{repo_id}/gcAdministrador deste repositórioInicia a coleta de lixo

O detalhe completo dos papéis, de seus ranques e do que cada um pode fazer está na página dedicada aos papéis e permissões.

Áreas da API não cobertas por esta página

As seguintes famílias de rotas existem, estão montadas e servidas pelo servidor, mas não são descritas aqui. Se a sua integração precisar delas, o mais confiável hoje é observar as chamadas que o cliente desktop faz, ou nos escrever.

PrefixoO que cobre
/api/advisor/*Project Health: a auditoria de projeto Unreal, seus achados, a pontuação por pilar e a triagem dos elementos a ignorar.
/api/distribution/*Distribuição entre projetos: links entre um repositório de origem e um repositório de destino, publicação de arquivos de um projeto para outro, histórico.
/api/binaries/*Binários de editor pré-compilados, associados a um commit, no modelo do UnrealGameSync.
/api/watchlist/*Além dos padrões de observação descritos acima: as notificações e a caixa de entrada.
/api/profile/*Perfil do usuário atual.
/api/admin/repositories/{repo_id}/adminsQuem administra um repositório: designação e retirada dos administradores de projeto.
/api/admin/repositories/{repo_id}/build-accessQuem pode baixar os builds deste projeto, concedido projeto por projeto.
/api/admin/licenceEstado da licença e assentos consumidos.
/api/admin/server/*Atualização do servidor a partir do painel de administração.

Saúde

GET /health

Endpoint não autenticado que retorna 200 OK se o servidor consegue acessar o banco. Usado para os health checks de balanceador de carga ou de monitoramento (Prometheus blackbox, Datadog synthetic, etc.).

Response 200: OK (text/plain).

Response 503: se o banco está inacessível.

GET /api/server-info

Endpoint público não autenticado que retorna os metadados do servidor (versão, etc.). Como /health, não é protegido pelo middleware de auth.

Formato de erro

Em caso de erro, uma rota autenticada responde { "success": false, "error": "..." }, e uma rota /api/auth/* responde { "error": "..." }, nos dois casos com um código HTTP adequado.

O campo error é uma mensagem destinada a um humano, não um código. Não existe nenhum catálogo de códigos de erro estáveis, nem no corpo, nem em um cabeçalho. Portanto não construa lógica sobre o seu conteúdo, e não analise o seu prefixo: estas frases são reformuladas de uma versão a outra, e um teste de string que quebra em silêncio é pior que nenhum teste.

O código HTTP é a única coisa sobre a qual ramificar um comportamento. A tabela abaixo dá a sua leitura.

HTTPSignificadoQuando
400Bad requestParâmetro inválido, JSON malformado, corpo grande demais (antes do limite duro 413)
401Not authenticatedToken ausente, expirado, assinatura inválida, ou token_version não corresponde
403Permission deniedCapacidade ausente, ou sem permissão sobre o path / repo
404Resource not foundRepo / arquivo / commit / usuário inexistente ou inacessível
409ConflictBloqueio já mantido, commit em concorrência, restrição única violada
413Payload too largeCorpo além do limite (1 GB em /files, 8 GB em /builds/upload)
429Too many requestsUnicamente em /auth/login (limitação de taxa por usuário)
500Internal server errorErro de DB, erro de IO. Logado do lado do servidor, anexe o timestamp se reportar o bug.
503Service unavailableBanco inacessível (health check) ou migração em curso