Files
runicgateway.com/src/data/reference.mjs
wtclaude 75c8ffc07a
All checks were successful
PR checks / checks (pull_request) Successful in 2m5s
feat(docs): phase 15 - the Rust group, its reference pages checked against their sources
PLAN.md §16 phase 15. A Rust group of fifteen pages after Ultima Online
(D70): the module, installing the bridge (installer, Pterodactyl egg, by
hand), connecting the site, verifying the bridge, linking Steam accounts,
permissions and groups, mod config, who sees who is online, events, zones
and rewards, optional plugins, the live map, troubleshooting, and three
reference pages - rust-link's sidecar.toml, the plugin's
RunicGateway.json, and the event kinds.

checkReference.mjs gains three Rust sections rather than a new check:
sidecar.toml keys and RUSTLINK_* overrides from Rust-Link config.rs,
RunicGateway.json from the plugin's PluginConfig, and the event kinds
in three classes against Module-Rust server/catalogue.js (D71). Every
admin screen was walked on the walk site before it was described (D36).

Core pages now hand off to the Rust group instead of linking out to the
docs repository. Three defects the re-read found here are fixed: the UO
/health sample's hard-coded protocol 4 (both games' samples now render
platform.json through <Code>), Authentication's missing Steam tab, and
two pages saying the site cannot reach EventsEnabled - false on Rust.

Installer v0.8.1 moved checkFacts red mid-phase; platform.json follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 15:23:07 -05:00

417 lines
20 KiB
JavaScript

