| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- {
- "openapi": "3.1.0",
- "info": {
- "title": "Manage – Protokoll v1",
- "version": "1.0.0",
- "summary": "Update- und Backup-Schnittstelle zwischen einer Projektinstanz und dem Manage-Server.",
- "description": "Die vier Endpunkte, die ein Client benötigt: Release-Manifest abrufen, Release-Paket herunterladen, Backup hochladen, Status melden.\n\nJede Anfrage authentifiziert sich mit zwei Headern (`X-Manage-Instance`, `X-Manage-Token`). Es gibt keine Sitzung, kein Cookie und kein gemeinsames Passwort. Der Server speichert nur den SHA-256-Hash des Tokens.\n\nJede erfolgreich authentifizierte Anfrage aktualisiert nebenbei `last_seen_at` und die letzte IP der Instanz.\n\nDie Referenzimplementierung des Clients steht vollständig im Handbuch, siehe `lib/remote.php`, `lib/updater.php` und `lib/backup.php`.",
- "license": {
- "name": "Siehe Handbuch"
- }
- },
- "servers": [
- {
- "url": "/api/v1",
- "description": "Diese Manage-Installation"
- }
- ],
- "tags": [
- {
- "name": "Update",
- "description": "Release ermitteln und Paket beziehen."
- },
- {
- "name": "Backup",
- "description": "Sicherungsarchive an den Server übertragen."
- },
- {
- "name": "Status",
- "description": "Zustand der Instanz melden."
- }
- ],
- "security": [
- {
- "instanceId": [],
- "instanceToken": []
- }
- ],
- "paths": {
- "/manifest.php": {
- "get": {
- "tags": ["Update"],
- "operationId": "getManifest",
- "summary": "Release abrufen, das die Instanz installieren soll",
- "description": "Liefert Version, Download-Adresse, Größe und SHA-256 des aktuellen Release.\n\n`package_url` wird aus der Serverkonfiguration (`MANAGE_PUBLIC_URL`) gebildet, nicht aus dem `Host`-Header der Anfrage. Ein gefälschter Header kann einen Client daher nicht auf einen fremden Server umlenken.\n\nRelease-Metadaten sind nicht öffentlich: Der Endpunkt verlangt ein gültiges Token.",
- "responses": {
- "200": {
- "description": "Aktuelles Release",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Manifest" },
- "example": {
- "success": true,
- "latest": "v1.3.0",
- "version": "v1.3.0",
- "package_url": "https://manage.example.org/api/v1/package.php?version=v1.3.0",
- "sha256": "70f17aae44a9afdd948de1767daa61f936bbecb52096753757791e230a22f024",
- "size": 2199,
- "published_at": "2026-08-20T09:20:43+00:00"
- }
- }
- }
- },
- "401": { "$ref": "#/components/responses/Unauthorized" },
- "403": { "$ref": "#/components/responses/Disabled" },
- "404": {
- "description": "Es ist kein gültiges Release veröffentlicht.",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Es ist kein gültiges Release veröffentlicht." }
- }
- }
- },
- "405": { "$ref": "#/components/responses/MethodNotAllowed" },
- "429": { "$ref": "#/components/responses/RateLimited" },
- "500": { "$ref": "#/components/responses/ServerError" }
- }
- }
- },
- "/package.php": {
- "get": {
- "tags": ["Update"],
- "operationId": "getPackage",
- "summary": "Release-ZIP herunterladen",
- "description": "Streamt das Release-Archiv. Antwort ist `application/zip` mit `Content-Length` und `Cache-Control: private, no-store`; bei Erfolg kein JSON.\n\nDer Client **muss** Größe und SHA-256 gegen das Manifest prüfen und die Datei bei Abweichung löschen. Ohne diese Prüfung wird beliebiger Code ausgerollt.",
- "parameters": [
- {
- "name": "version",
- "in": "query",
- "required": true,
- "description": "Version im Format `vMAJOR.MINOR.PATCH`.",
- "schema": { "$ref": "#/components/schemas/Version" },
- "example": "v1.3.0"
- }
- ],
- "responses": {
- "200": {
- "description": "Das Release-Archiv",
- "headers": {
- "Content-Disposition": {
- "description": "attachment; filename=\"…zip\"",
- "schema": { "type": "string" }
- },
- "Content-Length": {
- "description": "Größe in Bytes, identisch mit `size` aus dem Manifest.",
- "schema": { "type": "integer" }
- },
- "Cache-Control": {
- "description": "Immer `private, no-store`.",
- "schema": { "type": "string" }
- }
- },
- "content": {
- "application/zip": {
- "schema": { "type": "string", "format": "binary" }
- }
- }
- },
- "400": {
- "description": "Ungültiges Versionsformat",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Ungültige Version." }
- }
- }
- },
- "401": { "$ref": "#/components/responses/Unauthorized" },
- "403": { "$ref": "#/components/responses/Disabled" },
- "404": {
- "description": "Release existiert nicht",
- "content": {
- "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
- }
- },
- "405": { "$ref": "#/components/responses/MethodNotAllowed" },
- "429": { "$ref": "#/components/responses/RateLimited" },
- "500": { "$ref": "#/components/responses/ServerError" }
- }
- }
- },
- "/backup.php": {
- "post": {
- "tags": ["Backup"],
- "operationId": "uploadBackup",
- "summary": "Backup-Archiv hochladen",
- "description": "Nimmt ein ZIP entgegen und legt es unter der Instanz ab.\n\nGeprüft wird in dieser Reihenfolge: Upload-Fehlercode, `is_uploaded_file`, Größenlimit (`MANAGE_BACKUP_MAX_UPLOAD_BYTES`), ZIP-Signatur, Dateinamensmuster, Prüfsumme nach dem Speichern. Weicht die Prüfsumme ab, wird die Datei wieder gelöscht und `400` gemeldet.\n\nEin vorhandener Dateiname wird nie überschrieben: Der Server hängt `-2`, `-3` an und meldet den tatsächlich verwendeten Namen zurück.\n\nFehler beim S3-Archivieren lassen den Upload **nicht** fehlschlagen – die lokale Kopie ist gespeichert und wird später nachgezogen.",
- "requestBody": {
- "required": true,
- "content": {
- "multipart/form-data": {
- "schema": {
- "type": "object",
- "required": ["backup"],
- "properties": {
- "backup": {
- "type": "string",
- "format": "binary",
- "description": "Das ZIP-Archiv."
- },
- "filename": {
- "type": "string",
- "pattern": "^backup-\\d{8}-\\d{6}(?:-\\d+)?\\.zip$",
- "description": "Gewünschter Name. Ohne Angabe vergibt der Server `backup-YYYYmmdd-HHMMSS.zip`.",
- "example": "backup-20260820-092104.zip"
- },
- "sha256": {
- "type": "string",
- "pattern": "^[a-f0-9]{64}$",
- "description": "Prüfsumme des Archivs. Wird serverseitig neu berechnet und verglichen."
- },
- "meta": {
- "type": "string",
- "description": "JSON-Objekt mit `trigger`, `file_count`, `source_bytes`, `app_version`.",
- "example": "{\"trigger\":\"cron\",\"file_count\":3,\"source_bytes\":63,\"app_version\":\"v1.3.0\"}"
- }
- }
- },
- "encoding": {
- "backup": { "contentType": "application/zip" }
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Backup gespeichert",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/BackupResult" },
- "example": {
- "success": true,
- "instance": "meinprojekt-prod",
- "filename": "backup-20260820-092104.zip",
- "size": 427,
- "sha256": "824f3f8000000000000000000000000000000000000000000000000000000000",
- "retention": 30,
- "s3": { "enabled": false, "uploaded": false, "pending": 0 }
- }
- }
- }
- },
- "400": {
- "description": "Upload abgelehnt: Datei fehlt, Limit überschritten, kein ZIP, Name oder Prüfsumme falsch.",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Die hochgeladene Datei muss ein ZIP-Archiv sein." }
- }
- }
- },
- "401": { "$ref": "#/components/responses/Unauthorized" },
- "403": { "$ref": "#/components/responses/Disabled" },
- "405": { "$ref": "#/components/responses/MethodNotAllowed" },
- "429": { "$ref": "#/components/responses/RateLimited" }
- }
- }
- },
- "/heartbeat.php": {
- "post": {
- "tags": ["Status"],
- "operationId": "sendHeartbeat",
- "summary": "Status melden und Update-Information erhalten",
- "description": "Meldet den Zustand der Instanz. Alle Felder sind optional; fehlende Felder lassen den bisherigen Wert auf dem Server unverändert.\n\nDie Antwort enthält die Update-Information, ersetzt für einfache Überwachung also einen zusätzlichen Aufruf von `manifest.php`.",
- "requestBody": {
- "required": false,
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/HeartbeatRequest" },
- "example": {
- "version": "v1.3.0",
- "php_version": "8.3.6",
- "disk_free": 12884901888,
- "pending_migrations": 0,
- "last_backup_at": "2026-08-20T09:21:04+00:00"
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Status übernommen",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/HeartbeatResponse" },
- "example": {
- "success": true,
- "instance": "meinprojekt-prod",
- "latest": "v1.3.0",
- "update_available": false,
- "server_time": "2026-08-20T09:23:11+00:00"
- }
- }
- }
- },
- "400": {
- "description": "Anfrage-Body ist kein gültiges JSON.",
- "content": {
- "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
- }
- },
- "401": { "$ref": "#/components/responses/Unauthorized" },
- "403": { "$ref": "#/components/responses/Disabled" },
- "405": { "$ref": "#/components/responses/MethodNotAllowed" },
- "429": { "$ref": "#/components/responses/RateLimited" }
- }
- }
- }
- },
- "components": {
- "securitySchemes": {
- "instanceId": {
- "type": "apiKey",
- "in": "header",
- "name": "X-Manage-Instance",
- "description": "Kennung der Instanz, zum Beispiel `meinprojekt-prod`."
- },
- "instanceToken": {
- "type": "apiKey",
- "in": "header",
- "name": "X-Manage-Token",
- "description": "Das beim Anlegen der Instanz einmalig angezeigte Token."
- }
- },
- "responses": {
- "Unauthorized": {
- "description": "Header fehlen, Instanz unbekannt oder Token falsch – bewusst nicht unterscheidbar, damit Instanz-Kennungen nicht durchprobiert werden können.",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Authentifizierung fehlgeschlagen." }
- }
- }
- },
- "Disabled": {
- "description": "Instanz existiert, ist aber deaktiviert.",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Diese Instanz ist deaktiviert." }
- }
- }
- },
- "MethodNotAllowed": {
- "description": "Falsche HTTP-Methode. Die Antwort trägt einen `Allow`-Header.",
- "headers": {
- "Allow": { "schema": { "type": "string" } }
- },
- "content": {
- "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
- }
- },
- "RateLimited": {
- "description": "Zu viele fehlgeschlagene Authentifizierungen von dieser IP (`MANAGE_API_RATE_LIMIT_MAX` je `MANAGE_API_RATE_LIMIT_WINDOW` Sekunden, ab Werk 240 je 300 s). Eine erfolgreiche Anmeldung setzt den Zähler zurück.",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Zu viele Anfragen. Bitte später erneut versuchen." }
- }
- }
- },
- "ServerError": {
- "description": "Unerwarteter Serverfehler; Einzelheiten stehen im Serverprotokoll.",
- "content": {
- "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
- }
- }
- },
- "schemas": {
- "Version": {
- "type": "string",
- "pattern": "^v\\d+\\.\\d+\\.\\d+$",
- "examples": ["v1.3.0"]
- },
- "Error": {
- "type": "object",
- "description": "Aufbau **aller** Fehlerantworten.",
- "required": ["success", "error"],
- "properties": {
- "success": { "type": "boolean", "const": false },
- "error": { "type": "string", "description": "Meldung in Klartext, für Protokoll und Anzeige." }
- }
- },
- "Manifest": {
- "type": "object",
- "required": ["success", "latest", "version", "package_url", "sha256", "size", "published_at"],
- "properties": {
- "success": { "type": "boolean", "const": true },
- "latest": { "$ref": "#/components/schemas/Version" },
- "version": {
- "allOf": [{ "$ref": "#/components/schemas/Version" }],
- "description": "Gleichbedeutend mit `latest`; beide Felder existieren aus Kompatibilitätsgründen."
- },
- "package_url": {
- "type": "string",
- "format": "uri",
- "description": "Absolute Adresse für den Download, aus `MANAGE_PUBLIC_URL` gebildet."
- },
- "sha256": {
- "type": "string",
- "pattern": "^[a-f0-9]{64}$",
- "description": "Prüfsumme des Pakets. Vom Client zwingend zu verifizieren."
- },
- "size": { "type": "integer", "description": "Paketgröße in Bytes." },
- "published_at": { "type": "string", "format": "date-time" }
- }
- },
- "BackupResult": {
- "type": "object",
- "required": ["success", "instance", "filename", "size", "sha256", "retention", "s3"],
- "properties": {
- "success": { "type": "boolean", "const": true },
- "instance": { "type": "string" },
- "filename": {
- "type": "string",
- "description": "Der tatsächlich vergebene Name – kann vom gewünschten abweichen, wenn er bereits belegt war."
- },
- "size": { "type": "integer" },
- "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
- "retention": {
- "type": "integer",
- "description": "Wie viele Tage der Server dieses Archiv aufbewahrt."
- },
- "s3": {
- "type": "object",
- "description": "Zustand des optionalen S3-Archivs. Nie ein Grund für einen Fehlschlag.",
- "properties": {
- "enabled": { "type": "boolean" },
- "uploaded": { "type": "boolean" },
- "pending": { "type": "integer" }
- }
- }
- }
- },
- "HeartbeatRequest": {
- "type": "object",
- "properties": {
- "version": {
- "type": "string",
- "description": "Installierte Version der Anwendung. Leer, wenn nicht ermittelbar."
- },
- "php_version": { "type": "string", "examples": ["8.3.6"] },
- "disk_free": { "type": "integer", "description": "Freier Speicher in Bytes." },
- "pending_migrations": { "type": "integer", "minimum": 0 },
- "last_backup_at": { "type": "string", "format": "date-time" }
- }
- },
- "HeartbeatResponse": {
- "type": "object",
- "required": ["success", "instance", "latest", "update_available", "server_time"],
- "properties": {
- "success": { "type": "boolean", "const": true },
- "instance": { "type": "string" },
- "latest": {
- "type": "string",
- "description": "Aktuelles Release auf dem Server, oder leer, wenn keines veröffentlicht ist."
- },
- "update_available": {
- "type": "boolean",
- "description": "Ergebnis eines `version_compare` zwischen `latest` und der gemeldeten Version. `false`, solange die Instanz keine Version meldet."
- },
- "server_time": { "type": "string", "format": "date-time" }
- }
- }
- }
- }
- }
|