Files
Module-Rust/server/swagger/doc.js
wtclaude 383e89442e feat(rust): what the site has given a player, as the player reads it
Phase 8's website half. Phase 7 made the site the author of in-game
privilege and gave an operator every view of it; this is the other side,
and it is the first time a player can see what they hold without asking
one.

`GET /player/rust/permissions` is self-scoped in SQL and read-only by
construction — a grant a player could change would not be a grant. Three
things make it a different shape from the admin read rather than a
filtered one:

  * the scope arithmetic is answered on the server. A client handed `*`
    would have to know what the fleet is to say anything, and then
    `inScope` exists twice. Each entry carries the servers it reaches,
    already resolved and already marked.
  * `live` is the pushed ledger, never the authored row. A grant is not a
    privilege in a game until a sync confirmed it, and phase 7 is careful
    never to record a push that silently did nothing — so "waiting" is
    honest, and the alternative is the site claiming to have given
    something it has not.
  * nothing says WHY it is waiting. An offline server, a permission no
    loaded plugin registered and a store that has never seen the account
    all look the same from here; telling them apart is an operator's
    diagnosis and an inventory of what is installed.

An entitlement that reaches nobody still lists, and the page says so —
authored against the website account, it exists before a Steam id does,
and hiding it until one turns up is the defect the admin user page
shipped in phase 7 (PLAN.md §20.5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
2026-09-22 20:09:51 -05:00

509 lines
22 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 OpenAPI fragment: the shared half ─────────────────────────────────
//
// The tags and component schemas the `#swagger.*` annotations refer to.
// `scripts/swaggerFragment.js` feeds this to swagger-autogen; the per-endpoint
// detail lives beside each route, exactly as it does in core.
//
// **Two rules about names, and both belong to the MERGED document rather than to
// this file** (MODULE_API.md §6.1a). Core merges every started module's fragment
// over its own committed spec and serves the result at `/api/docs.json`, and core
// wins any key collision:
//
// • **Namespace what you DEFINE.** `RustServerList`, not `ServerList`. A second
// game's module describing the same idea under the same bare name would
// silently clobber this one or be clobbered by it.
// • **Reference what CORE defines by core's name.** `#/components/schemas/Error`
// and `ValidationError` are core's; point at them and do not redefine them.
//
// **swagger-autogen renders `components.schemas` from an EXAMPLE object, not from
// raw OpenAPI.** `{ type: 'object' }` comes back as a meta-description of itself.
// That is uniform across core's committed spec and is the house shape.
module.exports = {
tags: [
{
name: 'Public · Rust',
description: 'The Rust servers this site follows, as each one last reported itself',
},
{
name: 'Player · Rust',
description: 'The Rust surface for a signed-in player',
},
{
name: 'Admin · Rust',
description: 'Configuring the Rust servers and their sidecars',
},
],
components: {
schemas: {
RustServerList: {
type: 'object',
description: 'Every Rust server this site follows (GET /public/rust/servers).',
properties: {
servers: {
type: 'array',
items: { $ref: '#/components/schemas/RustServer' },
},
},
},
RustServer: {
type: 'object',
description: 'One Rust server, as it last reported itself.',
properties: {
id: { type: 'string', example: 'main' },
name: { type: 'string', example: 'Main · Vanilla' },
online: { type: 'boolean', example: true },
players: { type: 'integer', example: 42 },
maxPlayers: { type: 'integer', example: 100 },
hostname: { type: 'string', nullable: true, example: 'Runic Gateway · Main' },
level: { type: 'string', nullable: true, example: 'Procedural Map' },
worldSize: { type: 'integer', nullable: true, example: 4000 },
seed: { type: 'integer', nullable: true, example: 1234 },
updatedAt: { type: 'string', format: 'date-time', nullable: true },
stale: {
type: 'boolean',
description: 'Has nothing reported in longer than the freshness window? A stale row is reported offline.',
example: false,
},
},
},
RustAdminServerList: {
type: 'object',
description: 'The configured servers, with their sidecar settings (GET /admin/rust/servers).',
properties: {
servers: {
type: 'array',
items: { $ref: '#/components/schemas/RustAdminServer' },
},
},
},
RustAdminServer: {
type: 'object',
description: 'One configured server. The sidecar token is never included — `hasToken` reports only whether one is stored.',
properties: {
id: { type: 'string', example: 'main' },
name: { type: 'string', example: 'Main · Vanilla' },
sidecarBaseUrl: { type: 'string', example: 'http://10.0.0.5:8090' },
hasToken: { type: 'boolean', example: true },
protocol: { type: 'integer', example: 1 },
enabled: { type: 'boolean', example: true },
sortOrder: { type: 'integer', example: 0 },
reachable: {
type: 'boolean',
description: 'Did the sidecar answer on the last poll? Separate from `online`, which is about the game rather than the bridge.',
example: true,
},
bootId: { type: 'string', nullable: true, example: 'boot-20260915T194502Z' },
sidecarProtocol: { type: 'integer', nullable: true, example: 1 },
online: { type: 'boolean', example: true },
players: { type: 'integer', example: 42 },
stale: { type: 'boolean', example: false },
},
},
RustLink: {
type: 'object',
description: 'One Steam account linked to a website user. Never carries a code.',
properties: {
steamId: { type: 'string', example: '76561198000000000' },
name: {
type: 'string',
nullable: true,
description: 'What the player was called in game when they linked. A display name only — a Rust name changes on a whim and nothing identifies anybody by it.',
example: 'Wanderer',
},
serverId: {
type: 'string',
nullable: true,
description: 'Which server minted the code. Not part of the identity — a link is fleet-wide — but it is where a support conversation starts.',
example: 'main',
},
linkedAt: { type: 'string', format: 'date-time' },
},
},
RustLinkList: {
type: 'object',
description: 'The Steam accounts one website user holds (GET /player/rust/links).',
properties: {
links: { type: 'array', items: { $ref: '#/components/schemas/RustLink' } },
},
},
RustPlayerReach: {
type: 'object',
description: 'One server an entitlements scope reaches, and whether it is there yet.',
properties: {
id: { type: 'string', example: 'main' },
name: { type: 'string', example: 'Main · Vanilla+' },
live: {
type: 'boolean',
description: 'True only when a sync confirmed this into that servers own store. False covers every way it has not arrived — the server is offline, no loaded plugin registered the name, or its store has never seen the account — and the difference between those is an operators diagnosis, not a players.',
example: true,
},
},
},
RustPlayerPermissions: {
type: 'object',
description: 'What the site has given the signed-in player in game (GET /player/rust/permissions).',
properties: {
accounts: {
type: 'integer',
description: 'How many Steam accounts the caller has linked. Zero is why an entitlement can be authored and reach nobody.',
example: 1,
},
groups: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', example: 'vip' },
title: { type: 'string', example: 'VIP' },
scope: { type: 'string', description: 'A server id, or `*` for the whole fleet.', example: '*' },
since: { type: 'string', format: 'date-time' },
permissions: { type: 'array', items: { type: 'string' }, example: ['kits.vip'] },
reach: { type: 'array', items: { $ref: '#/components/schemas/RustPlayerReach' } },
},
},
},
grants: {
type: 'array',
items: {
type: 'object',
properties: {
permission: { type: 'string', example: 'kits.vip' },
scope: { type: 'string', example: '*' },
source: { type: 'string', description: 'Who authored it — `admin` now, an event action later.', example: 'admin' },
note: { type: 'string', nullable: true },
since: { type: 'string', format: 'date-time' },
reach: { type: 'array', items: { $ref: '#/components/schemas/RustPlayerReach' } },
},
},
},
},
},
RustLinkRequest: {
type: 'object',
required: ['code'],
properties: {
code: {
type: 'string',
description: 'The six-character code /link handed the player in game. Good for five minutes, and it works once.',
example: 'K7M2PQ',
},
},
},
RustLinkResult: {
type: 'object',
description: 'The result of redeeming a code.',
properties: {
linked: { type: 'boolean', example: true },
link: { $ref: '#/components/schemas/RustLink' },
already: {
type: 'boolean',
description: 'True when this Steam id was already linked to the caller — a second press of the button, not an error.',
example: false,
},
},
},
RustAdminLinkList: {
type: 'object',
description: 'One users Rust identity, for the admin.users.detail panel (GET /admin/users/{id}/rust/links).',
properties: {
links: {
type: 'array',
items: {
type: 'object',
properties: {
steamId: { type: 'string', example: '76561198000000000' },
name: {
type: 'string',
nullable: true,
description: 'What the game last saw this player called, falling back to the name recorded at link time.',
example: 'Wanderer',
},
linkedName: { type: 'string', nullable: true, example: 'Wanderer' },
serverId: { type: 'string', nullable: true, example: 'main' },
linkedAt: { type: 'string', format: 'date-time' },
firstSeen: { type: 'string', format: 'date-time', nullable: true },
lastSeen: { type: 'string', format: 'date-time', nullable: true },
servers: {
type: 'array',
description: 'All-time totals per server, summed across every wipe.',
items: {
type: 'object',
properties: {
serverId: { type: 'string', example: 'main' },
serverName: { type: 'string', example: 'Main · Vanilla' },
kills: { type: 'integer', example: 41 },
deaths: { type: 'integer', example: 37 },
npcKills: { type: 'integer', example: 120 },
structures: { type: 'integer', example: 64 },
playtimeSec: { type: 'integer', example: 43200 },
wipes: { type: 'integer', example: 2 },
lastSeen: { type: 'string', format: 'date-time', nullable: true },
},
},
},
},
},
},
},
},
RustPermissionModel: {
type: 'object',
description:
'The whole permission model (GET /admin/rust/permissions): what the site authors, what each game reported back, and the names a grant may use.',
properties: {
groups: {
type: 'array',
description: 'Groups the site authors, mirrored into each in-scope game as a real group.',
items: {
type: 'object',
properties: {
name: { type: 'string', example: 'vip' },
title: { type: 'string', example: 'VIP' },
rank: { type: 'integer', example: 10 },
scope: {
type: 'string',
description: 'A server id, or `*` for every server.',
example: '*',
},
permissions: { type: 'array', items: { type: 'string', example: 'kits.vip' } },
members: {
type: 'array',
items: {
type: 'object',
properties: {
userId: { type: 'integer', example: 42 },
username: { type: 'string', example: 'wanderer' },
steamId: {
type: 'string',
nullable: true,
description: 'Null when this account has linked no Steam id, in which case the membership reaches nobody yet.',
example: '76561198000000000',
},
playerName: { type: 'string', nullable: true, example: 'Wanderer' },
},
},
},
},
},
},
grants: {
type: 'array',
description: 'Permissions held by one person without a group. Unlike membership, a direct grant reaches a player who has never connected.',
items: {
type: 'object',
properties: {
id: { type: 'integer', example: 7 },
userId: { type: 'integer', example: 42 },
username: { type: 'string', example: 'wanderer' },
permission: { type: 'string', example: 'kits.gold' },
scope: { type: 'string', example: 'main' },
source: {
type: 'string',
description: 'What authored it — `admin`, `adopted`, or a later phases own writer.',
example: 'admin',
},
note: { type: 'string', nullable: true, example: null },
grantedAt: { type: 'string', format: 'date-time' },
accounts: {
type: 'array',
description: 'The Steam accounts this grant reaches. Empty means it reaches nobody yet.',
items: {
type: 'object',
properties: {
steamId: { type: 'string', example: '76561198000000000' },
name: { type: 'string', nullable: true, example: 'Wanderer' },
},
},
},
},
},
},
servers: {
type: 'array',
description: 'The state of the mirror, per configured server.',
items: { $ref: '#/components/schemas/RustPermissionSyncState' },
},
drift: {
type: 'array',
description: 'What a game holds that the site did not author. Reported, never undone.',
items: {
type: 'object',
properties: {
id: { type: 'integer', example: 3 },
serverId: { type: 'string', example: 'main' },
kind: {
type: 'string',
description: 'One of `grant`, `member`, `group-permission`.',
example: 'grant',
},
subject: {
type: 'string',
description: 'A Steam id, or a group name.',
example: '76561198000000000',
},
object: {
type: 'string',
description: 'A permission name, or a group name.',
example: 'kits.admin',
},
username: {
type: 'string',
nullable: true,
description: 'The website account holding that Steam id, when there is one. Without it the drift cannot be adopted, only revoked.',
example: 'wanderer',
},
firstSeen: { type: 'string', format: 'date-time' },
},
},
},
catalogue: {
type: 'array',
items: { $ref: '#/components/schemas/RustPermissionCatalogueEntry' },
},
},
},
RustPermissionSyncState: {
type: 'object',
description: 'Whether one servers store matches what the site authors, and what its last report said.',
properties: {
serverId: { type: 'string', example: 'main' },
state: {
type: 'string',
description: 'One of `pending`, `ok`, `failed`.',
example: 'ok',
},
inSync: {
type: 'boolean',
description: 'True when the last successful push carried the set the site currently authors.',
example: true,
},
dirty: { type: 'boolean', example: false },
lastAttemptAt: { type: 'string', format: 'date-time', nullable: true },
lastOkAt: { type: 'string', format: 'date-time', nullable: true },
error: {
type: 'string',
nullable: true,
description: 'Why the last attempt failed — a transport word (`timeout`, `no-token`, `protocol-mismatch`) or the games own refusal.',
example: null,
},
report: {
type: 'object',
nullable: true,
description: 'The plugins report from the last successful sync.',
properties: {
applied: {
type: 'object',
properties: {
grants: { type: 'integer', example: 2 },
revokes: { type: 'integer', example: 0 },
groupsCreated: { type: 'integer', example: 1 },
members: { type: 'integer', example: 3 },
},
},
alreadyCorrect: { type: 'integer', example: 14 },
unresolved: {
type: 'array',
description: 'Permission names no loaded plugin on that server has registered. A grant naming one lands nowhere and is not recorded as pushed.',
items: { type: 'string', example: 'kits.gold' },
},
pending: {
type: 'array',
description: 'Memberships waiting on a first connection: the store has no user record to put in a group yet.',
items: { type: 'string', example: '76561198000000000:vip' },
},
},
},
},
},
RustPermissionCatalogue: {
type: 'object',
description: 'Every permission name the configured servers have registered (GET /admin/rust/permissions/catalogue).',
properties: {
permissions: {
type: 'array',
items: { $ref: '#/components/schemas/RustPermissionCatalogueEntry' },
},
},
},
RustPermissionCatalogueEntry: {
type: 'object',
description: 'One registered permission name, and which servers know it.',
properties: {
permission: { type: 'string', example: 'kits.vip' },
servers: { type: 'array', items: { type: 'string', example: 'main' } },
},
},
RustPermissionSyncResult: {
type: 'object',
description: 'What a forced sync produced (POST /admin/rust/permissions/sync).',
properties: {
servers: { type: 'array', items: { $ref: '#/components/schemas/RustPermissionSyncState' } },
drift: { type: 'array', items: { type: 'object' } },
},
},
RustUserPermissions: {
type: 'object',
description: 'One persons Rust privileges, for the admin.users.detail panel (GET /admin/users/{id}/rust/permissions).',
properties: {
groups: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', example: 'vip' },
title: { type: 'string', example: 'VIP' },
scope: { type: 'string', example: '*' },
permissions: { type: 'array', items: { type: 'string', example: 'kits.vip' } },
},
},
},
grants: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer', example: 7 },
permission: { type: 'string', example: 'kits.gold' },
scope: { type: 'string', example: 'main' },
source: { type: 'string', example: 'admin' },
grantedAt: { type: 'string', format: 'date-time' },
},
},
},
reaches: {
type: 'array',
description: 'The Steam accounts these privileges reach. Empty means this person has linked nothing and holds them on paper only.',
items: { type: 'string', example: '76561198000000000' },
},
},
},
RustSidecarProbe: {
type: 'object',
description: 'What a sidecar said when probed (POST /admin/rust/servers/{id}/test).',
properties: {
ok: { type: 'boolean', example: true },
status: {
type: 'string',
description: 'What happened, in one word — this is what tells a wrong URL from a wrong token from a mismatched protocol. One of `ok`, `no-token`, `unauthorized`, `protocol-mismatch`, `timeout`, `transport-error`, or `http-<code>`.',
example: 'ok',
},
sidecar: {
type: 'object',
nullable: true,
description: 'The sidecars own health document, or the mismatch detail on a protocol disagreement.',
properties: {
status: { type: 'string', example: 'ok' },
protocol: { type: 'integer', example: 1 },
plugin_connected: { type: 'boolean', example: true },
database: { type: 'string', example: 'ok' },
uptime: { type: 'string', example: '3h 2m' },
last_event: { type: 'string', format: 'date-time', nullable: true },
},
},
},
},
},
},
}