All checks were successful
PR checks / checks (pull_request) Successful in 1m13s
Twenty pages completing the tree section 10 planned: Modules (8), Architecture
(5) and Reference (7). Four decisions, D38-D41, recorded in PLAN.md section 10.
D39 is the one that shaped the phase. Section 1 forbids re-specifying a
contract, and a Reference section is exactly where that rule is most tempting to
break, so the line is drawn at names: every environment variable, config key,
installer command, visibility rung and canonical document is listed with one
terse line saying what it is FOR, while shapes, semantics and every "why" stay
in the canonical document.
That is only safe because the names are checked. checkReference.mjs compares six
enumerations against the repositories that own them, over the Gitea API, as set
comparisons in BOTH directions -- and the second direction is the one that earns
its keep, because a reference page does not usually rot by describing something
that vanished, it rots by quietly not mentioning what was added since.
The check went green on its first run, which is the least trustworthy possible
outcome, so it was verified by breaking it: seven mutations, all caught. The one
worth keeping is the visibility ladder REORDERED with its membership unchanged
-- it is a security boundary, and a set comparison alone would have passed it.
D41 turns plannedSidebar from a checklist into a checked invariant, and finding
out why was the phase's first defect: it had already drifted, because phase 7
added the Content page under D37 and never updated the list. Nothing failed,
because nothing read it. checkSidebar.mjs now asserts the two trees agree on
groups, labels and order -- order because the order of Getting started IS the
installation path.
Two more things the writing found. PLAN.md's page count was wrong and had been
since section 10 was written ("roughly 38, 37 planned" for a tree of forty).
And module.json's `mounts` and the SPA's paths are different mechanisms that no
single document stated plainly -- module-uo declares admin: ["/shard",
"/uo-link"] while its screen lives at /admin/uo/link, because API routes are
deliberately NOT namespaced while SPA routes are. That is precisely the
distinction the installer got wrong in v0.1.0, and it now has a named home.
D40: the docs link to /architecture/'s drawn diagrams rather than importing
them. Those components carry marketing chrome and depend on diagram.css, which
Starlight does not load; the docs use text diagrams, which paste into an issue.
npm run verify green: 40 pages across 5 groups agree with plannedSidebar, 2390
internal links resolve, 123 repository links point at a branch, 19 facts, 59
quickstart checks, 22 reference enumerations, astro check 0 errors, 36 tests.
Co-Authored-By: Claude <noreply@anthropic.com>
191 lines
8.5 KiB
JavaScript
191 lines
8.5 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 the shard side: sync the overlay, 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',
|
|
},
|
|
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',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 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`. */
|
|
export const canonicalDocs = {
|
|
'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/SHARD_VISIBILITY.md': 'The audience ladder, for administrators',
|
|
'website/THEMING_AND_NAV.md': 'Admin-configurable theme, assets and navigation',
|
|
'website/TRUSTED_DEVICES_MFA.md': 'Trusted devices and the second factor',
|
|
'link/PLAN.md': 'The sidecar design of record, the data catalog and the wire protocol',
|
|
'link/INTEGRATION.md': 'Integrating with the sidecar',
|
|
'link/v4.md': 'Protocol 4, and its cross-repository obligations',
|
|
'link/ADMIN_CONTROLS.md': 'What the site may command the game to do',
|
|
'installer/INSTALL.md': 'The operator guide for setting a shard up',
|
|
'installer/PLAN.md': "The installer's design of record",
|
|
'modules/rust-dryrun.md': 'A second module designed on paper, to test that the contract generalises',
|
|
'modules/uo/API.md': "module-uo's own API, including its audience rules",
|
|
'android/PLAN.md': 'The Android app',
|
|
};
|