# ForgeGuard website — deployment

The ForgeGuard site is one Netlify deploy that serves **everyone**:

- **Public pages**, including a free inventory editor that needs no account.
- **A console** where anyone can create an account, add their Minecraft servers, invite staff with
  per-server roles, and manage players, inventories, bans, evidence and detection from any browser.
- **Server functions** for accounts, workspaces, purchase verification, the plugin's outbound pairing
  relay and player-enabled screenshots, plus scheduled screenshot cleanup.

The site does not host Minecraft. Each server links with a code from its admin GUI and a final
in-game approval, then connects **out** over HTTPS: no port forwarding, reverse proxy or inbound
control port. See the [pairing guide](public/downloads/PAIRING-GUIDE.md).

---

## 1. Install the plugin

Website pairing is part of the modern **ForgeGuard 0.6.0-alpha.2** core. It needs **Paper**, the Java
version that server requires, and **PacketEvents 2.14.0 for Spigot**. Paper 1.21.11 uses Java 21 or
newer. The movement model still covers only its documented native 1.21.11 contexts. Use a staging
server first; [Version support](public/downloads/VERSION-SUPPORT.md) lists what was qualified for
earlier builds.

1. Stop the Paper server normally.
2. Put `packetevents-spigot-2.14.0.jar` and the ForgeGuard 0.6.0-alpha.2 protected JAR in `plugins/`.
3. Start the server, confirm both enable, and run `/forgeguard health`.
4. Leave `protection.mode`, `guards.mode` and `behaviour.mode` at `OBSERVE` while you collect
   baseline data.

Do not install this Paper plugin into a Forge or NeoForge mod server. The universal loader's
portable staff-only fallback cannot pair with the website.

---

## 2. Deploy to Netlify

