| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- {
- "openapi": "3.1.0",
- "info": {
- "title": "Manage – Protocol v1",
- "version": "1.0.0",
- "summary": "Update and backup interface between a project instance and the Manage server.",
- "description": "The four endpoints a client needs: fetch the release manifest, download the release package, upload a backup, report status.\n\nEvery request authenticates with two headers (`X-Manage-Instance`, `X-Manage-Token`). There is no session, no cookie and no shared password. The server stores only the SHA-256 hash of the token.\n\nEvery successfully authenticated request additionally updates `last_seen_at` and the instance's latest IP.\n\nThe reference implementation of the client is fully documented in the handbook; see `lib/remote.php`, `lib/updater.php` and `lib/backup.php`.\n\nNote: the server's actual `error` strings in the response examples below are still German text — this document's descriptions are in English, the API's literal responses are not.",
- "license": {
- "name": "See the handbook"
- }
- },
- "servers": [
- {
- "url": "/api/v1",
- "description": "This Manage installation"
- }
- ],
- "tags": [
- {
- "name": "Update",
- "description": "Determine the release and fetch the package."
- },
- {
- "name": "Backup",
- "description": "Transfer backup archives to the server."
- },
- {
- "name": "Status",
- "description": "Report the instance's state."
- }
- ],
- "security": [
- {
- "instanceId": [],
- "instanceToken": []
- }
- ],
- "paths": {
- "/manifest.php": {
- "get": {
- "tags": ["Update"],
- "operationId": "getManifest",
- "summary": "Fetch the release the instance should install",
- "description": "Returns version, download address, size and SHA-256 of the current release.\n\n`package_url` is built from the server configuration (`MANAGE_PUBLIC_URL`), not from the request's `Host` header. A spoofed header therefore can't redirect a client to a foreign server.\n\nRelease metadata is not public: the endpoint requires a valid token.",
- "responses": {
- "200": {
- "description": "Current 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": "No valid release is published.",
- "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": "Download the release ZIP",
- "description": "Streams the release archive. The response is `application/zip` with `Content-Length` and `Cache-Control: private, no-store`; no JSON on success.\n\nThe client **must** check size and SHA-256 against the manifest and delete the file on mismatch. Without this check, arbitrary code gets deployed.",
- "parameters": [
- {
- "name": "version",
- "in": "query",
- "required": true,
- "description": "Version in `vMAJOR.MINOR.PATCH` format.",
- "schema": { "$ref": "#/components/schemas/Version" },
- "example": "v1.3.0"
- }
- ],
- "responses": {
- "200": {
- "description": "The release archive",
- "headers": {
- "Content-Disposition": {
- "description": "attachment; filename=\"…zip\"",
- "schema": { "type": "string" }
- },
- "Content-Length": {
- "description": "Size in bytes, identical to `size` from the manifest.",
- "schema": { "type": "integer" }
- },
- "Cache-Control": {
- "description": "Always `private, no-store`.",
- "schema": { "type": "string" }
- }
- },
- "content": {
- "application/zip": {
- "schema": { "type": "string", "format": "binary" }
- }
- }
- },
- "400": {
- "description": "Invalid version format",
- "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 does not exist",
- "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": "Upload a backup archive",
- "description": "Accepts a ZIP and stores it under the instance.\n\nChecked in this order: upload error code, `is_uploaded_file`, size limit (`MANAGE_BACKUP_MAX_UPLOAD_BYTES`), ZIP signature, filename pattern, checksum after storing. If the checksum doesn't match, the file is deleted again and `400` is reported.\n\nAn existing filename is never overwritten: the server appends `-2`, `-3` and reports the name actually used.\n\nErrors during S3 archiving do **not** fail the upload – the local copy is stored and gets caught up later.",
- "requestBody": {
- "required": true,
- "content": {
- "multipart/form-data": {
- "schema": {
- "type": "object",
- "required": ["backup"],
- "properties": {
- "backup": {
- "type": "string",
- "format": "binary",
- "description": "The ZIP archive."
- },
- "filename": {
- "type": "string",
- "pattern": "^backup-\\d{8}-\\d{6}(?:-\\d+)?\\.zip$",
- "description": "Desired name. The server assigns `backup-YYYYmmdd-HHMMSS.zip` if omitted.",
- "example": "backup-20260820-092104.zip"
- },
- "sha256": {
- "type": "string",
- "pattern": "^[a-f0-9]{64}$",
- "description": "Checksum of the archive. Recomputed and compared server-side."
- },
- "meta": {
- "type": "string",
- "description": "JSON object with `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 stored",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/BackupResult" },
- "example": {
- "success": true,
- "instance": "myproject-prod",
- "filename": "backup-20260820-092104.zip",
- "size": 427,
- "sha256": "824f3f8000000000000000000000000000000000000000000000000000000000",
- "retention": 30,
- "s3": { "enabled": false, "uploaded": false, "pending": 0 }
- }
- }
- }
- },
- "400": {
- "description": "Upload rejected: file missing, limit exceeded, not a ZIP, wrong name or checksum.",
- "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": "Report status and receive update information",
- "description": "Reports the instance's state. All fields are optional; missing fields leave the server's current value unchanged.\n\nThe response includes the update information, so it replaces a separate call to `manifest.php` for simple monitoring.",
- "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 accepted",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/HeartbeatResponse" },
- "example": {
- "success": true,
- "instance": "myproject-prod",
- "latest": "v1.3.0",
- "update_available": false,
- "server_time": "2026-08-20T09:23:11+00:00"
- }
- }
- }
- },
- "400": {
- "description": "Request body is not valid 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": "The instance id, for example `myproject-prod`."
- },
- "instanceToken": {
- "type": "apiKey",
- "in": "header",
- "name": "X-Manage-Token",
- "description": "The token shown once when the instance was created."
- }
- },
- "responses": {
- "Unauthorized": {
- "description": "Headers missing, instance unknown, or token wrong – deliberately indistinguishable, so instance ids can't be brute-forced.",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Authentifizierung fehlgeschlagen." }
- }
- }
- },
- "Disabled": {
- "description": "Instance exists but is deactivated.",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Diese Instanz ist deaktiviert." }
- }
- }
- },
- "MethodNotAllowed": {
- "description": "Wrong HTTP method. The response carries an `Allow` header.",
- "headers": {
- "Allow": { "schema": { "type": "string" } }
- },
- "content": {
- "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
- }
- },
- "RateLimited": {
- "description": "Too many failed authentications from this IP (`MANAGE_API_RATE_LIMIT_MAX` per `MANAGE_API_RATE_LIMIT_WINDOW` seconds, 240 per 300s out of the box). A successful login resets the counter.",
- "content": {
- "application/json": {
- "schema": { "$ref": "#/components/schemas/Error" },
- "example": { "success": false, "error": "Zu viele Anfragen. Bitte später erneut versuchen." }
- }
- }
- },
- "ServerError": {
- "description": "Unexpected server error; details are in the server log.",
- "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": "Shape of **every** error response.",
- "required": ["success", "error"],
- "properties": {
- "success": { "type": "boolean", "const": false },
- "error": { "type": "string", "description": "Plain-text message, for logging and display." }
- }
- },
- "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": "Equivalent to `latest`; both fields exist for compatibility reasons."
- },
- "package_url": {
- "type": "string",
- "format": "uri",
- "description": "Absolute download address, built from `MANAGE_PUBLIC_URL`."
- },
- "sha256": {
- "type": "string",
- "pattern": "^[a-f0-9]{64}$",
- "description": "Checksum of the package. Must be verified by the client."
- },
- "size": { "type": "integer", "description": "Package size 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": "The name actually assigned – may differ from the one requested if it was already taken."
- },
- "size": { "type": "integer" },
- "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
- "retention": {
- "type": "integer",
- "description": "How many days the server keeps this archive."
- },
- "s3": {
- "type": "object",
- "description": "State of the optional S3 archive. Never a reason for failure.",
- "properties": {
- "enabled": { "type": "boolean" },
- "uploaded": { "type": "boolean" },
- "pending": { "type": "integer" }
- }
- }
- }
- },
- "HeartbeatRequest": {
- "type": "object",
- "properties": {
- "version": {
- "type": "string",
- "description": "Installed version of the application. Empty if it can't be determined."
- },
- "php_version": { "type": "string", "examples": ["8.3.6"] },
- "disk_free": { "type": "integer", "description": "Free disk space 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": "Current release on the server, or empty if none is published."
- },
- "update_available": {
- "type": "boolean",
- "description": "Result of a `version_compare` between `latest` and the reported version. `false` as long as the instance reports no version."
- },
- "server_time": { "type": "string", "format": "date-time" }
- }
- }
- }
- }
- }
|