/**
* The Reference section's enumerations.
*
* §1 says a Reference page is "a navigable summary plus a link to the canonical document —
* never a re-specification". This file is the line between those two things, and it is
* worth being explicit about where it falls:
*
* * The NAMES are here — every environment variable, every config key, every command,
* every event kind. A reference section that cannot answer "what variables are there?"
* without a click-through is a link farm.
* * The SEMANTICS are not. One terse line each, saying what a thing is FOR. Shapes,
* defaults that matter, interactions, and every "why" stay in the canonical document.
*
* Everything below is checked against its source by `scripts/checkReference.mjs`, in both
* directions — a name that disappears upstream fails, and a name that appears upstream and
* is missing here fails too. That is the whole reason it is safe to write names down at
* all: the enumeration cannot rot into fiction without turning the build red.
*
* Descriptions are NOT checked, and cannot be. They are the part a human has to keep
* honest, which is why they are kept short enough to re-read.
*/
/** `website` root `.env.example` — the file a Compose deployment actually reads. */
export const envVars = {
IMAGE_TAG: 'Which published image tag to run',
NODE_ENV: 'production or development — several refusals are production-only',
PORT: 'The port the app listens on',
INTERNAL_PORT: 'The internal-only listener, for the bot channel',
UPLOAD_DIR: 'Where uploads are written',
LOG_LEVEL: 'Console log level',
FILE_LOG_LEVEL: 'File log level, set separately',
LOG_TO_FILE: 'Whether to write a log file at all',
LOG_DIR: 'Directory for the log file',
LOG_FILE: 'Log file name',
BRAND_NAME: 'Site name — branding is data, not a build',
BRAND_SHORT_NAME: 'Short form, for tight spaces',
BRAND_TAGLINE: 'One line under the name',
BRAND_DESCRIPTION: 'Meta description',
BRAND_CONTACT_EMAIL: 'Published contact address',
BRAND_URL: 'Canonical public URL',
BRAND_ACCENT_COLOR: 'Accent colour',
BRAND_LOGO: 'Logo path',
BRAND_HERO: 'Hero image path',
BRAND_FAVICON: 'Favicon path',
DB_HOST: 'Database host',
DB_PORT: 'Database port',
DB_NAME: 'Database name',
DB_USER: 'Database user',
DB_PASSWORD: 'Database password',
DB_ROOT_PASSWORD: "The database container's root password",
JWT_SECRET: 'Signs session tokens. Rotating it logs everyone out',
SECRET_ENC_KEY:
'Encrypts secrets at rest. Required in production, and rotating it ORPHANS every stored secret',
JWT_EXPIRES_IN: 'Session lifetime',
COOKIE_SECURE: 'auto decides Secure per request, so HTTPS and LAN HTTP both work',
COOKIE_NAME: 'Session cookie name. Changing it invalidates existing sessions',
TRUST_PROXY: 'Needed behind a reverse proxy for secure cookies, real IPs and rate limiting',
DEBUG_TRUST_PROXY: 'Diagnostic for the above',
TOTP_CHALLENGE_TTL: 'How long a pending 2FA challenge is valid',
ADMIN_USERNAME: 'First admin, created only when no users exist',
ADMIN_PASSWORD: 'First admin password. Set it before the first boot, not after',
CLIENT_ORIGIN: 'Dev only — the Vite origin allowed through CORS',
BOT_INTERNAL_URL: 'Where the Discord bot listens',
BOT_INTERNAL_KEY:
'Authenticates the site↔bot channel. Required in production EVEN IF you run no bot',
NTFY_BASE_URL: 'Push notification relay base URL',
};
/** `link/sidecar/src/config.rs` → the TOML the sidecar writes on first run. */
export const sidecarConfig = {
'shard.bind': 'Loopback address the game plugin dials out to',
'web.bind': 'Address the website reaches the sidecar on',
'web.auth_token': 'Shared secret the website must present. Generated on first run if blank',
'store.path': "The sidecar's own durable store",
};
/** `installer` — `src/cli.rs`'s `Command`. */
export const installerCommands = {
Install: "Set up a game server's side: deploy the bridge plugin, install the sidecar, register its service",
Doctor: 'Diagnose an existing install',
Update: 'Move to a newer bundle',
Uninstall: 'Remove what install put there',
};
/** `servuo-plugins/overlay/Config/Bridge.cfg` — the plugin's config, grouped for reading. */
export const bridgeCfg = {
Connection: {
Host: 'Sidecar address the shard dials out to',
Port: 'Sidecar port',
QueueCap: 'Bounded queue depth. Full means drop-oldest — never block the game',
PublicConnectAddress: 'Address players connect to, published to the site',
LinkUrl: 'Where in-game account linking sends a player',
},
Sweeps: {
StatSweepSeconds: 'Character stat sweep interval',
DecaySweepSeconds: 'House decay sweep',
EconomySweepSeconds: 'Economy totals sweep',
ChampSweepSeconds: 'Champion spawn sweep',
PageSweepSeconds: 'Staff page sweep',
GuildSweepSeconds: 'Guild roster sweep',
CitySweepSeconds: 'City / governor sweep',
PresenceSweepSeconds: 'Who is online',
HousingSweepSeconds: 'Housing sweep',
},
Guilds: {
GuildRosterMembersPerLine: 'Frame cap — a roster is split rather than sent oversized',
GuildRosterGuildsPerTick: 'How many guilds are swept per tick',
},
Points: {
PointsSweepSeconds: 'Points sweep interval',
PointsLeaderboardEnabled: 'Publish a leaderboard at all',
PointsTopN: 'Leaderboard length',
PointsSystems: 'Which point systems to include',
PointsProfileEnabled: 'Show points on a character profile',
PointsProfileRank: 'Show rank as well as total',
},
Market: {
MarketEnabled: 'Publish player vendor listings',
MarketSweepSeconds: 'Market sweep interval',
MarketSweepBatch: 'Vendors per sweep',
MarketMaxListings: 'Cap on listings published',
},
Ruleset: {
RulesetEnabled: 'Publish the shard ruleset',
RulesetIncludeSchedule: 'Include the event schedule with it',
},
'Town crier': {
TownCrierMaxLines: 'Lines per notice',
TownCrierMaxLineLength: 'Characters per line',
TownCrierMaxActive: 'Concurrent notices',
TownCrierMaxDurationSec: 'Longest a notice may run',
},
News: {
NewsMaxTitleLength: 'Title cap',
NewsMaxBodyLength: 'Body cap',
NewsMaxExternal: 'How many site posts are carried in-game',
NewsAnnounceDurationSec: 'How long an announcement shows',
},
'Admin commands': {
AdminWriteEnabled: 'Whether the site may write to the game at all. Off by default',
AdminAccessFloor: 'Minimum in-game access level for admin actions',
AdminBroadcastMaxLength: 'Broadcast cap',
AdminReasonMaxLength: 'Reason field cap',
AdminBanMaxDurationSec: 'Longest ban the site may set',
},
'Scheduled events': {
EventsEnabled:
'Whether the website may run scheduled events against this world at all. Off by ' +
'default, and deliberately a separate switch from AdminWriteEnabled',
EventsSweepSeconds: 'How often expired gates are collected and lost objects pruned',
EventsMinSaveIntervalSec:
'Shortest gap between world saves, counted from the last save by anyone. A save ' +
'asked for too soon is refused rather than queued',
},
'Event caps': {
EventsMaxCreatures: 'Creatures one call may spawn',
EventsMaxBosses: 'Enhanced "boss" variants one call may spawn',
EventsMaxNpcs: 'Oracle NPCs one call may place',
EventsMaxDecor: 'Decoration items one call may place',
EventsMaxGateMinutes: 'Longest a temporary gate may stand',
EventsMaxOwnedPerRun: 'Objects one run may own across every verb — the runaway bound',
EventsMaxSpread: 'How far from the chosen spot things may be scattered',
EventsMaxBossMultiplier: 'How much harder than normal a boss may be made',
EventsMaxGrantPerRun: 'How many characters one item grant may reach',
EventsMaxGrantStack: 'How large one granted stack may be',
},
'The oracle NPC': {
EventsOracleMaxLines: 'Keyword lines it will answer to',
EventsOracleGreetRange: 'How close a player must be to be greeted',
EventsOracleSpeechRange: 'How close a player must be to be heard',
EventsOracleGreetCooldownSec: 'How often it greets the same player',
EventsOracleAnswerCooldownSec: 'How often it answers the same player',
},
Leases: {
LeaseMaxDurationSec:
'Longest lease this shard will hold, whatever the site asks for. A longer request ' +
'is refused rather than shortened',
LeaseGraceSec: 'How long a restored lease stays listed, so a late teardown still gets a verdict',
},
Participation: {
ParticipationSweepSeconds: "How often everyone standing in a run's area is credited",
ParticipationKillWeight: 'What one kill inside the area is worth against one minute in it',
ParticipationMaxRuns: 'Runs counted at once',
ParticipationMaxMembers: 'Members counted per run',
ParticipationMaxRadius: 'Widest area an event may declare',
ParticipationGraceSec: "How long a closed run's tally stays readable",
ParticipationSnapshotChunk: "Members resolved per yield of the game's core thread",
},
Accounts: {
SignupMode: 'How game accounts may be created',
AccountCreateEnabled: 'Allow creation at all',
RequireIpForCreate: 'Require a real client IP',
AccountNameMaxLength: 'Account name cap',
AccountPasswordMaxLength: 'Account password cap',
},
'Client assets': {
AssetsEnabled:
'Whether the website may read the UO client files on this host \u2014 art, animations, ' +
'the string table \u2014 over the link at all. Its own switch, because it is its own consent',
AssetBatchBytes:
'Byte budget for one reply page, inside the 1 MiB line cap the sidecar accepts',
AssetBodyBatch:
'Bodies one catalogue request may name. Counted in items rather than bytes, because ' +
'what it bounds is building and deleting that many real mobiles on the core thread. ' +
'A larger request is refused, never truncated',
AssetFetchKeys:
'How many keys one fetch may name. The byte budget above still decides where a page is cut',
AssetScanMs: 'How long a catalogue scan may run before the page it has is returned',
AssetPlayerDirection:
'Which of the five directions a player body renders as. 0 is head-on, facing the viewer',
AssetCreatureDirection:
'The same for everything else. 1 is the front three-quarter \u2014 a wolf seen head-on ' +
'is a dark blob',
},
'Spawn files': {
TreeEnabled:
'Whether the shard configuration itself \u2014 spawn files, regions, locations, champion ' +
'spawns, decoration \u2014 may cross the bridge. A third switch for a third consent: ' +
'this is the work of the operator rather than the game client. Off means the spawn ' +
'atlas needs a shared filesystem again',
TreeChunkBytes: 'How large a slice of one file may be before it is compressed and sent',
},
};
/**
* rust-link's `sidecar.toml` — `Rust-Link` `sidecar/src/config.rs`, parsed from its serde
* structs the same way as uo-link's above. Each section is one `struct XCfg`.
*/
export const rustSidecarConfig = {
'game.bind': 'Loopback address the bridge plugin dials. Never move it off loopback',
'game.server_id':
"A cross-check against the id the plugin announces. A plugin naming another server is refused before anything it sent is stored",
'web.bind': 'Address the website reaches the sidecar on',
'web.auth_token': 'Shared secret the website must present. Generated and saved on first run if blank',
'store.path': "The sidecar's own database. Relative paths resolve beside the config file",
'store.retain_days': 'Days of raw event history the sidecar keeps. 0 keeps everything',
};
/**
* The environment variables rust-link reads, each overriding one key above. Read from the
* `get("RUSTLINK_…")` calls in the same file. An empty variable is ignored, which is what
* lets a panel declare one and leave it blank.
*/
export const rustSidecarEnv = {
RUSTLINK_GAME_BIND: 'game.bind',
RUSTLINK_SERVER_ID: 'game.server_id',
RUSTLINK_WEB_BIND: 'web.bind',
RUSTLINK_WEB_TOKEN: 'web.auth_token',
RUSTLINK_DB_PATH: 'store.path',
RUSTLINK_RETAIN_DAYS: 'store.retain_days',
};
/**
* The bridge plugin's own config — `Rust-Plugins` `overlay/oxide/plugins/RunicGateway.cs`,
* the public fields of its `PluginConfig` class. The framework writes it as
* `oxide/config/RunicGateway.json` or `carbon/configs/RunicGateway.json`.
*
* `readOnlyFromSite` marks the three keys the site's mod config editor shows but will not
* write: changing them from the website would cut the link carrying the change, or strand
* every row the site holds for this server.
*/
export const rustPluginConfig = {
Connection: {
Host: { why: 'Where the sidecar listens. Loopback is the trust boundary', readOnlyFromSite: true },
Port: { why: "The sidecar's game port. Must equal its game.bind port", readOnlyFromSite: true },
QueueCap: { why: 'Lines that may wait for a wedged sidecar before the oldest is dropped' },
ServerId: {
why: "This server's permanent identity on the website. Not the hostname, on purpose",
readOnlyFromSite: true,
},
},
Events: {
EventsEnabled: {
why: 'Whether the website may borrow values or change this world for events at all. Off by default',
},
EventsMaxCrates: { why: 'Crates one event step may place. Lowerable, never raisable past the ceiling' },
EventsMaxNpcs: { why: 'NPCs one event step may place' },
EventsMaxRecipients: { why: 'People one reward step may name' },
EventsMaxTallies: { why: 'Participation tallies counting at once' },
EventsMaxChat: { why: 'Longest line an event or a news post may say in chat' },
EventsMaxWorldCalls: { why: 'World events one run may call: helicopters, cargo ships, airdrops' },
},
Map: {
MapMaxSleepers: { why: 'Offline sleepers one live-map answer carries' },
MapMaxBases: { why: 'Tool cupboards and vending machines one live-map answer carries' },
},
};
/**
* Every event kind the Rust bridge sends, through its current protocol, as `Module-Rust`
* `server/catalogue.js` classifies them (PLAN.md §16, D71). The module is the side that
* enforces the boundary, so it is what this list is checked against — the same reason the
* UO ladder below is checked against the module and not against prose.
*
* - `public`: a signed-out visitor may see the kind. Some are filtered field by field
* inside it — a name for the presence audience, a position for staff — and that is the
* module's business, summarised on the page rather than restated here.
* - `presence`: public kinds that say a NAMED player was on the server. Their audience is
* an operator's choice on Admin → Rust visibility, staff only by default. A subset of
* the module's public list, kept separate here because that is how a reader needs it.
* - `staff`: admins and moderators, and nobody else.
*/
export const rustEventKinds = {
public: {
'server.wipe': 'The map was wiped',
'server.initialized': 'The server came up',
'server.shutdown': 'The server went down',
'npc.boss.spawned': 'A RunicNPC boss appeared',
'world.event': "Rust's own world events: cargo, helicopters, Bradley, airdrops, the vendor",
'pvp.zone.created': 'Dynamic PVP opened a PvP zone',
'dt.started': 'A Dangerous Treasures event opened',
'dt.won': 'A Dangerous Treasures chest was won',
'gc.started': 'A guarded crate landed',
'gc.ended': 'A guarded crate was cleared',
'raid.started': 'A RaidableBases base appeared',
'raid.completed': 'A raid base was completed',
'raid.purchased': 'A raid base was bought',
'raid.locked': 'An admin locked a raid base to a player',
},
presence: {
'player.connected': 'A player joined',
'player.disconnected': 'A player left',
'player.respawned': 'A player respawned',
'player.death': 'A death — the kill feed',
'player.chat': 'A line of in-game chat',
'player.tally': "A minute's gathering, kills and title counts, summed rather than sent per swing",
'npc.boss.died': 'A RunicNPC boss was killed, naming the killer',
},
staff: {
'entity.destroyed': 'A building part was destroyed — it says where somebody lives',
'player.reported': "One player's report about another",
'player.banned': 'A ban, with the IP address',
'player.unbanned': 'An unban, with the IP address',
'player.login.attempt': 'A login attempt, with the IP address',
'player.approved': 'A login approved, with the IP address',
'account.link.requested': 'Somebody asked for a link code — never the code itself',
'account.unlinked': 'A Steam account was unlinked in game',
'perm.drift': 'A permission or group was changed in game, outside the site',
'config.outcome': "How a config save's reload ended, with the log tail when it failed",
'plugin.loaded': 'A plugin loaded, with the permissions it registers',
'plugin.unloaded': 'A plugin unloaded',
'lease.expired': 'A value an event borrowed was given back on its deadline',
'world.expired': 'Something an event placed ended on its deadline',
'clan.created': 'A clan was created',
'clan.disbanded': 'A clan was disbanded',
'clan.member.added': 'Somebody joined a clan',
'clan.member.left': 'Somebody left a clan',
'clan.member.kicked': 'Somebody was removed from a clan',
'npc.died': 'A RunicNPC NPC died, naming everyone who hurt it',
'npc.health': 'A RunicNPC NPC fell to a health threshold',
'npc.placement.changed': 'A RunicNPC placement changed',
'npc.boss.phase': "A boss changed phase — told in game only to the players near it",
'pvp.zone.deleted': 'A Dynamic PVP zone closed',
'dt.ended': 'A Dangerous Treasures event ended, won or not',
'gc.failed': 'A guarded crate timed out with its guards standing',
'raid.ended': 'A raid base despawned',
},
};
/**
* The five-rung visibility ladder, from `module-uo`'s `server/utils/shardVisibility.js`.
*
* This one is a SECURITY boundary, not a convenience filter, which is why it is enumerated
* rather than described: a reader needs to see the whole ladder at once to reason about it.
*/
export const visibilityLadder = ['anonymous', 'logged_in', 'player', 'staff', 'admin'];
/**
* Canonical documents, by the question each answers. Checked to still exist in `docs`.
*
* Grouped by whose documents they are (PLAN.md §16, D63): `core` describes the platform in
* core's words, and each game's group may use that game's. The keys of the outer object are
* `core` plus keys of `platform.json`'s `games`, which is what lets the page label a game's
* group with the game's name and `checkTerms.mjs` hold each group to its own vocabulary.
*/
export const canonicalDocs = {
core: {
'website/ARCHITECTURE.md': 'How the website fits together — the canonical diagram',
'website/BACKEND_DESIGN.md': 'The API, schema and security contract',
'website/MODULE_SYSTEM.md': 'Why the module system is shaped this way',
'website/MODULE_API.md': 'Everything a module may do — the contract',
'website/TEAMS.md': 'Teams as a platform primitive',
'website/EVENTS.md': 'The Event System — leases, the ledger, and the module seam',
'website/THEMING_AND_NAV.md': 'Admin-configurable theme, assets and navigation',
'website/TRUSTED_DEVICES_MFA.md': 'Trusted devices and the second factor',
'installer/INSTALL.md': 'The operator guide for setting a game server up',
'installer/PLAN.md': "The installer's design of record",
'modules/rust-dryrun.md': 'The second game module as first designed on paper, to test that the contract generalises — since built',
'android/PLAN.md': 'The Android app',
},
uo: {
'link/PLAN.md': 'The sidecar design of record, the data catalog and the wire protocol',
'link/INTEGRATION.md': 'Integrating with the sidecar',
'link/v8.md': 'The current protocol, and its cross-repository obligations',
'link/SHARD_PREREQS.md': 'What a shard host needs before any of this works',
'link/ADMIN_CONTROLS.md': 'What the site may command the game to do',
'website/SHARD_VISIBILITY.md': 'The audience ladder, for administrators',
'modules/uo/API.md': "The module's own API, including its audience rules",
},
rust: {
'rust-link/INSTALL.md': 'Installing the Rust bridge — the installer, the Pterodactyl egg, or by hand',
'rust-link/INTEGRATION.md': 'Integrating with the sidecar',
'rust-link/PROTOCOL.md': 'The wire protocol between the bridge plugin and the sidecar',
'modules/rust/PLAN.md': 'The Rust module design of record',
'modules/rust/OPERATING.md': "Running Oxide: uMod's operator pages, plus the notes on RaidableBases and Guarded Crate",
'modules/rust/CARBON.md': 'Where Carbon differs from Oxide, and nowhere else',
},
};