Use Node.js 22 or newer. Work in the directory that contains `package.json` and `netlify.toml`.
Install and authenticate the Netlify CLI, create a project and run `netlify link`. See
[Netlify's CLI setup guide](https://docs.netlify.com/api-and-cli-guides/cli-guides/get-started-with-cli/).

```bash
npm ci
npm test          # unit, relay and DOM tests, no network required
npm run build     # verifies every asset reference and writes the security headers
netlify deploy --prod
```

Alternatively, connect the repository and set Netlify's base directory to this website directory.
`netlify.toml` declares the build command, `public` as the publish directory and `netlify/functions`
as the functions directory.

### Netlify Blobs

Accounts, sessions, workspaces, memberships, invitations, throttling counters, pairing records,
status snapshots, queued operations and audit logs live in Netlify Blobs under the production store
`forgeguard-control`. Deploy previews use a separate suffixed store. Production functions refuse to
run without durable storage. A drag-and-drop upload of `public/` has no functions, so it only gives
you the demo console and the inventory editor.

### Function routing

Every function declares its own routes, so no redirect rules are needed:

| Function | Routes |
|---|---|
| `netlify/functions/auth.mjs` | `/api/auth`, `/api/auth/*` |
| `netlify/functions/servers.mjs` | `/api/servers`, `/api/servers/*` |
| `netlify/functions/link.mjs` | `/api/link/device/*` (the plugin only) |
| `netlify/functions/purchases.mjs` | `/api/purchases`, `/api/purchases/*` |
| `netlify/functions/capture.mjs` | `/api/capture`, `/api/capture/*` |
| `netlify/functions/capture-cleanup.mjs` | Scheduled every five minutes on published deployments; no public route |

### Player-enabled screen capture

Deploy both the updated JAR and this full website source to enable the native and web screenshot
controls. The player must open their private Minecraft invitation, enable sharing and select their
game window in a supported desktop browser. Production requires HTTPS. Preserve the supplied
`display-capture=(self)` permissions policy and `blob:` image/media CSP directives.

Screenshots use private Blobs records, never `public/`. Only the latest bounded PNG is retained,
for at most 15 minutes, and capture authorization expires after 30 minutes. Stop/revocation clears
the latest image earlier. Check that Netlify lists `capture-cleanup` as a scheduled function after
deployment; reads enforce expiry even before physical cleanup. Deploy previews do not run scheduled
functions. See [Client screen capture](public/downloads/CLIENT-SCREEN-CAPTURE.md) for the complete
workflow and evidence limitations. This feature provides screenshots, not video clips or covert capture.

---

## 3. Environment variables

Set these under **Site configuration → Environment variables**, scoped to Functions, then redeploy.

For a paid public service, also configure `FORGEGUARD_PURCHASE_REQUIRED=true` and the store settings
in [PURCHASE-VERIFICATION.md](PURCHASE-VERIFICATION.md). Buyers create an account, open
`/purchase.html`, verify store ownership, then add and pair a server. Invited staff use the server
owner's entitlement and do not need to buy another copy. A local preview does not prove that seller
credentials, an OAuth callback or a production deployment have been configured.

| Variable | Default | Purpose |
|---|---|---|
| `FORGEGUARD_REGISTRATION` | `open` | `open`: anyone can create an account. `invite`: an account needs a workspace invitation. `closed`: no new accounts except the first via the setup token. |
| `FORGEGUARD_SETUP_TOKEN` | unset | Only for `invite` or `closed` deployments with no accounts yet. At least 24 random characters. Remove it once the first account exists. |
| `FORGEGUARD_ALLOW_LOCAL_HTTP` | unset | `true` permits plain-HTTP loopback pairing for local development. Ignored when Netlify runs the function. |
| `FORGEGUARD_FORCE_MEMORY_STORE` | unset | Forces the in-memory store. Tests and local runs only. |

0.5 removed `FORGEGUARD_BRIDGE_URL` and `FORGEGUARD_BRIDGE_TOKEN`: a shared site cannot hold one
server's bridge credential. Link servers with codes instead. The plugin's own authenticated
loopback bridge (`control:` in `config.yml`) is unchanged for self-hosted tooling.

**Upgrading a 0.4 deployment** needs no manual step. On the first request after deploying, the
existing single-server link, accounts and roles become one workspace named after the linked
server. Owners stay owners, admins stay admins, viewers stay viewers, and the plugin keeps its
pairing. The migration runs once.

---

## 4. First steps on the site

1. Open `/login.html?signup=1` and create an account. On `closed` or `invite` deployments with no
   accounts yet, the page offers the setup-token claim instead.
2. Turn on two-factor from **Your account** and store the ten recovery codes offline.
3. On **Your servers**, add a server. Its setup page shows the exact command for your site:
   `/fg link site https://your-site.example`.
4. In Minecraft, open `/fg menu` → **Link web console** → **Generate pairing code**. Enter the code on
   the setup page, then approve your account in the same Minecraft screen. That screen lists exactly
   what the website will be allowed to do (see `web:` below).
5. In **Members**, invite staff by e-mail with the role each needs and send them the one-time link.
   No e-mail is sent by the site, so no mail provider is needed.

Registration does not verify e-mail addresses. Invitations are bound to the invited address and to
a single-use code that only the invitee receives, so a stranger cannot join by registering someone
else's address.

---

## Roles (per server)

| Capability | Owner | Admin | Moderator | Viewer |
|---|---|---|---|---|
| Status, players, evidence, bans | yes | yes | yes | yes |
| Change checks, reload configuration | yes | yes | no | no |
| Freeze, kick, ban, unban | yes | yes | yes | no |
| View inventories and Ender Chests | yes | yes | yes | no |
| Edit inventories and Ender Chests | yes | yes | no | no |
| Read the server's website audit log | yes | yes | no | no |
| Invite and manage members | yes | no | no | no |
| Pair, unpair, rename, delete the workspace | yes | no | no | no |

Roles are per server: someone can own one server and be a viewer on another. The console hides
what a role cannot do, and the API refuses it independently on every request. Queued operations are
checked again when the plugin collects them, so removing a member or lowering their role also stops
work they queued but Minecraft has not run. A server keeps at least one owner.

**Two independent gates.** The server owner decides in `plugins/ForgeGuard/config.yml` what any
approved website may do at all:

```yaml
web:
  allow-config: true
  allow-moderation: true
  allow-inventory-view: true
  allow-inventory-edit: true
  notify-staff: true
  blocked-items: [command_block, chain_command_block, repeating_command_block, command_block_minecart,
                  structure_block, structure_void, jigsaw, debug_stick, test_block, test_instance_block]
```

Website roles can only narrow these. Website accounts never grant Minecraft permissions, and
Minecraft permissions never grant website access.

---

## Security model

**What the browser holds.** An opaque random token in a `__Host-fg_session` cookie: `HttpOnly`,
`Secure`, `SameSite=Strict`, no `Domain`. It carries no user id and no role. Everything
authoritative is looked up server-side per request.

**Passwords and two-factor.** scrypt (N=32768, r=8, p=1) with per-account salts; a 12-character
minimum with variety rules. RFC 6238 TOTP with replay refusal and ten hashed single-use recovery codes.

**Abuse resistance.** Sign-in limits per address (20 per 15 minutes) and per account (10 per
15 minutes), escalating lockout after six failures, sign-up limits per address (5 per hour) and per
site, a 45-minute idle timeout and a 12-hour absolute session lifetime. Unknown addresses and wrong
passwords get identical responses. An account can own at most 25 servers; a server has at most 50
members and pending invitations.

**Isolation between servers.** Every `/api/servers/<id>/…` request resolves the caller's membership in
that workspace. A non-member gets the same 404 as a missing workspace, so IDs cannot be probed.
Each workspace has its own link record, operation queue and audit log, and each plugin credential
is bound to one workspace and one site origin.

**CSRF.** Same-origin and `Sec-Fetch-Site` checks, `SameSite=Strict` cookies and a session-bound CSRF
token on every authenticated mutation.

**What reaches a Minecraft server.** Only these operations, validated before they are queued and
again by the plugin:

| Route under `/api/servers/<id>/control/` | Method | Capability |
|---|---|---|
| `status` | GET | read (served from the last heartbeat) |
| `cases/<uuid>` | GET | read |
| `catalogue`, `bans`, `players/<uuid>` | GET | read |
| `players/<uuid>/inventory` | GET | inventory.view |
| `players/<uuid>/inventory` | POST | inventory.edit |
| `players/<uuid>/moderate` | POST | moderate |
| `config`, `reload` | POST | write |

There is no console-command, operator, teleport, file or arbitrary-YAML endpoint. Inventory edits
are bounded (41 slots, eight lore lines of 100 characters, 24 enchantments of level 1–255, plain
text only). Each changed slot carries the fingerprint the member saw, so a slot that changed in-game
first is refused rather than overwritten. Moderation follows the server's own `admin:` limits,
never targets `forgeguard.protected` players, and writes the Minecraft audit entry before acting.

**Attribution.** The website keeps an audit log per workspace (pairing, membership, every queued
request and its result) and one per account (sign-ins, failures, security changes). Minecraft's own
`staff-audit.jsonl` records every website action with the member's e-mail.

**Relay data.** Status snapshots and requested results pass through Netlify Blobs. Results are
readable for five minutes after an operation expires. Treat them as player-related operational
data: restrict membership accordingly.

**Content Security Policy.** `default-src 'self'` with no `unsafe-inline` for scripts or styles.
Fonts and the prepared Minecraft item icons are self-hosted, so rendering them makes no third-party
requests. Textures imported by a visitor stay in that visitor's browser (IndexedDB) and are never
uploaded. The default textures come from the verified Mojang client download; Netlify needs no
Mojang API key or live texture proxy. Keep the generated assets and source manifest in the publish
directory. [MINECRAFT-TEXTURES.md](MINECRAFT-TEXTURES.md) documents regeneration and rendering limits;
the cached client JAR is outside `public/` and must not be deployed.

---

## Running it locally

```bash
cd site
npm ci
FORGEGUARD_ALLOW_LOCAL_HTTP=true npm run dev
# http://127.0.0.1:8787
```

The development server routes `/api/*` to the same handlers Netlify deploys, using an in-memory
store that is cleared when it stops.

To try the whole live flow without Minecraft, run the server simulator in another terminal. It
speaks the plugin's real device protocol, with players, inventories, moderation and cases:

```bash
node scripts/simulate-server.mjs --site http://127.0.0.1:8787            # type "approve" when claimed
node scripts/simulate-server.mjs --site http://127.0.0.1:8787 --auto-approve
```

To pair a real local Paper server instead, set `link.allow-loopback-http: true` and
`link.site-url: 'http://127.0.0.1:8787'` in its `config.yml`. Production links require HTTPS.

---

## Troubleshooting

**Waiting for Minecraft approval.** Claiming a code is only the first half. Open **Link web console**
in Minecraft, check the website address and the claiming e-mail, and approve. Generate a fresh code
if the request expired.

**Linked but offline.** Check the Minecraft process, the plugin log, the configured site URL,
outbound HTTPS, the deployed functions and Blobs. The link is kept during a temporary outage.

**A queued request expired.** Minecraft may have acted without the result arriving. Refresh the
player or server before retrying; the console never retries a write automatically.

**“Slot N changed in-game after you loaded it.”** The player's inventory changed while you edited.
Your draft is kept. Choose **Refresh from server**, review, and apply again.

**“This server does not allow … from the website.”** The server owner switched it off under `web:` in
`config.yml`, or the player is protected.

**“The ForgeGuard functions are not deployed on this site.”** The pages were uploaded as static files.
Deploy with the functions directory so `/api/*` resolves.

**Everyone is locked out.** Deleting the `forgeguard-control` blob store resets every account,
workspace, pairing and audit entry, so only do it as a deliberate full reset after a backup. It is not
a password-recovery procedure.
