diff --git a/link/ADMIN_CONTROLS.md b/link/ADMIN_CONTROLS.md index db39185..cd177f8 100644 --- a/link/ADMIN_CONTROLS.md +++ b/link/ADMIN_CONTROLS.md @@ -134,7 +134,12 @@ It turns "a staff member must be logged into the game to see the queue" into "th ## 4. Roadmap (decided) -> **Build status (2026-07-13):** Phase 1 **plugin side is built and live-verified** on the seeded shard — `BridgeAdmin.cs` + config, branch `feature/admin-controls`. All four verbs, the `web:` attribution, the audit stream, and the **Owner-protection floor** (an `admin.ban` on the Owner account was refused) confirmed end-to-end against a booted ServUO, no exceptions. **Remaining for Phase 1 to be usable from the site:** sidecar REST routes (`sidecar/src/web.rs`) and the `INTEGRATION.md` docs. The bidirectional-audit slice (§5.5, incl. the `CommandLogging` patch) is not yet started. +> **Build status (2026-07-13):** Phase 1 is **built and live-verified end-to-end**, branch `feature/admin-controls`. +> - *Plugin* (`BridgeAdmin.cs` + config): all four verbs, `web:` attribution, the audit stream, and the **Owner-protection floor** (an `admin.ban` on the Owner was refused) confirmed against a booted ServUO. +> - *Sidecar* (`sidecar/src/web.rs`): `POST /admin/{kick,ban,unban,broadcast}` routes with the status mapping in §6. Verified with the real sidecar + shard: 200 on success, **403** on the Owner floor, **404** unknown target, **400** missing actor, **401** no token. +> - *Docs*: `INTEGRATION.md` §6 documents the endpoints and the `admin.audit` event. +> +> **Remaining:** the bidirectional-audit slice (§5.5, `BridgeEvents` normalizer + the one-line `CommandLogging` patch) — not yet started. **Wire in, in order:** diff --git a/link/INTEGRATION.md b/link/INTEGRATION.md index b281755..17d46db 100644 --- a/link/INTEGRATION.md +++ b/link/INTEGRATION.md @@ -178,6 +178,7 @@ Every event has `t` (epoch ms) and `kind`. A nested actor object looks like `{"s | `cheat.fastwalk` | `who`, `ip` | The shard's own speed-hack detector fired. | | `audit.set` | `staff`, `prop`, `target`, `targetSerial`, `old`, `new` | A staff member used `[set` to change a property. `staff` may be null. | | `audit.command` | `staff`, `command`, `args` | A staff command was invoked. | +| `admin.audit` | `origin`, `action`, `actor`, `target`, `reason`, plus action-specific (`durationSec`, `sessions`, `hue`, `text`) | A moderation action was applied. `origin` is `"web"` (from the site, `actor:"web:"`) or `"in-game"` (a staff member in the game client). Broadcast to every dashboard so your moderation log stays complete regardless of who acted. Emitted alongside the `admin.ok` reply for web actions; see §6. | #### Account linking | kind | fields | notes | @@ -301,6 +302,55 @@ DELETE /towncrier/{id} Caps apply (line count/length, active entries, duration); an over-cap post returns `towncrier.error`. +### Staff moderation — the write plane + +Account and session moderation against the live shard. **These are privileged.** The sidecar does +not model per-user roles — **your site must authenticate the staff user and check their permission +before calling.** The shard trusts the loopback socket and applies each command with CoOwner-level +authority, with one hard floor it enforces itself: any target at or above CoOwner (e.g. the Owner +account) is refused (**403**). The whole plane is **opt-in on the shard** (`AdminWriteEnabled` in +`Bridge.cfg`); when it's off, every call returns **403** `"admin write plane disabled"`. + +Every request requires an **`actor`** — the website username/id of the staff member taking the +action. It is recorded in the shard console log, the ban's `BanDealer` tag, and the `admin.audit` +event, so actions are always attributable. A missing `actor` is **400**. + +``` +POST /admin/kick { "actor":"jane", "account":"griefer42" } # or "serial":"0x2E0" +POST /admin/ban { "actor":"jane", "account":"griefer42", "durationSec":604800, "reason":"harassment" } +POST /admin/unban { "actor":"jane", "account":"griefer42" } +POST /admin/broadcast { "actor":"jane", "text":"Server restart in 5 minutes", "hue":53 } +``` + +- **kick** — disconnects every live session of the target account (including one parked at + character-select). Target by `account` or `serial`. Reply carries `sessions` (how many were cut). +- **ban** — bans the account (works offline) and disconnects any live sessions. `durationSec > 0` + is a timed ban that auto-expires; `0`/absent is indefinite. Clamped to the shard's + `AdminBanMaxDurationSec`. +- **unban** — clears the ban. +- **broadcast** — a system message to everyone online. `hue` optional (default `53`, staff green). + Length-capped by the shard. + +Success → **200** with an `admin.ok`: + +```json +{ "kind":"admin.ok", "reqId":"r-2", "action":"ban", "target":"griefer42", "durationSec":604800, "sessions":1 } +``` + +Failure → an `admin.error` with a mapped status: + +| Status | When | +|--------|------| +| 400 | missing `actor`, malformed body, or bad parameter | +| 401 | missing/invalid auth token | +| 403 | target is protected (at/above the floor), or the write plane is disabled on the shard | +| 404 | unknown or accountless target | +| 503 / 504 | shard not connected / didn't reply in time | + +Each applied action also emits an unsolicited **`admin.audit`** frame on the WebSocket (§4) with +`origin:"web"`, so every connected dashboard — not just the caller — sees it. In-game moderation +by staff in the game client surfaces the same way with `origin:"in-game"`. + ### History (from the sidecar's database) ```