Wiki
REST API
Référence complète des endpoints HTTP du serveur uVersion : auth, repos, files, locks, comments, watchlist, modification requests, builds, admin.
Le serveur uVersion expose une API HTTP JSON authentifiée par JWT. Cette page documente l'intégralité
des endpoints utilisés par le client desktop, les plugins éditeur et le CLI uversion.
Vous pouvez les appeler directement pour intégrer uVersion dans votre tooling interne
(dashboard custom, scripts d'audit, webhooks, etc.).
Conventions
Base URL
Tous les chemins documentés sont relatifs à l'URL de votre instance. Les exemples utilisent
https://uversion.mygamestudio.com. Remplacez par la vôtre.
Headers requis
| Header | Valeur |
|---|---|
Authorization | Bearer <jwt> sur toutes les routes /api/* sauf /api/auth/* et /api/server-info (et /health) |
Content-Type | application/json pour les POST/PUT avec body JSON. application/octet-stream pour les uploads binaires (build files). |
Accept | application/json recommandé (le serveur retourne du JSON par défaut) |
Format de réponse uniforme
Toutes les routes répondent avec l'enveloppe suivante :
{
"success": true,
"data": { /* payload */ }
}
En cas d'erreur :
{
"success": false,
"error": "Permission denied: capability 'manage_users' required"
}
Le champ success est toujours présent et indique si la requête a réussi.
Le champ data est présent en cas de succès. Le champ error est présent en cas d'échec.
Jamais les deux ensemble.
Pagination
Les routes qui retournent des listes potentiellement longues acceptent limit et offset
en query params (?limit=20&offset=40). La réponse inclut total et has_more
pour faciliter l'itération. Limit max : 100.
Rate limiting
Le rate limiting global a été retiré (voir CLAUDE.md, section Security). Seul /api/auth/login
est rate-limité : 5 tentatives échouées par username toutes les 15 minutes. Les tentatives
valides ne comptent pas.
Token versioning
Chaque JWT contient un champ tv (token version) qui correspond à users.token_version en DB.
Le middleware d'auth vérifie cette valeur à chaque requête. Quand l'utilisateur fait POST /api/auth/logout,
quand un admin reset son password, ou quand un admin désactive son compte, le token_version est bumpé,
invalidant instantanément tous les tokens existants de cet utilisateur sur toutes ses machines.
Curl
Pour appeler l'API depuis un terminal :
TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}' | jq -r '.data.token')
curl -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories
Authentication
POST /api/auth/login
Authentifie un utilisateur et retourne un JWT 30 jours.
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
}
}
}
Erreurs :
401: credentials invalides ou utilisateur inactif429: 5 tentatives échouées atteintes pour ce username dans la fenêtre 15 min
Le serveur exécute toujours argon2id::verify_password (contre un hash dummy si l'utilisateur n'existe pas)
pour neutraliser les attaques par timing. La réponse prend le même temps qu'un utilisateur existe ou non.
POST /api/auth/refresh
Renouvelle le JWT sans repasser par le password.
Headers : Authorization: Bearer <current_token>. Pas de body.
Response 200 : identique à /login avec une expiration repoussée de 30 jours et un nouveau token.
Erreurs :
401: token actuel invalide, expiré, outoken_versionmismatch
POST /api/auth/logout
Invalide tous les tokens du user courant (sur toutes ses machines) en bumpant
users.token_version. Le user devra se relogger partout.
Response 200 : { "success": true }.
POST /api/auth/validate
Vérifie que le token est encore valide. Pour les tokens à moins de 7 jours d'expirer, le serveur inclut
un refreshed_token dans la réponse (auto-extend). Le client doit le persister à la place
de l'ancien.
Response 200 :
{
"success": true,
"data": {
"valid": true,
"user_id": 12,
"expires_at": "2026-06-13T14:30:00Z",
"refreshed_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
POST /api/auth/register
Crée un compte utilisateur (route publique, sans authentification).
POST /api/auth/change-password
Change le mot de passe de l'utilisateur courant. Bumpe token_version, ce qui invalide tous les anciens tokens.
Repositories
GET /api/repositories
Liste les repositories accessibles par l'utilisateur courant (filtrés via la table de permissions). Un user qui n'a aucun pattern de permission sur un repo ne le voit pas.
Query params : limit, offset.
Response 200 :
{
"success": true,
"data": [
{
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"created_at": "2026-01-15T10:00:00Z"
},
{
"id": 2,
"name": "shared-assets",
"owner": "acme",
"description": "Shared asset library",
"current_revision": "3a4b5c6d...",
"file_count": 412,
"size_bytes": 980000000,
"created_at": "2026-02-01T09:00:00Z"
}
]
}
GET /api/repositories/{repo_id}
Détail d'un repository, incluant les stats agrégées.
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"
}
}
Erreurs :
403: pas d'accès à ce repo404: repo inexistant
POST /api/repositories
Crée un nouveau repository. Capability create_repos requise (admin par défaut).
Body :
{
"name": "new-project",
"description": "Optional description"
}
Validation :
- name : 1 à 64 caractères, alphanumérique + tirets + underscores. Unique par owner.
- description : 0 à 1024 caractères. Optionnel.
Erreurs :
400: nom invalide ou trop long403: capabilitycreate_reposmanquante409: un repo avec ce nom existe déjà pour ce owner
Files
POST /api/files/{repo_id}/upload-chunks
Upload un batch de chunks binaires. Les chunks identiques (par SHA-256) sont dédupliqués automatiquement.
Le serveur ne re-stocke pas un chunk déjà présent. La réponse retourne pour chaque chunk son hash + taille +
taille compressée, à utiliser dans POST /commit suivant.
Body :
{
"chunks": [
{ "data": "<base64-encoded chunk bytes>" },
{ "data": "<base64-encoded chunk bytes>" }
]
}
Response 200 :
{
"success": true,
"data": {
"chunks": [
{ "hash": "abc123...", "size": 1048576, "compressed_size": 423152 },
{ "hash": "def456...", "size": 2097152, "compressed_size": 891204 }
]
}
}
Limites : body max 1 GB (configurable via security.max_body_size_files). Pour des fichiers plus gros, splitter en plusieurs batches.
POST /api/files/{repo_id}/commit
Crée un commit atomique avec une liste de fichiers et leurs chunks. Soit tous les fichiers passent, soit aucun.
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"
}
]
}
Actions possibles : added, modified, deleted.
Pour added et modified, le tableau chunks contient les hashes retournés
par /upload-chunks. Pour deleted, omettre chunks.
Response 200 :
{
"success": true,
"data": {
"commit_hash": "7f3a9b1c2d3e4f...",
"files_changed": 3,
"bytes_uploaded": 88080384,
"bytes_deduped": 4194304,
"revision": 47
}
}
Erreurs :
400: message vide, fichier sans action, chunks référencés inexistants403: pas la capabilitycheckin, ou pas de permission write sur un des fichiers409: lock déjà tenu par un autre user sur un fichier modifié, ou commit en concurrence (race condition sur le même fichier)
GET /api/files/{repo_id}/snapshot
Retourne l'état complet du repository à la révision courante : tous les fichiers, leurs révisions et leurs chunks. Utilisé par le client pour les opérations de clone et sync forcé.
Query params :
revision(optionnel) : snapshot à une révision passée. Défaut : 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
Télécharge le contenu d'un fichier à une révision donnée (le serveur réassemble les chunks). Utilisé par le clone et le sync.
Query params :
path: chemin du fichier (relatif à la racine du repo)revision(optionnel) : numéro de révision. Défaut : dernière.
Response 200 : le contenu binaire du fichier.
GET /api/files/{repo_id}/history
Historique paginé des commits du repository.
Query params :
limit(1 à 100, défaut 20)offset(défaut 0)path(optionnel) : filtre par chemin de fichier (commits qui ont modifié ce fichier)author(optionnel) : filtre par usernamesince(optionnel) : 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 incrémental
Endpoints utilisés par le client desktop pour synchroniser efficacement :
GET /api/files/{repo_id}/sync: fichiers changés depuis une révision, pour un sync delta.GET /api/files/{repo_id}/deletions: fichiers supprimés côté serveur, pour propager les suppressions en local.GET /api/files/{repo_id}/list: liste des fichiers du repo.POST /api/files/{repo_id}/checkout: acquiert les locks et prépare l'édition.
Locks
POST /api/locks/{repo_id}/acquire
Acquiert des locks sur une liste de chemins. Les acquisitions sont indépendantes : la liste acquired
contient les succès, la liste failed contient les échecs avec leur raison.
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"
}
]
}
}
Les locks expirent après 60 minutes sans heartbeat. Le champ expires_at reflète
cette échéance. Appelez /heartbeat périodiquement pour renouveler le lock, ou /release
pour le libérer.
Raisons de failed :
ALREADY_LOCKED: un autre user détient le lock (les champslock_holderetlock_acquired_atsont peuplés)PERMISSION_DENIED: pas de permission write sur ce pathINVALID_PATH: chemin malformé (absolu, contient.., etc.)
POST /api/locks/{repo_id}/release
Release des locks que vous possédez. Body :
{
"paths": ["Content/Maps/MainLevel.umap"],
"force": false
}
force: true requiert la capability force_unlock. L'action est auditée.
POST /api/locks/{repo_id}/heartbeat
Update last_heartbeat_at sur vos locks. Recommandé toutes les 5 minutes pour les long-running jobs CI
qui veulent que les admins voient l'activité.
GET /api/locks/{repo_id}/status
Liste tous les locks actifs du repository, joins sur users et files pour retourner directement les noms.
Query params : user (filtre par 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
}
}
Comments & Reviews
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Liste les commentaires d'un commit, threadés (parent → enfants).
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
Crée un commentaire. file_path est optionnel (commentaire de commit vs. de fichier). parent_id pour répondre à un thread existant.
Body :
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
Submit un review sur un commit. Capability approve_changes requise.
Body :
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
Status accepté : approved, changes_requested, pending.
Watchlist
GET /api/watchlist/{repo_id}/watchlist
Liste les patterns de watch de l'utilisateur courant sur ce 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
Ajoute un pattern de watch. Body :
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
Les événements possibles : commit (un commit a modifié un fichier matchant),
lock (un lock a été acquis), review (un review a été posté).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
Retire un pattern de watch. Retourne { "success": true }.
Production board (tasks)
L'ancienne API modification-requests a été retirée : le board de production l'absorbe
(les demandes deviennent des cartes, origin='request'). Les endpoints sont nichés sous
/api/tasks/{repo_id}.
| Route | Description |
|---|---|
GET /api/tasks/{repo_id}/board | Board complet : colonnes + cartes |
POST /api/tasks/{repo_id}/tasks | Crée une carte |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | Met à jour / supprime une carte |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | Assigne des utilisateurs à une carte |
GET /api/tasks/{repo_id}/assignable-users | Liste des utilisateurs assignables |
POST /api/tasks/{repo_id}/columns | Configure les colonnes du board |
Sous-routes également disponibles : commentaires de carte, liens vers assets et commits, et pièces
jointes (avec image de couverture). La configuration des colonnes requiert la capability
manage_board (admin / lead).
Builds
POST /api/builds/{repo_id}/upload?hash=<sha256>
Upload un fichier de build vers <storage_path>/builds/<hash>.
Le serveur vérifie le SHA-256 fourni en query param contre le contenu reçu. Si le hash existe déjà
côté serveur, retourne 200 immédiatement sans recopier (dédup côté build storage).
Headers : Content-Type: application/octet-stream.
Body : binaire brut (pas de JSON wrapping).
Limites : 8 GB par fichier.
Erreurs :
400: hash query param manquant ou malformé, ou SHA-256 calculé != hash fourni413: fichier > 8 GB
POST /api/builds/{repo_id}/publish
Enregistre le manifest d'un build après upload de tous ses fichiers. Capability publish_builds requise.
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}
Liste les builds publiés. Capability download_builds requise pour voir les liens de download.
Response 200 :
{
"success": true,
"data": [
{
"id": 5,
"version": "0.1.5-nightly",
"config": "Development",
"platform": "Win64",
"published_by": "ci-nightly",
"published_at": "2026-05-15T03:00:00Z",
"size_bytes": 2159829326,
"file_count": 142,
"release_notes": "Nightly build of main branch, 2026-05-15"
}
]
}
GET /api/builds/{repo_id}/{build_id}/file
Télécharge un fichier d'un build. Capability download_builds requise. Le manifest d'un build est disponible via GET /api/builds/{repo_id}/{build_id}/manifest.
Admin
Toutes les routes /api/admin/* requièrent que l'utilisateur ait au moins une capability admin
(selon l'endpoint : manage_users, manage_permissions, manage_rules, etc.).
Documentation succincte ci-dessous. La plupart suivent le pattern REST standard (GET/POST/PUT/DELETE).
| Route | Capability | Description |
|---|---|---|
GET/POST /api/admin/users | manage_users | Liste / crée des utilisateurs |
PUT/DELETE /api/admin/users/{id} | manage_users | Met à jour / supprime |
POST /api/admin/users/{id}/reset-password | manage_users | Reset le password, bumpe token_version |
GET/POST /api/admin/groups | manage_users | Gestion des groupes |
GET/POST /api/admin/permissions/{repo_id} | manage_permissions | Règles glob de permissions par chemin |
GET/POST /api/admin/repositories/{repo_id}/rules | manage_rules | Règles de validation pré-checkin |
GET/POST /api/admin/repositories/{repo_id}/webhooks | manage_rules | Discord / Slack / Teams / custom |
GET /api/admin/audit | view_all_activity | Audit log filtré et paginé |
GET /api/admin/stats | view_all_activity | Storage metrics, dedup ratio, croissance |
GET /api/admin/repositories/{repo_id}/gc/preview | admin | Preview du GC sans exécuter |
POST /api/admin/repositories/{repo_id}/gc | admin | Lance le garbage collection |
POST /api/admin/locks/{id}/force-release | force_unlock | Force-unlock un lock tiers, audité |
Health
GET /health
Endpoint non authentifié retournant 200 OK si le serveur peut accéder à la base.
Utilisé pour les health checks de load balancer ou de monitoring (Prometheus blackbox, Datadog synthetic, etc.).
Response 200 : OK (text/plain).
Response 503 : si la base est injoignable.
GET /api/server-info
Endpoint public non authentifié retournant les métadonnées du serveur (version, etc.).
Comme /health, il n'est pas protégé par le middleware d'auth.
Error format
En cas d'erreur, la réponse est { "success": false, "error": "..." } avec un status HTTP approprié.
Le champ error est un message lisible pour humain. Pour les clients qui ont besoin de codes
stables machine-readable, parsez le préfixe (ex: "Permission denied: ..." commence toujours par
"Permission denied").
| HTTP | Signification | Quand |
|---|---|---|
| 400 | Bad request | Paramètre invalide, JSON malformé, body trop gros (avant la limite hard 413) |
| 401 | Not authenticated | Token absent, expiré, signature invalide, ou token_version mismatch |
| 403 | Permission denied | Capability manquante, ou pas de permission sur le path / repo |
| 404 | Resource not found | Repo / fichier / commit / user inexistant ou inaccessible |
| 409 | Conflict | Lock déjà tenu, commit en concurrence, contrainte unique violée |
| 413 | Payload too large | Body au-delà de la limite (1 GB sur /files, 8 GB sur /builds/upload) |
| 429 | Too many requests | Uniquement sur /auth/login (rate limiting per-user) |
| 500 | Internal server error | DB error, IO error. Loggé côté serveur, joindre le timestamp si vous reportez le bug. |
| 503 | Service unavailable | Base injoignable (health check) ou migration en cours |