uVersion
Deutsch
Herunterladen →

Wiki

REST API

Referenz der HTTP-Endpunkte des uVersion-Servers: Authentifizierung, Repositories, Dateien, Sperren, Kommentare, Beobachtungsliste, Produktions-Board, Builds, Administration.

Der uVersion-Server stellt eine JSON-HTTP-API bereit. Aufrufe werden mit einem JWT (JSON Web Token) authentifiziert, also dem Sitzungstoken, das Ihnen der Server bei der Anmeldung aushändigt und das Sie danach bei jeder Anfrage zurücksenden. Diese Seite dokumentiert die Endpunkte, die vom Desktop-Client, den Editor-Plugins und der uversion-CLI verwendet werden. Sie können sie direkt aufrufen, um uVersion in Ihr internes Tooling zu integrieren (eigenes Dashboard, Audit-Skripte, Webhooks usw.).

Sie deckt nicht die gesamte API ab: Es gibt mehrere Routen-Familien, die hier nicht beschrieben sind. Die Liste finden Sie im Abschnitt Nicht abgedeckte Bereiche.

Konventionen

Basis-URL

Jeder dokumentierte Pfad ist relativ zur URL Ihrer Instanz. Die Beispiele verwenden https://uversion.mygamestudio.com. Ersetzen Sie sie durch Ihre eigene.

Erforderliche Header

