{
  "openapi": "3.1.0",
  "info": {"title": "Neworia Bot Control API", "version": "1.0.0", "description": "Fest auf den Neworia-Bot begrenzt. Bearer-Tokens werden nur im Authorization-Header akzeptiert, nie in URLs. Token-Erstellung und -Widerruf, Nutzerverwaltung und configure benötigen persönliche Browser-Cookie-Anmeldung. Jede Cookie-Schreibaktion braucht Origin https://deploy.neworia.net und X-CSRF-Token aus GET /api/me. Bearer-Anfragen dürfen Origin weglassen; eine vorhandene Origin muss exakt passen. Kein CORS. Tokens gelten 90 Tage, Sessions 12 Stunden. Entwickler sehen und verwenden nur eigene Releases/Aufträge; Owner darf alle sehen. Neue Versionen werden direkt live veröffentlicht, sobald der Worker sie erfolgreich verarbeitet."},
  "servers": [{"url": "https://deploy.neworia.net"}],
  "security": [{"bearerAuth": []}, {"cookieAuth": []}],
  "components": {
    "securitySchemes": {
      "bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "nwct_opaque"},
      "cookieAuth": {"type": "apiKey", "in": "cookie", "name": "__Host-nwcontrol"}
    },
    "schemas": {
      "Id": {"type": "string", "pattern": "^[a-f0-9]{32}$"},
      "Empty": {"type": "object", "additionalProperties": false},
      "Error": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}}},
      "User": {"type": "object", "properties": {"username": {"type": "string", "enum": ["ventiix", "juro"]}, "role": {"type": "string", "enum": ["owner", "developer"]}}},
      "AuthResult": {"type": "object", "properties": {"user": {"$ref": "#/components/schemas/User"}, "csrf": {"type": ["string", "null"]}}},
      "Release": {"type": "object", "properties": {"id": {"$ref": "#/components/schemas/Id"}, "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, "size": {"type": "integer"}, "label": {"type": "string"}, "actor": {"type": "string"}, "created_at": {"type": "number"}}},
      "Job": {"type": "object", "properties": {"id": {"$ref": "#/components/schemas/Id"}, "kind": {"type": "string", "enum": ["deploy", "restart", "stop", "configure"]}, "actor": {"type": "string"}, "payload": {"type": "object", "description": "Nur release_id bei deploy. Niemals Geheimnisse."}, "status": {"type": "string", "enum": ["queued", "running", "succeeded", "failed"]}, "created_at": {"type": "number"}, "started_at": {"type": ["number", "null"]}, "finished_at": {"type": ["number", "null"]}, "result": {"type": ["object", "null"], "properties": {"code": {"type": "string"}, "message": {"type": "string"}, "release_id": {"$ref": "#/components/schemas/Id"}}}}},
      "JobRequest": {"oneOf": [
        {"type": "object", "additionalProperties": false, "required": ["kind", "release_id"], "properties": {"kind": {"const": "deploy"}, "release_id": {"$ref": "#/components/schemas/Id"}}},
        {"type": "object", "additionalProperties": false, "required": ["kind"], "properties": {"kind": {"enum": ["restart", "stop"]}}},
        {"type": "object", "additionalProperties": false, "required": ["kind", "config"], "properties": {"kind": {"const": "configure"}, "config": {"type": "object", "additionalProperties": false, "minProperties": 1, "description": "Nur Owner mit Browser-Sitzung. Speicherung führt zu ready_for_activation, niemals automatischem Botstart.", "properties": {"DISCORD_TOKEN": {"type": "string", "writeOnly": true, "minLength": 1, "maxLength": 2048}, "DISCORD_CLIENT_SECRET": {"type": "string", "writeOnly": true, "minLength": 1, "maxLength": 2048}, "DISCORD_CLIENT_ID": {"type": "string", "pattern": "^[0-9]{17,22}$"}, "DASHBOARD_OWNER_IDS": {"type": "string", "pattern": "^[0-9]{17,22}(,[0-9]{17,22}){0,9}$"}}}}}
      ]}
    },
    "parameters": {
      "Id": {"name": "id", "in": "path", "required": true, "schema": {"$ref": "#/components/schemas/Id"}},
      "Origin": {"name": "Origin", "in": "header", "schema": {"type": "string", "const": "https://deploy.neworia.net"}, "description": "Bei Cookie-Schreibaktionen erforderlich. Bei Bearer optional; falls vorhanden exakt diese Origin."},
      "Csrf": {"name": "X-CSRF-Token", "in": "header", "schema": {"type": "string"}, "description": "Bei Cookie-Schreibaktionen erforderlich; aus GET /api/me. Für Bearer nicht erforderlich."}
    },
    "responses": {"Error": {"description": "Fehler: 400 ungültig, 401 Anmeldung, 403 Rechte/Origin/CSRF, 404 nicht gefunden, 409 Limit/Version verwendet, 411 Länge fehlt, 413 zu groß, 415 Inhaltstyp, 429 ausgelastet, 503 vorübergehend nicht verfügbar.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}
  },
  "paths": {
    "/health": {"get": {"security": [], "summary": "Control-HTTP-Dienst erreichbar", "responses": {"200": {"description": "ok:true"}}}},
    "/api/auth/login": {"post": {"security": [], "summary": "Persönliche Anmeldung", "parameters": [{"$ref": "#/components/parameters/Origin"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "required": ["username", "password"], "properties": {"username": {"type": "string", "enum": ["ventiix", "juro"]}, "password": {"type": "string", "writeOnly": true, "maxLength": 128}}}}}}, "responses": {"200": {"description": "Session-Cookie gesetzt", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthResult"}}}}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/auth/claim": {"post": {"security": [], "summary": "Einmalige Einladung einlösen", "description": "Code kommt aus dem URL-Fragment und wird vom Frontend sofort aus der Adresszeile entfernt. 24 Stunden gültig, atomar einmalig. Origin ist erforderlich.", "parameters": [{"$ref": "#/components/parameters/Origin"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "required": ["code", "password"], "properties": {"code": {"type": "string", "writeOnly": true}, "password": {"type": "string", "writeOnly": true, "minLength": 12, "maxLength": 128}}}}}}, "responses": {"200": {"description": "Passwort gesetzt und Session-Cookie erstellt", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthResult"}}}}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/auth/logout": {"post": {"summary": "Browser-Sitzung beenden", "parameters": [{"$ref": "#/components/parameters/Origin"}, {"$ref": "#/components/parameters/Csrf"}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Empty"}}}}, "responses": {"200": {"description": "Cookie und Sitzung entfernt"}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/me": {"get": {"summary": "Aktuelles Konto und CSRF-Token", "responses": {"200": {"description": "CSRF ist bei Bearer null", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthResult"}}}}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/status": {"get": {"summary": "Bereinigter Botstatus", "responses": {"200": {"description": "service, bot_ready, setup_mode, updated_at; keine Secrets oder fremden Aufträge"}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/releases": {
      "get": {"summary": "Eigene Versionen; Owner sieht alle", "responses": {"200": {"description": "releases[]", "content": {"application/json": {"schema": {"type": "object", "properties": {"releases": {"type": "array", "items": {"$ref": "#/components/schemas/Release"}}}}}}}, "default": {"$ref": "#/components/responses/Error"}}},
      "post": {"summary": "Quellcode-ZIP hochladen", "description": "Raw ZIP, kein multipart. Maximal 32 MiB; Content-Length erforderlich; kein Transfer-Encoding. Höchstens 20 Releases/256 MiB pro Konto. Upload führt keinen Code aus und startet keinen Deploy. Vollständige Archivprüfung erfolgt separat durch den Worker.", "parameters": [{"$ref": "#/components/parameters/Origin"}, {"$ref": "#/components/parameters/Csrf"}, {"name": "X-Filename", "in": "header", "required": true, "schema": {"type": "string", "example": "bot.zip"}, "description": "ASCII-Dateiname ohne Pfad, endet auf .zip"}, {"name": "X-Release-Label", "in": "header", "schema": {"type": "string", "example": "Version%202"}, "description": "Optionaler percent-encoded UTF-8-Versionsname; decodiert 1–80 Zeichen"}], "requestBody": {"required": true, "content": {"application/zip": {"schema": {"type": "string", "format": "binary", "maxLength": 33554432}}}}, "responses": {"201": {"description": "Release gespeichert", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Release"}}}}, "default": {"$ref": "#/components/responses/Error"}}}
    },
    "/api/releases/{id}": {"delete": {"summary": "Eigenen Upload löschen; Owner darf alle löschen", "description": "409, solange ein queued/running Job die Version verwendet. Die bereits installierte Bot-Version und historische Jobs bleiben erhalten.", "parameters": [{"$ref": "#/components/parameters/Id"}, {"$ref": "#/components/parameters/Origin"}, {"$ref": "#/components/parameters/Csrf"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Empty"}}}}, "responses": {"200": {"description": "Upload gelöscht"}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/jobs": {
      "get": {"summary": "Letzte 50 eigene Aufträge; Owner sieht alle", "responses": {"200": {"description": "jobs[]", "content": {"application/json": {"schema": {"type": "object", "properties": {"jobs": {"type": "array", "items": {"$ref": "#/components/schemas/Job"}}}}}}}, "default": {"$ref": "#/components/responses/Error"}}},
      "post": {"summary": "Deploy, Neustart oder Owner-Aktion beauftragen", "description": "Developer: eigenes deploy oder restart. Owner zusätzlich stop/configure. configure verlangt Cookie-Anmeldung. Maximal 4 queued/running Jobs global, 2 pro Konto; Owner-stop hat einen getrennten zusätzlichen Platz. Worker prüft Freigaben erneut vor der Ausführung.", "parameters": [{"$ref": "#/components/parameters/Origin"}, {"$ref": "#/components/parameters/Csrf"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/JobRequest"}}}}, "responses": {"202": {"description": "id,kind,status=queued"}, "default": {"$ref": "#/components/responses/Error"}}}
    },
    "/api/jobs/{id}": {"get": {"summary": "Eigenen Auftrag lesen; Owner darf alle lesen", "parameters": [{"$ref": "#/components/parameters/Id"}], "responses": {"200": {"description": "Auftrag", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Job"}}}}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/tokens": {
      "get": {"summary": "Metadaten eigener aktiver API-Tokens", "responses": {"200": {"description": "tokens[] mit id,label,created_at,expires_at; kein Tokenwert"}, "default": {"$ref": "#/components/responses/Error"}}},
      "post": {"security": [{"cookieAuth": []}], "summary": "Eigenes API-Token erstellen", "description": "Nur persönliche Browser-Anmeldung. Maximal 5 aktive Tokens. Klartextwert wird ausschließlich in dieser Antwort angezeigt. 90 Tage gültig.", "parameters": [{"$ref": "#/components/parameters/Origin"}, {"$ref": "#/components/parameters/Csrf"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "required": ["label"], "properties": {"label": {"type": "string", "minLength": 1, "maxLength": 60}}}}}}, "responses": {"201": {"description": "id, token (einmalig!), expires_at"}, "default": {"$ref": "#/components/responses/Error"}}}
    },
    "/api/tokens/{id}": {"delete": {"security": [{"cookieAuth": []}], "summary": "Eigenes API-Token widerrufen", "parameters": [{"$ref": "#/components/parameters/Id"}, {"$ref": "#/components/parameters/Origin"}, {"$ref": "#/components/parameters/Csrf"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Empty"}}}}, "responses": {"200": {"description": "Token widerrufen"}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/users": {"get": {"summary": "Owner: Kontostatus lesen", "responses": {"200": {"description": "users[] mit username,role,enabled"}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/users/juro/disable": {"post": {"security": [{"cookieAuth": []}], "summary": "Owner: Juro sperren und alle Control-Zugänge widerrufen", "description": "Löscht Sessions, API-Tokens und Einladungen; queued Jobs werden failed. Laufender Bot und externe Discord-Developer-Team-Rechte bleiben separat.", "parameters": [{"$ref": "#/components/parameters/Origin"}, {"$ref": "#/components/parameters/Csrf"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Empty"}}}}, "responses": {"200": {"description": "Zugang gesperrt"}, "default": {"$ref": "#/components/responses/Error"}}}},
    "/api/users/juro/enable": {"post": {"security": [{"cookieAuth": []}], "summary": "Owner: Persönliche Anmeldung für Juro wieder freigeben", "description": "Alte Sessions, Einladungen und API-Tokens bleiben widerrufen.", "parameters": [{"$ref": "#/components/parameters/Origin"}, {"$ref": "#/components/parameters/Csrf"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Empty"}}}}, "responses": {"200": {"description": "Konto freigegeben"}, "default": {"$ref": "#/components/responses/Error"}}}}
  }
}
