docs: document the uo-link shard integration in the README
Add a "Shard integration (uo-link)" section explaining that the live shard bridge is a separate sidecar service at UOM/link, how the site talks to it (admin-managed encrypted config, WebSocket ingest + REST round-trips, SSE fan-out with public vs admin channels), the in-game [link account-linking flow, and what the public / player / admin surfaces each expose. Also add an intro bullet, a contents entry, and the shard endpoint groups to the API endpoints table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kj5s1QCKobuFPYmqxjy1q
This commit is contained in:
73
README.md
73
README.md
@@ -6,6 +6,7 @@ shard — a full-stack app in one repo:
|
|||||||
- **Backend** — Node.js + Express REST API (layered `router → controller → model → db`), MariaDB, a provider-agnostic session layer (JWT cookie for web, bearer tokens for mobile, pluggable SSO).
|
- **Backend** — Node.js + Express REST API (layered `router → controller → model → db`), MariaDB, a provider-agnostic session layer (JWT cookie for web, bearer tokens for mobile, pluggable SSO).
|
||||||
- **Frontend** — React + Vite single-page app (public site, wiki, and the admin panel), dark "gothic" theme (Cinzel + Georgia).
|
- **Frontend** — React + Vite single-page app (public site, wiki, and the admin panel), dark "gothic" theme (Cinzel + Georgia).
|
||||||
- **Deploy** — Docker Compose (app + MariaDB) behind a Pangolin reverse proxy. Express serves the built SPA in production.
|
- **Deploy** — Docker Compose (app + MariaDB) behind a Pangolin reverse proxy. Express serves the built SPA in production.
|
||||||
|
- **Shard link** — a live bridge to the in-game ServUO shard through the **uo-link** sidecar ([UOM/link](https://gitea.whitlocktech.com/UOM/link)): the site ingests a live event feed and makes server-side REST calls to show shard status, economy, staff presence, IDOCs, live activity, and per-character sheets. See [Shard integration (uo-link)](#shard-integration-uo-link).
|
||||||
|
|
||||||
The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, schema, security).
|
The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, schema, security).
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, sc
|
|||||||
- [Pages & routes](#pages--routes)
|
- [Pages & routes](#pages--routes)
|
||||||
- [API endpoints](#api-endpoints)
|
- [API endpoints](#api-endpoints)
|
||||||
- [API documentation (Swagger)](#api-documentation-swagger)
|
- [API documentation (Swagger)](#api-documentation-swagger)
|
||||||
|
- [Shard integration (uo-link)](#shard-integration-uo-link)
|
||||||
- [Environment variables](#environment-variables)
|
- [Environment variables](#environment-variables)
|
||||||
- [Security](#security)
|
- [Security](#security)
|
||||||
- [Logging](#logging)
|
- [Logging](#logging)
|
||||||
@@ -207,6 +209,9 @@ npm start # node server → serves API + SPA at http://localhost:3
|
|||||||
| SSO | `/api/v1/auth` (`providers` — public discovery; `sso/:provider/start`, `sso/:provider/link`, `sso/:provider/callback`) | redirect flow |
|
| SSO | `/api/v1/auth` (`providers` — public discovery; `sso/:provider/start`, `sso/:provider/link`, `sso/:provider/callback`) | redirect flow |
|
||||||
| Public | `/api/v1/public` (`settings`, `status`, `posts/:category`, `posts/:category/:idOrSlug`, `wiki`, `wiki/:slug`, `contact`) | none |
|
| Public | `/api/v1/public` (`settings`, `status`, `posts/:category`, `posts/:category/:idOrSlug`, `wiki`, `wiki/:slug`, `contact`) | none |
|
||||||
| Admin | `/api/v1/admin` (`dashboard`, `site-mode`, `posts`, `posts/upload`, `wiki`, `settings`, `activity`, `bot-activity`, `bot-activity/unban`, `auth/providers` (CRUD), `users`, `account`, `account/totp/*`, `account/identities`) | cookie (admin) |
|
| Admin | `/api/v1/admin` (`dashboard`, `site-mode`, `posts`, `posts/upload`, `wiki`, `settings`, `activity`, `bot-activity`, `bot-activity/unban`, `auth/providers` (CRUD), `users`, `account`, `account/totp/*`, `account/identities`) | cookie (admin) |
|
||||||
|
| Public · Shard | `/api/v1/public/shard` (`status`, `feed`, `economy`, `online`, `idoc`, `stream`) | none |
|
||||||
|
| Player · Shard | `/api/v1/player/shard` (`link`, `accounts`, `roster/:account`, `vendors/:account`, `char/:serial`, `sales`) | cookie/bearer (player) |
|
||||||
|
| Admin · Shard | `/api/v1/admin/shard` (self linking, same as player) · `/api/v1/admin/uo-link` (`config`, `towncrier`, `stream`) | cookie (staff / admin) |
|
||||||
|
|
||||||
Post categories (URL form): `news`, `five-on-friday`, `newsletter`, `screenshots`.
|
Post categories (URL form): `news`, `five-on-friday`, `newsletter`, `screenshots`.
|
||||||
`authMethod` on a session ∈ `local · totp · mobile · google · discord · oidc`.
|
`authMethod` on a session ∈ `local · totp · mobile · google · discord · oidc`.
|
||||||
@@ -251,6 +256,74 @@ not crash).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Shard integration (uo-link)
|
||||||
|
|
||||||
|
The site is wired to the live in-game world through **uo-link**, a standalone sidecar service that
|
||||||
|
runs next to the ServUO shard. Its source lives in a separate repo:
|
||||||
|
**[UOM/link](https://gitea.whitlocktech.com/UOM/link)**. uo-link speaks the shard's internals and
|
||||||
|
exposes a small, authenticated HTTP + WebSocket API; this website is a *client* of it. The shard
|
||||||
|
itself is never exposed to the internet — only the sidecar is, and only the website's backend talks
|
||||||
|
to it.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
```
|
||||||
|
ServUO shard ──▶ uo-link sidecar (UOM/link) ──▶ website backend ──▶ browser
|
||||||
|
REST + WebSocket, bearer-auth ingest + REST same-origin JSON/SSE
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Connection is admin-managed, not env.** The sidecar's base URL, WebSocket URL, shared-secret
|
||||||
|
token, and protocol version are stored in the database (`uoLinkConfig`), edited from the
|
||||||
|
**Admin → Shard** panel. The token is **encrypted at rest** (AES-256-GCM) and is **write-only** in
|
||||||
|
the API — it is never returned to any client and never sent to the browser. Every call the backend
|
||||||
|
makes carries `Authorization: Bearer <token>` and an `X-UOLink-Version` header (a protocol
|
||||||
|
mismatch fails fast with `409` instead of being mis-parsed).
|
||||||
|
- **Live ingest (WebSocket).** When enabled, the backend opens an outbound WebSocket to the sidecar
|
||||||
|
and receives a stream of game events — `mob.login`/`logout`, `char.vitals`, `economy.supply`,
|
||||||
|
`vendor.sale`, `player.death`/`murdered`, `house.decay` (IDOC), staff `audit.*`/`cheat.*`,
|
||||||
|
`link.request`, and `server.hello`/`shutdown`. A single dispatcher (`utils/shardIngest.js`) routes
|
||||||
|
each event: state-changing kinds update `shard_online` / `shard_economy` / `shard_houses`; notable
|
||||||
|
kinds are appended to an append-only `shard_events` log; high-frequency kinds (vitals, supply
|
||||||
|
ticks) only update state and are not logged. A changed boot id on `server.hello` is detected as a
|
||||||
|
restart and stale "online" rows are cleared. On reconnect the backend backfills missed events via
|
||||||
|
the sidecar's `/history`.
|
||||||
|
- **Live round-trips (REST).** For point-in-time reads the backend calls the sidecar directly —
|
||||||
|
`/char/serial/:serial`, `/roster/:account`, `/vendors/:account`, `/economy`, `/history` — plus
|
||||||
|
commands `/link/confirm` and `/towncrier`. The REST client (`utils/uoLinkClient.js`) **never
|
||||||
|
throws**: every call returns `{ ok, data, status }`, so a shard that is down or mid-restart
|
||||||
|
degrades to a `503`/retry banner instead of a 500.
|
||||||
|
- **Fan-out to the browser.** Ingested events are pushed to browsers over **Server-Sent Events**.
|
||||||
|
Two channels exist: a **public** stream carrying only a safe allowlist of kinds, and an
|
||||||
|
**admin-only** stream that also includes sensitive kinds (staff audit, cheat detection, login
|
||||||
|
attempts, IPs). Sensitive kinds can never leak onto the public channel.
|
||||||
|
|
||||||
|
### Account linking
|
||||||
|
|
||||||
|
A player (or staff member) proves ownership of a game account without sharing any game credentials:
|
||||||
|
|
||||||
|
1. In game, the player runs **`[link`** and receives a one-time code.
|
||||||
|
2. On the website (Player portal, or Admin → Account for staff) they enter the code.
|
||||||
|
3. The backend confirms the code with the sidecar (`POST /link/confirm`), which permanently tags the
|
||||||
|
game account with the website user id, and mirrors the link locally in `shard_account_links`.
|
||||||
|
|
||||||
|
That mirror is the authorization basis for character reads: roster/vendor/character-sheet endpoints
|
||||||
|
are **ownership-checked** so a user only sees accounts they linked. **Admins may view any
|
||||||
|
character**; players and editor/moderator staff are limited to their own linked accounts.
|
||||||
|
|
||||||
|
### What each audience sees
|
||||||
|
|
||||||
|
| Surface | Endpoints | Who | Data |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Public** | `/api/v1/public/shard/*` (`status`, `feed`, `economy`, `online`, `idoc`, `stream`) | anyone | Shard up/down, gold-supply series, IDOC houses, a curated live feed, and **"Staff online"** — only players whose account is linked to a **staff** user (admin/editor/moderator), shown with name + map location. Linked *players* are never listed publicly; no vitals or account are exposed. |
|
||||||
|
| **Player** | `/api/v1/player/shard/*` (`link`, `accounts`, `roster/:account`, `vendors/:account`, `char/:serial`, `sales`) | logged-in player | Their own linked accounts: character rosters, character sheets, player-vendor snapshots, and recent vendor sales. |
|
||||||
|
| **Admin** | `/api/v1/admin/shard/*` (self-linking, same as player) · `/api/v1/admin/uo-link/*` (`config`, `towncrier`, `stream`) | staff / admin | Staff link their own accounts like players; **admins** additionally read *any* character's data, edit the sidecar connection config, publish/remove **town-crier** messages, and subscribe to the full event stream (incl. audit/cheat). |
|
||||||
|
|
||||||
|
The sidecar URL and token are set once in **Admin → Shard**; if uo-link is not configured (or the
|
||||||
|
shard is offline), every shard surface degrades gracefully — the public page still renders, showing
|
||||||
|
the shard as offline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.env` is git-ignored.**
|
Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.env` is git-ignored.**
|
||||||
|
|||||||
Reference in New Issue
Block a user