JuByteNexus – API-Vertrag v1 (verbindlich)

API-First: ACP und UCP sind nur Clients dieser API. Basis-URL: http://localhost:8080/api/v1.

Konventionen

  • JSON, UTF-8. Fehlerobjekt immer: { "code": "NX-1042", "message": "...", "docsUrl": "https://docs.jubyte.net/errors/NX-1042" }
  • Auth per Authorization: Bearer <accessToken> (JWT, 15 min). Refresh-Token-Rotation.
  • Pagination: ?cursor=<opaque>&limit=50 → Antwort { "items": [...], "nextCursor": "..." | null }
  • Permissions == API-Scopes (z. B. bansystem.ban.create).

Auth

MethodePfadBodyAntwort
POST/auth/login{email, password, totp?}{accessToken, refreshToken, user} oder 401 NX-1001; bei aktivem 2FA ohne totp: 401 NX-1002
POST/auth/refresh{refreshToken}{accessToken, refreshToken} (Rotation, Reuse-Detection → 401 NX-1003)
POST/auth/logout{refreshToken}204
GET/auth/me{id, email, displayName, mcName, mcId, rankLabel, rankColor, roles: [..], permissions: [..], totpEnabled}mcName/mcId = verknüpfter MC-Account (genau einer pro E-Mail), rankLabel/rankColor = Team-Rang fürs Account-Chip
POST/auth/totp/setup{secret, otpauthUrl} (ACP zeigt otpauthUrl als QR-Code)
POST/auth/totp/confirm{code}204
POST/auth/webauthn/login/options{email}Public; {ceremonyId, publicKey:{challenge, rpId, allowCredentials, …}} für navigator.credentials.get()
POST/auth/webauthn/login{ceremonyId, credentialId, authenticatorData, clientDataJSON, signature, userHandle?}Public; verifiziert die Assertion (webauthn4j) → {accessToken, refreshToken, user}
POST/auth/webauthn/register/optionsAuth; {ceremonyId, publicKey:{challenge, rp, user, pubKeyCredParams, …}} für navigator.credentials.create()
POST/auth/webauthn/register{ceremonyId, attestationObject, clientDataJSON, transports?, label?}Auth; speichert den Passkey → 204
GET/DELETE/auth/webauthn/credentials[/{id}]Auth; eigene Passkeys auflisten/entfernen

Passkeys (WebAuthn): Login passwortlos per Passkey; alle Binärfelder base64url-codiert. Challenges liegen kurz (5 min) serverseitig in-memory, geschlüsselt per ceremonyId. RP-Id = nexus.webauthn.rp-id oder der Origin-Host.

User-Objekt: {id, email, displayName, roles: [{id,name}], permissions: ["bansystem.view", ...], totpEnabled}

Players (PlayerManagement)

MethodePfadBeschreibung
GET`/players?query=<nameuuid>&cursor=&limit=`
GET/players/{id}Spielerakte: {id, name, game, gameId, online, currentServer, firstSeen, lastSeen, playtimeSeconds, sessions: [{serverId, serverName, joinedAt, leftAt}], notes: [{id, author, text, createdAt}]}
GET/players/overviewQuick-Stats: {total, online, newLast7d}; Permission core.player.view
POST/players/{id}/kick{reason}202; Permission core.player.kick; auditiert
POST/players/{id}/message{message}202 (Ingame-Nachricht via Bridge); Permission core.player.message; auditiert
POST/players/{id}/notes{text} → Note; Permission playermanagement.notes.write
GET/players/{id}/ipsIP-Historie: {items: [{ip, firstSeen, lastSeen, joinCount}]}; Permission playermanagement.ip.view; jeder Zugriff wird auditiert (DSGVO)
GET/players/{id}/altsAlt-Account-Erkennung über geteilte IPs: {items: [{playerId, name, gameId, online, firstSeen, lastSeen, sharedIps, sharedJoins, confidence}]}; Permission playermanagement.ip.view; auditiert
GET/players/ranksVault-Rang-Katalog {items: [{name, color}]}color automatisch aus LuckPerms/Permission-System (Gruppen-Prefix); Permission core.player.view
POST/players/{id}/rank{rank, exclusive?} → setzt den Ingame-Rang via Vault-Bridge (command.setrank {gameId, rank, exclusive}). exclusive=false (Default) fügt die Gruppe hinzu (vorhandene Ränge bleiben); exclusive=true ersetzt sie (für Team-Rollen). Permission playermanagement.rank.set; auditiert

