ForgeCraftPLUGIN WIKISDocumentationSupport ↗
Web console

ForgeGuard website integration contract

ForgeGuard · 0.6.0-alpha.2

This describes the 0.5.0-alpha.1 website API. Use it when building or redesigning a frontend. site/public/assets/js/api.js (the window.ForgeGuard client) and console.js are complete working references; site/netlify/functions/{auth,servers,link,purchases,capture}.mjs and site/netlify/lib/routes.mjs are the source of truth.

Architecture

Minecraft server: ForgeGuard plugin
  -> outgoing HTTPS polling -> /api/link/device/*  (device credential, one workspace)
Browser: same-origin session requests
  -> /api/auth/*     accounts and sessions
  -> /api/purchases/*  purchase ownership verification (provider exchange stays server-side)
  -> /api/servers/*  workspaces, members, pairing, live control, audit
  -> /api/capture/*  player-enabled screenshots, scoped recorder and staff controls
Netlify Functions + persistent Netlify Blobs

One deployment serves any number of accounts and servers. Each account can belong to many server workspaces; each workspace links to at most one Minecraft server. The browser never connects to a Minecraft port and never receives the plugin credential.

Real screenshots use the separate player-enabled browser flow documented in Client screen capture. Staff controls live under ForgeGuard.capture(serverId, playerId); the player page uses a short-lived recorder bearer, never a staff cookie. The in-game GUI can send an invitation or request a screenshot; the web console displays the resulting image. This does not give the server access to unconsenting clients or provide tamper-proof video.

Request conventions

Send JSON with Content-Type: application/json, Accept: application/json, credentials: "same-origin", cache: "no-store", redirect: "error". Errors are { "error": "machine_code", "message": "…" } with a non-2xx status: show message. The server sets an HttpOnly __Host-fg_session cookie; never read or store it. Session responses include csrf: keep it in memory and send it as X-ForgeGuard-CSRF on every mutation. Cross-origin requests are refused. On 401, end the signed-in UI.

Accounts — /api/auth

Endpoint Body Result
GET session — Session payload, or {authenticated:false, reason, registration, setupRequired, setupTokenConfigured, passwordMinimum}
POST signup {email,name,password,inviteCode?} 201 session payload. Refused when registration is closed; invite mode requires a matching invitation
POST login {email,password,code?} Session payload, or {authenticated:false,twoFactorRequired:true}
POST logout {} {authenticated:false}
POST bootstrap {setupToken,email,name,password} 201; only for invite/closed deployments with no accounts
PATCH account {name} {user}
POST account/delete {password} Deletes the account; 409 owns_shared_server if it is the only owner of a workspace with other members
POST password {currentPassword,newPassword} Replacement session; other sessions end
GET sessions / POST sessions/revoke — / {} List / sign out other devices
POST totp/start · totp/enable · totp/disable {} · {code} · {password} Two-factor enrolment; recovery codes are shown once
GET audit?limit= — This account's own security log

Session payload: {authenticated:true, user:{id,email,name,status,createdAt,lastLoginAt,twoFactor,recoveryRemaining,mustChangePassword}, csrf, expiresInSeconds, absoluteLifetimeSeconds, idleTimeoutSeconds}. Accounts have no site-wide role.

Purchase verification — /api/purchases

GET /api/purchases returns the authenticated account's {required,providers,entitlement,canCreateServers}. Start BuiltByBit verification with POST /api/purchases/builtbybit/start {} using the normal session and CSRF header; the response is {authorizationUrl,expiresAt}. The OAuth callback is handled by the backend. Send customers to purchase.html for the complete flow, including its same-origin continuation step. A callback query parameter or a claimed receipt is not proof of ownership; retrieve the saved entitlement from the API.

When the purchase gate is enabled, server creation and pairing require a verified purchase. Connected control uses the workspace purchaser's entitlement; invited staff do not each need a separate purchase. This does not grant membership or additional capabilities. Show a link to purchase.html on purchase_required, and explain provider configuration or verification failures instead of granting access. Seller credentials and the real product resource ID are still deployment requirements. See Purchase ownership verification (../site/PURCHASE-VERIFICATION.md in the release documentation) for the exact provider response shapes, setup, license rechecks and remaining live storefront acceptance checks.

Workspaces — /api/servers

Endpoint Capability Result
GET /api/servers signed in {servers:[summary], roles:[{role,label,capabilities}]}
POST /api/servers {name} signed in 201 {server:summary} (max 25 owned)
GET invites/probe?code= none {serverName,email,role,expiresAt}
POST invites/accept {code} signed in, e-mail must match {server,message}
GET <id> member {server:summary}
PATCH <id> {name} · DELETE <id> link Rename · delete workspace (revokes the link)
GET <id>/members members {members:[{userId,name,email,role,twoFactor,lastLoginAt,you}], invites:[…], roles}
POST <id>/members {email,role} members 201 {invite, inviteCode, accountExists}: the link is /login.html?invite=<code>, shown once
PATCH <id>/members/<userId> {role} members Last owner protected
DELETE <id>/members/<userId> members, or yourself Remove or leave
DELETE <id>/invites/<inviteId> members Revoke
GET <id>/link member Link view (below)
POST <id>/link/claim {code} · POST <id>/link/unlink {} link Pair / unpair
GET|POST <id>/control/<route> per route Live control (below)
GET <id>/operations/<requestId> the requester, or an owner Poll a queued operation
GET <id>/audit?limit= audit Workspace audit log

A server summary is {id,name,createdAt,role,capabilities,link:{state,online,server,lastSeenAt},players,cases}. A non-member receives the same 404 as a missing workspace.

Roles and capabilities (per workspace): owner = read write moderate inventory.view inventory.edit members audit link; admin = read write moderate inventory.view inventory.edit audit; moderator = read moderate inventory.view; viewer = read. Render controls from the returned capabilities; the API enforces them independently.

Pairing

  1. The member runs /fg link site <this origin> in Minecraft, then generates a code in Link web console.
  2. An owner POST <id>/link/claim {code} → 202 with state:"awaiting_approval". That is not linked yet. Poll GET <id>/link about every five seconds.
  3. The Minecraft administrator approves the claimant in-game. The website cannot approve itself.
  4. state:"active", online:true → load GET <id>/control/status. Allow a short “starting” period (503 link_starting) until the first status upload.

Link view: {state, server:{id,name,serverVersion,pluginVersion}, siteOrigin, lastSeenAt, statusAt, online, claimedAt, expiresAt, claimant:{email,name}}. unlinked returns {state:"unlinked", server:null, siteOrigin, lastSeenAt:null, online:false}.

Live control and queued operations

Everything except status is queued for the plugin. The first response is 202 {requestId, status:"queued", expiresAt, pollAfterMs}. Poll GET <id>/operations/<requestId> until it is no longer 202; the final response carries the plugin's own status and body (for example 409 slot_changed). Never show success on 202, never resubmit a write automatically, and after a 504 operation_expired refresh before offering a retry. There are at most four pending operations per workspace.

Route Method Capability Body → result
status GET read Latest snapshot (below), served from the heartbeat
cases/<uuid> GET read Full case record
cases/<uuid>/replay GET read {caseId,check,replayable,reproduced,tracePackets,message} from the real replay engine
cases/<uuid>/recording GET read Saved visual gameplay clip for this case, or 404 when none exists
recordings/<uuid> GET read Saved visual gameplay clip or capture-in-progress summary
players/<uuid>/recordings GET read {recordings:[summary]}; retained manual and automatic clips
players/<uuid>/recordings POST moderate {recordingId:<stable UUID>,reason:<plain text 1–120 chars>} → capture receipt with id and status
catalogue GET read {minecraft, items:[{id,maxStack,maxDurability,edible,block,blocked?}], enchantments:[{key,max}]}
bans GET read {bans:[{playerId,playerName,reason,createdAt,expiresAt,byConsole}], total}
players/<uuid> GET read Player details: online, world, position, health, food, ping, gameMode, frozen, protected, effects, ban, cases…
players/list GET read {players:[{id,name,online,lastSeen,firstPlayed}],total,onlineCount,offlineCount,truncated,limit}
players/<uuid>/deliveries GET inventory.view {deliveries,total,truncated}, latest 20 receipts
players/<uuid>/deliveries POST inventory.edit {deliveryId,items:[{id,count,name?,lore?,enchantments?,damage?,unbreakable?}],note?} → individual delivery receipt
players/<uuid>/travel POST moderate {action:'teleport',destination:<online-player-uuid>,reason} or {action:'return',reason}
players/<uuid>/inventory GET inventory.view {player, loadedAt, editable, inventory:{slots[41], crafting[4]}, enderChest:{slots[27]}}
players/<uuid>/inventory POST inventory.edit Edit (below) → the fresh inventory plus applied
players/<uuid>/moderate POST moderate {action, reason, seconds?} → {message, player}
config POST write {revision, config} → {revision, mode, config}
reload POST write {revision} → same shape

Slots follow Bukkit: 0–8 hotbar, 9–35 storage, 36 boots, 37 leggings, 38 chestplate, 39 helmet, 40 offhand; the Ender Chest has 0–26. Each slot is {slot, fingerprint, id?, count?, maxStack?, name?, lore?, enchantments?, damage?, maxDamage?, unbreakable?, custom?, display?}. custom:true means the stack carries data the web cannot express; moving or re-counting it preserves it.

display is optional, read-only Minecraft tooltip metadata from the modern core. It contains the effective name component, styled lore components, rarity, and optional hideTooltip, hideEnchantments, hideUnbreakable, hideLore and truncated flags. Components permit text, translate, fallback, with, extra, RGB color and boolean text decorations; explicit false must override inherited styling. Click/hover events, arbitrary NBT and custom fonts are not sent. The display is bounded to 32 lore lines and 24 KiB per item, with bounded component depth/nodes/text. Keep these components separate from the clipped, plain editable name/lore fields, and never include display in an edit or delivery: the server rejects it. Older plugins without this field still receive a readable plain-text preview. The renderer and asset limits are documented in site/MINECRAFT-TEXT.md.

Directory and feature availability

players/list reads the Minecraft server's known player registry, including offline players who have joined before. Results put online players first, then sort by name and UUID. The response contains at most 1,000 players; total, onlineCount and offlineCount describe the complete registry, while truncated:true means the returned list is incomplete. Do not turn an absent entry into a claim that the player does not exist. The console retains direct UUID lookup. lastSeen and firstPlayed are milliseconds; a zero timestamp means no recorded value. Offline inventory editing is not supported: offer an item delivery rather than attempting to edit offline player files.

The status snapshot advertises web.playerDirectory, web.deliveries, web.travel and web.caseReplay. Require the relevant value to be explicitly true, as well as the workspace capability, before offering the new operation. A missing flag means the plugin has not reported support. deliveries follows the server's inventory-edit policy, and travel follows its moderation policy. The plugin rechecks those settings and player protections when executing the request.

Direct item deliveries

The POST delivery payload describes new stacks to add, not slot edits or console commands. Supply 1–36 stacks, each with an explicit item ID and count, and an optional plain-text note of at most 200 characters. copyOf, raw NBT and arbitrary item fields are refused. The inventory metadata limits below also apply; actual items and stack sizes must be valid on the linked Minecraft server. The plugin preserves existing items and does not drop overflow on the ground. An offline player can receive a queued delivery when they next join.

An individual receipt is {deliveryId,playerId,status,createdAt,updatedAt,items,note,actor,detail}; POST also includes a human-readable message. History returns the latest 20 receipts, with total and truncated. Receipt timestamps are milliseconds. The plugin permits at most 100 pending deliveries per player and 10,000 retained receipts overall; delivery_capacity requires waiting for pending work or an operator-reviewed capacity migration. Do not delete receipts to bypass these limits, because they preserve duplicate protection.

Receipt status UI meaning
queued Accepted and waiting for the player or another safe attempt; not delivered yet
applying Minecraft is processing the transfer; check status without issuing a replacement
delivered Minecraft confirmed the item transfer and player-data save
blocked_full No sufficient inventory space; existing items were preserved
blocked_protected Player protection currently prevents the transfer
blocked_policy Server policy or item support currently prevents the transfer
needs_review The outcome cannot be proved after an interrupted or failed transfer; staff must inspect it

The plugin retries pending work on join and about every 30 seconds while the recipient is online. Blocked states remain pending; delivered and needs_review are terminal. Never present queued, applying, a relay 202, or an HTTP success alone as an in-game delivery confirmation. Display the receipt status and detail. In particular, needs_review must not trigger automatic redelivery or a new delivery ID.

Generate a fresh UUID once per intentional delivery, before submitting it. Preserve that UUID and the exact payload when a response is lost, the relay expires or the browser navigates away. Check history first; if a retry is necessary, send the same UUID, player UUID and contents. Minecraft returns the existing receipt for an identical request. Reusing an ID for another player or changed contents returns 409 delivery_conflict. The relay's requestId and the delivery's deliveryId serve different purposes: poll a known relay request without posting again; the delivery ID protects an explicit retry when the transport outcome is uncertain.

The reference console stores pending delivery IDs and payloads in session storage scoped to account, workspace and recipient, and locks submitted contents. It allows editing after a definite HTTP 400 input rejection, but retains uncertain requests for status checks or exact retries. It also waits for room in the four-operation relay queue before posting a delivery, so opening a profile and loading its catalogue, inventory and history cannot immediately crowd out the write.

Visual gameplay recordings

These routes are separate from cases/<uuid>/replay, which reruns a deterministic check. Only expose the recording controls when status.web.recordings === true. Older builds and the portable staff adapter do not acquire a recorder through a website update. See Gameplay recordings for the capture and runtime limits.

A manual capture is accepted only for an online, unprotected player, with the moderate web capability and the plugin's policy enabled. The browser creates recordingId once and keeps the exact UUID and reason for any retry. Reusing that ID with a different payload is refused. Do not invent a second ID after an uncertain response. A queued relay response still requires the usual operation polling; a final plugin response with status:"recording" means capture/persistence is still underway, not that a playable clip has been committed.

Poll recordings/<uuid> at a bounded interval until its status is ready or partial, or let staff return to the recording list. Handle capacity, unavailable recorder, expired records, server disconnects and failed persistence as errors. The reference console automatically checks pending clips for up to a minute and then offers a manual refresh. Never turn a missing clip into synthetic footage. Old case JSON remains usable even when no recording exists.

Version-1 clips contain schema, id, optional caseId, playerId, playerName, source, status, reason, createdMillis, startMillis, triggerMillis, durationMillis, frames, scenes, events, limits and warnings. Wall-clock fields are milliseconds since epoch. Frame/scene/event t values are milliseconds relative to startMillis; the trigger marker is triggerMillis - startMillis. Frames carry the observed player pose and nearby entity snapshots. Scenes carry a world UUID, integer origin:[x,y,z], size:[width,height,depth], block-material palette, and sparse blocks:[[relativeX,relativeY,relativeZ,paletteIndex],…]. Recorded air is explicit: an omitted cell means unknown, not air. Never interpolate across worlds, discontinuities or missing scene data.

The browser viewer draws simplified geometry from those observations. It is not screen capture, an exact client render, an MP4, or proof of cheating. JSON export preserves the original clip data. Public demo clips have source:"synthetic-demo" and explicit demo warnings; live clips come only from the authenticated linked server. Inputs remain within 32 KiB and result bodies within the existing 2 MiB relay limit. Recording access is scoped to the workspace with the same membership, session and purchase checks as other control operations.

Player travel and evidence replay

POST players/<subject-uuid>/travel moves the explicitly selected Minecraft subject. A web account has no implicit Minecraft character. For action:"teleport", require a different online destination player's UUID and clearly name both players before submitting. action:"return" uses the location saved by that web actor's last successful teleport of that subject; no destination is sent. The return location is session state, not a durable waypoint. Both operations require a plain-text reason of up to 120 characters, online players, the moderate capability and the server's travel policy. Protected subjects are refused, and other plugins may cancel movement. The response is {message,player} with fresh subject details. After an uncertain response, inspect the actual location before another travel request; travel does not use delivery UUID semantics.

GET cases/<case-uuid>/replay runs the recorded case through the server's replay engine and returns {caseId,check,replayable,reproduced,tracePackets,message}. reproduced may be null when no replay result is available. Show unsupported/non-replayable outcomes explicitly. A reproduced finding confirms the implementation's result on that recorded trace; it is not proof that a player cheated.

Existing inventory edits

Inventory edit:

{ "container": "inventory",
  "changes": [
    { "slot": 5, "expect": "<fingerprint the member saw>", "item": null },
    { "slot": 7, "expect": "0000000000000000", "item": { "copyOf": 5, "count": 12 } },
    { "slot": 9, "expect": "0000000000000000", "item": { "id": "minecraft:diamond_sword", "count": 1,
      "name": "Blade", "lore": ["Line"], "enchantments": { "minecraft:sharpness": 5 }, "damage": 0, "unbreakable": true } },
    { "slot": 12, "expect": "<fingerprint>", "keep": true } ] }

Every listed slot must still have its expect fingerprint, or nothing changes and the plugin answers 409 slot_changed. copyOf clones the stack that was in that slot (the source must be listed too, as a change or as keep) and then applies any other fields; null clears a field. New items must exist on the server, must not be in web.blocked-items (403 item_blocked), and counts must fit the item's stack size. Limits: 41/27 changes, 8 lore lines of 100 characters, 24 enchantments of level 1–255, plain text only. Unchanged plain name/lore values preserve the source item's full rich components, even when the editable snapshot is clipped. Changing those fields replaces only that field with the submitted plain text; intentional blank lore lines are retained. Count/damage changes do not flatten lore.

Moderation actions are freeze (seconds), unfreeze, kick, tempban (seconds), ban and unban, with a plain reason of up to 120 characters. The server's admin: limits apply; protected players are refused. Travel uses its separate route above. Bans work for offline players by UUID.

Status snapshot fields: product, version, mode, revision, statusAt, onlineCount, maxPlayers, tps, health{…}, players[{id,name,coverage,coverageReason,packets,findings,dropped,clientVersion,world,health,food,gameMode,ping,frozen}], cases[{id,playerId,playerName,createdAtMillis,check,confidence,severity}], config{…}, checks[{key,name,enabled,confidence,enforcement,coverage}], guards{…}, behaviour{enabled,mode,checked,findings,cancelled,lastAttack}, administration{enabled,activeFreezes,activeBans,maxFreezeSeconds,defaultFreezeSeconds,maxTempBanSeconds,maxReasonLength}, web{config,moderation,inventoryView,inventoryEdit,playerDirectory,deliveries,travel,caseReplay}, link{…}. Missing values mean “not reported”, not zero.

Check keys: packet.invalid-number, packet.movement-rate, movement.horizontal-envelope, movement.air-trajectory, timing.window, combat.reach, combat.hit-angle, combat.attack-rate, combat.multi-target, world.fast-break, world.fast-place, world.scaffold, movement.ground-spoof, movement.hover. A full case's finding.check uses the Java enum name (COMBAT_REACH, …).

The configuration editor manages only the flat movement settings (mode, the four movement checks and six thresholds, as in 0.4). behaviour:, guards:, admin: and web: are edited in config.yml and reloaded.

Error codes to handle

Code Meaning
validation, code_invalid, weak_password Fix the input
unauthenticated, session_ended, credentials, totp_invalid Sign in again / wrong credentials
forbidden, csrf, registration_closed, invite_required, invite_email Not allowed; refresh the session for csrf
not_found Workspace missing or not yours
already_linked, code_used, link_pending, link_changed, last_owner, conflict, owns_shared_server State conflict; explain and refresh
revision_conflict, disk_revision_conflict Configuration changed; fetch latest
slot_changed Inventory changed in-game; refresh and review
delivery_conflict Delivery ID already belongs to different contents or another player; inspect its receipt
delivery_capacity Pending or retained delivery capacity reached; preserve receipts and inspect capacity
invalid_delivery, invalid_item Delivery input was rejected; correct the request
purchase_required Verify the purchaser's ownership through purchase.html
configuration_unavailable, verification_unavailable Purchase provider is unconfigured or cannot currently verify access; do not invent an entitlement
item_blocked, unknown_item, invalid_count, protected, web_disabled, player_offline, moderation_refused Refused by the plugin; show the message
locked, rate_limited, queue_full Wait (honour Retry-After)
link_missing, link_starting, link_offline, link_revoked Server not linked, starting or offline
operation_expired, operation_missing Outcome uncertain; refresh before retrying
no_functions, timeout, offline (client-side) Static upload or network problem

Device protocol (plugin only; never call from a browser)

POST /api/link/device/start {deviceId,secret,serverName,serverVersion,pluginVersion} → {deviceId,code,expiresAt,siteOrigin,pollAfterSeconds}. Afterwards the plugin sends X-ForgeGuard-Device and Authorization: Bearer <secret> to poll ({status?, results?:[{id,status,body}]} → {state, commands[≤1], pollAfterSeconds, …}), approve {claimId} and revoke {}. pollAfterSeconds is 1 while work is pending or the console is in use, otherwise 5. The protocol is unchanged from 0.4 apart from that hint and the new command routes.

Search all plugin wikis.