Wiki
REST API
Riferimento degli endpoint HTTP del server uVersion: autenticazione, repository, file, lock, commenti, watchlist, bacheca di produzione, build, amministrazione.
Il server uVersion espone un'API HTTP JSON. Le chiamate sono autenticate con un
JWT (JSON Web Token), ovvero il token di sessione che il server ti consegna
al momento dell'accesso e che poi rinvii a ogni richiesta. Questa pagina documenta gli
endpoint usati dal client desktop, dai plugin dell'editor e dalla CLI uversion.
Puoi chiamarli direttamente per integrare uVersion nella tua strumentazione interna
(dashboard personale, script di audit, webhook, ecc.).
Non copre l'intera API: esistono diverse famiglie di route che non sono descritte qui. L'elenco si trova nella sezione Aree non coperte.
Convenzioni
URL di base
Ogni percorso documentato è relativo all'URL della tua istanza. Gli esempi usano
https://uversion.mygamestudio.com. Sostituiscilo con il tuo.
Header richiesti
| Header | Valore |
|---|---|
Authorization | Bearer <jwt> su ogni route /api/* tranne /api/auth/* e /api/server-info (e /health) |
Content-Type | application/json per POST/PUT con corpo JSON. application/octet-stream per gli upload binari (file di build). |
Accept | application/json consigliato (il server restituisce JSON per impostazione predefinita) |
Due formati di risposta, e devi sapere quale stai leggendo
Il server non ha un formato di risposta ma due, e confonderli è l'errore più costoso per chi avvia un'integrazione.
1. Le route autenticate (tutte le /api/* tranne /api/auth/*)
rispondono con un involucro:
{
"success": true,
"data": { /* payload */ }
}
In caso di errore, le stesse route rispondono:
{
"success": false,
"error": "No write permission on this repository"
}
Il campo success è sempre presente. data è presente in caso di successo,
error in caso di fallimento, mai entrambi insieme.
2. Le route di autenticazione (/api/auth/login,
/refresh, /validate, /logout, /register,
/change-password) non usano questo involucro. Restituiscono
l'oggetto nudo, senza success né data:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": 12, "username": "alice", ... },
"must_change_password": false
}
E i loro errori sono un oggetto a un solo campo, senza success:
{ "error": "Invalid credentials" }
Conseguenza pratica: su /api/auth/login, il token si legge in .token
e non in .data.token. Uno script che interroga .data.token riceve
null senza alcun errore visibile.
Infine, GET /api/files/{repo_id}/content non restituisce né l'uno né l'altro: è il
contenuto binario del file, così com'è.
Paginazione
Non esiste una convenzione di paginazione comune: ogni route ha la sua, oppure
non ne ha nessuna. Non dare per scontato né limit, né total, né has_more.
| Route | Parametri accettati | Forma della risposta |
|---|---|---|
GET /api/files/{repo_id}/history |
limit (predefinito 50), offset (predefinito 0), path |
Array piatto di commit. Nessun total, nessun has_more: hai raggiunto la fine quando l'array contiene meno elementi di limit. |
GET /api/repositories |
solo include_inactive (ed è rispettato solo per un super amministratore) |
Array piatto. Né limit né offset vengono letti. |
GET /api/locks/{repo_id}/status |
Nessuno | Array piatto di tutti i lock del repository. |
GET /api/files/{repo_id}/snapshot |
commit_hash, obbligatorio |
Array piatto di file. |
GET /api/admin/audit |
page e per_page, non limit/offset, più filtri (user_id, action, entity_type, from, to) |
Riservata al super amministratore. Illustra bene l'assenza di una convenzione comune: è l'unica route che pagina per numero di pagina. |
Per ogni route non elencata qui, considera che restituisce l'intero risultato in una sola chiamata.
Limitazione di frequenza
Non c'è una limitazione di frequenza generale: un progetto Unreal conta migliaia di file e le operazioni in blocco (acquisizione, rilascio di lock) farebbero scattare un throttle di continuo. Puoi quindi chiamare l'API in volume.
Una sola route è limitata, /api/auth/login:
5 tentativi falliti per nome utente ogni 15 minuti. Gli accessi
riusciti non vengono conteggiati.
Revoca dei token
Ogni token di sessione porta un campo tv (token version), che rispecchia la colonna
users.token_version nel database. Il server confronta i due a ogni richiesta.
Una chiamata a POST /api/auth/logout, un reset della password da parte di un
amministratore o la disattivazione di un account incrementano questo valore, il che rende
immediatamente non validi tutti i token esistenti di quell'utente, su tutte le sue
macchine.
Curl
Per chiamare l'API da un terminale. Nota il .token: la risposta di
/api/auth/login è l'oggetto nudo, non c'è alcun .data da attraversare.
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')
# Le route autenticate, invece, usano eccome l'involucro:
curl -s -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories | jq '.data'
Autenticazione
Promemoria: nessuna route di questa sezione usa l'involucro
{"success", "data"}. Restituiscono l'oggetto nudo, e i loro errori hanno la forma
{"error": "..."}.
POST /api/auth/login
Autentica un utente e restituisce un token di sessione valido 30 giorni.
Body:
{
"username": "alice",
"password": "secret"
}
Response 200:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead"
},
"must_change_password": false
}
Non c'è alcun campo expires_at: la durata di validità si legge nel
token stesso, o si deduce dalla configurazione del server (30 giorni per impostazione predefinita).
Non ignorare must_change_password. Questo campo vale true
quando l'account gira ancora con una password temporanea: quella che l'installer ha generato
per l'account amministratore iniziale, o quella che un amministratore ha appena impostato durante un
reset. Un client che non lo guarda lascia l'utente su questa password provvisoria
a tempo indeterminato. Il comportamento atteso è reindirizzare subito verso
POST /api/auth/change-password prima di qualsiasi altra azione.
Errori:
401: credenziali non valide o account disattivato429: 5 tentativi falliti raggiunti per questo nome utente nella finestra di 15 minuti
Il server esegue sempre la verifica Argon2id della password, anche contro un'impronta fittizia quando l'account non esiste. La risposta impiega quindi lo stesso tempo in entrambi i casi, il che impedisce di indovinare l'esistenza di un account cronometrando le richieste.
POST /api/auth/refresh
Rinnova il token di sessione senza ripassare per la password.
Header: Authorization: Bearer <token_attuale>. Nessun body.
Response 200: esattamente la stessa forma di /login
(token, user, must_change_password), con un token nuovo.
Errori:
401: token non valido, scaduto, account disattivato, otoken_versionobsoleto
POST /api/auth/logout
Invalida tutti i token dell'utente corrente, su tutte le sue macchine,
incrementando users.token_version. Dovrà riconnettersi ovunque: client desktop,
plugin dell'editor e CLI inclusi.
Response 200: { "logged_out": true }.
POST /api/auth/validate
Verifica che un token sia ancora valido e restituisce l'utente a cui corrisponde. Il controllo
riguarda anche is_active e token_version, quindi un token revocato viene
respinto anche se non è ancora scaduto.
Attenzione al body: non è un oggetto, è una stringa JSON nuda, ovvero il token racchiuso tra virgolette doppie.
curl -X POST https://uversion.mygamestudio.com/api/auth/validate \
-H "Content-Type: application/json" \
-d '"eyJhbGciOiJIUzI1NiIs..."'
Response 200: un oggetto utente nudo.
{
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead"
}
Non c'è né valid, né expires_at, né refreshed_token: la
validità si legge nel codice HTTP (200 o 401), e il rinnovo passa per
/api/auth/refresh, mai per questa route.
POST /api/auth/register
Questa route rifiuta per impostazione predefinita. La registrazione aperta è disattivata a meno che
l'operatore non l'abbia esplicitamente attivata nella configurazione del server; altrimenti la risposta è
403 con {"error": "Open registration is disabled; contact your administrator"}.
Nel funzionamento normale, gli account si creano tramite l'amministrazione:
POST /api/admin/users, o la scheda Utenti del pannello di amministrazione. Non
costruire un'integrazione che dipenda da /register.
POST /api/auth/change-password
Cambia la password dell'utente corrente. Incrementa token_version, il che
invalida tutti i token precedenti, incluso quello appena usato per fare la chiamata.
Repository
GET /api/repositories
Elenca i repository accessibili dall'utente corrente, filtrati dalla tabella dei permessi. Un
utente che non ha alcuna regola di permesso su un repository non lo vede. I ruoli
admin e lead vedono tutti i repository.
Query param: uno solo, include_inactive (booleano, predefinito
false), ed è rispettato solo per un super amministratore. Non c'è
né limit né offset: la route restituisce tutto l'elenco.
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"
}
]
}
Questi sono gli unici campi restituiti. In particolare, non c'è né owner, né
current_revision, né file_count, né size_bytes, né
last_commit_at: un repository non ha un proprietario nel senso dell'API, e le
volumetrie si ottengono tramite le route di statistiche di amministrazione.
GET /api/repositories/{repo_id}
Dettaglio di un repository. Stessa forma d'oggetto della lista, con gli stessi campi.
Errori:
403: nessun accesso a questo repository404: repository inesistente
POST /api/repositories
Crea un repository. Riservato al super amministratore, ovvero al ruolo
admin e a lui soltanto. Non è una capability: il controllo riguarda direttamente il
ruolo, quindi un project_admin o un lead riceve un 403. Non
esiste alcuna capability create_repos nel prodotto.
Body:
{
"name": "new-project",
"description": "Descrizione facoltativa"
}
Validazione:
name: da 1 a 255 caratteri. Il server non applica alcun vincolo di set di caratteri. Il percorso di archiviazione su disco si deriva dal nome normalizzandolo.description: facoltativa.
L'unicità riguarda il nome da solo, a livello di server. Non c'è la nozione di proprietario, quindi nessuna unicità «per proprietario».
Errori:
400: nome vuoto o oltre 255 caratteri403: il chiamante non ha il ruoloadmin409: un repository porta già questo nome
File
POST /api/files/{repo_id}/upload-chunks
Invia un file intero, e non una lista di pezzi. Il nome della route è fuorviante: è il server a suddividere il file in blocchi (i chunks), non tu. Un chiamante non deve mai fare questa suddivisione da sé.
I blocchi identici, riconosciuti dalla loro impronta SHA-256, vengono deduplicati: un blocco già presente sul server non viene memorizzato una seconda volta, qualunque sia il file o il repository da cui proviene. È questo che fa sì che un grande file binario modificato al margine non costi quasi nulla in spazio su disco.
Body: un oggetto a un solo campo, contenente il file completo codificato in base64.
{
"data": "<l'intero file, codificato in base64>"
}
Qualsiasi altra forma, in particolare un array chunks, viene respinta dal server.
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
}
}
L'array chunks va riutilizzato così com'è, oggetti completi inclusi,
nel POST /commit che segue. chunks_stored conta i blocchi realmente
scritti su disco e chunks_deduplicated quelli che esistevano già.
Permesso: la scrittura sul repository è richiesta, altrimenti 403.
Limiti: corpo della richiesta limitato a 1 GB (regolabile tramite
security.max_body_size_files). Un file più grande di questo limite non può
passare per questa route.
POST /api/files/{repo_id}/commit
Crea un commit atomico a partire da una lista di file e dei loro blocchi. O passano tutti i file, o nessuno.
action sono add, modify e delete
Non added, non modified, non deleted.
Non è un dettaglio di forma. Il server verifica letteralmente
action == "delete" e tratta tutto il resto come un'aggiunta o una
modifica. Inviare "deleted" non elimina quindi proprio nulla: il file finisce nel
ramo di scrittura, con un array chunks vuoto, e il server registra una
revisione dal contenuto vuoto. Nessun errore viene sollevato. Il file resta presente, la sua ultima
versione viene sovrascritta dal vuoto, e la perdita si vede solo al prossimo sync di
qualcun altro.
Body:
{
"message": "Updated main level + hero pose pass",
"commit_hash": "facoltativo: per raggruppare più lotti sotto un unico 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 non è facoltativo, anche su un delete.
Il campo deve essere presente: ometterlo fa fallire la deserializzazione dell'intera richiesta. Per
una cancellazione, invia un array vuoto.
L'array contiene oggetti, non stringhe: riutilizza senza modificarle le
voci {hash, offset, size, compressed_size} restituite da
/upload-chunks. Ogni hash deve essere un'impronta esadecimale di 64
caratteri, altrimenti l'intero commit viene respinto con 400.
Il campo commit_hash alla radice è facoltativo. Serve a far portare più chiamate
successive da un solo e medesimo commit, cosa che il client desktop fa quando suddivide un
invio voluminoso in lotti. Omesso, il server ne calcola uno.
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 }
]
}
}
Questi sono gli unici campi restituiti. Non c'è né files_changed, né
bytes_uploaded, né bytes_deduped, né revision. Nota che
revision_number è un contatore per file, non un numero di
versione del repository: è commit_hash a identificare il commit, ed è lui che va
conservato per referenziare uno stato.
Errori:
400: corpo malformato, impronta di blocco non valida (atteso: 64 caratteri esadecimali), campochunksassente403: nessun permesso di scrittura su uno dei percorsi409: lock detenuto da qualcun altro su un file modificato, o commit concorrente sullo stesso file
GET /api/files/{repo_id}/snapshot
Restituisce lo stato del repository così com'era al momento di un dato commit: la lista dei file presenti, con il loro numero di revisione e la loro dimensione. Usato dal client desktop per il clone e per la sincronizzazione forzata.
Query param:
-
commit_hash: obbligatorio. È l'impronta del commit che funge da punto di riferimento. Senza questo parametro la richiesta viene respinta, e non esiste un valore predefinito «ultimo stato».
Non c'è alcun parametro revision. Lo snapshot si richiede per
impronta di commit, mai per numero. Un commit sconosciuto risponde 404.
Response 200: un array piatto, senza oggetto contenitore.
{
"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
}
]
}
La risposta non contiene le liste di blocchi. Per recuperare un contenuto, passa
per GET /api/files/{repo_id}/content, che riassembla il file lato server.
GET /api/files/{repo_id}/content
Scarica il contenuto di un file a una data revisione (il server riassembla i chunk). Usato dal clone e dal sync.
Query param:
path: percorso del file (relativo alla radice del repo)revision(facoltativo): numero di revisione. Predefinito: l'ultima.
Response 200: il contenuto binario del file.
GET /api/files/{repo_id}/history
Cronologia dei commit del repository, dal più recente al più vecchio.
Query param:
limit: numero di commit, predefinito 50offset: predefinito 0path(facoltativo): conserva solo i commit che hanno toccato questo file
Questi sono i tre unici parametri letti. Non c'è né author né
since: un parametro sconosciuto viene ignorato in silenzio, il che dà una risposta
plausibile ma non filtrata. Filtra per autore o per data lato chiamante.
Response 200: un array piatto di commit.
{
"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
}
]
}
]
}
Non c'è né total, né has_more, né richiamo di limit e
offset. Per percorrere tutta la cronologia, incrementa offset finché
ricevi meno elementi di limit.
Ogni commit porta direttamente la lista dei file toccati. Una voce la cui revisione è una cancellazione è marcata come tale e non ha contenuto da scaricare.
Sync incrementale
Endpoint usati dal client desktop per sincronizzare in modo efficiente:
GET /api/files/{repo_id}/sync: file cambiati da una revisione, per un sync delta.GET /api/files/{repo_id}/deletions: file eliminati lato server, per propagare le cancellazioni in locale.GET /api/files/{repo_id}/list: lista dei file del repo.POST /api/files/{repo_id}/checkout: acquisisce i lock e prepara la modifica.
Lock
Un lock è tenuto finché non viene esplicitamente rilasciato: da un checkin, da un revert, o da uno sblocco forzato di un amministratore. Non c'è alcuna scadenza automatica, né dopo un'ora, né dopo un mese.
Il campo expires_at esiste unicamente perché la colonna corrispondente nel database
non accetta un valore vuoto. Il server vi scrive un valore sentinella a cento anni: un lock
preso oggi mostra una scadenza situata intorno al 2126. Non costruire nulla su questo
campo, e non mostrare questa data a un utente.
L'heartbeat non prolunga quindi nulla. È un segnale di monitoraggio, il cui unico scopo è mostrare agli amministratori quali lock sono ancora usati attivamente.
POST /api/locks/{repo_id}/acquire
Acquisisce lock su una lista di percorsi. Le acquisizioni sono indipendenti: la lista
acquired contiene i successi, la lista failed i fallimenti con il loro 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"
}
]
}
}
La scadenza al 2126 in questo esempio non è un refuso: è il valore sentinella descritto sopra. Il lock è permanente.
Motivi di fallimento. Il campo reason è una frase in inglese
destinata a essere mostrata, non un codice stabile. Non scrivere logica che confronti questa
stringa, e non «parsare» il suo prefisso: può essere riformulata da una versione all'altra.
I valori attualmente prodotti sono:
reason | Significato | locked_by |
|---|---|---|
File is locked | Qualcun altro detiene già il lock | Il nome della persona |
No write permission | Nessun diritto di scrittura su questo percorso | null |
Failed to create lock | La posa del lock è fallita nel database | null |
Database error | Errore di database su questo percorso | null |
Un percorso non valido (assoluto, contenente .., vuoto, oltre 4096 caratteri o portatore
di un byte nullo) non produce una voce in failed: fa fallire l'intera
richiesta.
POST /api/locks/{repo_id}/release
Rilascia lock che detieni. Body:
{
"paths": ["Content/Maps/MainLevel.umap"]
}
Non c'è un campo force su questa route. Aggiungerne uno non ha alcun
effetto: il server ignora i campi che non conosce, la richiesta riesce, e il lock altrui
resta al suo posto. Un integratore che ci conta crede di aver rilasciato il file mentre non è così.
Per togliere il lock di qualcun altro, l'unica route è
POST /api/admin/locks/{lock_id}/force-release. È riservata all'amministrazione del
repository interessato, e l'operazione è registrata nel log di audit. Prende l'identificativo del lock,
che ottieni tramite GET /api/locks/{repo_id}/status.
POST /api/locks/{repo_id}/heartbeat
Aggiorna la marca temporale di attività dei tuoi lock. Questo non prolunga nulla, dato che nulla scade: è un segnale di monitoraggio, che permette a un amministratore di distinguere un lock ancora usato da un lock dimenticato.
Body: un array JSON di percorsi, direttamente, senza oggetto contenitore.
["Content/Maps/MainLevel.umap", "Content/Characters/Hero.uasset"]
I percorsi non validi vengono ignorati singolarmente invece di far fallire l'intero lotto, affinché un residuo di tracciamento obsoleto non blocchi gli altri.
GET /api/locks/{repo_id}/status
Elenca tutti i lock del repository, con il nome del file e quello della persona che lo detiene.
Query param: nessuno. Non c'è né filtro user, né limit,
né offset. La route restituisce la totalità dei lock del repository, da filtrare lato chiamante.
Response 200: un array piatto, senza oggetto contenitore né 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"
}
]
}
Il campo id è quello da passare a
POST /api/admin/locks/{lock_id}/force-release.
Commenti & revisioni
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Elenca i commenti di un commit, in thread (genitore → figli).
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 commento. file_path è facoltativo (commento di commit vs. di file). parent_id per rispondere a un thread esistente.
Body:
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
Invia una revisione su un commit. Capability approve_changes richiesta.
Body:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
Status accettato: approved, changes_requested, pending.
Watchlist
GET /api/watchlist/{repo_id}/watchlist
Elenca i pattern di watch dell'utente corrente su questo repository.
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
Aggiunge un pattern di watch. Body:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
Gli eventi possibili: commit (un commit ha modificato un file corrispondente),
lock (un lock è stato acquisito), review (una revisione è stata pubblicata).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
Rimuove un pattern di watch. Restituisce { "success": true }.
Bacheca di produzione (task)
La vecchia API modification-requests è stata rimossa: la bacheca di produzione la assorbe
(le richieste diventano schede, origin='request'). Gli endpoint sono annidati sotto
/api/tasks/{repo_id}.
| Route | Descrizione |
|---|---|
GET /api/tasks/{repo_id}/board | Bacheca completa: colonne + schede |
POST /api/tasks/{repo_id}/tasks | Crea una scheda |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | Aggiorna / elimina una scheda |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | Assegna utenti a una scheda |
GET /api/tasks/{repo_id}/assignable-users | Lista degli utenti assegnabili |
POST /api/tasks/{repo_id}/columns | Configura le colonne della bacheca |
Sotto-route anch'esse disponibili: commenti di scheda, collegamenti ad asset e commit, e allegati
(con immagine di copertina). La configurazione delle colonne richiede la capability manage_board
(admin / lead).
Build
POST /api/builds/{repo_id}/upload?hash=<sha256>
Carica un file di build in <storage_path>/builds/<hash>.
Il server verifica lo SHA-256 fornito nel query param contro il contenuto ricevuto. Se l'hash esiste già
lato server, restituisce 200 immediatamente senza ricopiare (dedup lato build storage).
Header: Content-Type: application/octet-stream.
Body: binario grezzo (nessun wrapping JSON).
Limiti: 8 GB per file.
Errori:
400: query param hash mancante o malformato, o SHA-256 calcolato != hash fornito413: file > 8 GB
POST /api/builds/{repo_id}/publish
Registra il manifest di un build dopo l'upload di tutti i suoi file. Capability publish_builds richiesta.
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}
Elenca i build pubblicati di questo progetto.
L'accesso ai build si concede progetto per progetto. La capability
download_builds non basta: essendo valida su tutto il server, dice soltanto che l'
account non è un semplice spettatore. Serve inoltre o un accesso al repository, o un'
autorizzazione esplicita sui build di questo progetto, concessa dal suo amministratore tramite
/api/admin/repositories/{repo_id}/build-access. Un amministratore di progetto accede
d'ufficio a quelli che amministra, un super amministratore a tutti.
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
Scarica un file di un build. Capability download_builds richiesta. Il manifest di un build è disponibile tramite GET /api/builds/{repo_id}/{build_id}/manifest.
Admin
Cosa protegge davvero queste route
Contrariamente a quanto ci si potrebbe aspettare, le route /api/admin/* non sono
protette ciascuna da una capability. Passano per uno di quattro controlli, che quasi tutti
riguardano il ruolo. Una capability è un diritto con nome legato a un ruolo;
il punto importante è che è
valida su tutto il server, poiché non porta alcun riferimento a un repository. Non
può quindi mai servire a confinare qualcuno su un progetto.
| Controllo | Passa per | A cosa serve |
|---|---|---|
| Super amministratore | Il ruolo admin, e lui soltanto |
Tutto ciò che vale per l'intero server: account, gruppi, log di audit, statistiche globali, licenza, aggiornamento del server. |
| Amministratore di questo repository | admin, o un project_admin che amministra questo preciso repository |
Tutto ciò che è proprio di un progetto: permessi, regole di validazione, webhook, accesso ai build, sblocco forzato, garbage collection. |
| Capability valida su tutto il server | Ogni ruolo che detiene la capability nominata | Alcune route trasversali. Attenzione: la capability vale su tutti i repository, è la sua natura. Un project_admin, che non ne detiene alcuna, è ammesso al suo posto solo sui repository che amministra. |
| Sola ammissione | admin o project_admin |
Fa entrare, ma non autorizza nulla. La route che la impiega deve poi restringere essa stessa i propri risultati ai repository amministrati dal chiamante. |
In altre parole: le capability manage_users e manage_permissions
esistono solo sulla carta. Vengono sì create nel database al momento dell'installazione,
ma nessuna riga di codice le interroga. Concederle a un ruolo non cambia proprio nulla.
Non scrivere un'integrazione che presuma che un account non-admin possa gestire
utenti perché gli è stata data manage_users: riceverà un 403.
| Route | Controllo reale | Descrizione |
|---|---|---|
GET /api/admin/users | Sola ammissione, poi filtraggio | Un project_admin vede solo gli account del suo perimetro |
POST /api/admin/users | Sola ammissione | Crea un account |
PUT/DELETE /api/admin/users/{id} | Super amministratore | Aggiorna o elimina un account |
POST /api/admin/users/{id}/reset-password | Super amministratore | Reimposta la password e revoca tutti i token dell'account |
GET/POST /api/admin/groups | Super amministratore | I gruppi sono globali, non possono essere delegati per progetto |
GET/POST /api/admin/permissions/{repo_id} | Amministratore di questo repository | Regole di permesso per pattern di percorso, concesse a un gruppo o a un utente |
GET/POST /api/admin/repositories/{repo_id}/rules | Amministratore di questo repository | Regole di validazione applicate prima dell'invio |
GET/POST /api/admin/repositories/{repo_id}/webhooks | Amministratore di questo repository | Discord, Slack, Teams, o webhook generico |
GET /api/admin/locks | Sola ammissione, poi filtraggio | Lock, ristretti ai repository amministrati |
POST /api/admin/locks/{lock_id}/force-release | Amministratore di questo repository | Toglie il lock di qualcun altro. Registrato nel log di audit |
GET /api/admin/audit | Super amministratore | Log di audit, filtrabile e paginato |
GET /api/admin/stats | Super amministratore | Volumetria di archiviazione, tasso di deduplicazione, crescita |
GET /api/admin/licence | Super amministratore | Stato della licenza e posti consumati |
GET /api/admin/repositories/{repo_id}/gc/preview | Amministratore di questo repository | Simula la garbage collection senza eliminare nulla |
POST /api/admin/repositories/{repo_id}/gc | Amministratore di questo repository | Avvia la garbage collection |
Il dettaglio completo dei ruoli, dei loro ranghi e di ciò che ciascuno può fare è nella pagina dedicata ai ruoli e ai permessi.
Aree dell'API non coperte da questa pagina
Le seguenti famiglie di route esistono, sono montate e servite dal server, ma non sono descritte qui. Se la tua integrazione ne ha bisogno, la cosa più affidabile oggi è osservare le chiamate che fa il client desktop, o scriverci.
| Prefisso | Cosa copre |
|---|---|
/api/advisor/* | Project Health: l'audit del progetto Unreal, i suoi rilievi, il punteggio per pilastro e lo smistamento degli elementi da ignorare. |
/api/distribution/* | Distribuzione tra progetti: collegamenti tra un repository sorgente e un repository destinazione, pubblicazione di file da un progetto a un altro, cronologia. |
/api/binaries/* | Binari dell'editor precompilati, associati a un commit, sul modello di UnrealGameSync. |
/api/watchlist/* | Oltre ai pattern di watch descritti sopra: le notifiche e la casella di posta. |
/api/profile/* | Profilo dell'utente corrente. |
/api/admin/repositories/{repo_id}/admins | Chi amministra un repository: nomina e rimozione degli amministratori di progetto. |
/api/admin/repositories/{repo_id}/build-access | Chi può scaricare i build di questo progetto, concesso progetto per progetto. |
/api/admin/licence | Stato della licenza e posti consumati. |
/api/admin/server/* | Aggiornamento del server dal pannello di amministrazione. |
Salute
GET /health
Endpoint non autenticato che restituisce 200 OK se il server riesce ad accedere al database.
Usato per gli health check di load balancer o di monitoraggio (Prometheus blackbox, Datadog synthetic, ecc.).
Response 200: OK (text/plain).
Response 503: se il database è irraggiungibile.
GET /api/server-info
Endpoint pubblico non autenticato che restituisce i metadati del server (versione, ecc.).
Come /health, non è protetto dal middleware di auth.
Formato degli errori
In caso di errore, una route autenticata risponde { "success": false, "error": "..." },
e una route /api/auth/* risponde { "error": "..." }, in entrambi i casi
con un codice HTTP adeguato.
Il campo error è un messaggio destinato a un umano, non un codice.
Non esiste alcun catalogo di codici di errore stabili, né nel corpo, né in un header. Quindi non
costruire logica sul suo contenuto, e non analizzarne il prefisso: queste frasi vengono
riformulate da una versione all'altra, e un test di stringa che si rompe in silenzio è peggio di nessun
test.
Il codice HTTP è l'unica cosa su cui basare un comportamento. La tabella qui sotto ne dà la lettura.
| HTTP | Significato | Quando |
|---|---|---|
| 400 | Bad request | Parametro non valido, JSON malformato, corpo troppo grande (prima del limite rigido 413) |
| 401 | Not authenticated | Token assente, scaduto, firma non valida, o token_version non corrispondente |
| 403 | Permission denied | Capability mancante, o nessun permesso sul path / repo |
| 404 | Resource not found | Repo / file / commit / utente inesistente o inaccessibile |
| 409 | Conflict | Lock già detenuto, commit in concorrenza, vincolo unique violato |
| 413 | Payload too large | Corpo oltre il limite (1 GB su /files, 8 GB su /builds/upload) |
| 429 | Too many requests | Unicamente su /auth/login (limitazione di frequenza per utente) |
| 500 | Internal server error | Errore DB, errore IO. Registrato lato server, allega il timestamp se segnali il bug. |
| 503 | Service unavailable | Database irraggiungibile (health check) o migrazione in corso |