Files
Module-Rust/server/sidecarClient.js
wtclaude e4591af906
All checks were successful
PR Checks / server-tests (pull_request) Successful in 37s
PR Checks / frozen-manifest (pull_request) Successful in 1m0s
PR Checks / client-build (pull_request) Successful in 7m23s
feat(rust): Backpacks — staff and player views, the Bigger backpack step (PLAN_REDESIGNS §11.3)
The site half of §11.3.2, on protocol 14:

- `rust.backpack.size` ("Bigger backpack"): a kit reward with a different
  permission (D337) — one rust_perm_run_grants row per website user with
  backpacks.size.N, no kit, no credit; the perm mirror pushes it and the run's
  revert deletes it. risk change, reversible ledger, a new `rust.backpacks`
  budget, rust.kit.entitle's recipient modes including `linked`. Refused with
  "Backpacks is not installed on this server" in the dry run too (D199), and a
  size the server does not offer is refused by name. Its message is never
  silent: empty is the take-back warning (D338).
- `rust.options.backpacksizes`: each server's sizes above its smallest, from
  the last hello, grouped by server.
- GET /player/rust/backpacks (own links only, 20 reads a minute) and
  GET /admin/users/:id/rust/backpacks (that user's links only). Live, never
  stored; a stranger's id is a 404 and never reaches the game.
- Client: a shared Backpack panel (pick server and account, open, read once)
  on the account page and the admin user page; the grid draws pages of
  pageSize, each slot named by the game (D340, no icons), blueprints by what
  they make (D341), overflow marked. Backpacks joins the server's mod line.

Server 548/548 (13 new in test/backpacks.test.js), client 79/79 (5 new),
routes.manifest regenerated against the pinned core (89 routes), swagger
fragment regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-12 14:35:55 -05:00

589 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── The near end of a call whose far end is a Rust server ─────────────────
//
// Every other file in this module reads its own tables. This one is different in
// kind: it is the only place that leaves the process.
//
// **The website process never opens a connection to a game server**
// (MODULE_API.md §2.7). It opens one to a `rust-link` sidecar, which owns the
// socket to the game, persists what the game says before forwarding it, and
// answers reads from that store. `test/noGameConnection.test.js` enforces the
// decidable half of that rule and names this file as the one that may reach the
// network:
//
// const MAY_OPEN_SOCKETS = new Set(['sidecarClient.js'])
//
// ── One client per configured server ──────────────────────────────────────
//
// R8: the bridge is one game server to one sidecar. So this file takes the
// server row as an argument rather than holding a single configured endpoint —
// six servers is six base URLs and six tokens, and core never learns there is
// more than one.
//
// ── TIMEOUT_MS is not a tuning knob. It is half of a rule. ────────────────
//
// An event action declares `budgetMs`, and core's dispatcher enforces it: when
// the budget expires it stops waiting and classifies the failure as **retry**,
// unconditionally, without asking the action — it cannot ask, the action is still
// awaiting a socket. So if core's deadline is shorter than this one, an action
// never gets to classify its own failure and `{ ok: false, retry: false }` is
// unreachable code. `budgetMs` must EXCEED this.
//
// It is also bounded from the other side: the sidecar's own RPC reply timeout is
// ten seconds, so a value below that would give up while the sidecar is still
// legitimately waiting for the game. The ordering is
// `sidecar RPC timeout < TIMEOUT_MS < budgetMs`, and every one of the three
// is written down somewhere the other two can be checked against.
//
// ── This file never throws ────────────────────────────────────────────────
//
// Every call answers `{ ok, status, data }`. A module that let a socket failure
// escape into a controller would hand an exception to a page whose whole job is
// to render while the game is off. The public site degrades; it does not 500.
const core = require('./core')
const log = core.logger('sidecar')
/** How long this client waits before giving up on a sidecar. See the header. */
const TIMEOUT_MS = 12000
/**
* The wire version this module speaks. Declared in FOUR places that must agree:
* here, `PROTOCOL_VERSION` in the sidecar, `ProtocolVersion` in the bridge
* plugin, and `protocol` in its `overlay.toml`.
*
* **8 — the leases.** Protocol 2 was the read path, 3 the first
* message the WEBSITE originates (`link.confirm`), 4 the first that writes to
* the game's permission store, 5 the first that writes to the game HOST'S
* FILESYSTEM; 6 adds the `clans` board and five clan events core's Teams are
* built from, and no route at all; **7** widens `entity.destroyed` to doors,
* walls and the cupboard and names who is authorised there, which is what the
* raid alert is sent to (PLAN.md §25); **8** adds the leases — `GET /lease`,
* `POST /lease` and `POST /lease/release` — which is what lets an event borrow
* a value on a server and give it back (PLAN.md §27); **9** adds the world
* verbs — `/world/monuments`, `/world/owned`, `/world/zone`, `/world/place` and
* `/world/revert` — what an event places in the world and gives back (PLAN.md
* §28); **10** adds the rewards — `/tally/open`, `/tally/snapshot`,
* `/tally/close`, `/kits` and `/chat`, and a `credits` field on the permission
* sync (PLAN.md §29); **11** adds the map — `/map`, `/map/chunk`,
* `/map/render` and `/map/live`, its picture and what moves on it (PLAN.md
* §30); **12** adds `/titles`, the chat titles BetterChat shows, and gives a
* permission sync a group's BetterChat style and a chat line a delivery and a
* voice (PLAN.md §33); **13** is the first player walk's fixes and redesigns and
* RunicNPC (PLAN_FIXES.md, PLAN_REDESIGNS.md §1–§6); **14** adds `/kits/uses`
* and a permission sync's `notices` — the step editor's kit warning and reward
* message (PLAN_REDESIGNS.md §2, D332, D333). The bump lands here in the same change as the emitters,
* because the sidecar refuses a client declaring a different version with a
* `409`: a module left on 2 would stop being able to read the server board it
* has been reading all along. A constant that lags the deployment is not a safe
* default; it is an outage with a version number on it.
*
* It is sent on every request as `X-RustLink-Version`, which turns a mismatched
* deployment into a `409` naming both numbers instead of a parse failure three
* layers further in.
*/
const PROTOCOL_VERSION = 14
/** What a caller gets back. Shaped once so every call site reads the same. */
function reply(ok, status, data = null) {
return { ok, status, data }
}
/**
* Normalises a configured base URL into something `new URL(path, base)` will not
* surprise anybody with.
*
* A trailing slash on the base and a leading slash on the path is the classic
* way to lose a path segment, and an operator pasting a URL out of a terminal
* supplies the trailing slash about half the time.
*/
function joinUrl(baseUrl, path) {
return `${String(baseUrl).replace(/\/+$/, '')}${path}`
}
/**
* One request to one sidecar.
*
* @param {object} server a `rust_servers` row, token already decrypted
* @param {string} server.baseUrl
* @param {string|null} server.token
* @param {string} path e.g. `/server`
* @param {object} [options]
* @param {string} [options.method]
* @param {object} [options.body]
* @param {number} [options.timeoutMs] shorter than `TIMEOUT_MS` only where a
* caller has a tighter budget of its own to fit inside — see `LEASE_TIMEOUT_MS`
*/
async function request(server, path, { method = 'GET', body = null, timeoutMs = TIMEOUT_MS } = {}) {
if (!server || !server.baseUrl) return reply(false, 'not-configured')
// A sidecar with auth off does not exist — it generates and persists a token on
// first start — so a missing token here is a half-finished admin form, not a
// sidecar to try unauthenticated. Saying so beats a 401 the operator has to
// interpret.
if (!server.token) return reply(false, 'no-token')
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
const res = await fetch(joinUrl(server.baseUrl, path), {
method,
signal: controller.signal,
headers: {
Authorization: `Bearer ${server.token}`,
'X-RustLink-Version': String(PROTOCOL_VERSION),
...(body ? { 'Content-Type': 'application/json' } : {}),
},
...(body ? { body: JSON.stringify(body) } : {}),
})
// A protocol mismatch is a deployment fault and deserves its own status, not
// to be folded into "the sidecar said no". The operator's fix is an upgrade
// of one component, and the message has to be able to say which.
if (res.status === 409) {
const detail = await safeJson(res)
log.warn('protocol mismatch', {
server: server.id,
module: PROTOCOL_VERSION,
sidecar: detail && detail.sidecar_protocol,
})
return reply(false, 'protocol-mismatch', detail)
}
if (res.status === 401) return reply(false, 'unauthorized')
// 204 is an ANSWER, not an absence of one: the sidecar is up and reports that
// the game has never connected. Collapsing it into a failure would make a
// freshly installed server indistinguishable from an unreachable one.
if (res.status === 204) return reply(true, 'empty', null)
if (!res.ok) return reply(false, `http-${res.status}`)
return reply(true, 'ok', await safeJson(res))
} catch (err) {
// `AbortError` is this client's own deadline firing, and it is worth telling
// apart from a refused connection: one means the sidecar is slow or the game
// is not answering, the other means nothing is listening.
const status = err && err.name === 'AbortError' ? 'timeout' : 'transport-error'
log.warn('sidecar request failed', { server: server.id, path, status, error: err.message })
return reply(false, status)
} finally {
clearTimeout(timer)
}
}
async function safeJson(res) {
try {
return await res.json()
} catch {
// A sidecar that answered 200 with something that is not JSON is a sidecar
// this module cannot use, but it is not a reason to throw at a page.
return null
}
}
/** Liveness, the protocol version, and whether the plugin is connected. Unauthenticated at the far end, but sent authenticated anyway so one code path covers every call. */
const health = (server) => request(server, '/health')
/** The last `server.hello` the sidecar stored. Answers while the game is off. */
const serverBoard = (server) => request(server, '/server')
/** A live round trip through the sidecar to the game. Fails when the game is down, by design. */
const liveStatus = (server) => request(server, '/status')
/** Every board at once: what is true now, before following what happens next. */
const boards = (server) => request(server, '/boards')
/**
* The ingest cursor: events after `since`, oldest first.
*
* **`since` is required here, unlike on the wire.** The sidecar treats an omitted
* cursor as "tell me where the end is", which is a genuinely useful question and
* a catastrophic default for an ingest loop that would silently store nothing
* and advance past everything. So the question is asked explicitly, by name, and
* a caller cannot get it by forgetting an argument.
*/
const feed = (server, since, limit = 200) =>
request(server, `/feed?since=${encodeURIComponent(since)}&limit=${encodeURIComponent(limit)}`)
/** Where the sidecar's history currently ends. What a new server's cursor starts at. */
const feedTail = (server) => request(server, '/feed')
/**
* Redeem a one-time link code against one server (protocol 3).
*
* **The only call in this file that is not a GET**, and the only one that asks
* the game a question rather than reading what it already said. The sidecar
* forwards the code to the plugin, which holds the pending codes in memory, and
* hands back what it answers.
*
* **A refused code comes back `{ ok: true }`.** `link.ok` and `link.error` are
* both answers — the sidecar reserves its own failures for the transport (503
* when the game is down, 504 when it is up and silent) — and the caller has to
* tell "that code is wrong" from "the game never replied" to say the right thing
* to a player. So the discrimination happens on `data.kind`, not on `ok`.
*
* A code is spent on the plugin's FIRST lookup whether or not it turns out to be
* expired, so this must never be called speculatively for its answer alone.
*/
const confirmLink = (server, code) =>
request(server, '/link/confirm', { method: 'POST', body: { code } })
/**
* What one server's loaded plugins have registered, and the groups its store
* holds (protocol 4).
*
* The option source behind the authoring form (D33). It is a live read through
* to the game rather than anything cached at the sidecar, because the answer
* changes when an operator loads a plugin — and the whole reason to ask is to
* offer names that will actually resolve. It therefore fails when the game is
* down, like `/status` and unlike every store-backed read.
*/
const permCatalogue = (server) => request(server, '/permissions/catalogue')
/**
* Push the whole permission set this site authors for one server (protocol 4).
*
* **The second call in this file that is not a GET, and the first that changes
* the game.** The body is the desired set plus what the site has withdrawn; the
* plugin diffs it against the live store, applies the difference and answers
* with a report — counts, the names it could not resolve, the memberships that
* are waiting on a first connection, and every holder the site did not author.
*
* **A refusal comes back `{ ok: true }`**, like a refused link code: `perm.error`
* and `perm.report` are both answers, and the sidecar keeps its own status codes
* for the transport. The caller discriminates on `data.kind`.
*/
const permSync = (server, set) => request(server, '/permissions/sync', { method: 'POST', body: set })
/**
* One page of the game's whole permission store (protocol 13, PLAN_REDESIGNS
* §1.2): every permission with the plugin that registered it, every group, every
* holder, and what event leases hold. `{ page: 0 }` starts a fresh read; later
* pages name the `snapshotId` page 0 answered with.
*
* Like the sync, a refusal (`perm.error`: `busy`, `stale`, `too-large`) comes
* back `{ ok: true }` and is told apart by `data.kind`.
*/
const permInventory = (server, { snapshotId = null, page = 0 } = {}) =>
request(server, '/permissions/inventory', {
method: 'POST',
body: snapshotId ? { snapshotId, page } : { page },
})
/**
* The whole inventory, every page, or `{ ok: false, error }`. A snapshot that
* goes stale mid-read is started again once from page 0: a page from a different
* snapshot would tear the answer.
*/
async function readInventory(server) {
for (let attempt = 0; attempt < 2; attempt++) {
// eslint-disable-next-line no-await-in-loop
const first = await permInventory(server, { page: 0 })
if (!first.ok) return { ok: false, error: first.status || 'unreachable' }
const head = first.data || {}
if (head.kind === 'perm.error') return { ok: false, error: `the game refused the read: ${head.reason || 'unknown'}` }
if (head.kind !== 'perm.inventory') return { ok: false, error: 'the game does not know perm.inventory (protocol 13)' }
let users = Array.isArray(head.users) ? head.users : []
let stale = false
for (let page = 1; page < Number(head.pages || 1); page++) {
// eslint-disable-next-line no-await-in-loop
const next = await permInventory(server, { snapshotId: head.snapshotId, page })
const data = (next.ok && next.data) || {}
if (data.kind !== 'perm.inventory') {
stale = true
break
}
users = users.concat(Array.isArray(data.users) ? data.users : [])
}
if (stale) continue
return {
ok: true,
inventory: {
snapshotId: head.snapshotId,
permissions: Array.isArray(head.permissions) ? head.permissions : [],
groups: Array.isArray(head.groups) ? head.groups : [],
leased: Array.isArray(head.leased) ? head.leased : [],
users,
stats: head.stats || null,
},
}
}
return { ok: false, error: 'the inventory changed while it was being read, twice' }
}
/**
* Every settings file on one game host, and every plugin loaded to reload one
* (protocol 5, R18).
*
* A description of the tree, never its contents: paths, sizes, which files are
* too large to edit, and the plugin each one probably belongs to. **Probably**
* is the operative word and it survives all the way to the form — a folder name
* is convention, not contract, and reloading the wrong plugin would report
* success while the edited one never re-read anything.
*
* Live, like `/status`: what is on a host's disk has no stale answer worth
* giving, and a cached one would be an edit an operator made over SSH that the
* website then overwrote.
*/
const configFiles = (server) => request(server, '/config/files')
/** One settings file as text, with the version a write has to present back. */
const configFile = (server, path) =>
request(server, `/config/file?path=${encodeURIComponent(path)}`)
/**
* Replace a set of settings files and reload what owns them (protocol 5).
*
* **The only call in this module that writes to a filesystem**, and the only one
* whose reply routinely takes seconds: the plugin holds it open across the
* reload it is watching, and across the rollback if that reload never arrives.
*
* Like every other write on this bridge, a refusal comes back `{ ok: true }`
* with the answer in `data.kind` — `config.report` or `config.error`. The
* transport keeps its own codes, and a `504` here is the one case worth reading
* carefully: the plugin writes a whole set or restores a whole set, never half
* of either, so the state is knowable by re-reading rather than by guessing.
*/
const configWrite = (server, body) => request(server, '/config/write', { method: 'POST', body })
/**
* How long ONE lease call waits — shorter than every other call here, and for
* the same rule `TIMEOUT_MS` is written for, applied to a caller with a tighter
* budget.
*
* A lease is taken by core's `core.lease`, which declares no `budgetMs` and so
* runs under the dispatcher's default of 10 seconds — and inside that it makes
* TWO calls into this module, `read()` for the baseline and then `apply()`. At
* `TIMEOUT_MS` each, one slow read would let the dispatcher give up and call the
* attempt a retry while the module is still waiting, which is the ordering the
* header of this file exists to forbid. Two of these fit inside core's budget
* with a second to spare, and `leases.test.js` asserts the arithmetic rather than
* trusting it.
*
* It is below the sidecar's own ten-second reply timeout, so this end can give
* up on a call the game is still going to answer. For `read` that costs nothing.
* For `apply` it would leave a value held that core believes it never took — so
* `eventLeases.js` follows a timed-out apply with a release (§27.3).
*/
const LEASE_TIMEOUT_MS = 4500
/** The dispatcher's default action budget, which is what `core.lease` runs under. Mirrored, not imported: core does not export it. */
const CORE_LEASE_BUDGET_MS = 10000
/**
* What one server lends, what it holds now, and every hold in force
* (protocol 8). Narrowed to one key and target when given.
*/
const leaseList = (server, { key, target } = {}) => {
const q = []
if (key) q.push(`key=${encodeURIComponent(key)}`)
if (target) q.push(`target=${encodeURIComponent(target)}`)
return request(server, `/lease${q.length ? `?${q.join('&')}` : ''}`, { timeoutMs: LEASE_TIMEOUT_MS })
}
/**
* Borrow a value (protocol 8). Like every write on this bridge, a refusal
* comes back `{ ok: true }` with `data.kind` of `lease.error`; the transport
* keeps its own codes.
*/
const leaseApply = (server, body) =>
request(server, '/lease', { method: 'POST', body, timeoutMs: LEASE_TIMEOUT_MS })
/**
* Give a value back — compare-and-set at the far end. `data.kind` is
* `lease.ok`, `lease.drifted` (a 200: the plugin compared and declined to
* overwrite somebody's change) or `lease.error`.
*/
const leaseRelease = (server, body) =>
request(server, '/lease/release', { method: 'POST', body, timeoutMs: LEASE_TIMEOUT_MS })
/**
* This wipe's monuments, the plugin's placeable allowlist and its bounds
* (protocol 9). Live, because a map changes at every wipe.
*/
const worldMonuments = (server) => request(server, '/world/monuments')
/**
* What the world still holds of what events made, looked for by net id on the
* game (a restart is not proof a crate is gone, §28.1). One run, or all.
*/
const worldOwned = (server, { runId } = {}) =>
request(server, `/world/owned${runId ? `?runId=${encodeURIComponent(runId)}` : ''}`)
/**
* Open a zone, or place crates or NPCs, for a run. `data.kind` is `world.ok`
* (with `placed`) or `world.error` (with `reason`); a repeated idempotency key
* is answered with the first call's ids and `repeat: true`.
*/
const worldZone = (server, body) => request(server, '/world/zone', { method: 'POST', body })
const worldPlace = (server, body) => request(server, '/world/place', { method: 'POST', body })
/**
* Give back what a run owns: named ids, else everything under a key, else the
* whole run. `data` lists `removed`, `gone` (already not there — a success)
* and `refused` (there, and not this run's to erase).
*/
const worldRevert = (server, body) => request(server, '/world/revert', { method: 'POST', body })
/**
* Start counting who takes part in a run (protocol 10). `data.kind` is
* `tally.ok` or `tally.error`; a repeated key is answered `repeat: true`.
*/
const tallyOpen = (server, body) => request(server, '/tally/open', { method: 'POST', body })
/** Who has taken part in a run so far, scored by the plugin. `tally.error` `no-tally` when it holds none. */
const tallySnapshot = (server, runId) =>
request(server, `/tally/snapshot?runId=${encodeURIComponent(String(runId))}`)
/** Stop counting and forget. `closed: false` means it was already gone, which is a success. */
const tallyClose = (server, body) => request(server, '/tally/close', { method: 'POST', body })
/** The kits this server's Kits plugin has, and the plugin's reward bounds. `kits.error` when Kits is not loaded. */
const kits = (server) => request(server, '/kits')
/**
* Protocol 14 (PLAN_REDESIGNS.md §2.6). Of these players, how many have used `kit`
* up and how many are on its cooldown: `kits.uses` with `{ kit, max, cooldown,
* asked, exhausted, onCooldown }`, counts only. `kits.error` when Kits is not
* loaded or has no such kit.
*/
const kitsUses = (server, kit, steamIds) =>
request(server, '/kits/uses', { method: 'POST', body: { kit, steamIds } })
/**
* Economics (protocol 14, PLAN_REDESIGNS.md §11.2.2). `economics.paid` lists each
* player's balance before and after; a key already paid answers `repeat: true`
* and pays nobody, so a retry is safe. `economics.error` has a reason.
*/
const economicsPay = (server, body) => request(server, '/economics/pay', { method: 'POST', body })
/** Each player's balance, and the server's starting balance when its config says. */
const economicsBalances = (server, steamIds) =>
request(server, '/economics/balances', { method: 'POST', body: { steamIds } })
/** Staff: `{ key, steamId, op, amount }`, op deposit · withdraw · set. `economics.adjusted` has before and after. */
const economicsAdjust = (server, body) => request(server, '/economics/adjust', { method: 'POST', body })
/**
* One player's backpack (protocol 14, PLAN_REDESIGNS.md §11.3.2), read live.
* `backpacks.contents` has `exists`, and with a backpack its capacity, page
* size and stacks; `backpacks.error` has a reason. Nothing is written.
*/
const backpacksRead = (server, steamId) => request(server, '/backpacks/read', { method: 'POST', body: { steamId } })
/**
* Say one line in the server's chat. `chat.ok` carries `said: false` when the
* key was said in the last ten minutes — a retry, answered and not repeated.
*/
const chat = (server, body) => request(server, '/chat', { method: 'POST', body })
/**
* Replace the chat titles a server's plugin holds (protocol 12). Whole, so a
* retry is harmless; `titles.ok` says whether BetterChat is there to show them.
*/
const titles = (server, body) => request(server, '/titles', { method: 'POST', body })
/**
* What one server's map is and where its picture comes from (protocol 11,
* stage one): `mapKey`, `source` (`companion`, `rendered` or `none`), the
* picture's size, `sha256` and slice count, the grid, and the monuments.
* `hashing: true` in place of `sha256` means ask again in a moment. A refusal
* is `data.kind` `map.error`, like every other write-shaped answer here.
*/
const mapInfo = (server) => request(server, '/map')
/**
* One slice of the picture, base64 in `data.data` (stage two). `map.error`
* `stale` when the map or picture moved since `mapInfo` — the caller starts
* again rather than splicing two maps.
*/
const mapChunk = (server, { mapKey, sha256, n }) =>
request(
server,
`/map/chunk?mapKey=${encodeURIComponent(mapKey)}&sha256=${encodeURIComponent(sha256)}&n=${encodeURIComponent(n)}`,
)
/**
* Ask the game to draw its own map (D109). Answered `accepted` at once; the
* render stalls the game on a LATER frame, and `mapInfo` says `rendered` when
* it is done.
*/
const mapRender = (server, body) => request(server, '/map/render', { method: 'POST', body })
/** Everything that moves on one server's map, every layer, unfiltered. The caller filters (§8.5). */
const mapLive = (server) => request(server, '/map/live')
/**
* RunicNPC (runicnpc PLAN.md stage 4, protocol 13). The server's own profiles as
* RunicNPC holds them (`npc.profiles`: `managed`, `profiles`, `refused`), read
* before the site's first push so it can adopt them (D244); a push, which
* replaces them whole and marks the server managed (`npc.ok` lists what
* RunicNPC refused); every placement; and one change to one placement, by `op`.
* Each refusal is `data.kind` `npc.error` with a `reason` and RunicNPC's own sentence.
*/
const npcProfiles = (server) => request(server, '/npc/profiles')
const npcProfilesSet = (server, profiles, factions) =>
request(server, '/npc/profiles', { method: 'POST', body: factions ? { profiles, factions } : { profiles } })
const npcPlacements = (server) => request(server, '/npc/placements')
const npcPlacement = (server, body) => request(server, '/npc/placement', { method: 'POST', body })
module.exports = {
TIMEOUT_MS,
LEASE_TIMEOUT_MS,
CORE_LEASE_BUDGET_MS,
PROTOCOL_VERSION,
request,
health,
serverBoard,
liveStatus,
boards,
feed,
feedTail,
confirmLink,
permCatalogue,
permSync,
permInventory,
readInventory,
configFiles,
configFile,
configWrite,
leaseList,
leaseApply,
leaseRelease,
worldMonuments,
worldOwned,
worldZone,
worldPlace,
worldRevert,
tallyOpen,
tallySnapshot,
tallyClose,
kits,
kitsUses,
economicsPay,
economicsBalances,
economicsAdjust,
backpacksRead,
chat,
titles,
mapInfo,
mapChunk,
mapRender,
mapLive,
npcProfiles,
npcProfilesSet,
npcPlacements,
npcPlacement,
joinUrl,
}