Vault-Rang-Bridge: Der Ingame-Primärrang (Vault-Gruppe) wird beim player.join als Feld rank hochgemeldet und im Player gespeichert; das Plugin meldet seinen Gruppen-Katalog per vault.groups. Setzt das ACP einen Rang, sendet der Core command.setrank {gameId, rank} an den Server, wo die Vault-Bridge die Gruppe setzt. Spieler-DTO enthält rank.

Die Bridge sendet beim player.join optional das Feld ip; der Core speichert es pro Session (player_sessions.player_ip) und aggregiert es in player_ip_history. Der DSGVO-Export (/ucp/me/data-export) enthält die IP-Historie des eigenen Accounts.

Servers (Bridges)

MethodePfadBeschreibung
GET/servers`{items: [{id, name, type: "PAPER"
POST/servers/tokens{name}{token} Einmal-Token für Bridge-Setup; Permission core.server.manage
POST/servers/{id}/broadcast{message}202

Servers (Gruppen)

| POST | /servers/{id}/group | {group} – Server einer Gruppe zuordnen (leer = entfernen); Permission core.server.manage. Config-Scopes lösen über die Gruppe auf |

Modules

MethodePfadBeschreibung
GET/modules`{items: [{id, name, version, description, enabled, health: "OK"
POST/modules/{id}/enable / /disable204; Premium ohne Entitlement → 403 NX-2001 LICENSE_REQUIRED
GET/modules/{id}/config?scope=Schema-getriebene Konfiguration an einem Scope (16.2): {scope, items: [{key, type, defaultValue, description, value, origin, changed}]}origin zeigt die Herkunft des wirksamen Werts (default/global/group:<g>/server:<s>)
PUT/modules/{id}/config?scope={values: {key: value}} – Scope global (Default), group:<name> oder server:<name>; leerer Wert an Gruppe/Server entfernt den Override (Vererbung); validiert, Audit-Diff, Event config.changed (Hot-Reload). Auflösung: Server → Gruppe des Servers → Global → Default

Roles & Team

MethodePfadBeschreibung
GET/permissionsAlle registrierten Permissions, gruppiert: {items: [{key, module, description}]}
GET/POST/roles{id, name, permissions: [..], system, teamRole, requireTotp, ingameRank, color, colorOverride}; POST/PUT {name, permissions, ingameRank?, color?, teamRole?, requireTotp?}. Systemrollen sind editierbar (Name gesperrt, Rechte/Rang/Farbe/Flags änderbar; nur Löschen blockiert). color = ACP-Override, sonst Auto-Farbe der Vault-Gruppe. teamRole steuert, wer als Staff zählt; requireTotp erzwingt 2FA-Setup beim nächsten ACP-Login
PUT/DELETE/roles/{id}Update/Löschen (System-Rollen editierbar, aber nicht löschbar → 409 NX-1102); ein Account verlinkt genau einen MC-Account (1:1)
GET/teamACP-Accounts: {id, email, displayName, roles, roleIds, totpEnabled, totpRequired, lastLoginAt, identities: [{playerId, name, gameId, online, rank}]} – inkl. verknüpfter Spiel-Identitäten mit Ingame-Rang
POST/team/invite{email, roleIds} → Einladung (Dev: Account mit Temp-Passwort in Antwort)
PUT/team/{id}/roles{roleIds} → ersetzt die ACP-Rollen eines Mitglieds (Team-Rollen exklusiv); Permission core.team.manage; auditiert

Audit-Log

| GET | /audit?actor=&action=&module=&from=&to=&cursor= | {items: [{id, at, actorEmail, action, module, targetType, targetId, oldValue, newValue, ip, chainHash}]} |

License

| GET | /license | {plan, status, expiresAt, gracePeriod, limits: {servers, players, apiRph}, features: [..], modules: [..]} | | POST | /license/activate | {key} |

Modul: BanSystem (/modules/bansystem/...)

MethodePfadBeschreibung
GET/modules/bansystem/bans?playerId=&active=&type=&cursor={items: [Ban]}
POST/modules/bansystem/bans`{playerId, type: "BAN"
DELETE/modules/bansystem/bans/{id}Revoke ({reason} im Body); Permission bansystem.ban.revoke
GET/POST/modules/bansystem/templatesStrafvorlagen {id, name, type, reason, durationSeconds}; POST braucht bansystem.templates.manage
DELETE/modules/bansystem/templates/{id}Vorlage löschen; Permission bansystem.templates.manage

Ban-Objekt: {id, playerId, playerName, type, reason, createdAt, expiresAt, active, revokedAt, revokedBy, revokeReason, actorEmail}

Ingame-Ban-Modul (Bridge, nicht im Core): /ban, /tempban, /mute, /tempmute, /kick, /warn, /unban, /unmute senden punishment.request/punishment.revoke über die Bridge; der Core legt die Strafe via PunishmentCreator-SPI an. Neue Strafen werden über ban.created an alle Bridges verteilt und bei BAN/TEMPBAN/KICK sofort gegen Online-Spieler durchgesetzt (Kick), nicht erst beim nächsten Login. /punish <Spieler> <Vorlage> wendet eine Strafvorlage per Name an (Core löst Typ/Grund/Dauer auf). Ingame-/report <Spieler> <VorlagenName> übernimmt Kategorie + Grund der Report-Vorlage.

Modul: ReportSystem (/modules/reports/...)

MethodePfadBeschreibung
GET/modules/reports?status=&playerId=&limit={items: [Report], openCount}; Permission reports.view
POST/modules/reports/{id}/claimClaim-Workflow; teleportiert die Ingame-Identität des Bearbeiters zum Ziel (sofern beide online); Permission reports.manage
POST/modules/reports/{id}/resolve{resolution, dismiss?}RESOLVED/DISMISSED
GET/POST/modules/reports/templatesReport-Vorlagen {id, name, category, reason, suggestedPunishment?, suggestedDurationSeconds?}; POST braucht reports.templates.manage
DELETE/modules/reports/templates/{id}Vorlage löschen; Permission reports.templates.manage

Report-Objekt: {id, reporterPlayerId?, reporterName, targetPlayerId?, targetName, category, reason, serverName, status: "OPEN"|"CLAIMED"|"RESOLVED"|"DISMISSED", claimedBy?, resolution?, chatLog?, createdAt, resolvedAt?}. Ingame-Quellen: /report <Spieler> <Grund> (Kategorie GENERAL) und /reportchat <Spieler> [Grund] (Kategorie CHAT) – Bridge-Event player.report mit optionalem Feld category. Nur CHAT-Reports bekommen einen chatLog-Snapshot der letzten Chat-Zeilen des Ziels (via ChatLogProvider-SPI), der im Report-Detail direkt angezeigt wird. Eine Report-Vorlage mit Kategorie CHAT erzeugt ebenfalls CHAT-Reports. Spam-Schutz: max. offene Reports pro Melder (Config spam.max-open-per-reporter).

Modul: TicketSystem (/modules/tickets/...)

MethodePfadBeschreibung
GET/modules/tickets?status=&limit=Staff-Inbox {items: [Ticket], openCount}; Permission tickets.view
GET/modules/tickets/{id}Ticket inkl. messages (auch interne Notizen)
POST/modules/tickets/{id}/reply{body, internal?}; Permission tickets.manage
POST/modules/tickets/{id}/assign{assignee}
POST/modules/tickets/{id}/closeSchließen
GET/POST/modules/tickets/mySelf-Service (UCP): eigene Tickets / `{subject, category: "UNBAN"
GET/modules/tickets/my/{id}Eigenes Ticket inkl. Verlauf (ohne interne Notizen)
POST/modules/tickets/my/{id}/reply / /close / /ratingAntworten, schließen, bewerten ({rating: 1-5}, nur nach Schließung)

Entbannungsanträge (ban.appeal.created) haben ihren eigenen Appeals-Posteingang und erzeugen daher standardmäßig kein Ticket. Wer möchte, kann pro Antrag ein UNBAN-Ticket spiegeln lassen (Opt-in via Config appeals.create-tickets, Soft-Dependency über den Event-Bus).

Modul: ChatGuard (/modules/chatguard/...)

MethodePfadBeschreibung
GET/modules/chatguard/messages?playerId=&q=&flagged=&limit=Durchsuchbares Chat-Archiv; Permission chatguard.view
GET/POST/modules/chatguard/rulesFilterregeln `{name, type: "WORD"
DELETE/modules/chatguard/rules/{id}Regel löschen

WORD-Regeln matchen leetspeak-normalisiert; Caps-/Wiederholungs-Spam wird engine-seitig erkannt. WARN/MUTE eskalieren über die PunishmentCreator-SPI ins BanSystem (ohne BanSystem: Degradierung zu FLAG). Aufbewahrung per Retention-Job (Config retention-days, DSGVO).

Modul: Statistics (/modules/stats/...)

MethodePfadBeschreibung
GET/modules/stats/overviewNetzwerk-Analytics: {trackedPlayers, totalJoins, totalPlaytimeSeconds, dailyJoins: {date: n}, hourlyJoins: {"00".."23": n}}; Permission stats.view
GET`/modules/stats/leaderboard?metric=playtimejoins`
GET/modules/stats/mePersönliche Stats über alle verknüpften Identitäten (UCP)

Modul: Maintenance (/modules/maintenance/...)

MethodePfadBeschreibung
GET/modules/maintenance/state{active, message}; Permission maintenance.view
GET/POST/modules/maintenance/whitelistWhitelist-Einträge {playerName, durationSeconds?} (zeitlich begrenzt möglich); Schreiben: maintenance.manage
DELETE/modules/maintenance/whitelist/{id}Eintrag entfernen

Der Wartungs-Schalter selbst ist Modul-Config (PUT /modules/maintenance/config, Keys maintenance.enabled/maintenance.message) – ein Mechanismus für alles (Audit, Hot-Reload). Beim Join werden Nicht-Whitelist-Spieler über die PlayerActions-SPI gekickt; Event maintenance.blocked.

Modul: Friends (/modules/friends/my/..., Self-Service)

MethodePfadBeschreibung
GET/modules/friends/my{friends, incoming, outgoing, blocked} – Freund-Objekte {id, displayName, email, status, online, since}; online = irgendeine verknüpfte Spiel-Identität ist online (netzwerkweit)
POST/modules/friends/my/requests{email} → PENDING-Anfrage; geblockte Anfragende erhalten dieselbe Antwort wie „Account nicht gefunden“
POST/modules/friends/my/requests/{id}/accept / /declineAnfrage beantworten (nur Adressat)
DELETE/modules/friends/my/{id}Freundschaft/Anfrage entfernen
POST/modules/friends/my/block{email} → blockt (entfernt bestehende Relation, verhindert neue Anfragen)

Events: friend.requested, friend.accepted, friend.removed. Party (gemeinsamer Serverwechsel) folgt mit der Proxy-Move-Capability der Bridges.

Modul: AntiCheat-Hub (/modules/anticheat-hub/..., Pro)

Premium-Modul: ohne Lizenz-Entitlement anticheat-hub ist die Aktivierung gesperrt (403 NX-2001).

MethodePfadBeschreibung
POST/modules/anticheat-hub/violationsAdapter-API für Anticheats: `{gameId?
GET/modules/anticheat-hub/violations?playerId=&limit=Violations (Spielerakte/Übersicht); Permission anticheat.view

Korrelation: Severity-Summe im Fenster (Config correlation.window-hours); ab escalation.threshold automatischer Tempban über die PunishmentCreator-SPI, einmal pro Fenster (Marker-Eintrag ESCALATION). Events: anticheat.violation, anticheat.escalated.

Modul: Vote & Rewards (/modules/votes/...)

MethodePfadBeschreibung
POST/modules/votes/hook/{siteId}Vote-Callback (öffentlich, Auth per Header X-Vote-Token der Seite): {playerName}{streak, totalVotes}
GET/modules/votes/sitesUCP: aktive Vote-Links + eigene Streak/Gesamt über verknüpfte Identitäten
GET/POST/modules/votes/admin/sitesACP: Seiten verwalten; POST {name, url} erzeugt Token + Hook-Pfad; Permission votes.manage
DELETE/modules/votes/admin/sites/{id}Seite entfernen
GET/modules/votes/admin/topTop-Voter (Gesamt, Streak, zuletzt); Permission votes.view

Streak = aufeinanderfolgende UTC-Tage mit Vote. Jeder Vote publiziert vote.received (für Reward-Ketten/Webhooks), dankt dem Spieler ingame und broadcastet optional (Config).

Modul: Shop (/modules/shop/..., Pro)

Premium-Modul (Entitlement shop). Bestellungen sind eine Zustandsmaschine PENDING_PAYMENT → PAID → DELIVERED (oder CANCELLED); PAID-Bestellungen bilden die Command-Queue mit Zustellgarantie: Auslieferung wird alle 30 s wiederholt, bis der Server des Spielers den Konsolen-Befehl annimmt (Capability EXECUTE, Bridge-Frame command.execute).

MethodePfadBeschreibung
GET/modules/shop/catalogStorefront (UCP): aktive Produkte {id, name, description, priceCents, currency, category}
POST/modules/shop/ordersCheckout: {productId, playerId}playerId muss eine verknüpfte Identität des Käufers sein
GET/modules/shop/orders/myEigene Bestellhistorie
GET/POST/modules/shop/admin/productsACP: Produkte (POST mit {name, description?, priceCents, currency?, category, deliveryCommand?}{player} wird ersetzt); Permission shop.manage
DELETE/modules/shop/admin/products/{id}Produkt entfernen
GET/modules/shop/admin/ordersAlle Bestellungen; Permission shop.view
POST/modules/shop/admin/orders/{id}/mark-paidManuelle Zahlungsbestätigung – PSP-Webhooks (Stripe/PayPal) nutzen denselben Übergang
POST/modules/shop/admin/orders/{id}/cancelStornieren (nicht nach Auslieferung)

Zahlungen: Sandbox-Provider (Config payments.sandbox, Default an) bestätigt sofort. Events: shop.order.created/paid/delivered/cancelled. Belege sind Produkt-Snapshots (Preisänderungen wirken nie rückwirkend).

Nachrichten-Editor (/messages)

MethodePfadBeschreibung
GET/messagesKatalog aller spielersichtbaren Texte: `{items: [{key, description, defaultContent, content (Override
PUT/messages/{key}{content} – Override setzen (auditiert mit Diff)
DELETE/messages/{key}Override entfernen (zurück zum Standard)

Permission core.messages.manage. Jede Änderung publiziert messages.changed; der Core konvertiert serverseitig nach Legacy-§ (inkl. §x-Hex; Gradients werden echt gerendert: pro Zeichen interpolierte §x-Hex-Codes, Multi-Stop unterstützt, {platzhalter} bleiben als Token erhalten) und pusht das Bundle als messages.sync an alle verbundenen Bridges – Hot-Reload ohne Server-Neustart. Beim Bridge-Handshake wird das Bundle initial gesendet; die Bridge fällt vor dem ersten Sync auf eingebaute Defaults zurück.

Observability

| GET | /actuator/health | Liveness (öffentlich) | | GET | /actuator/prometheus | Prometheus-Metriken (öffentlich; hinter Reverse-Proxy ggf. abschirmen) |

Webhooks (/webhooks)

MethodePfadBeschreibung
GET/POST/webhooks`{name, url, secret?, eventPattern (z. B. "ban.*"), format: "GENERIC"
PUT/DELETE/webhooks/{id}Ändern/Löschen (Hot-Reload, kein Neustart)
POST/webhooks/{id}/testSendet ein Test-Event, Antwort enthält delivered

Zustellung asynchron mit Retry/Backoff; GENERIC ist HMAC-signiert (X-Nexus-Signature: sha256=<hex> über den Body), DISCORD liefert Embeds. embedTemplate (nur DISCORD) ist das JSON eines Embed-Builders (z. B. embed.dan.onl) — ein einzelnes Embed-Objekt oder ein komplettes Payload mit embeds; die Platzhalter {type}, {label}, {at}, {data} und {data.<feld>} werden pro Event ersetzt. eventTitles ist eine Map Event-Typ → Anzeigetitel (z. B. {"ban.created": "Neuer Bann"}): sie bestimmt den Titel des Standard-Embeds und den {label}-Platzhalter. Ohne Template wird das eingebaute Standard-Embed gesendet.

Setup (First-Run-Wizard, öffentlich bis zur Ersteinrichtung)

MethodePfadBeschreibung
GET/setup/status{needsSetup} – true solange kein Staff-Account existiert
POST/setup{email, password (≥10), displayName, networkName?} → Owner-Account; danach dauerhaft 400

UCP

MethodePfadBeschreibung
POST/ucp/register{email, password, displayName}
POST/ucp/link{code} – Einmal-Code aus /nexus link ingame
GET/ucp/me/punishmentsEigene Strafen (über verknüpfte Identitäten)
POST/ucp/me/punishments/{id}/appeal{message} → Entbannungsantrag
GET/ucp/me/data-exportDSGVO-Export als JSON

Realtime API (WebSocket)

ws://localhost:8080/api/v1/ws?token=<accessToken>

Nachrichten (Server→Client): { "type": "<event>", "data": {...}, "at": "<iso>" } Events: player.join, player.quit, ban.created, ban.revoked, server.online, server.offline. Client→Server: { "type": "subscribe", "events": ["player.*", "ban.*"] } (Default: alles, was die Permissions erlauben).

Standard-Fehlercodes

CodeBedeutung
NX-1001Ungültige Zugangsdaten
NX-10022FA-Code erforderlich
NX-1003Refresh-Token ungültig/wiederverwendet
NX-1042Validierungsfehler
NX-1101Keine Berechtigung
NX-1102System-Rolle nicht veränderbar
NX-1090Rate-Limit überschritten (429, Retry-After-Header; Login/Setup/Registrierung pro Client-IP)
NX-2001Lizenz/Entitlement erforderlich
NX-3001Bridge-Token ungültig

Dev-Seed (Profil dev)

  • Admin: [email protected] / admin1234 (Rolle Owner, alle Permissions, 2FA aus)
  • Beispiel-Spieler + Beispiel-Bans werden geseedet.
  • CORS erlaubt: http://localhost:5173, http://localhost:5174.