HeaderWert
AuthorizationBearer <jwt> auf jeder /api/*-Route außer /api/auth/* und /api/server-info (und /health)
Content-Typeapplication/json für POST/PUT mit JSON-Body. application/octet-stream für binäre Uploads (Build-Dateien).
Acceptapplication/json empfohlen (der Server liefert standardmäßig JSON)

Zwei Antwortformate, und Sie müssen wissen, welches Sie lesen

Der Server hat nicht ein Antwortformat, sondern zwei, und sie zu verwechseln ist der teuerste Fehler für alle, die eine Integration beginnen.

1. Authentifizierte Routen (alle /api/* außer /api/auth/*) antworten mit einer Hülle:

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

Im Fehlerfall antworten dieselben Routen:

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

Das Feld success ist immer vorhanden. data ist bei Erfolg vorhanden, error bei einem Fehlschlag, niemals beide zusammen.

2. Die Authentifizierungsrouten (/api/auth/login, /refresh, /validate, /logout, /register, /change-password) verwenden diese Hülle nicht. Sie liefern das nackte Objekt, ohne success oder data:

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

Und ihre Fehler sind ein Objekt mit einem einzigen Feld, ohne success:

{ "error": "Invalid credentials" }

Praktische Folge: Bei /api/auth/login wird das Token unter .token gelesen und nicht unter .data.token. Ein Skript, das .data.token abfragt, erhält null ohne sichtbaren Fehler.

Schließlich liefert GET /api/files/{repo_id}/content weder das eine noch das andere: Es ist der rohe Binärinhalt der Datei, unverändert.

Paginierung

Es gibt keine gemeinsame Paginierungskonvention: Jede Route hat ihre eigene oder gar keine. Setzen Sie weder limit noch total noch has_more voraus.

RouteAkzeptierte ParameterAntwortform
GET /api/files/{repo_id}/history limit (Standard 50), offset (Standard 0), path Flaches Array von Commits. Kein total, kein has_more: Das Ende ist erreicht, wenn das Array weniger Elemente enthält als limit.
GET /api/repositories nur include_inactive (und nur für einen Super-Administrator wird es berücksichtigt) Flaches Array. Weder limit noch offset werden gelesen.
GET /api/locks/{repo_id}/status Keine Flaches Array aller Sperren des Repositorys.
GET /api/files/{repo_id}/snapshot commit_hash, erforderlich Flaches Array von Dateien.
GET /api/admin/audit page und per_page, nicht limit/offset, dazu Filter (user_id, action, entity_type, from, to) Nur für den Super-Administrator. Ein gutes Beispiel für die fehlende gemeinsame Konvention: Es ist die einzige Route, die nach Seitennummer paginiert.

Gehen Sie bei jeder hier nicht aufgeführten Route davon aus, dass sie ihr gesamtes Ergebnis in einem einzigen Aufruf liefert.

Ratenbegrenzung

Es gibt keine allgemeine Ratenbegrenzung: Ein Unreal-Projekt hat Tausende von Dateien und Massenoperationen (Sperren erwerben, freigeben) würden ständig eine Drosselung auslösen. Sie können die API daher in großem Umfang aufrufen.

Nur eine einzige Route ist begrenzt, /api/auth/login: 5 fehlgeschlagene Versuche pro Benutzername alle 15 Minuten. Erfolgreiche Anmeldungen werden nicht gezählt.

Widerruf von Token

Jedes Sitzungstoken trägt ein Feld tv (token version), das die Spalte users.token_version in der Datenbank widerspiegelt. Der Server vergleicht beide bei jeder Anfrage. Ein Aufruf von POST /api/auth/logout, ein Zurücksetzen des Passworts durch einen Administrator oder die Deaktivierung eines Kontos erhöht diesen Wert, was sofort alle bestehenden Token dieses Benutzers ungültig macht, auf allen seinen Rechnern.

Curl

Um die API von einem Terminal aus aufzurufen. Beachten Sie das .token: Die Antwort von /api/auth/login ist das nackte Objekt, es gibt kein .data, durch das man navigieren müsste.

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

# Die authentifizierten Routen hingegen verwenden sehr wohl die Hülle:
curl -s -H "Authorization: Bearer $TOKEN" \
  https://uversion.mygamestudio.com/api/repositories | jq '.data'

Authentifizierung

Erinnerung: Keine Route in diesem Abschnitt verwendet die {"success", "data"}-Hülle. Sie liefern das nackte Objekt, und ihre Fehler haben die Form {"error": "..."}.

POST /api/auth/login

Authentifiziert einen Benutzer und liefert ein 30 Tage gültiges Sitzungstoken.

Body:

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

Response 200:

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

Es gibt kein Feld expires_at: Die Gültigkeitsdauer wird aus dem Token selbst gelesen oder aus der Serverkonfiguration abgeleitet (standardmäßig 30 Tage).

Ignorieren Sie must_change_password nicht. Dieses Feld ist true, wenn das Konto noch mit einem temporären Passwort läuft: dem, das der Installer für das anfängliche Administratorkonto generiert hat, oder dem, das ein Administrator gerade bei einer Zurücksetzung gesetzt hat. Ein Client, der es nicht beachtet, belässt den Benutzer unbegrenzt auf diesem provisorischen Passwort. Das erwartete Verhalten ist, sofort zu POST /api/auth/change-password weiterzuleiten, vor jeder anderen Aktion.

Fehler:

  • 401: ungültige Anmeldedaten oder deaktiviertes Konto
  • 429: 5 fehlgeschlagene Versuche für diesen Benutzernamen im 15-Minuten-Fenster erreicht

Der Server führt immer die Argon2id-Passwortprüfung aus, auch gegen einen Dummy-Hash, wenn das Konto nicht existiert. Die Antwort braucht daher in beiden Fällen gleich lange, was verhindert, die Existenz eines Kontos durch Zeitmessung der Anfragen zu erraten.

POST /api/auth/refresh

Erneuert das Sitzungstoken, ohne erneut das Passwort einzugeben.

Header: Authorization: Bearer <aktuelles_Token>. Kein Body.

Response 200: genau dieselbe Form wie /login (token, user, must_change_password), mit einem frischen Token.

Fehler:

  • 401: ungültiges oder abgelaufenes Token, deaktiviertes Konto oder veraltetes token_version

POST /api/auth/logout

Macht alle Token des aktuellen Benutzers ungültig, auf jedem Rechner, indem users.token_version erhöht wird. Er muss sich überall neu anmelden: Desktop-Client, Editor-Plugin und CLI inbegriffen.

Response 200: { "logged_out": true }.

POST /api/auth/validate

Prüft, ob ein Token noch gültig ist, und liefert den zugehörigen Benutzer zurück. Die Prüfung umfasst auch is_active und token_version, sodass ein widerrufenes Token abgelehnt wird, selbst wenn es noch nicht abgelaufen ist.

Achten Sie auf den Body: Es ist kein Objekt, es ist eine nackte JSON-Zeichenkette, also das Token in doppelten Anführungszeichen.

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

Response 200: ein nacktes Benutzerobjekt.

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

Es gibt kein valid, kein expires_at, kein refreshed_token: Die Gültigkeit wird am HTTP-Code (200 oder 401) abgelesen, und die Erneuerung läuft über /api/auth/refresh, niemals über diese Route.

POST /api/auth/register

Diese Route lehnt standardmäßig ab. Die offene Registrierung ist deaktiviert, sofern der Betreiber sie nicht ausdrücklich in der Serverkonfiguration aktiviert hat; andernfalls ist die Antwort 403 mit {"error": "Open registration is disabled; contact your administrator"}.

Im Normalbetrieb werden Konten über die Administration erstellt: POST /api/admin/users oder der Reiter „Benutzer" des Administrationsbereichs. Bauen Sie keine Integration, die von /register abhängt.

POST /api/auth/change-password

Ändert das Passwort des aktuellen Benutzers. Erhöht token_version, was alle vorherigen Token ungültig macht, einschließlich desjenigen, das gerade für den Aufruf verwendet wurde.

Repositories

GET /api/repositories

Listet die für den aktuellen Benutzer zugänglichen Repositories auf, gefiltert nach der Berechtigungstabelle. Ein Benutzer ohne Berechtigungsregel für ein Repository sieht es nicht. Die Rollen admin und lead sehen alle Repositories.

Query-Parameter: nur einer, include_inactive (Boolean, Standard false), und er wird nur für einen Super-Administrator berücksichtigt. Es gibt weder limit noch offset: Die Route liefert die gesamte Liste.

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

Das sind die einzigen zurückgegebenen Felder. Insbesondere gibt es kein owner, kein current_revision, kein file_count, kein size_bytes, kein last_commit_at: Ein Repository hat im Sinne der API keinen Eigentümer, und die Volumendaten erhält man über die Administrations-Statistikrouten.

GET /api/repositories/{repo_id}

Details eines Repositorys. Dieselbe Objektform wie in der Liste, mit denselben Feldern.

Fehler:

  • 403: kein Zugriff auf dieses Repository
  • 404: Repository existiert nicht

POST /api/repositories

Erstellt ein Repository. Nur für den Super-Administrator, also die Rolle admin und nur sie. Das ist keine Capability: Die Prüfung bezieht sich direkt auf die Rolle, sodass ein project_admin oder ein lead ein 403 erhält. Es gibt im Produkt keine Capability create_repos.

Body:

{
  "name": "new-project",
  "description": "Optionale Beschreibung"
}

Validierung:

  • name: 1 bis 255 Zeichen. Der Server erzwingt keine Zeichensatz-Beschränkung. Der Speicherpfad auf der Festplatte wird durch Normalisierung des Namens abgeleitet.
  • description: optional.

Die Eindeutigkeit bezieht sich auf den Namen allein, serverweit. Es gibt keinen Begriff von Eigentümer, also auch keine Eindeutigkeit „pro Eigentümer".

Fehler:

  • 400: leerer Name oder über 255 Zeichen
  • 403: der Aufrufer hat nicht die Rolle admin
  • 409: ein Repository trägt diesen Namen bereits

Dateien

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

Sendet eine ganze Datei, nicht eine Liste von Teilen. Der Routenname ist irreführend: Es ist der Server, der die Datei in Blöcke (die chunks) zerlegt, nicht Sie. Ein Aufrufer muss diese Zerlegung nie selbst vornehmen.

Identische Blöcke, erkannt an ihrem SHA-256-Hash, werden dedupliziert: Ein bereits auf dem Server vorhandener Block wird kein zweites Mal gespeichert, unabhängig von der Datei oder dem Repository, aus dem er stammt. Das ist der Grund, warum eine große, nur geringfügig geänderte Binärdatei fast keinen Speicherplatz kostet.

Body: ein Objekt mit einem einzigen Feld, das die vollständige, in base64 codierte Datei enthält.

{
  "data": "<die gesamte Datei, base64-codiert>"
}

Jede andere Form, insbesondere ein chunks-Array, wird vom Server abgelehnt.

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

Das chunks-Array ist unverändert zu übernehmen, vollständige Objekte inbegriffen, im nachfolgenden POST /commit. chunks_stored zählt die tatsächlich auf die Festplatte geschriebenen Blöcke und chunks_deduplicated diejenigen, die bereits vorhanden waren.

Berechtigung: Schreibzugriff auf das Repository ist erforderlich, andernfalls 403.

Grenzen: Anfragebody auf 1 GB begrenzt (einstellbar über security.max_body_size_files). Eine Datei, die größer als diese Grenze ist, kann nicht über diese Route übertragen werden.

POST /api/files/{repo_id}/commit

Erstellt einen atomaren Commit aus einer Liste von Dateien und ihren Blöcken. Entweder gehen alle Dateien durch oder keine.

Die einzigen drei akzeptierten Werte für action sind add, modify und delete

Nicht added, nicht modified, nicht deleted.

Das ist kein bloßes Formdetail. Der Server prüft wörtlich action == "delete" und behandelt alles andere als Hinzufügen oder Ändern. "deleted" zu senden löscht also gar nichts: Die Datei geht in den Schreibzweig, mit einem leeren chunks-Array, und der Server verzeichnet eine Revision mit leerem Inhalt. Es wird kein Fehler ausgelöst. Die Datei bleibt vorhanden, ihre letzte Version wird durch Leere überschrieben, und der Verlust zeigt sich erst beim nächsten sync einer anderen Person.

Body:

{
  "message": "Updated main level + hero pose pass",
  "commit_hash": "optional: um mehrere Stapel unter einem einzigen Commit zusammenzufassen",
  "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 ist nicht optional, auch bei einem delete. Das Feld muss vorhanden sein: Es wegzulassen lässt die Deserialisierung der gesamten Anfrage scheitern. Für eine Löschung senden Sie ein leeres Array.

Das Array enthält Objekte, keine Zeichenketten: Übernehmen Sie unverändert die von /upload-chunks zurückgegebenen Einträge {hash, offset, size, compressed_size}. Jeder hash muss ein hexadezimaler Digest aus 64 Zeichen sein, sonst wird der gesamte Commit mit 400 abgelehnt.

Das Feld commit_hash auf oberster Ebene ist optional. Es dient dazu, mehrere aufeinanderfolgende Aufrufe von ein und demselben Commit tragen zu lassen, was der Desktop-Client tut, wenn er einen großen Upload in Stapel zerlegt. Weggelassen, berechnet der Server einen.

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

Das sind die einzigen zurückgegebenen Felder. Es gibt kein files_changed, kein bytes_uploaded, kein bytes_deduped, kein revision. Beachten Sie, dass revision_number ein Zähler pro Datei ist, keine Versionsnummer des Repositorys: Es ist commit_hash, der den Commit identifiziert, und den müssen Sie aufbewahren, um einen Zustand zu referenzieren.

Fehler:

  • 400: fehlerhafter Body, ungültiger Block-Digest (erwartet: 64 hexadezimale Zeichen), fehlendes Feld chunks
  • 403: keine Schreibberechtigung für einen der Pfade
  • 409: Sperre von jemand anderem auf einer geänderten Datei gehalten, oder gleichzeitiger Commit auf derselben Datei

GET /api/files/{repo_id}/snapshot

Liefert den Zustand des Repositorys, wie er zum Zeitpunkt eines bestimmten Commits war: die Liste der vorhandenen Dateien, mit ihrer Revisionsnummer und ihrer Größe. Wird vom Desktop-Client für das Klonen und für die erzwungene Synchronisierung verwendet.

Query-Parameter:

  • commit_hash: erforderlich. Es ist der Hash des Commits, der als Referenzpunkt dient. Ohne diesen Parameter wird die Anfrage abgelehnt, und es gibt keinen Standardwert „letzter Zustand".

Es gibt keinen Parameter revision. Der Snapshot wird per Commit-Hash angefordert, nie per Nummer. Ein unbekannter Commit antwortet 404.

Response 200: ein flaches Array, ohne umschließendes Objekt.

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

Die Antwort enthält nicht die Blocklisten. Um einen Inhalt abzurufen, gehen Sie über GET /api/files/{repo_id}/content, das die Datei serverseitig wieder zusammensetzt.

GET /api/files/{repo_id}/content

Lädt den Inhalt einer Datei bei einer bestimmten Revision herunter (der Server setzt die Chunks wieder zusammen). Wird von Klon und Sync verwendet.

Query-Parameter:

  • path: Dateipfad (relativ zum Repo-Stamm)
  • revision (optional): Revisionsnummer. Standard: letzte.

Response 200: der Binärinhalt der Datei.

GET /api/files/{repo_id}/history

Commit-Verlauf des Repositorys, vom neuesten zum ältesten.

Query-Parameter:

  • limit: Anzahl der Commits, Standard 50
  • offset: Standard 0
  • path (optional): behält nur die Commits, die diese Datei berührt haben

Das sind die einzigen drei gelesenen Parameter. Es gibt weder author noch since: Ein unbekannter Parameter wird stillschweigend ignoriert, was eine Antwort ergibt, die plausibel, aber ungefiltert ist. Filtern Sie nach Autor oder Datum auf der Aufruferseite.

Response 200: ein flaches Array von Commits.

{
  "success": true,
  "data": [
    {
      "commit_hash": "7f3a9b1c...",
      "message": "Fixed lighting in main level",
      "author": "alice",
      "created_at": "2026-05-15T08:30:00Z",
      "files": [
        {
          "path": "Content/Maps/MainLevel.umap",
          "revision_number": 12,
          "file_size": 84934656
        }
      ]
    }
  ]
}

Es gibt kein total, kein has_more, keine Wiederholung von limit und offset. Um den gesamten Verlauf zu durchlaufen, erhöhen Sie offset, bis Sie weniger Elemente als limit erhalten.

Jeder Commit trägt direkt die Liste der berührten Dateien. Ein Eintrag, dessen Revision eine Löschung ist, wird als solcher markiert und hat keinen Inhalt zum Herunterladen.

Inkrementeller Sync

Endpunkte, die der Desktop-Client zur effizienten Synchronisierung verwendet:

  • GET /api/files/{repo_id}/sync: seit einer Revision geänderte Dateien, für einen Delta-Sync.
  • GET /api/files/{repo_id}/deletions: serverseitig gelöschte Dateien, um Löschungen lokal zu propagieren.
  • GET /api/files/{repo_id}/list: Liste der Dateien des Repos.
  • POST /api/files/{repo_id}/checkout: erwirbt die Sperren und bereitet die Bearbeitung vor.

Sperren

Sperren laufen niemals ab

Eine Sperre wird gehalten, bis sie ausdrücklich freigegeben wird: durch einen checkin, durch einen revert oder durch eine erzwungene Entsperrung durch einen Administrator. Es gibt kein automatisches Ablaufen, weder nach einer Stunde noch nach einem Monat.

Das Feld expires_at existiert nur, weil die entsprechende Datenbankspalte keinen leeren Wert akzeptiert. Der Server schreibt einen Sentinelwert mit hundert Jahren hinein: Eine Sperre, die heute genommen wird, zeigt einen Ablauf um etwa 2126 an. Bauen Sie nichts auf diesem Feld auf, und zeigen Sie dieses Datum keinem Benutzer an.

Der heartbeat verlängert also nichts. Es ist ein Überwachungssignal, dessen einziger Zweck darin besteht, Administratoren zu zeigen, welche Sperren noch aktiv genutzt werden.

POST /api/locks/{repo_id}/acquire

Erwirbt Sperren auf einer Liste von Pfaden. Die Erwerbungen sind unabhängig: Die Liste acquired enthält die Erfolge, die Liste failed die Fehlschläge mit ihrem Grund.

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

Der Ablauf 2126 in diesem Beispiel ist kein Tippfehler: Es ist der oben beschriebene Sentinelwert. Die Sperre ist dauerhaft.

Fehlschlaggründe. Das Feld reason ist ein englischer Satz, der angezeigt werden soll, kein stabiler Code. Schreiben Sie keine Logik, die diese Zeichenkette vergleicht, und „parsen" Sie ihr Präfix nicht: Sie kann von einer Version zur nächsten umformuliert werden. Die derzeit erzeugten Werte sind:

reasonBedeutunglocked_by
File is lockedJemand anderes hält die Sperre bereitsDer Name der Person
No write permissionKeine Schreibberechtigung für diesen Pfadnull
Failed to create lockDas Setzen der Sperre schlug in der Datenbank fehlnull
Database errorDatenbankfehler auf diesem Pfadnull

Ein ungültiger Pfad (absolut, mit .., leer, über 4096 Zeichen oder mit einem Nullbyte) erzeugt keinen Eintrag in failed: Er lässt die gesamte Anfrage scheitern.

POST /api/locks/{repo_id}/release

Gibt Sperren frei, die Sie halten. Body:

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

Es gibt kein Feld force auf dieser Route. Eines hinzuzufügen hat keine Wirkung: Der Server ignoriert ihm unbekannte Felder, die Anfrage gelingt, und die fremde Sperre bleibt bestehen. Ein Integrator, der sich darauf verlässt, glaubt, die Datei freigegeben zu haben, obwohl nicht.

Um die Sperre einer anderen Person zu entfernen, ist die einzige Route POST /api/admin/locks/{lock_id}/force-release. Sie ist der Administration des betroffenen Repositorys vorbehalten, und der Vorgang wird im Audit-Protokoll vermerkt. Sie nimmt die Sperr-Kennung entgegen, die Sie über GET /api/locks/{repo_id}/status erhalten.

POST /api/locks/{repo_id}/heartbeat

Aktualisiert den Aktivitätszeitstempel Ihrer Sperren. Das verlängert nichts, da nichts abläuft: Es ist ein Überwachungssignal, das einem Administrator erlaubt, eine noch genutzte Sperre von einer vergessenen zu unterscheiden.

Body: ein JSON-Array von Pfaden, direkt, ohne umschließendes Objekt.

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

Ungültige Pfade werden einzeln ignoriert, statt den ganzen Stapel scheitern zu lassen, damit ein veralteter Überwachungsrest die anderen nicht blockiert.

GET /api/locks/{repo_id}/status

Listet alle Sperren des Repositorys auf, mit dem Dateinamen und dem Namen der Person, die sie hält.

Query-Parameter: keine. Es gibt keinen Filter user, kein limit, kein offset. Die Route liefert die Gesamtheit der Sperren des Repositorys, zu filtern auf der Aufruferseite.

Response 200: ein flaches Array, ohne umschließendes Objekt und ohne 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"
    }
  ]
}

Das Feld id ist dasjenige, das an POST /api/admin/locks/{lock_id}/force-release zu übergeben ist.

Kommentare & Reviews

GET /api/comments/{repo_id}/comments?commit_hash=<hash>

Listet die Kommentare eines Commits auf, verschachtelt (Eltern → Kinder).

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

Erstellt einen Kommentar. file_path ist optional (Commit-Kommentar vs. Datei-Kommentar). parent_id, um auf einen bestehenden Thread zu antworten.

Body:

{
  "commit_hash": "7f3a9b1c...",
  "file_path": "Content/Maps/MainLevel.umap",
  "body": "LGTM, ship it",
  "parent_id": null
}

POST /api/comments/{repo_id}/reviews

Reicht ein Review zu einem Commit ein. Capability approve_changes erforderlich.

Body:

{
  "commit_hash": "7f3a9b1c...",
  "status": "approved",
  "comment": "Lighting looks great, approving"
}

Akzeptierter Status: approved, changes_requested, pending.

Beobachtungsliste

GET /api/watchlist/{repo_id}/watchlist

Listet die Beobachtungsmuster des aktuellen Benutzers für dieses Repository auf.

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

Fügt ein Beobachtungsmuster hinzu. Body:

{
  "pattern": "Content/Characters/Hero/**",
  "notify_on": ["commit", "lock"]
}

Die möglichen Ereignisse: commit (ein Commit hat eine passende Datei geändert), lock (eine Sperre wurde erworben), review (ein Review wurde gepostet).

DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}

Entfernt ein Beobachtungsmuster. Liefert { "success": true }.

Produktions-Board (Tasks)

Die alte API modification-requests wurde entfernt: Das Produktions-Board absorbiert sie (Anfragen werden zu Karten, origin='request'). Die Endpunkte sind unter /api/tasks/{repo_id} verschachtelt.

RouteBeschreibung
GET /api/tasks/{repo_id}/boardVollständiges Board: Spalten + Karten
POST /api/tasks/{repo_id}/tasksErstellt eine Karte
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}Aktualisiert / löscht eine Karte
PUT /api/tasks/{repo_id}/tasks/{task_id}/assigneesWeist einer Karte Benutzer zu
GET /api/tasks/{repo_id}/assignable-usersListe der zuweisbaren Benutzer
POST /api/tasks/{repo_id}/columnsKonfiguriert die Spalten des Boards

Ebenfalls verfügbare Unterrouten: Kartenkommentare, Verknüpfungen zu Assets und Commits sowie Anhänge (mit Titelbild). Die Konfiguration der Spalten erfordert die Capability manage_board (admin / lead).

Builds

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

Lädt eine Build-Datei nach <storage_path>/builds/<hash> hoch. Der Server prüft den im Query-Parameter angegebenen SHA-256 gegen den empfangenen Inhalt. Wenn der Hash bereits serverseitig existiert, liefert er sofort 200 zurück, ohne erneut zu kopieren (Dedup auf der Build-Storage-Seite).

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

Body: rohe Binärdaten (kein JSON-Wrapping).

Grenzen: 8 GB pro Datei.

Fehler:

  • 400: Hash-Query-Parameter fehlt oder ist fehlerhaft, oder berechneter SHA-256 != angegebener Hash
  • 413: Datei > 8 GB

POST /api/builds/{repo_id}/publish

Registriert das Manifest eines Builds, nachdem alle seine Dateien hochgeladen wurden. Capability publish_builds erforderlich.

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}

Listet die veröffentlichten Builds dieses Projekts auf.

Der Zugriff auf Builds wird Projekt für Projekt vergeben. Die Capability download_builds genügt nicht: Da sie serverweit ist, sagt sie nur, dass das Konto kein bloßer Zuschauer ist. Zusätzlich braucht es entweder Zugriff auf das Repository oder eine explizite Freigabe für die Builds dieses Projekts, die sein Administrator über /api/admin/repositories/{repo_id}/build-access erteilt. Ein Projektadministrator greift automatisch auf die von ihm verwalteten zu, ein Super-Administrator auf alle.

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

Lädt eine Datei eines Builds herunter. Capability download_builds erforderlich. Das Manifest eines Builds ist über GET /api/builds/{repo_id}/{build_id}/manifest verfügbar.

Admin

Was diese Routen tatsächlich absichert

Anders als man erwarten könnte, sind die Routen /api/admin/* nicht jeweils durch eine Capability abgesichert. Sie durchlaufen eine von vier Prüfungen, die fast alle auf die Rolle abzielen. Eine capability ist ein benanntes Recht, das an eine Rolle geknüpft ist; der wichtige Punkt ist, dass sie auf dem gesamten Server gilt, denn sie trägt keinen Bezug zu einem Repository. Sie kann daher niemals dazu dienen, jemanden auf ein Projekt zu beschränken.

PrüfungGilt fürWozu sie dient
Super-Administrator Die Rolle admin, und nur sie Alles, was für den gesamten Server gilt: Konten, Gruppen, Audit-Protokoll, globale Statistiken, Lizenz, Serveraktualisierung.
Administrator dieses Repositorys admin oder ein project_admin, der genau dieses Repository verwaltet Alles, was projektspezifisch ist: Berechtigungen, Validierungsregeln, Webhooks, Build-Zugriff, erzwungene Entsperrung, Garbage Collection.
Serverweite Capability Jede Rolle, welche die benannte Capability besitzt Einige übergreifende Routen. Achtung: Die Capability gilt für alle Repositories, das ist ihre Natur. Ein project_admin, der keine besitzt, wird stattdessen nur auf den von ihm verwalteten Repositories zugelassen.
Nur Zulassung admin oder project_admin Lässt herein, autorisiert aber nichts. Die Route, die sie verwendet, muss danach ihre eigenen Ergebnisse selbst auf die vom Aufrufer verwalteten Repositories beschränken.

Anders gesagt: Die Capabilities manage_users und manage_permissions existieren nur auf dem Papier. Sie werden zwar bei der Installation in der Datenbank angelegt, aber keine Codezeile fragt sie ab. Sie einer Rolle zu erteilen ändert überhaupt nichts. Bauen Sie keine Integration, die annimmt, dass ein Konto, das nicht admin ist, Benutzer verwalten kann, weil man ihm manage_users gegeben hat: Es erhält ein 403.

RouteTatsächliche PrüfungBeschreibung
GET /api/admin/usersNur Zulassung, dann FilterungEin project_admin sieht nur die Konten seines Bereichs
POST /api/admin/usersNur ZulassungErstellt ein Konto
PUT/DELETE /api/admin/users/{id}Super-AdministratorAktualisiert oder löscht ein Konto
POST /api/admin/users/{id}/reset-passwordSuper-AdministratorSetzt das Passwort zurück und widerruft alle Token des Kontos
GET/POST /api/admin/groupsSuper-AdministratorGruppen sind global, sie können nicht pro Projekt delegiert werden
GET/POST /api/admin/permissions/{repo_id}Administrator dieses RepositorysBerechtigungsregeln nach Pfadmuster, einer Gruppe oder einem Benutzer erteilt
GET/POST /api/admin/repositories/{repo_id}/rulesAdministrator dieses RepositorysVor dem Upload angewandte Validierungsregeln
GET/POST /api/admin/repositories/{repo_id}/webhooksAdministrator dieses RepositorysDiscord, Slack, Teams oder generischer Webhook
GET /api/admin/locksNur Zulassung, dann FilterungSperren, beschränkt auf die verwalteten Repositories
POST /api/admin/locks/{lock_id}/force-releaseAdministrator dieses RepositorysEntfernt die Sperre einer anderen Person. Im Audit-Protokoll vermerkt
GET /api/admin/auditSuper-AdministratorAudit-Protokoll, filterbar und paginiert
GET /api/admin/statsSuper-AdministratorSpeichervolumen, Deduplizierungsrate, Wachstum
GET /api/admin/licenceSuper-AdministratorLizenzstatus und verbrauchte Sitze
GET /api/admin/repositories/{repo_id}/gc/previewAdministrator dieses RepositorysSimuliert die Garbage Collection, ohne etwas zu löschen
POST /api/admin/repositories/{repo_id}/gcAdministrator dieses RepositorysStartet die Garbage Collection

Die vollständigen Details zu den Rollen, ihren Rängen und dem, was jede tun darf, stehen auf der Seite zu Rollen und Berechtigungen.

Auf dieser Seite nicht abgedeckte API-Bereiche

Die folgenden Routen-Familien existieren, sind eingehängt und werden vom Server bereitgestellt, sind aber hier nicht beschrieben. Wenn Ihre Integration sie benötigt, ist heute am zuverlässigsten, die Aufrufe zu beobachten, die der Desktop-Client macht, oder uns zu schreiben.

PräfixWas es abdeckt
/api/advisor/*Project Health: das Audit des Unreal-Projekts, seine Befunde, der Score pro Säule und die Sortierung der zu ignorierenden Elemente.
/api/distribution/*Verteilung zwischen Projekten: Verknüpfungen zwischen einem Quell- und einem Ziel-Repository, Veröffentlichung von Dateien von einem Projekt in ein anderes, Verlauf.
/api/binaries/*Vorkompilierte Editor-Binärdateien, an einen Commit gebunden, nach dem Modell von UnrealGameSync.
/api/watchlist/*Über die oben beschriebenen Beobachtungsmuster hinaus: die Benachrichtigungen und der Posteingang.
/api/profile/*Profil des aktuellen Benutzers.
/api/admin/repositories/{repo_id}/adminsWer ein Repository verwaltet: Ernennung und Entzug von Projektadministratoren.
/api/admin/repositories/{repo_id}/build-accessWer die Builds dieses Projekts herunterladen darf, Projekt für Projekt vergeben.
/api/admin/licenceLizenzstatus und verbrauchte Sitze.
/api/admin/server/*Serveraktualisierung über den Administrationsbereich.

Health

GET /health

Nicht authentifizierter Endpunkt, der 200 OK zurückgibt, wenn der Server die Datenbank erreichen kann. Wird für Health-Checks von Load-Balancern oder Monitoring verwendet (Prometheus Blackbox, Datadog Synthetic usw.).

Response 200: OK (text/plain).

Response 503: wenn die Datenbank nicht erreichbar ist.

GET /api/server-info

Öffentlicher, nicht authentifizierter Endpunkt, der die Servermetadaten zurückgibt (Version usw.). Wie /health ist er nicht durch die Auth-Middleware geschützt.

Fehlerformat

Im Fehlerfall antwortet eine authentifizierte Route { "success": false, "error": "..." }, und eine /api/auth/*-Route antwortet { "error": "..." }, in beiden Fällen mit einem passenden HTTP-Code.

Das Feld error ist eine an einen Menschen gerichtete Meldung, kein Code. Es gibt keinen Katalog stabiler Fehlercodes, weder im Body noch in einem Header. Bauen Sie also keine Logik auf seinem Inhalt auf, und analysieren Sie sein Präfix nicht: Diese Sätze werden von einer Version zur nächsten umformuliert, und ein Zeichenkettentest, der still bricht, ist schlimmer als gar kein Test.

Der HTTP-Code ist das Einzige, worauf man ein Verhalten stützen sollte. Die Tabelle unten gibt seine Lesart wieder.

HTTPBedeutungWann
400Bad requestUngültiger Parameter, fehlerhaftes JSON, Body zu groß (vor dem harten 413-Limit)
401Not authenticatedToken fehlt, abgelaufen, ungültige Signatur oder token_version-Nichtübereinstimmung
403Permission deniedFehlende Capability oder keine Berechtigung auf dem Pfad / Repo
404Resource not foundRepo / Datei / Commit / Benutzer nicht vorhanden oder nicht zugänglich
409ConflictSperre bereits gehalten, gleichzeitiger Commit, Unique-Constraint verletzt
413Payload too largeBody über dem Limit (1 GB bei /files, 8 GB bei /builds/upload)
429Too many requestsNur bei /auth/login (Ratenbegrenzung pro Benutzer)
500Internal server errorDB-Fehler, IO-Fehler. Serverseitig protokolliert, geben Sie den Zeitstempel an, wenn Sie den Bug melden.
503Service unavailableDatenbank nicht erreichbar (Health-Check) oder Migration im Gange