'Fehler', 'content' => ['application/json' => ['schema' => ['$ref' => '#/components/schemas/Error']]], ]; $spec = [ 'openapi' => '3.0.3', 'info' => [ 'title' => 'Deploymentcenter API', 'version' => (string)Config::get('app.version', '2.0.0'), 'description' => "Zentrale Schnittstelle fuer Bugtracker, UpdateService, Watchdog, Lizenzen und Token-Provisionierung.\n\n" . "**Zwei Antwortformate.** Der Regelfall ist der Umschlag `{\"status\":\"success\",...}` bzw.\n" . "`{\"status\":\"error\",\"error\":{\"code\":...}}`. Die Endpunkte unter `/api/license/v1/`\n" . "antworten davon abweichend ohne Umschlag - dort ist `status` der Lizenzzustand.\n" . "Siehe das Schema `LicenseResult`.\n\n" . "Authentifizierung ueber `Authorization: Bearer ` oder `X-Agent-Token`.\n" . "Tokens werden im WebUI erzeugt (Master-Token) und koennen sich per\n" . "`/api/tokens/v1/provision` selbst in Sub-Tokens aufteilen.\n\n" . "Typischer Agenten-Ablauf:\n" . "1. `POST /api/bugtracker/v1/manage?action=next` - naechstes Item holen und uebernehmen\n" . "2. Arbeiten, Zwischenstand per `?action=comment` dokumentieren\n" . "3. `?action=resolve` mit `resolved_in_build`\n" . "4. Beim Release meldet `POST /api/updateservice/v1/publish` den Build; passende Items schliessen sich selbst.", ], 'servers' => [['url' => $baseUrl]], 'components' => [ 'securitySchemes' => [ 'bearerAuth' => ['type' => 'http', 'scheme' => 'bearer'], 'agentToken' => ['type' => 'apiKey', 'in' => 'header', 'name' => 'X-Agent-Token'], ], 'schemas' => [ 'Error' => [ 'type' => 'object', 'properties' => [ 'status' => ['type' => 'string', 'enum' => ['error']], 'error' => [ 'type' => 'object', 'properties' => [ 'code' => ['type' => 'string', 'description' => 'Stabiler, maschinenlesbarer Fehlercode'], 'message' => ['type' => 'string'], ], ], ], ], 'BugtrackerItem' => [ 'type' => 'object', 'properties' => [ 'id' => ['type' => 'integer'], 'project_slug' => ['type' => 'string'], 'type' => ['type' => 'string', 'enum' => BugRepo::TYPES], 'title' => ['type' => 'string'], 'description' => ['type' => 'string', 'nullable' => true], 'error_message' => ['type' => 'string', 'nullable' => true], 'stack_trace' => ['type' => 'string', 'nullable' => true], 'environment' => ['type' => 'string', 'enum' => BugRepo::ENVIRONMENTS], 'severity' => ['type' => 'string', 'enum' => BugRepo::SEVERITIES], 'status' => ['type' => 'string', 'enum' => BugRepo::STATUSES], 'occurrence_count' => ['type' => 'integer'], 'claimed_by' => ['type' => 'string', 'nullable' => true], 'lease_until' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true], 'repo_url' => ['type' => 'string', 'nullable' => true], 'git_branch' => ['type' => 'string', 'nullable' => true], 'commit_sha' => ['type' => 'string', 'nullable' => true], 'file_path' => ['type' => 'string', 'nullable' => true], 'line_no' => ['type' => 'integer', 'nullable' => true], 'resolved_in_build' => ['type' => 'string', 'nullable' => true], 'updated_at' => ['type' => 'string', 'format' => 'date-time'], ], ], 'ReportRequest' => [ 'type' => 'object', 'required' => ['title'], 'properties' => [ 'project_slug' => ['type' => 'string', 'example' => 'deploymentcenter'], 'type' => ['type' => 'string', 'enum' => BugRepo::TYPES, 'default' => 'bug'], 'title' => ['type' => 'string', 'maxLength' => 255], 'description' => ['type' => 'string'], 'error_message' => ['type' => 'string'], 'stack_trace' => ['type' => 'string'], 'severity' => ['type' => 'string', 'enum' => BugRepo::SEVERITIES], 'environment' => ['type' => 'string', 'enum' => BugRepo::ENVIRONMENTS], 'build_version' => ['type' => 'string'], 'push_id' => ['type' => 'string'], 'target_agent' => ['type' => 'string'], 'tags' => ['type' => 'string', 'description' => 'Kommagetrennt'], 'client_ref' => [ 'type' => 'string', 'description' => 'Idempotenz-Schluessel. Ein erneuter Aufruf mit demselben Wert legt kein Duplikat an. Alternativ als Header Idempotency-Key.', ], 'repo_url' => ['type' => 'string'], 'git_branch' => ['type' => 'string'], 'commit_sha' => ['type' => 'string'], 'file_path' => ['type' => 'string'], 'line_no' => ['type' => 'integer'], 'context' => ['type' => 'object', 'description' => 'Beliebiger strukturierter Zusatzkontext'], 'evidence' => [ 'type' => 'string', 'minLength' => 40, 'description' => 'Pflicht, wenn project_slug ein fremdes Projekt bezeichnet: was wurde ' . 'beobachtet, und warum liegt die Ursache dort und nicht im eigenen ' . 'Projekt? Wird bei einer Meldung ins eigene Projekt ignoriert.', ], 'reported_from' => [ 'type' => 'string', 'readOnly' => true, 'description' => 'Herkunft der Meldung. Wird vom Server aus der Token-Bindung ' . 'abgeleitet; ein Wert im Body wird verworfen.', ], ], ], 'Release' => [ 'type' => 'object', 'description' => 'Release-Zeile aus updateservice_releases. Die Feldnamen sind die der Datenbank ' . '(snake_case) und unterscheiden sich von der statischen latest.json, die der ' . 'Packager in camelCase erzeugt.', 'properties' => [ 'id' => ['type' => 'integer'], 'product_slug' => ['type' => 'string'], 'version' => ['type' => 'string', 'example' => '1.4.3'], 'channel' => ['type' => 'string', 'default' => 'prod'], 'platform' => [ 'type' => 'string', 'default' => 'any', 'example' => 'win-x64', 'description' => 'Laufzeitkennung (.NET-RID) oder "any" fuer plattformunabhaengige Releases. ' . 'Ein Client, der "platform" mitschickt, bekommt nur Pakete seiner Plattform ' . 'oder "any"; ohne Angabe ausschliesslich "any".', ], 'release_notes' => ['type' => 'string', 'nullable' => true, 'description' => 'Entspricht "changelog" in der latest.json.'], 'download_url' => ['type' => 'string', 'description' => 'Entspricht "packageUrl" in der latest.json.'], 'sha256_hash' => ['type' => 'string', 'nullable' => true, 'description' => 'Entspricht "sha256" in der latest.json.'], 'git_commit' => ['type' => 'string', 'nullable' => true], 'size_bytes' => ['type' => 'integer'], 'manifest_signature' => [ 'type' => 'string', 'nullable' => true, 'description' => 'RSA-SHA256 ueber die kanonische Release-Zeile, base64. Pruefbar mit dem ' . 'Schluessel aus /api/updateservice/v1/pubkey. Null, wenn auf dem Server ' . 'kein Signierschluessel hinterlegt ist.', ], 'is_critical' => ['type' => 'boolean', 'description' => 'Kommt als TINYINT(1), also 0 oder 1.'], 'created_at' => ['type' => 'string', 'format' => 'date-time'], ], ], 'Monitor' => [ 'type' => 'object', 'properties' => [ 'source' => ['type' => 'string'], 'instance' => ['type' => 'string'], 'type' => ['type' => 'string', 'enum' => ['heartbeat', 'host', 'hypervisor_node', 'guest']], 'state' => ['type' => 'string', 'enum' => ['up', 'warning', 'down', 'error', 'stopped', 'maintenance']], 'expected_interval_sec' => ['type' => 'integer'], 'last_seen_utc' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true], 'last_status' => ['type' => 'string', 'enum' => ['ok', 'warning', 'error'], 'nullable' => true], 'last_message' => ['type' => 'string', 'nullable' => true], 'app_version' => ['type' => 'string', 'nullable' => true, 'description' => 'Vom Heartbeat gemeldete Anwendungsversion.'], 'group_key' => ['type' => 'string', 'nullable' => true], 'parent_source' => ['type' => 'string', 'nullable' => true], 'os' => ['type' => 'string', 'nullable' => true], 'metrics_json' => ['type' => 'object', 'nullable' => true], 'failing_checks' => ['type' => 'string', 'nullable' => true], 'is_muted' => ['type' => 'boolean'], ], ], 'LicenseResult' => [ 'type' => 'object', 'description' => 'ACHTUNG: ohne den status/error-Umschlag der uebrigen API. Das Feld "status" ' . 'traegt hier den Lizenzzustand, nicht "success".', 'properties' => [ 'type' => ['type' => 'string', 'enum' => ['validation_result']], 'status' => [ 'type' => 'string', 'enum' => ['valid', 'not_found', 'revoked', 'suspended', 'expired', 'activation_limit'], 'description' => 'valid = gueltig und aktiviert; not_found = Projekt oder Schluessel unbekannt; ' . 'revoked = widerrufen oder diese Hardware gesperrt; suspended = voruebergehend ' . 'ausgesetzt; expired = Ablaufdatum ueberschritten; activation_limit = maximale ' . 'Anzahl Aktivierungen erreicht.', ], 'issued_at' => ['type' => 'integer', 'description' => 'Unix-Zeit der Ausstellung.'], 'expires_at' => ['type' => 'integer', 'nullable' => true, 'description' => 'Unix-Zeit des Lizenzablaufs, null bei unbefristet.'], 'cache_ttl_hours' => [ 'type' => 'integer', 'example' => 168, 'description' => 'Laenge der Offline-Gnadenfrist in Stunden. Auswerten - nicht durch expires_at ersetzen.', ], 'product' => ['type' => 'string'], 'license_key' => ['type' => 'string'], 'hardware_id' => ['type' => 'string'], 'nonce' => ['type' => 'string', 'description' => 'Der uebergebene Wert, unveraendert.'], 'endpoints' => ['type' => 'object'], 'message' => ['type' => 'string', 'nullable' => true], ], ], ], ], 'security' => [['bearerAuth' => []], ['agentToken' => []]], 'paths' => [ '/api/health' => [ 'get' => [ 'tags' => ['System'], 'summary' => 'Verfuegbarkeit und Schema-Status', 'security' => [], 'responses' => ['200' => ['description' => 'Zustand'], '503' => ['description' => 'Nicht bereit']], ], ], '/api/bugtracker/v1/report' => [ 'post' => [ 'tags' => ['Bugtracker'], 'summary' => 'Bug, Feature Request oder Idee melden', 'description' => "Benoetigt den Scope bugtracker:report. Gleiche Fehler werden automatisch " . "zusammengefasst und hochgezaehlt.\n\n" . "**Meldung gegen ein fremdes Projekt.** Ein Token vom Typ dev_agent " . "(Werkbank-Token: Claude Code, MCP-Adapter, CI) darf project_slug auf ein " . "anderes Projekt setzen - etwa wenn beim Integrieren des SDK ein Fehler im " . "Deploymentcenter selbst auffaellt. Laufzeit-Token duerfen das nicht; sie " . "stecken im ausgelieferten Client und sind extrahierbar.\n\n" . "Fuer solche Meldungen gilt:\n" . "- `evidence` ist Pflicht (mindestens 40 Zeichen): was wurde beobachtet, " . "und warum liegt die Ursache dort und nicht im eigenen Projekt?\n" . "- `reported_from` wird aus der Token-Bindung abgeleitet und aus dem Body " . "ignoriert - die Herkunft ist bezeugt, nicht behauptet.\n" . "- Das Item landet im Status `triage_external` und zaehlt erst nach " . "Bestaetigung in die Statistik.\n" . "- Der Schweregrad wird auf `medium` gedeckelt; `high` und `critical` " . "vergibt die empfangende Seite.\n" . "- Meldet ein zweites Projekt dieselbe Sache, entsteht kein neues Item - " . "es waechst die Liste der Betroffenen (`affects`).\n\n" . "**Schicke immer `build_version`.** Trifft die Meldung auf ein bereits " . "behobenes Item, entscheidet der Vergleich mit dessen `resolved_in_build` " . "ueber drei sehr verschiedene Ausgaenge - ohne die Angabe faellt die " . "Entscheidung im Zweifel auf den unguenstigsten:\n" . "- **aeltere Fassung als der Fix** -> es passiert nichts. Die Antwort traegt " . "`known_fixed_in`, kein Item wird angelegt und kein Zaehler erhoeht. " . "Aktualisieren und erneut pruefen.\n" . "- **Fix war noch nicht bestaetigt** (`resolved`, `awaiting_verification`, " . "`in_verification`) -> das bestehende Item wechselt auf `fix_insufficient` " . "und liegt wieder in der Arbeitsschlange. `fix_insufficient: true` in der " . "Antwort. Kein neues Item: Versuch und Fehlschlag gehoeren auf einen Strang.\n" . "- **Item war bestaetigt geschlossen** -> Regression, neues Item mit " . "`regression_of` auf das alte.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => ['$ref' => '#/components/schemas/ReportRequest']]], ], 'responses' => [ '201' => ['description' => 'Neu angelegt'], '200' => ['description' => 'Bestehendes Item aktualisiert (Duplikat oder Idempotenz-Treffer)'], '400' => $errorResponse, '401' => $errorResponse, '403' => $errorResponse, '429' => $errorResponse, ], ], ], '/api/errors/v1/report' => [ 'post' => [ 'tags' => ['Fehler'], 'summary' => 'Laufzeitfehler melden', 'description' => "Schlanker Eingang fuer den globalen Exception-Handler. Titel und Dringlichkeit " . "leitet der Server ab.\n\n" . "Gleiche Fehler werden zu einer Gruppe zusammengefasst; veraenderliche Anteile " . "(Werte in Anfuehrungszeichen mit Ziffern, Adressen, GUIDs, Zeitstempel) werden " . "dabei ausgeblendet.\n\n" . "Greift eine Ignore-Regel, kommt `\"ignored\": true` zurueck: der Fehler wird " . "gezaehlt, aber nicht gemeldet. Ueberschreitet er die hinterlegte Alarmschwelle, " . "meldet die Antwort `\"rate_alerted\": true`.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'project_slug' => ['type' => 'string'], 'exception' => ['type' => 'string', 'example' => 'PDOException'], 'message' => ['type' => 'string'], 'stack_trace' => ['type' => 'string'], 'level' => ['type' => 'string', 'enum' => BugRepo::ERROR_LEVELS, 'default' => 'error'], 'build' => ['type' => 'string'], 'environment' => ['type' => 'string', 'enum' => BugRepo::ENVIRONMENTS], 'file' => ['type' => 'string'], 'line' => ['type' => 'integer'], 'client_ref' => ['type' => 'string'], 'context' => ['type' => 'object'], ], ]]], ], 'responses' => [ '201' => ['description' => 'Neue Fehlergruppe angelegt'], '200' => ['description' => 'Bestehende Gruppe hochgezaehlt'], '401' => $errorResponse, '429' => $errorResponse, ], ], ], '/api/watchdog/v1/metrics' => [ 'get' => [ 'tags' => ['Watchdog'], 'summary' => 'Metrik-Verlauf abrufen', 'description' => 'Ohne "metric" die verfuegbaren Namen, mit "metric" den verdichteten Verlauf samt Abweichung vom eigenen Durchschnitt.', 'parameters' => [ ['name' => 'source', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'string']], ['name' => 'metric', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'hours', 'in' => 'query', 'schema' => ['type' => 'integer', 'default' => 24]], ['name' => 'bucket', 'in' => 'query', 'schema' => ['type' => 'integer', 'default' => 15], 'description' => 'Fenstergroesse in Minuten'], ], 'responses' => ['200' => ['description' => 'Verlauf'], '401' => $errorResponse], ], ], '/api/bugtracker/v1/projects' => [ 'get' => [ 'tags' => ['Bugtracker'], 'summary' => 'Projekte auflisten (Discovery)', 'description' => "Benoetigt den Scope bugtracker:read.\n\n" . "Jeder Eintrag traegt `relation`: wie das Projekt mit dem eigenen " . "zusammenhaengt (`uses_sdk`, `depends_on`, `shares_code`, Richtung und " . "integrierte Fassung). `null` heisst schlicht: gar nicht.\n\n" . "Ein Laufzeit-Token sieht nur sein eigenes Projekt. Ein Werkbank-Token " . "(dev_agent) sieht alle - es soll ja entscheiden koennen, wohin ein " . "Fehler gehoert, und dafuer muss es die Nachbarn kennen.", 'responses' => ['200' => ['description' => 'Projektliste'], '401' => $errorResponse], ], ], '/api/bugtracker/v1/manage' => [ 'get' => [ 'tags' => ['Bugtracker'], 'summary' => 'Items lesen', 'description' => 'Scope bugtracker:read. action=list|get|stats|projects.', 'parameters' => [ ['name' => 'action', 'in' => 'query', 'schema' => ['type' => 'string', 'enum' => ['list', 'get', 'stats', 'projects'], 'default' => 'list']], ['name' => 'id', 'in' => 'query', 'schema' => ['type' => 'integer'], 'description' => 'Pflicht bei action=get'], ['name' => 'project_slug', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'status', 'in' => 'query', 'schema' => ['type' => 'string'], 'description' => 'Mehrere kommagetrennt, z. B. open,in_progress'], ['name' => 'severity', 'in' => 'query', 'schema' => ['type' => 'string'], 'description' => 'Mehrere kommagetrennt'], ['name' => 'target_agent', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'unclaimed_only', 'in' => 'query', 'schema' => ['type' => 'boolean']], ['name' => 'updated_since', 'in' => 'query', 'schema' => ['type' => 'string', 'format' => 'date-time'], 'description' => 'Delta-Abfrage fuer Polling'], ['name' => 'order', 'in' => 'query', 'schema' => ['type' => 'string', 'enum' => ['newest', 'oldest', 'updated', 'severity', 'occurrences']]], ['name' => 'limit', 'in' => 'query', 'schema' => ['type' => 'integer', 'default' => 100, 'maximum' => 500]], ['name' => 'offset', 'in' => 'query', 'schema' => ['type' => 'integer', 'default' => 0]], ], 'responses' => ['200' => ['description' => 'Trefferliste mit total/has_more'], '401' => $errorResponse], ], 'post' => [ 'tags' => ['Bugtracker'], 'summary' => 'Items veraendern', 'description' => "Scope bugtracker:manage.\n\n" . "- `action=next` holt die naechsten offenen Items und uebernimmt sie exklusiv\n" . "- `action=claim&id=` uebernimmt ein bestimmtes Item (409 wenn bereits vergeben)\n" . "- `action=release&id=` gibt es wieder frei\n" . "- `action=comment&id=` haengt einen Ermittlungsschritt an\n" . "- `action=status&id=` setzt den Status\n" . "- `action=update&id=` aendert mehrere Felder\n" . "- `action=resolve&id=` schliesst mit resolved_in_build\n" . "- `action=bulk_update` aendert mehrere Items (ids[] + updates{})", 'parameters' => [ ['name' => 'action', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'string', 'enum' => ['claim', 'next', 'release', 'comment', 'status', 'update', 'resolve', 'bulk_update']]], ['name' => 'id', 'in' => 'query', 'schema' => ['type' => 'integer']], ], 'responses' => [ '200' => ['description' => 'Erfolg'], '409' => ['description' => 'Item bereits von einem anderen Agenten uebernommen'], '401' => $errorResponse, ], ], ], '/api/bugtracker/v1/roadmap.php' => [ 'get' => [ 'tags' => ['Bugtracker'], 'summary' => 'Roadmap lesen', 'description' => "Scope bugtracker:read.\n\n" . "**Keine eigene Datenhaltung.** Ein Roadmap-Eintrag ist ein " . "bugtracker_item mit gesetztem `epic`; dieser Endpunkt ist die Sicht " . "darauf. Angelegt wird ueber `/report`, abgeschlossen ueber `/manage`.\n\n" . "- `action=list` Eintraege, nach Bereich und Reihenfolge sortiert\n" . "- `action=epics` Themenbereiche samt Zaehlung je Lage\n" . "- `action=next` die vorderen Eintraege der Warteschlange\n" . "- `action=active` wer haelt gerade welches Item\n" . "- `action=render` der erzeugte Markdown-Block fuer die Projektdatei\n" . "- `action=check` Abgleich einer mitgeschickten Datei (`content`) gegen " . "die Items\n\n" . "`status` nimmt eine Lage statt eines ENUM-Werts: `aktiv`, `offen`, " . "`arbeit`, `erledigt`, `zurueckgestellt`, `verworfen`. Einzelstatus " . "funktionieren weiterhin.", 'parameters' => [ ['name' => 'action', 'in' => 'query', 'schema' => ['type' => 'string', 'enum' => ['list', 'epics', 'next', 'active', 'render', 'check'], 'default' => 'list']], ['name' => 'project_slug', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'epic', 'in' => 'query', 'schema' => ['type' => 'string'], 'description' => 'Themenbereich, z. B. "A"'], ['name' => 'status', 'in' => 'query', 'schema' => ['type' => 'string'], 'description' => 'Lage oder kommagetrennte Einzelstatus'], ['name' => 'target_build', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'claimed_by', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'q', 'in' => 'query', 'schema' => ['type' => 'string'], 'description' => 'Volltextsuche'], ['name' => 'limit', 'in' => 'query', 'schema' => ['type' => 'integer', 'default' => 200, 'maximum' => 500]], ], 'responses' => ['200' => ['description' => 'Roadmap-Eintraege bzw. Bereiche'], '401' => $errorResponse], ], 'post' => [ 'tags' => ['Bugtracker'], 'summary' => 'Roadmap-Angaben setzen', 'description' => "Scope bugtracker:manage. Nur im eigenen Projekt - fremde Planung wird " . "nicht umsortiert.\n\n" . "- `action=set&id=` setzt `epic`, `roadmap_rank`, `target_build`, " . "`defer_trigger`, `doc_anchor` und optional `status`\n" . "- `action=move&id=` schiebt einen Eintrag eine Position (`direction=up|down`)\n" . "- `action=reorder` vergibt die Reihenfolge eines Bereichs neu (`epic`, `ids[]`)\n" . "- `action=epic` legt einen Themenbereich an oder aendert ihn\n\n" . "**Zurueckstellen verlangt einen Ausloeser.** `status=deferred` ohne " . "`defer_trigger` wird mit 400 abgelehnt: Ohne benannten Anlass ist ein " . "Vorhaben nicht zurueckgestellt, sondern vergessen.", 'parameters' => [ ['name' => 'action', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'string', 'enum' => ['set', 'move', 'reorder', 'epic']]], ['name' => 'id', 'in' => 'query', 'schema' => ['type' => 'integer']], ], 'responses' => [ '200' => ['description' => 'Erfolg'], '400' => ['description' => 'Regelverletzung, etwa Zurueckstellen ohne Ausloeser'], '403' => ['description' => 'Item gehoert einem anderen Projekt'], '401' => $errorResponse, ], ], ], '/api/supervisor/v1/observe' => [ 'post' => [ 'tags' => ['Supervisor'], 'summary' => 'Befund einliefern', 'description' => "Scope supervisor:report.\n\n" . "Ein Befund ist keine Fehlermeldung. Er behauptet nicht, dass etwas kaputt " . "ist, sondern dass etwas ungewoehnlich ist - deshalb loest er keine " . "Benachrichtigung aus. Die Einstufung trifft die empfangende Seite, die den " . "Flottenblick hat; `severity_hint` heisst darum Hinweis und nicht " . "Schweregrad.\n\n" . "`client_ref` bewusst aus Detektor, Gegenstand und Fassung bauen statt " . "zufaellig zu erzeugen: derselbe Befund darf beliebig oft gemeldet werden " . "und erhoeht dann nur den Zaehler. Ein Beobachter, der minuetlich nachsieht, " . "legt sonst 1.440 Eintraege am Tag an. Fehlt das Feld, baut der Server es " . "selbst zusammen.\n\n" . "Die Liste gueltiger Detektornamen liefert `?action=catalog`. Ein " . "unbekannter Name wird angenommen, in der Antwort aber mit " . "`known_detector: false` gekennzeichnet - stilles Verwerfen waere " . "schlimmer, denn dann suchte jemand nach Befunden, die nie ankamen.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['detector'], 'properties' => [ 'project_slug' => ['type' => 'string'], 'detector' => ['type' => 'string', 'example' => 'trend.monotonic'], 'subject' => ['type' => 'string', 'example' => 'memory.working_set'], 'instance' => ['type' => 'string', 'example' => 'kunde-4711/app-01'], 'severity_hint' => ['type' => 'string', 'enum' => FindingRepo::SEVERITY_HINTS], 'observed' => ['type' => 'object', 'description' => 'Was wurde gemessen?'], 'baseline' => ['type' => 'object', 'description' => 'Wogegen?'], 'build' => ['type' => 'string'], 'commit_sha' => ['type' => 'string'], 'environment' => ['type' => 'string', 'enum' => FindingRepo::ENVIRONMENTS], 'client_ref' => ['type' => 'string'], ], ]]], ], 'responses' => [ '201' => ['description' => 'Neuer Befund - finding_id, finding_status, ' . 'occurrence_count, known_detector'], '200' => ['description' => 'Bekannter Befund, Zaehler erhoeht'], '400' => $errorResponse, '401' => $errorResponse, '429' => $errorResponse, ], ], ], '/api/supervisor/v1/findings' => [ 'get' => [ 'tags' => ['Supervisor'], 'summary' => 'Befunde abfragen', 'description' => "Scope supervisor:read. Filter: project_slug, detector, status, instance, " . "build, environment, since, limit, offset.\n\n" . "`detector` nimmt auch einen Familiennamen: \"deviation\" liefert " . "deviation.self und deviation.fleet.\n\n" . "`status` ist standardmaessig \"active\" (open und acknowledged).", 'responses' => ['200' => ['description' => 'Befundliste'], '401' => $errorResponse], ], ], '/api/supervisor/v1/summary' => [ 'get' => [ 'tags' => ['Supervisor'], 'summary' => 'Befunde je Detektor zaehlen', 'description' => 'Scope supervisor:read. Der Ueberblick vor dem Einzelbefund.', 'responses' => ['200' => ['description' => 'Zaehlung'], '401' => $errorResponse], ], ], '/api/supervisor/v1/catalog' => [ 'get' => [ 'tags' => ['Supervisor'], 'summary' => 'Detektor-Katalog', 'description' => "Scope supervisor:read. Alle bekannten Detektoren mit Herkunft (client oder " . "server), wonach sie suchen und was sie dafuer brauchen.\n\n" . "Die Namen sind stabil und wandern nicht - nur so bleiben Befunde aus " . "verschiedenen Projekten vergleichbar.", 'responses' => ['200' => ['description' => 'Katalog'], '401' => $errorResponse], ], ], '/api/supervisor/v1/status' => [ 'post' => [ 'tags' => ['Supervisor'], 'summary' => 'Befund einstufen', 'description' => "Scope supervisor:manage - bewusst getrennt von supervisor:report. Ein " . "Beobachter auf Kundenhardware darf melden, aber nicht entscheiden, dass " . "seine eigene Meldung erledigt ist.\n\n" . "Status: open, acknowledged, promoted, resolved, ignored.", 'responses' => [ '200' => ['description' => 'Geaendert'], '400' => $errorResponse, '401' => $errorResponse, '404' => $errorResponse, ], ], ], '/api/updateservice/v1/check' => [ 'get' => [ 'tags' => ['UpdateService'], 'summary' => 'Auf Update pruefen', 'description' => "Ohne Token erreichbar.\n\n" . "**Achtung, zwei verschiedene Formate:** diese Antwort ist in snake_case und " . "reicht unter `latest_release` die Datenbankzeile durch (`download_url`, " . "`sha256_hash`, `release_notes`, `size_bytes`). Die statische " . "`/releases/{product}/{channel}/latest.json`, die der Packager erzeugt, " . "verwendet dagegen camelCase (`packageUrl`, `sha256`, `changelog`, `isCritical`). " . "Wer beide Quellen unterstuetzt, braucht zwei Modelle.\n\n" . "`is_critical` steht auf oberster Ebene, nicht im Release-Objekt.\n\n" . "**Plattform:** ohne `platform` werden ausschliesslich Releases mit `platform=any` " . "beruecksichtigt. Das ist Absicht - ein Client, der seine Laufzeitkennung nicht " . "nennt, soll lieber kein Update bekommen als das Paket einer fremden Plattform.", 'security' => [], 'parameters' => [ ['name' => 'product', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'string']], ['name' => 'version', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'string']], ['name' => 'channel', 'in' => 'query', 'schema' => ['type' => 'string', 'default' => 'prod']], ['name' => 'platform', 'in' => 'query', 'schema' => ['type' => 'string', 'example' => 'win-x64'], 'description' => '.NET-RID des anfragenden Systems. Alias: rid.'], ], 'responses' => [ '200' => [ 'description' => 'Vergleich nach semantischer Versionsordnung', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'update_available' => ['type' => 'boolean'], 'current_version' => ['type' => 'string'], 'latest_version' => ['type' => 'string'], 'is_critical' => ['type' => 'boolean', 'description' => 'Nur vorhanden, wenn ein Update ansteht.'], 'latest_release' => ['$ref' => '#/components/schemas/Release'], 'message' => ['type' => 'string'], ], ]]], ], ], ], ], '/api/updateservice/v1/latest' => [ 'get' => [ 'tags' => ['UpdateService'], 'summary' => 'Hoechstes Release eines Kanals', 'description' => 'Ohne Token erreichbar. Unabhaengig von der Version des Aufrufers.', 'security' => [], 'parameters' => [ ['name' => 'product', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'string']], ['name' => 'channel', 'in' => 'query', 'schema' => ['type' => 'string', 'default' => 'prod']], ['name' => 'platform', 'in' => 'query', 'schema' => ['type' => 'string', 'example' => 'win-x64']], ], 'responses' => [ '200' => [ 'description' => 'Release', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => ['release' => ['$ref' => '#/components/schemas/Release']], ]]], ], '404' => $errorResponse, ], ], ], '/api/updateservice/v1/releases' => [ 'get' => [ 'tags' => ['UpdateService'], 'summary' => 'Releases auflisten', 'description' => 'Ohne Token erreichbar. Ohne "product" alle Produkte.', 'security' => [], 'parameters' => [ ['name' => 'product', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'channel', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'platform', 'in' => 'query', 'schema' => ['type' => 'string'], 'description' => 'Filtert exakt auf diese Plattform - dieser Endpunkt listet den Bestand, er waehlt kein Paket aus.'], ['name' => 'limit', 'in' => 'query', 'schema' => ['type' => 'integer', 'default' => 200]], ], 'responses' => [ '200' => [ 'description' => 'Liste', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'count' => ['type' => 'integer'], 'releases' => ['type' => 'array', 'items' => ['$ref' => '#/components/schemas/Release']], ], ]]], ], ], ], ], '/api/updateservice/v1/pubkey' => [ 'get' => [ 'tags' => ['UpdateService'], 'summary' => 'Oeffentlichen Schluessel fuer Release-Signaturen holen', 'description' => "Ohne Token erreichbar - der Schluessel ist oeffentlich, und ein Agent braucht ihn, " . "bevor er irgendetwas anderes pruefen kann.\n\n" . "Signiert wird die kanonische Release-Zeile (`canonical-line-v1`): die Felder " . "`dc-release-v1`, product_slug, version, channel, platform, sha256_hash (klein " . "geschrieben), download_url und size_bytes, verbunden mit \\n. Geprueft wird " . "`manifest_signature` (base64) mit RSA-SHA256.\n\n" . "404 bedeutet, dass auf diesem Server kein Signierschluessel hinterlegt ist.", 'security' => [], 'responses' => [ '200' => [ 'description' => 'Oeffentlicher Schluessel im PEM-Format', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'algorithm' => ['type' => 'string', 'example' => 'RSA-SHA256'], 'format' => ['type' => 'string', 'example' => 'canonical-line-v1'], 'public_key' => ['type' => 'string', 'description' => 'PEM'], 'fingerprint' => ['type' => 'string', 'description' => 'SHA256 ueber das PEM, zum Wiedererkennen.'], ], ]]], ], '404' => $errorResponse, ], ], ], '/api/updateservice/v1/publish' => [ 'post' => [ 'tags' => ['UpdateService'], 'summary' => 'Release veroeffentlichen', 'description' => "Scope updateservice:publish. Schliesst automatisch alle Bugtracker-Items, deren " . "resolved_in_build dieser Version entspricht.\n\n" . "Die Signatur wird serverseitig gebildet und ist nicht Teil des Aufrufs - der " . "Packager laeuft auf Entwicklerrechnern und darf den Signierschluessel nicht besitzen.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['product_slug', 'version', 'download_url'], 'properties' => [ 'product_slug' => ['type' => 'string'], 'version' => ['type' => 'string', 'example' => '1.4.3'], 'channel' => ['type' => 'string', 'default' => 'prod'], 'platform' => ['type' => 'string', 'default' => 'any', 'example' => 'win-x64'], 'download_url' => ['type' => 'string'], 'sha256_hash' => ['type' => 'string', 'pattern' => '^[0-9a-fA-F]{64}$'], 'git_commit' => ['type' => 'string'], 'size_bytes' => ['type' => 'integer'], 'release_notes' => ['type' => 'string'], 'is_critical' => ['type' => 'boolean'], 'manifest_json' => [ 'type' => 'object', 'description' => 'Dateimanifest des Pakets (Pfade, Groessen, SHA256, preserve-Liste). ' . 'Damit kann die API als vollwertiger Rueckfall dienen, wenn die ' . 'statische latest.json fehlt. Maximal 4 MB.', ], ], ]]], ], 'responses' => ['201' => ['description' => 'Angelegt'], '200' => ['description' => 'Aktualisiert'], '401' => $errorResponse], ], ], '/api/setup/v1/login' => [ 'post' => [ 'tags' => ['Setup'], 'summary' => 'Anmelden und ein kurzlebiges Setup-Token holen', 'description' => "Ohne Token erreichbar - hier entsteht das Token erst.\n\n" . "Gedacht fuer Konten der Rolle `installer`, die sich ihrerseits **nicht** am " . "WebUI anmelden koennen. Die Zugangsdaten werden auf jedem Zielsystem " . "eingegeben; mit einem Administratorkonto verteilte man damit den Zugang zur " . "gesamten Verwaltung.\n\n" . "Das Token laeuft nach 30 Minuten ab, traegt nur `setup:install` und wird " . "nicht mitgeschrieben - es ist nach der Ausgabe nirgends mehr abrufbar.\n\n" . "Konten mit hinterlegtem TOTP-Geheimnis werden mit 501 abgewiesen: eine " . "TOTP-Pruefung gibt es noch nicht, und sie stillschweigend zu uebergehen " . "waere ein Rueckschritt.", 'security' => [], 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['username', 'password'], 'properties' => [ 'username' => ['type' => 'string'], 'password' => ['type' => 'string', 'format' => 'password'], 'hostname' => ['type' => 'string', 'description' => 'Rechnername, erscheint im Token-Namen und im Protokoll.'], ], ]]], ], 'responses' => [ '201' => [ 'description' => 'Angemeldet', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'setup_token' => ['type' => 'string'], 'expires_at' => ['type' => 'string', 'format' => 'date-time'], 'expires_in' => ['type' => 'integer', 'example' => 1800], 'scopes' => ['type' => 'array', 'items' => ['type' => 'string']], 'role' => ['type' => 'string', 'enum' => ['admin', 'installer']], ], ]]], ], '401' => $errorResponse, '429' => $errorResponse, '501' => $errorResponse, ], ], ], '/api/setup/v1/catalog' => [ 'get' => [ 'tags' => ['Setup'], 'summary' => 'Installierbare Anwendungen fuer dieses System', 'description' => "Scope `setup:catalog`. Zeigt ausschliesslich, was zur angefragten " . "Laufzeitkennung passt - ein Projekt, von dem es nur ein Windows-Paket gibt, " . "taucht auf einem Linux-Rechner gar nicht erst auf.\n\n" . "Eine WebUI-Session wird hier bewusst **nicht** akzeptiert.", 'parameters' => [ ['name' => 'platform', 'in' => 'query', 'schema' => ['type' => 'string', 'example' => 'win-x64'], 'description' => 'Laufzeitkennung des Zielsystems. Ohne Angabe gelten nur Releases mit platform=any.'], ], 'responses' => [ '200' => [ 'description' => 'Katalog', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'platform' => ['type' => 'string'], 'count' => ['type' => 'integer'], 'projects' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'slug' => ['type' => 'string'], 'name' => ['type' => 'string'], 'notes' => ['type' => 'string', 'nullable' => true], 'channels' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'channel' => ['type' => 'string'], 'version' => ['type' => 'string'], 'platform' => ['type' => 'string'], 'size_bytes' => ['type' => 'integer'], 'is_critical' => ['type' => 'boolean'], 'signed' => ['type' => 'boolean'], 'download_url' => ['type' => 'string'], ], ], ], ], ], ], ], ]]], ], '401' => $errorResponse, ], ], ], '/api/setup/v1/token' => [ 'post' => [ 'tags' => ['Setup'], 'summary' => 'Dauertoken fuer die eingerichtete Anwendung', 'description' => "Scope `setup:install`. Stellt das Token aus, mit dem die frisch installierte " . "Anwendung spaeter Heartbeats sendet und Fehler meldet.\n\n" . "**Welche Rechte vergeben werden, entscheidet der Server.** Die angefragten " . "`scopes` koennen nur einschraenken; erlaubt sind ausschliesslich " . "`watchdog:ping`, `watchdog:read`, `bugtracker:report` und " . "`updateservice:read`. Andernfalls waere der Umweg ueber ein kurzlebiges " . "Setup-Token wirkungslos - wer einmal installieren darf, stellte sich sonst " . "ein Token mit allen Rechten aus und behielte es.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['project'], 'properties' => [ 'project' => ['type' => 'string'], 'scopes' => ['type' => 'array', 'items' => ['type' => 'string']], 'hostname' => ['type' => 'string'], ], ]]], ], 'responses' => [ '201' => [ 'description' => 'Token ausgestellt - wird nur einmal ausgeliefert', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'token' => ['type' => 'string'], 'project' => ['type' => 'string'], 'scopes' => ['type' => 'array', 'items' => ['type' => 'string']], ], ]]], ], '401' => $errorResponse, '403' => $errorResponse, '404' => $errorResponse, ], ], ], '/api/setup/v1/agent' => [ 'post' => [ 'tags' => ['Setup'], 'summary' => 'Watchdog-Agent fuer die Maschine selbst', 'description' => "Scope `setup:install`. Legt den Monitor an, stellt ein Token mit genau " . "`watchdog:ping` aus und liefert das fertige Skript fuer diesen Host - " . "Shell unter Linux, PowerShell unter Windows.\n\n" . "Gedacht fuer den Installer, der ohnehin auf dem Zielsystem laeuft: der Weg " . "ueber die Oberflaeche verlangte Monitor anlegen, Token erzeugen, Skript " . "herunterladen, kopieren und einplanen - fuenf Schritte an einem anderen " . "Rechner.\n\n" . "Einen bereits vorhandenen Monitor gleichen Namens lehnt der Endpunkt mit " . "`409 monitor_exists` ab. Sonst uebernaehme eine zweite Maschine " . "stillschweigend die Historie der ersten; `overwrite: true` erlaubt es " . "ausdruecklich.\n\n" . "Das Skript enthaelt das Token im Klartext und wird nur einmal ausgeliefert.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['project', 'source'], 'properties' => [ 'project' => ['type' => 'string', 'description' => 'Produkt, an das das Token gebunden wird.'], 'source' => ['type' => 'string', 'description' => 'Name des Monitors im Dashboard.'], 'os' => ['type' => 'string', 'enum' => ['linux', 'windows'], 'default' => 'linux'], 'interval' => ['type' => 'integer', 'default' => 60, 'description' => 'Erwarteter Abstand der Heartbeats in Sekunden (30 bis 3600).'], 'group' => ['type' => 'string', 'description' => 'Gruppierung im Dashboard.'], 'parent' => ['type' => 'string', 'description' => 'Uebergeordneter Monitor, etwa der Hypervisor.'], 'overwrite' => ['type' => 'boolean', 'default' => false, 'description' => 'Einen bestehenden Monitor gleichen Namens uebernehmen.'], ], ]]], ], 'responses' => [ '201' => [ 'description' => 'Monitor eingerichtet, Skript enthaelt das Token', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'source' => ['type' => 'string'], 'os' => ['type' => 'string'], 'filename' => ['type' => 'string'], 'script' => ['type' => 'string'], 'schedule' => ['type' => 'string', 'description' => 'Einplanungsbefehl; {PFAD} steht fuer den Ablageort.'], 'interval' => ['type' => 'integer'], 'token_id' => ['type' => 'string'], 'existing' => ['type' => 'boolean'], ], ]]], ], '401' => $errorResponse, '403' => $errorResponse, '404' => $errorResponse, '409' => $errorResponse, ], ], ], '/api/watchdog/v1/ping' => [ 'post' => [ 'tags' => ['Watchdog'], 'summary' => 'Heartbeat senden', 'description' => 'Scope watchdog:ping. Alternativ ein Agent-Token aus watchdog_agent_tokens.', 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['source'], 'properties' => [ 'source' => ['type' => 'string', 'description' => 'Eindeutiger Name des Dienstes oder Hosts.'], 'instance' => ['type' => 'string', 'default' => 'default'], 'type' => [ 'type' => 'string', 'enum' => ['heartbeat', 'host', 'hypervisor_node', 'guest'], 'default' => 'heartbeat', 'description' => 'Bestimmt die Einordnung in der Hierarchie.', ], 'group' => [ 'type' => 'string', 'description' => 'Gruppierung im Dashboard, z. B. "Applications". Alias: group_key.', ], 'version' => [ 'type' => 'string', 'description' => 'Version der laufenden Anwendung, erscheint in der Monitorliste. Alias: app_version. Optional - bestehende Agenten senden es nicht.', 'example' => '1.4.3', ], 'status' => [ 'type' => 'string', 'enum' => ['ok', 'warning', 'error', 'stopped', 'maintenance'], 'description' => 'stopped und maintenance sind angekuendigte Zustaende - der Evaluator meldet dafuer keinen Ausfall.', ], 'interval' => ['type' => 'integer', 'description' => 'Erwarteter Abstand in Sekunden. Nach dem Doppelten gilt der Monitor als auffaellig, nach dem Vierfachen als ausgefallen.'], 'message' => ['type' => 'string'], 'checks' => [ 'type' => 'object', 'description' => 'Selbst ermittelter Gesundheitszustand, z. B. {"db":{"ok":true},"feed":{"ok":false,"message":"..."}}. Die Namen werden nicht interpretiert; eine fehlgeschlagene Pruefung stuft einen als ok gemeldeten Heartbeat auf warning herab.', ], 'metrics' => ['type' => 'object', 'description' => 'Numerische Werte; landen im Verlauf und sind ueber /metrics abrufbar.'], 'os' => ['type' => 'string'], ], ]]], ], 'responses' => ['200' => ['description' => 'Empfangen'], '401' => $errorResponse], ], ], '/api/watchdog/v1/event' => [ 'post' => [ 'tags' => ['Watchdog'], 'summary' => 'Einzelnes Ereignis protokollieren', 'description' => "Scope watchdog:ping, alternativ ein Agent-Token. Fuer einmalige Vorkommnisse " . "statt zyklischer Meldungen - etwa ein angekuendigtes Herunterfahren oder eine " . "wiederhergestellte Verbindung.\n\n" . "Zustandswechsel durch Heartbeats werden ohnehin selbst protokolliert; dieser " . "Endpunkt ist fuer alles, was dazwischen passiert.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['source'], 'properties' => [ 'source' => ['type' => 'string'], 'instance' => ['type' => 'string', 'default' => 'default'], 'kind' => ['type' => 'string', 'enum' => EventLog::KINDS, 'default' => 'started'], 'severity' => ['type' => 'string', 'enum' => EventLog::SEVERITIES, 'default' => 'info'], 'message' => ['type' => 'string'], 'from_state' => ['type' => 'string'], 'to_state' => ['type' => 'string'], 'meta' => ['type' => 'object', 'description' => 'Beliebiger strukturierter Zusatzkontext.'], ], ]]], ], 'responses' => ['201' => ['description' => 'Protokolliert'], '401' => $errorResponse], ], ], '/api/watchdog/v1/status' => [ 'get' => [ 'tags' => ['Watchdog'], 'summary' => 'Alle Monitore abrufen', 'description' => 'Scope watchdog:read. Liefert jede Zeile aus watchdog_monitors, einschliesslich app_version und group_key.', 'responses' => [ '200' => [ 'description' => 'Monitorliste', 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'count' => ['type' => 'integer'], 'monitors' => ['type' => 'array', 'items' => ['$ref' => '#/components/schemas/Monitor']], ], ]]], ], '401' => $errorResponse, ], ], ], '/api/watchdog/v1/events' => [ 'get' => [ 'tags' => ['Watchdog'], 'summary' => 'Ereignisprotokoll lesen', 'description' => 'Scope watchdog:read.', 'parameters' => [ ['name' => 'limit', 'in' => 'query', 'schema' => ['type' => 'integer', 'default' => 50]], ['name' => 'source', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'instance', 'in' => 'query', 'schema' => ['type' => 'string']], ['name' => 'severity', 'in' => 'query', 'schema' => ['type' => 'string', 'enum' => EventLog::SEVERITIES]], ], 'responses' => ['200' => ['description' => 'Ereignisliste'], '401' => $errorResponse], ], ], '/api/watchdog/v1/evaluate' => [ 'get' => [ 'tags' => ['Watchdog'], 'summary' => 'Evaluationslauf ausloesen', 'description' => 'Nur mit Shared Key oder angemeldeter Sitzung. Per Cron minuetlich aufrufen, sonst bleiben ausgefallene Monitore gruen.', 'responses' => ['200' => ['description' => 'Ergebnis des Laufs'], '401' => $errorResponse], ], ], // ------------------------------------------------------------------ // Lizenzmodul // // Fehlte hier bisher vollstaendig, obwohl README und Prompt-Vorlage // Agenten ausdruecklich auf diese Beschreibung verweisen. Wer sich // daran orientierte, konnte das Lizenzsystem nicht anbinden - und // kannte insbesondere dessen abweichendes Antwortformat nicht. // ------------------------------------------------------------------ '/api/license/v1/validate' => [ 'post' => [ 'tags' => ['Lizenz'], 'summary' => 'Lizenz und Hardware pruefen', 'description' => "Bewusst ohne Token erreichbar: ausgelieferte Anwendungen pruefen hier ihre Lizenz.\n\n" . "**Abweichendes Antwortformat.** Diese Antwort traegt NICHT den " . "`status`/`error`-Umschlag der uebrigen API. `status` auf oberster Ebene ist der " . "Lizenzzustand (`valid`, `not_found`, `revoked`, `suspended`, `expired`, " . "`activation_limit`), nicht `success`. Das ist ein bereits ausgerollter Vertrag - " . "ein Umschlag mit `status: \"success\"` wuerde von jedem bestehenden Client als " . "\"nicht valid\" gelesen.\n\n" . "**Nur ein gelesenes Urteil ist ein Urteil.** Ein HTTP-Fehler (429 durch " . "Drosselung, 5xx) ist keine Aussage ueber die Lizenz. Clients muessen in diesem " . "Fall ihren lokalen Cache verwenden und es spaeter erneut versuchen, statt die " . "Anwendung zu blockieren.\n\n" . "`cache_ttl_hours` bestimmt, wie lange ohne Serverkontakt weitergearbeitet " . "werden darf. Der Wert stammt aus `dc_projects.default_cache_ttl_hours` und ist " . "auszuwerten - nicht das Ablaufdatum der Lizenz dafuer heranzuziehen.\n\n" . "Begrenzt auf 120 Anfragen pro Minute und IP.", 'security' => [], 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['product', 'license_key', 'hardware_id'], 'properties' => [ 'product' => ['type' => 'string', 'description' => 'Slug aus dc_projects.'], 'license_key' => ['type' => 'string', 'example' => 'XXXXX-XXXXX-XXXXX-XXXXX-XXXXX'], 'hardware_id' => ['type' => 'string', 'example' => '2:win:a765bd47...', 'description' => 'Hardware-ID v2, Format 2::<64 Hex>.'], 'legacy_hardware_id' => ['type' => 'string', 'description' => 'Hardware-ID v1. Mitschicken, damit eine bestehende Aktivierung ohne zusaetzlichen Platz auf v2 umzieht.'], 'hwid_version' => ['type' => 'integer', 'default' => 2], 'hwid_source' => ['type' => 'string', 'example' => 'machine-guid'], 'platform' => ['type' => 'string', 'example' => 'win'], 'hostname' => ['type' => 'string'], 'app_version' => ['type' => 'string', 'example' => '1.4.3', 'description' => 'Version der Anwendung; erscheint in der Aktivierungsliste.'], 'nonce' => ['type' => 'string', 'description' => 'Zufallswert, wird unveraendert zurueckgegeben.'], ], ]]], ], 'responses' => [ '200' => [ 'description' => 'Lizenzurteil (auch bei ungueltiger Lizenz)', 'content' => ['application/json' => ['schema' => ['$ref' => '#/components/schemas/LicenseResult']]], ], '429' => $errorResponse, ], ], ], '/api/license/v1/deactivate' => [ 'post' => [ 'tags' => ['Lizenz'], 'summary' => 'Aktivierung freigeben', 'description' => "Scope license:deactivate, alternativ der `shared_key` aus config/config.php als " . "`Authorization: Bearer`. Geschuetzt, weil sonst jeder fremde Installationen " . "abmelden koennte.\n\n" . "Der Schluessel gehoert auf den Administrationsrechner, nicht in die " . "ausgelieferte Anwendung. Fuer Einzelfaelle genuegt der Knopf **Freigeben** in " . "der Hardware-Liste des WebUI.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['product', 'license_key', 'hardware_id'], 'properties' => [ 'product' => ['type' => 'string'], 'license_key' => ['type' => 'string'], 'hardware_id' => ['type' => 'string'], 'nonce' => ['type' => 'string'], ], ]]], ], 'responses' => ['200' => ['description' => 'Freigegeben'], '401' => $errorResponse], ], ], '/api/license/v1/dev-key' => [ 'post' => [ 'tags' => ['Lizenz'], 'summary' => 'Entwicklungsschluessel abholen (Selbstbedienung)', 'description' => "Scope `license:dev`. Gedacht fuer Werkbank-Token auf Testmaschinen.\n\n" . "**Antwortet im Hausformat** (`status: \"success\"`) - anders als `validate` und " . "`deactivate` daneben, die ihr abweichendes Altformat behalten.\n\n" . "**Der Aufruf ist wiederholbar.** Fuer dieselbe Kombination aus `project_slug` " . "und `instance_id` kommt derselbe Schluessel zurueck und seine Frist wird " . "verlaengert. Ein Testskript darf ihn deshalb an den Anfang jedes Laufs setzen, " . "ohne bei jedem Lauf einen weiteren Schluessel zu hinterlassen. `created` sagt, " . "ob er neu ist.\n\n" . "Der ausgestellte Schluessel ist eine gewoehnliche Lizenz: befristet (Vorgabe 30 " . "Tage, einstellbar ueber `license.dev_ttl_days`), mit wenigen Aktivierungen, und " . "in `customer_email` als Testschluessel markiert. Er oeffnet damit auch die " . "Release-Ablage des Produkts - die `.htpasswd` wird beim Ausstellen sofort " . "nachgezogen.\n\n" . "Projektgebundene Token kommen nur an ihr eigenes Projekt; die Ausnahme fuer " . "Werkbank-Token gilt hier **nicht**. Ein Lizenzschluessel oeffnet Pakete, eine " . "Meldung tut das nicht.\n\n" . "Zum Zurueckgeben `action_mode: \"revoke\"` senden. Abgelaufene Schluessel " . "verschwinden ohnehin von selbst.\n\n" . "Begrenzt auf 10 Anfragen pro Minute und IP, und auf 12 gleichzeitig gueltige " . "Schluessel je Projekt.", 'requestBody' => [ 'required' => true, 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'required' => ['project_slug', 'instance_id'], 'properties' => [ 'project_slug' => ['type' => 'string', 'description' => 'Slug aus dc_projects.'], 'instance_id' => ['type' => 'string', 'example' => 'testvm', 'description' => 'Kennung der Maschine. Ueber sie findet ein wiederholter Aufruf denselben Schluessel wieder; ein FQDN wird auf den kurzen Namen gekuerzt.'], 'ttl_days' => ['type' => 'integer', 'example' => 30, 'description' => 'Gewuenschte Laufzeit, begrenzt auf 90 Tage.'], 'action_mode' => ['type' => 'string', 'enum' => ['issue', 'revoke'], 'default' => 'issue'], ], ]]], ], 'responses' => [ '200' => ['description' => 'Bestehender Schluessel verlaengert oder zurueckgegeben'], '201' => ['description' => 'Neuer Schluessel ausgestellt'], '400' => $errorResponse, '401' => $errorResponse, '403' => $errorResponse, '409' => $errorResponse, '429' => $errorResponse, ], ], ], '/api/license/v1/status' => [ 'get' => [ 'tags' => ['Lizenz'], 'summary' => 'Verfuegbarkeitspruefung des Moduls', 'security' => [], 'responses' => ['200' => ['description' => 'Modulzustand']], ], ], '/api/tokens/v1/provision' => [ 'post' => [ 'tags' => ['Tokens'], 'summary' => 'Sub-Token aus Master-Token erzeugen', 'description' => 'Master-Token im Header X-Master-Token. Rechte koennen nur eingeschraenkt, nicht erweitert werden.', 'requestBody' => [ 'content' => ['application/json' => ['schema' => [ 'type' => 'object', 'properties' => [ 'client_name' => ['type' => 'string'], 'instance_id' => ['type' => 'string'], 'scopes' => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => TokenManager::KNOWN_SCOPES]], 'environment' => ['type' => 'string', 'enum' => TokenManager::ENVIRONMENTS], ], ]]], ], 'responses' => ['201' => ['description' => 'Sub-Token erstellt'], '403' => $errorResponse], ], ], ], ]; // Direkte Ausgabe statt Http::ok(), damit die Spezifikation nicht in einen // status-Umschlag verpackt wird. if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: public, max-age=300'); } echo json_encode($spec, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);