Add uo-link sidecar foundation: config store + REST client (phase 0)

Introduces the DB-backed connection config for the uo-link sidecar (the
HTTP + WebSocket bridge to the ServUO shard) and a never-throw REST client,
mirroring the existing Discord-bot integration:

- uo_link_config singleton table (base/ws URL, AES-256-GCM-encrypted shared
  token, protocol pin, enabled, and last-known status/plugin_connected/
  last_event_at/boot_id mirrors for the admin panel).
- model/uoLinkConfig: getSafe (never returns the token — only hasToken),
  getWithToken (server-side decrypt), save (blank token = unchanged),
  recordStatus (mirror the sidecar's reported state).
- utils/uoLinkClient: never-throw fetch client returning {ok,data,status,
  error}; Bearer token + X-UOLink-Version on every call; brief config cache;
  helpers for health/char/roster/vendors/history/economy/link/towncrier.
- .env.example: UOLINK_BASE_URL/WS_URL/PROTOCOL defaults (token stays
  admin-managed in the DB, never an env var).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
This commit is contained in:
2026-07-11 02:02:40 -05:00
parent d49008e9f2
commit ab647756f0
5 changed files with 280 additions and 0 deletions

View File

@@ -259,6 +259,36 @@ CREATE TABLE IF NOT EXISTS email_config (
CONSTRAINT chk_email_config_singleton CHECK (id = 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ── uo-link sidecar ────────────────────────────────────────────────────────
-- Connection config for the uo-link sidecar (the HTTP + WebSocket bridge to the
-- ServUO shard). Singleton row (id = 1), mirroring bot_config/email_config: the
-- DB only ever holds the AES-256-GCM-encrypted shared-secret auth token, never
-- plaintext, and it is only decrypted server-side (to call the sidecar). It is
-- never returned to the admin UI — responses expose only `hasToken`. base_url is
-- the REST endpoint, ws_url the live-feed endpoint; both are configurable because
-- in production the sidecar runs on a different host from the website. `status`/
-- `plugin_connected`/`last_event_at`/`boot_id` mirror the sidecar's last-known
-- state for the admin panel between polls; `boot_id` tracks server.hello.bootId
-- so a shard restart can be detected (and caches dropped).
CREATE TABLE IF NOT EXISTS uo_link_config (
id INT PRIMARY KEY DEFAULT 1,
base_url VARCHAR(255) NULL,
ws_url VARCHAR(255) NULL,
auth_token_enc TEXT NULL,
protocol INT NOT NULL DEFAULT 1,
enabled TINYINT(1) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'disconnected',
status_detail VARCHAR(500) NULL,
plugin_connected TINYINT(1) NOT NULL DEFAULT 0,
last_event_at DATETIME NULL,
boot_id VARCHAR(64) NULL,
updated_by INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_uo_link_config_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT chk_uo_link_config_singleton CHECK (id = 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Discord bot moderation core (Phase 2). These tables are owned by the bot
-- process (its own DB pool, bot/src/db.js) — the main server never reads or
-- writes them. They live in the same physical database as everything else