// ── 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' } }, }, }, 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 user’s 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 phase’s 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 server’s 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 game’s own refusal.', example: null, }, report: { type: 'object', nullable: true, description: 'The plugin’s 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 person’s 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-`.', example: 'ok', }, sidecar: { type: 'object', nullable: true, description: 'The sidecar’s 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 }, }, }, }, }, }, }, }