Wiki
REST API
Riferimento completo degli endpoint HTTP del server uVersion: auth, repos, files, locks, comments, watchlist, modification requests, builds, admin.
Il server uVersion espone un'API HTTP JSON autenticata tramite JWT. Questa pagina documenta la totalità
degli endpoint usati dal client desktop, dai plugin per editor e dalla CLI uversion.
Puoi chiamarli direttamente per integrare uVersion nel tuo tooling interno
(dashboard personalizzata, script di audit, webhook, ecc.).
Convenzioni
URL di base
Tutti i percorsi documentati sono relativi all'URL della tua istanza. Gli esempi usano
https://uversion.mygamestudio.com. Sostituiscilo con il tuo.
Header richiesti
| Header | Valore |
|---|---|
Authorization | Bearer <jwt> su tutte le route /api/* tranne /api/auth/* e /api/server-info (e /health) |
Content-Type | application/json per i POST/PUT con body JSON. application/octet-stream per gli upload binari (build files). |
Accept | application/json consigliato (il server restituisce JSON per impostazione predefinita) |
Formato di risposta uniforme
Tutte le route rispondono con il seguente involucro:
{
"success": true,
"data": { /* payload */ }
}
In caso di errore:
{
"success": false,
"error": "Permission denied: capability 'manage_users' required"
}
Il campo success è sempre presente e indica se la richiesta è andata a buon fine.
Il campo data è presente in caso di successo. Il campo error è presente in caso di fallimento.
Mai entrambi insieme.
Paginazione
Le route che restituiscono liste potenzialmente lunghe accettano limit e offset
come query param (?limit=20&offset=40). La risposta include total e has_more
per facilitare l'iterazione. Limit massimo: 100.
Rate limiting
Il rate limiting globale è stato rimosso (vedi CLAUDE.md, sezione Security). Solo /api/auth/login
è soggetto a rate limiting: 5 tentativi falliti per username ogni 15 minuti. I tentativi
validi non contano.
Versioning dei token
Ogni JWT contiene un campo tv (token version) che corrisponde a users.token_version nel DB.
Il middleware di auth verifica questo valore a ogni richiesta. Quando l'utente esegue POST /api/auth/logout,
quando un admin ne reimposta la password, o quando un admin ne disattiva l'account, il token_version viene incrementato,
invalidando istantaneamente tutti i token esistenti di quell'utente su tutte le sue macchine.
Curl
Per chiamare l'API da un terminale:
TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}' | jq -r '.data.token')
curl -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories
Autenticazione
POST /api/auth/login
Autentica un utente e restituisce un JWT di 30 giorni.
Body:
{
"username": "alice",
"password": "secret"
}
Response 200:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_at": "2026-06-13T14:30:00Z",
"user": {
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead",
"is_active": true
}
}
}
Errori:
401: credenziali non valide o utente inattivo429: raggiunti 5 tentativi falliti per questo username nella finestra di 15 min
Il server esegue sempre argon2id::verify_password (contro un hash fittizio se l'utente non esiste)
per neutralizzare gli attacchi di timing. La risposta impiega lo stesso tempo che l'utente esista o meno.
POST /api/auth/refresh
Rinnova il JWT senza reinserire la password.
Header: Authorization: Bearer <current_token>. Nessun body.
Response 200: identica a /login con una scadenza posticipata di 30 giorni e un nuovo token.
Errori:
401: token attuale non valido, scaduto, otoken_versionnon corrispondente
POST /api/auth/logout
Invalida tutti i token dell'utente corrente (su tutte le sue macchine) incrementando
users.token_version. L'utente dovrà rieffettuare l'accesso ovunque.
Response 200: { "success": true }.
POST /api/auth/validate
Verifica che il token sia ancora valido. Per i token a meno di 7 giorni dalla scadenza, il server include
un refreshed_token nella risposta (auto-estensione). Il client deve persisterlo al posto
di quello vecchio.
Response 200:
{
"success": true,
"data": {
"valid": true,
"user_id": 12,
"expires_at": "2026-06-13T14:30:00Z",
"refreshed_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
POST /api/auth/register
Crea un account utente (route pubblica, senza autenticazione).
POST /api/auth/change-password
Cambia la password dell'utente corrente. Incrementa token_version, il che invalida tutti i vecchi token.
Repository
GET /api/repositories
Elenca i repository accessibili all'utente corrente (filtrati tramite la tabella dei permessi). Un utente che non ha alcun pattern di permesso su un repo non lo vede.
Query param: limit, offset.
Response 200:
{
"success": true,
"data": [
{
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"created_at": "2026-01-15T10:00:00Z"
},
{
"id": 2,
"name": "shared-assets",
"owner": "acme",
"description": "Shared asset library",
"current_revision": "3a4b5c6d...",
"file_count": 412,
"size_bytes": 980000000,
"created_at": "2026-02-01T09:00:00Z"
}
]
}
GET /api/repositories/{repo_id}
Dettaglio di un repository, incluse le statistiche aggregate.
Response 200:
{
"success": true,
"data": {
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"dedup_ratio": 4.2,
"created_at": "2026-01-15T10:00:00Z",
"last_commit_at": "2026-05-15T08:30:00Z"
}
}
Errori:
403: nessun accesso a questo repo404: repo inesistente
POST /api/repositories
Crea un nuovo repository. Capability create_repos richiesta (admin per impostazione predefinita).
Body:
{
"name": "new-project",
"description": "Optional description"
}
Validazione:
- name: da 1 a 64 caratteri, alfanumerico + trattini + underscore. Univoco per owner.
- description: da 0 a 1024 caratteri. Facoltativa.
Errori:
400: nome non valido o troppo lungo403: capabilitycreate_reposmancante409: esiste già un repo con questo nome per questo owner
File
POST /api/files/{repo_id}/upload-chunks
Carica un batch di chunk binari. I chunk identici (per SHA-256) vengono deduplicati automaticamente.
Il server non ri-memorizza un chunk già presente. La risposta restituisce per ogni chunk il suo hash + dimensione +
dimensione compressa, da usare nel successivo POST /commit.
Body:
{
"chunks": [
{ "data": "<base64-encoded chunk bytes>" },
{ "data": "<base64-encoded chunk bytes>" }
]
}
Response 200:
{
"success": true,
"data": {
"chunks": [
{ "hash": "abc123...", "size": 1048576, "compressed_size": 423152 },
{ "hash": "def456...", "size": 2097152, "compressed_size": 891204 }
]
}
}
Limiti: body max 1 GB (configurabile tramite security.max_body_size_files). Per file più grandi, suddividere in più batch.
POST /api/files/{repo_id}/commit
Crea un commit atomico con una lista di file e i loro chunk. O passano tutti i file, o nessuno.
Body:
{
"message": "Updated main level + hero pose pass",
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"action": "modified",
"chunks": ["abc123...", "def456..."]
},
{
"path": "Content/Characters/NewVillain.uasset",
"action": "added",
"chunks": ["fed789..."]
},
{
"path": "Content/OldAsset.uasset",
"action": "deleted"
}
]
}
Azioni possibili: added, modified, deleted.
Per added e modified, l'array chunks contiene gli hash restituiti
da /upload-chunks. Per deleted, omettere chunks.
Response 200:
{
"success": true,
"data": {
"commit_hash": "7f3a9b1c2d3e4f...",
"files_changed": 3,
"bytes_uploaded": 88080384,
"bytes_deduped": 4194304,
"revision": 47
}
}
Errori:
400: messaggio vuoto, file senza azione, chunk referenziati inesistenti403: manca la capabilitycheckin, o nessun permesso write su uno dei file409: lock già detenuto da un altro utente su un file modificato, o commit concorrente (race condition sullo stesso file)
GET /api/files/{repo_id}/snapshot
Restituisce lo stato completo del repository alla revisione corrente: tutti i file, le loro revisioni e i loro chunk. Usato dal client per le operazioni di clone e sync forzato.
Query param:
revision(facoltativo): snapshot a una revisione passata. Predefinito: HEAD.
Response 200:
{
"success": true,
"data": {
"revision": 47,
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"revision": 12,
"size_bytes": 84934656,
"chunks": ["abc123...", "def456..."]
}
]
}
}
GET /api/files/{repo_id}/content
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 paginata dei commit del repository.
Query param:
limit(da 1 a 100, predefinito 20)offset(predefinito 0)path(facoltativo): filtra per percorso di file (commit che hanno modificato questo file)author(facoltativo): filtra per usernamesince(facoltativo): ISO 8601 timestamp
Response 200:
{
"success": true,
"data": {
"commits": [
{
"hash": "7f3a9b1c...",
"author": "alice",
"author_id": 12,
"date": "2026-05-15T08:30:00Z",
"message": "Fixed lighting in main level",
"files_changed": 1,
"bytes_uploaded": 84934656
}
],
"total": 423,
"limit": 20,
"offset": 0,
"has_more": true
}
}
Sync 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 eliminazioni in locale.GET /api/files/{repo_id}/list: elenco dei file del repo.POST /api/files/{repo_id}/checkout: acquisisce i lock e prepara la modifica.
Lock
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 contiene i fallimenti con la loro motivazione.
Body:
{
"paths": [
"Content/Maps/MainLevel.umap",
"Content/Characters/Hero.uasset"
]
}
Response 200:
{
"success": true,
"data": {
"acquired": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"file_id": 12345,
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"last_heartbeat_at": "2026-05-15T14:30:00Z",
"expires_at": "2026-05-15T15:30:00Z"
}
],
"failed": [
{
"path": "Content/Characters/Hero.uasset",
"reason": "ALREADY_LOCKED",
"lock_holder": "bob",
"lock_acquired_at": "2026-05-15T13:00:00Z"
}
]
}
}
I lock scadono dopo 60 minuti senza heartbeat. Il campo expires_at riflette
questa scadenza. Chiama /heartbeat periodicamente per rinnovare il lock, o /release
per rilasciarlo.
Motivazioni di failed:
ALREADY_LOCKED: un altro utente detiene il lock (i campilock_holderelock_acquired_atsono popolati)PERMISSION_DENIED: nessun permesso write su questo percorsoINVALID_PATH: percorso malformato (assoluto, contiene.., ecc.)
POST /api/locks/{repo_id}/release
Rilascia i lock che possiedi. Body:
{
"paths": ["Content/Maps/MainLevel.umap"],
"force": false
}
force: true richiede la capability force_unlock. L'azione viene registrata nell'audit.
POST /api/locks/{repo_id}/heartbeat
Aggiorna last_heartbeat_at sui tuoi lock. Consigliato ogni 5 minuti per i job CI a lunga esecuzione
che vogliono che gli admin vedano l'attività.
GET /api/locks/{repo_id}/status
Elenca tutti i lock attivi del repository, con join su users e files per restituire direttamente i nomi.
Query param: user (filtra per username), limit, offset.
Response 200:
{
"success": true,
"data": {
"locks": [
{
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"last_heartbeat_at": "2026-05-15T16:00:00Z"
}
],
"total": 1
}
}
Commenti e revisioni
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Elenca i commenti di un commit, in thread (padre → 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 | Elenco degli utenti assegnabili |
POST /api/tasks/{repo_id}/columns | Configura le colonne della bacheca |
Sotto-route anche disponibili: commenti di scheda, link ad assets 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 rispetto al 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 (senza 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 una 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 le build pubblicate. Capability download_builds richiesta per vedere i link di download.
Response 200:
{
"success": true,
"data": [
{
"id": 5,
"version": "0.1.5-nightly",
"config": "Development",
"platform": "Win64",
"published_by": "ci-nightly",
"published_at": "2026-05-15T03:00:00Z",
"size_bytes": 2159829326,
"file_count": 142,
"release_notes": "Nightly build of main branch, 2026-05-15"
}
]
}
GET /api/builds/{repo_id}/{build_id}/file
Scarica un file di una build. Capability download_builds richiesta. Il manifest di una build è disponibile tramite GET /api/builds/{repo_id}/{build_id}/manifest.
Admin
Tutte le route /api/admin/* richiedono che l'utente abbia almeno una capability admin
(a seconda dell'endpoint: manage_users, manage_permissions, manage_rules, ecc.).
Documentazione sintetica qui sotto. La maggior parte segue il pattern REST standard (GET/POST/PUT/DELETE).
| Route | Capability | Descrizione |
|---|---|---|
GET/POST /api/admin/users | manage_users | Elenca / crea utenti |
PUT/DELETE /api/admin/users/{id} | manage_users | Aggiorna / elimina |
POST /api/admin/users/{id}/reset-password | manage_users | Reimposta la password, incrementa token_version |
GET/POST /api/admin/groups | manage_users | Gestione dei gruppi |
GET/POST /api/admin/permissions/{repo_id} | manage_permissions | Regole glob dei permessi per percorso |
GET/POST /api/admin/repositories/{repo_id}/rules | manage_rules | Regole di validazione pre-checkin |
GET/POST /api/admin/repositories/{repo_id}/webhooks | manage_rules | Discord / Slack / Teams / custom |
GET /api/admin/audit | view_all_activity | Audit log filtrato e paginato |
GET /api/admin/stats | view_all_activity | Storage metrics, dedup ratio, crescita |
GET /api/admin/repositories/{repo_id}/gc/preview | admin | Anteprima del GC senza eseguire |
POST /api/admin/repositories/{repo_id}/gc | admin | Avvia il garbage collection |
POST /api/admin/locks/{id}/force-release | force_unlock | Forza lo sblocco di un lock altrui, registrato nell'audit |
Salute
GET /health
Endpoint non autenticato che restituisce 200 OK se il server può 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, la risposta è { "success": false, "error": "..." } con uno status HTTP appropriato.
Il campo error è un messaggio leggibile dall'uomo. Per i client che hanno bisogno di codici
stabili leggibili dalla macchina, effettua il parse del prefisso (es.: "Permission denied: ..." inizia sempre con
"Permission denied").
| HTTP | Significato | Quando |
|---|---|---|
| 400 | Bad request | Parametro non valido, JSON malformato, body 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 percorso / repo |
| 404 | Resource not found | Repo / file / commit / utente inesistente o inaccessibile |
| 409 | Conflict | Lock già detenuto, commit concorrente, vincolo unique violato |
| 413 | Payload too large | Body oltre il limite (1 GB su /files, 8 GB su /builds/upload) |
| 429 | Too many requests | Solo su /auth/login (rate limiting 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 |