From d72c2dadfc60065a3e8a1812c20e09532216e280 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 09:29:45 -0500 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_018kj5s1QCKobuFPYmqxjy1q --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/README.md b/README.md index 5887e73..a22b14a 100644 --- a/README.md +++ b/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). - **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. +- **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). @@ -24,6 +25,7 @@ The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, sc - [Pages & routes](#pages--routes) - [API endpoints](#api-endpoints) - [API documentation (Swagger)](#api-documentation-swagger) +- [Shard integration (uo-link)](#shard-integration-uo-link) - [Environment variables](#environment-variables) - [Security](#security) - [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 | | 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) | +| 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`. `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 ` 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 Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.env` is git-ignored.**