uVersion
Italiano
Scarica →

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

HeaderValore
AuthorizationBearer <jwt> su ogni route /api/* tranne /api/auth/* e /api/server-info (e /health)
Content-Typeapplication/json per POST/PUT con corpo JSON. application/octet-stream per gli upload binari (file di build).
Acceptapplication/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 successdata:

{
  "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.

RouteParametri accettatiForma 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é limitoffset 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 disattivato
  • 429: 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, o token_version obsoleto

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'è limitoffset: 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 repository
  • 404: 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 caratteri
  • 403: il chiamante non ha il ruolo admin
  • 409: 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.

Gli unici tre valori accettati per 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), campo chunks assente
  • 403: nessun permesso di scrittura su uno dei percorsi
  • 409: 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 50
  • offset: predefinito 0
  • path (facoltativo): conserva solo i commit che hanno toccato questo file

Questi sono i tre unici parametri letti. Non c'è authorsince: 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

I lock non scadono mai

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:

reasonSignificatolocked_by
File is lockedQualcun altro detiene già il lockIl nome della persona
No write permissionNessun diritto di scrittura su questo percorsonull
Failed to create lockLa posa del lock è fallita nel databasenull
Database errorErrore di database su questo percorsonull

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

RouteDescrizione
GET /api/tasks/{repo_id}/boardBacheca completa: colonne + schede
POST /api/tasks/{repo_id}/tasksCrea una scheda
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}Aggiorna / elimina una scheda
PUT /api/tasks/{repo_id}/tasks/{task_id}/assigneesAssegna utenti a una scheda
GET /api/tasks/{repo_id}/assignable-usersLista degli utenti assegnabili
POST /api/tasks/{repo_id}/columnsConfigura 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 fornito
  • 413: 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.

ControlloPassa perA 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.

RouteControllo realeDescrizione
GET /api/admin/usersSola ammissione, poi filtraggioUn project_admin vede solo gli account del suo perimetro
POST /api/admin/usersSola ammissioneCrea un account
PUT/DELETE /api/admin/users/{id}Super amministratoreAggiorna o elimina un account
POST /api/admin/users/{id}/reset-passwordSuper amministratoreReimposta la password e revoca tutti i token dell'account
GET/POST /api/admin/groupsSuper amministratoreI gruppi sono globali, non possono essere delegati per progetto
GET/POST /api/admin/permissions/{repo_id}Amministratore di questo repositoryRegole di permesso per pattern di percorso, concesse a un gruppo o a un utente
GET/POST /api/admin/repositories/{repo_id}/rulesAmministratore di questo repositoryRegole di validazione applicate prima dell'invio
GET/POST /api/admin/repositories/{repo_id}/webhooksAmministratore di questo repositoryDiscord, Slack, Teams, o webhook generico
GET /api/admin/locksSola ammissione, poi filtraggioLock, ristretti ai repository amministrati
POST /api/admin/locks/{lock_id}/force-releaseAmministratore di questo repositoryToglie il lock di qualcun altro. Registrato nel log di audit
GET /api/admin/auditSuper amministratoreLog di audit, filtrabile e paginato
GET /api/admin/statsSuper amministratoreVolumetria di archiviazione, tasso di deduplicazione, crescita
GET /api/admin/licenceSuper amministratoreStato della licenza e posti consumati
GET /api/admin/repositories/{repo_id}/gc/previewAmministratore di questo repositorySimula la garbage collection senza eliminare nulla
POST /api/admin/repositories/{repo_id}/gcAmministratore di questo repositoryAvvia 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.

PrefissoCosa 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}/adminsChi amministra un repository: nomina e rimozione degli amministratori di progetto.
/api/admin/repositories/{repo_id}/build-accessChi può scaricare i build di questo progetto, concesso progetto per progetto.
/api/admin/licenceStato 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.

HTTPSignificatoQuando
400Bad requestParametro non valido, JSON malformato, corpo troppo grande (prima del limite rigido 413)
401Not authenticatedToken assente, scaduto, firma non valida, o token_version non corrispondente
403Permission deniedCapability mancante, o nessun permesso sul path / repo
404Resource not foundRepo / file / commit / utente inesistente o inaccessibile
409ConflictLock già detenuto, commit in concorrenza, vincolo unique violato
413Payload too largeCorpo oltre il limite (1 GB su /files, 8 GB su /builds/upload)
429Too many requestsUnicamente su /auth/login (limitazione di frequenza per utente)
500Internal server errorErrore DB, errore IO. Registrato lato server, allega il timestamp se segnali il bug.
503Service unavailableDatabase irraggiungibile (health check) o migrazione in corso