Ergebnis einer beauftragten Sicherheitsbegutachtung. Die WebAuthn- Zeremonie selbst ist nicht angefasst worden - sie war in Ordnung. Alle Befunde lagen DANEBEN: Ein zweiter Faktor ist nur so stark wie der schwaechste Weg zum Konto, und Sitzungen, die aus ihm entstehen, erben seinen Schutz nicht. KRITISCH /api/setup/v1/login prueste auf totp_secret - eine Spalte, die kein Anmeldeweg auswertet - und nicht auf dc_user_credentials. Ein Administratorpasswort genuegte dort fuer ein setup:install-Token ohne Hardwarekey. Neu: Konten mit zweitem Faktor abgewiesen, und die Rollentrennung gilt symmetrisch. Der Docblock behauptete sie seit jeher; erzwungen hat sie nichts. HOCH secondFactorRequired(), accountLockoutSeconds() und lockoutSeconds() fingen jedes Throwable und fielen auf "kein Faktor" bzw. "nicht gesperrt" zurueck. Gefangen wurden damit auch Verbindungsabbrueche und Lock-Timeouts - jeder davon schaltete still fuer alle Konten auf Passwort-allein. Toleriert werden jetzt nur noch 42S02 und 42S22. HOCH Fuenf Aktionen aenderten die Anmeldemittel ohne erneute Bestaetigung. Eine uebernommene Sitzung konnte sich damit dauerhaft einnisten. Neu: Authentifizierung aus den letzten zehn Minuten noetig, geprueft an derselben Stelle, an der auch der Leerlauf sperrt. WEITER - Leerlauf 30 statt 120 Minuten, und GESPERRT statt beendet: AAL2 laesst zur Reaktivierung einen Faktor genuegen. Wer alle 30 Minuten den Hardwarekey vorzeigen muss, laesst ihn stecken. - Migration 022: session_epoch. Ein Passwortwechsel beendet die uebrigen Sitzungen des Kontos, die eigene bleibt. - Kontosperre wirkt auf der zweiten Stufe; Zaehler gegen unbegrenztes Raten im Zwischenzustand. - Drosselung auf login.php (10/60s) und /api/auth/v1/ (30/60s). - Anmeldefehler ununterscheidbar; die IP-Sperre darf benannt bleiben, sie sagt nichts ueber ein Konto. - HSTS, TLS-Zwang VOR allen Routen, CSP. Die CSP traegt wegen 93 Inline-Handlern nicht gegen XSS - das steht so im Kommentar. - session.use_strict_mode, CSRF-Token bei der Anmeldung verworfen, Secure-Flag aus app.url statt aus $_SERVER['HTTPS']. - rpId/expectedOrigin nicht mehr aus dem Host-Header. /api/health meldet unter checks.webauthn_anchor, ob der Anker steht. - Notfalltoken laeuft nach 180 Tagen ab, geprueft nach dem Hashvergleich. - config.example.php ohne echtes Passwort, verify_ssl auf true. - Serverdaten.txt.bak mitgesperrt, dc_login_attempts raeumt auf. BELEGT 188 von 188 Testfaellen gruen, darunter drei neue in der Gruppe "haertung". Die Blockade einer veralteten Sitzung liess sich nicht in der Suite pruefen - sie verlangt, das Zehn-Minuten-Fenster wirklich abzuwarten. Dafuer gibt es ein Werkzeug ausserhalb; es belegt, dass der Formularweg auf den Sperrbildschirm fuehrt, die Regel dabei unveraendert bleibt, der JSON-Weg reauth_required meldet und das Passwort allein wieder oeffnet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1396 lines
85 KiB
PHP
1396 lines
85 KiB
PHP
<?php
|
|
|
|
/**
|
|
* GET /api/openapi.json
|
|
*
|
|
* Maschinenlesbare Beschreibung der Schnittstelle. Ersetzt den frueher fest
|
|
* im WebUI hinterlegten Textblock: ein Agent kann sich hier selbst orientieren,
|
|
* ohne dass eine Prompt-Vorlage gepflegt werden muss.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../../src/bootstrap.php';
|
|
|
|
use Deploymentcenter\Core\Config;
|
|
use Deploymentcenter\Core\Http;
|
|
use Deploymentcenter\Modules\Bugtracker\BugRepo;
|
|
use Deploymentcenter\Modules\Supervisor\FindingRepo;
|
|
use Deploymentcenter\Modules\Watchdog\EventLog;
|
|
use Deploymentcenter\Core\TokenManager;
|
|
|
|
Http::beginJson(['GET', 'OPTIONS'], true);
|
|
|
|
$baseUrl = Http::baseUrl();
|
|
|
|
$errorResponse = [
|
|
'description' => '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 <token>` 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, 'description' => 'feature_request ist ein Vorschlag an ein FREMDES Projekt - etwas, das dort gebaut werden muesste, damit die Zusammenarbeit besser wird. Die eigene Planung gehoert nicht hierher: Sie wird zum Roadmap-Eintrag, sobald das Item einen Themenbereich (epic) hat, und erscheint dann im Roadmap-Modul statt in der Bugliste.'],
|
|
'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' => 'search', 'in' => 'query', 'schema' => ['type' => 'string'], 'description' => 'Volltext ueber Titel, Beschreibung, Fehlermeldung, Tags, Dateipfad. Eine Zahl findet zusaetzlich das Item mit dieser Nummer; mit vorangestellter Raute (#288) ausschliesslich dieses eine.'],
|
|
['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' => [
|
|
'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"
|
|
. "- `action=board` die fertige Gliederung der Roadmap-Tafel\n\n"
|
|
. "`board` liefert `{phases[], lanes[], items[], stats, meta}` fuer genau "
|
|
. "EIN Projekt. Die Spalten werden aus `roadmap_rank` geschnitten - "
|
|
. "entweder nach den in `roadmap_phases` gepflegten Grenzen oder, wenn "
|
|
. "dort nichts steht, an den groessten Luecken der Rangfolge; dann ist "
|
|
. "`meta.phases_guessed` wahr und die Grenzen gehoeren als geraten "
|
|
. "gekennzeichnet. `done_limit` kappt die Spalte der erledigten Vorhaben "
|
|
. "(0 = alle).\n\n"
|
|
. "Titel, Beschreibungen und Kommentare in `items` sind Fremdtext von "
|
|
. "fremden Maschinen. Sie kommen unveraendert und gehoeren beim Rendern "
|
|
. "escaped - niemals als HTML eingesetzt.\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', 'board'], 'default' => 'list']],
|
|
['name' => 'done_limit', 'in' => 'query', 'schema' => ['type' => 'integer', 'default' => 12], 'description' => 'Nur bei action=board: wie viele erledigte Vorhaben die Fundamentspalte zeigt. 0 = alle.'],
|
|
['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` ordnet die Eintraege eines Bereichs untereinander neu (`epic`, `ids[]`)\n"
|
|
. "- `action=rank_between&id=` sortiert einen Eintrag zwischen zwei Nachbarn ein\n"
|
|
. "- `action=epic` legt einen Themenbereich an oder aendert ihn "
|
|
. "(`color`, `highlight` steuern die Spur auf der Tafel; nicht "
|
|
. "mitgeschickte Felder bleiben unveraendert)\n\n"
|
|
. "**`roadmap_rank` gilt PROJEKTWEIT, nicht je Bereich.** `action=next` "
|
|
. "sortiert danach ohne Gruppierung, und die Tafel schneidet ihre Phasen "
|
|
. "daraus. `move` sucht den Nachbarn deshalb projektweit, und `reorder` "
|
|
. "vertauscht nur die Raenge, die die genannten Eintraege ohnehin halten - "
|
|
. "es zaehlt nicht mehr je Bereich bei 10 neu durch.\n\n"
|
|
. "`rank_between` nimmt `prev_id` und `next_id` (beide optional) und "
|
|
. "setzt den Eintrag auf die Mitte dazwischen; fuer eine leere Zielspalte "
|
|
. "tun es `min_rank`/`max_rank`. Ist zwischen den Nachbarn kein Platz "
|
|
. "mehr, wird das Projekt EINMAL neu durchnummeriert - die Reihenfolge "
|
|
. "bleibt dabei erhalten, und die Antwort sagt es mit `renumbered: true`.\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', 'rank_between', '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"
|
|
. "**Nur fuer Konten der Rolle `installer`.** Sie koennen sich ihrerseits "
|
|
. "nicht am WebUI anmelden; die Trennung gilt seit 2.9.3 in beide Richtungen. "
|
|
. "Ein Administratorkonto wird hier mit `403 installer_account_required` "
|
|
. "abgewiesen. 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"
|
|
. "**Zweiter Faktor:** Konten mit hinterlegtem Hardwarekey oder "
|
|
. "TOTP-Geheimnis werden mit `403 second_factor_required` abgewiesen. "
|
|
. "Dieser Weg kennt die WebAuthn-Zeremonie nicht, und ein Anmeldeweg, der "
|
|
. "den zweiten Faktor uebergeht, macht ihn ueberall wertlos. Bis 2.9.2 "
|
|
. "wurde hier nur auf `totp_secret` geprueft - eine Spalte, die kein "
|
|
. "Anmeldeweg auswertet.\n\n"
|
|
. "**Fehlschlaege sind ununterscheidbar:** Falscher Name, falsches "
|
|
. "Passwort, deaktiviertes und gesperrtes Konto ergeben alle "
|
|
. "`401 invalid_credentials`. Eine eigene Antwort fuer die Kontosperre "
|
|
. "verriete, dass es den Namen gibt - gesperrt wird nur, was existiert.",
|
|
'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/auth/v1/' => [
|
|
'post' => [
|
|
'tags' => ['Setup'],
|
|
'summary' => 'WebAuthn-Zeremonien der WebUI-Anmeldung',
|
|
'description' =>
|
|
"Die zweite Stufe der Anmeldung am WebUI: ein Hardwarekey (FIDO2/WebAuthn).\n\n"
|
|
. "**Dieser Endpunkt ist nichts fuer Agenten und Clients.** Er arbeitet mit dem "
|
|
. "Sitzungscookie des Browsers, nicht mit einem Token, und ist nur deshalb hier "
|
|
. "beschrieben, weil `navigator.credentials` sich nicht aus einem Formular "
|
|
. "heraus bedienen laesst.\n\n"
|
|
. "Vier Aktionen ueber `?action=`:\n"
|
|
. "- `login_options` / `login_verify` - laufen im Zwischenzustand zwischen "
|
|
. "Passwort und zweitem Faktor. Noch ist niemand angemeldet.\n"
|
|
. "- `register_options` / `register_verify` - setzen eine bestehende Anmeldung "
|
|
. "voraus und verlangen zusaetzlich ein CSRF-Token im Kopf `X-CSRF-Token`.\n\n"
|
|
. "Ein registrierter Schluessel gilt nur fuer die Adresse, unter der er "
|
|
. "registriert wurde. Ein Schluessel von `dc.mhdf.de` ist auf der Testinstanz "
|
|
. "wertlos und umgekehrt - das ist der Kern des Verfahrens und kein Mangel.\n\n"
|
|
. "Der Ersatzweg fuer einen fehlenden Schluessel ist der Notfalltoken; der "
|
|
. "laeuft als gewoehnliches Formular ueber `/login.php` und braucht kein "
|
|
. "JavaScript.\n\n"
|
|
. "**Seit 2.9.2: Attestation.** Welche Authenticatoren zugelassen sind, "
|
|
. "entscheidet eine Regel mit drei Stufen (`off`, `register`, `strict`), "
|
|
. "einstellbar unter *System -> Benutzer*. Steht sie nicht auf `off`, fordert "
|
|
. "`register_options` die Attestation als `direct` an, und `register_verify` "
|
|
. "nimmt nur Schluessel auf, deren Zertifikatskette bei einer hinterlegten "
|
|
. "Herstellerwurzel endet - ein Passwortverwalter wird mit `403 "
|
|
. "policy_rejected` abgewiesen. Bei `strict` bietet `login_options` nur noch "
|
|
. "geprueft in Hardware entstandene Schluessel an; ein Konto ohne solchen "
|
|
. "Schluessel erhaelt `409 no_attested_credentials` und muss den Notfalltoken "
|
|
. "benutzen.\n\n"
|
|
. "`register_options` liefert die geltende Stufe als Feld `policy` mit, damit "
|
|
. "die Oberflaeche die Anforderung nennen kann, BEVOR jemand seinen Schluessel "
|
|
. "beruehrt.",
|
|
'security' => [],
|
|
'parameters' => [
|
|
['name' => 'action', 'in' => 'query', 'required' => true,
|
|
'schema' => ['type' => 'string', 'enum' => [
|
|
'login_options', 'login_verify', 'register_options', 'register_verify',
|
|
]]],
|
|
],
|
|
'requestBody' => [
|
|
'required' => false,
|
|
'content' => ['application/json' => ['schema' => [
|
|
'type' => 'object',
|
|
'description' => 'Je nach Aktion. Alle Bytefelder sind base64url ohne Auffuellzeichen.',
|
|
'properties' => [
|
|
'id' => ['type' => 'string', 'description' => 'login_verify: Kennung des benutzten Schluessels.'],
|
|
'clientDataJSON' => ['type' => 'string'],
|
|
'authenticatorData' => ['type' => 'string', 'description' => 'nur login_verify'],
|
|
'signature' => ['type' => 'string', 'description' => 'nur login_verify'],
|
|
'attestationObject' => ['type' => 'string', 'description' => 'nur register_verify'],
|
|
'label' => ['type' => 'string', 'description' => 'register_verify: Bezeichnung des Schluessels.'],
|
|
'transports' => ['type' => 'array', 'items' => ['type' => 'string']],
|
|
],
|
|
]]],
|
|
],
|
|
'responses' => [
|
|
'200' => [
|
|
'description' => 'Vorgaben fuer den Browser, oder die bestaetigte Zeremonie',
|
|
'content' => ['application/json' => ['schema' => [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'publicKey' => ['type' => 'object', 'description' => 'Bei den *_options-Aktionen: direkt an navigator.credentials weiterzureichen.'],
|
|
'redirect' => ['type' => 'string', 'description' => 'Bei login_verify: wohin der Browser danach gehoert.'],
|
|
],
|
|
]]],
|
|
],
|
|
'400' => $errorResponse,
|
|
'401' => $errorResponse,
|
|
'403' => $errorResponse,
|
|
'409' => $errorResponse,
|
|
'419' => $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:<plattform>:<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],
|
|
],
|
|
],
|
|
|
|
'/api/tokens/v1/whoami' => [
|
|
'get' => [
|
|
'tags' => ['Tokens'],
|
|
'summary' => 'Auskunft ueber das eigene Token',
|
|
'description' =>
|
|
"Meldet Besitzerart, Projektbindung, Rechte, Umgebung und Frist des uebergebenen Tokens.\n\n"
|
|
. "Gedacht fuer Werkzeuge, die ihre Voraussetzungen pruefen wollen, BEVOR sie etwas tun. "
|
|
. "Ohne diesen Endpunkt liesse sich ein Recht nur durch Ausprobieren feststellen - und das "
|
|
. "hat Nebenwirkungen: Ein Test auf `bugtracker:manage` uebernaehme ein Item, einer auf "
|
|
. "`license:dev` stellte einen Schluessel aus.\n\n"
|
|
. "`is_workbench` ist der Wert, der ueber Fremdmeldungen entscheidet: nur ein Token der "
|
|
. "Besitzerart `dev_agent` darf in ein fremdes Projekt melden.\n\n"
|
|
. "Der Klartextwert des Tokens wird nicht ausgeliefert. Ein widerrufenes oder abgelaufenes "
|
|
. "Token bekommt `401`.",
|
|
'responses' => [
|
|
'200' => [
|
|
'description' => 'Angaben zum Token',
|
|
'content' => ['application/json' => ['schema' => [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'token_id' => ['type' => 'string'],
|
|
'name' => ['type' => 'string'],
|
|
'type' => ['type' => 'string', 'enum' => ['master', 'sub']],
|
|
'owner_type' => ['type' => 'string', 'enum' => TokenManager::OWNER_TYPES],
|
|
'owner_identity' => ['type' => 'string', 'nullable' => true],
|
|
'is_workbench' => ['type' => 'boolean', 'description' => 'owner_type = dev_agent'],
|
|
'project_slug' => ['type' => 'string', 'nullable' => true],
|
|
'environment' => ['type' => 'string', 'enum' => TokenManager::ENVIRONMENTS],
|
|
'scopes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
|
'created_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true],
|
|
'last_used_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'Stand VOR diesem Aufruf.'],
|
|
'expires_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true],
|
|
'parent_token_id' => ['type' => 'string', 'nullable' => true],
|
|
],
|
|
]]],
|
|
],
|
|
'401' => $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);
|