@babylonjs-toolkit/server 0.0.0-stage → 9.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CUSTOM.md +1690 -0
  2. package/DEPLOY.md +2012 -0
  3. package/README.md +1081 -2
  4. package/lib/AbuseLimits.d.ts +64 -0
  5. package/lib/AbuseLimits.js +121 -0
  6. package/lib/Authentication.d.ts +86 -0
  7. package/lib/Authentication.js +272 -0
  8. package/lib/BootSessionOwner.d.ts +86 -0
  9. package/lib/BootSessionOwner.js +116 -0
  10. package/lib/ColyseusLoggerAdapter.d.ts +32 -0
  11. package/lib/ColyseusLoggerAdapter.js +73 -0
  12. package/lib/DedicatedSceneRegistry.d.ts +52 -0
  13. package/lib/DedicatedSceneRegistry.js +74 -0
  14. package/lib/DedicatedSession.d.ts +152 -0
  15. package/lib/DedicatedSession.js +173 -0
  16. package/lib/DualTransport.d.ts +112 -0
  17. package/lib/DualTransport.js +197 -0
  18. package/lib/GameServer.d.ts +64 -0
  19. package/lib/GameServer.js +647 -0
  20. package/lib/GameServerConfig.d.ts +387 -0
  21. package/lib/GameServerConfig.js +1059 -0
  22. package/lib/GameServerLobbyRoom.d.ts +33 -0
  23. package/lib/GameServerLobbyRoom.js +56 -0
  24. package/lib/GameServerLogger.d.ts +111 -0
  25. package/lib/GameServerLogger.js +190 -0
  26. package/lib/GameServerMetrics.d.ts +158 -0
  27. package/lib/GameServerMetrics.js +276 -0
  28. package/lib/GameServerState.d.ts +164 -0
  29. package/lib/GameServerState.js +26 -0
  30. package/lib/HeadlessRuntime.d.ts +316 -0
  31. package/lib/HeadlessRuntime.js +380 -0
  32. package/lib/JoinGate.d.ts +34 -0
  33. package/lib/JoinGate.js +221 -0
  34. package/lib/NetworkAddresses.d.ts +33 -0
  35. package/lib/NetworkAddresses.js +90 -0
  36. package/lib/PlayerTokens.d.ts +17 -0
  37. package/lib/PlayerTokens.js +24 -0
  38. package/lib/RoomServerTransport.d.ts +73 -0
  39. package/lib/RoomServerTransport.js +90 -0
  40. package/lib/ServerEndpoints.d.ts +133 -0
  41. package/lib/ServerEndpoints.js +247 -0
  42. package/lib/ServerInit.d.ts +28 -0
  43. package/lib/ServerInit.js +113 -0
  44. package/lib/SessionRoom.d.ts +391 -0
  45. package/lib/SessionRoom.js +930 -0
  46. package/lib/TlsCertificates.d.ts +35 -0
  47. package/lib/TlsCertificates.js +130 -0
  48. package/lib/admin/AccountRoutes.d.ts +8 -0
  49. package/lib/admin/AccountRoutes.js +502 -0
  50. package/lib/admin/AdminAccounts.d.ts +329 -0
  51. package/lib/admin/AdminAccounts.js +607 -0
  52. package/lib/admin/AdminApiRouter.d.ts +202 -0
  53. package/lib/admin/AdminApiRouter.js +465 -0
  54. package/lib/admin/AdminApiTypes.d.ts +1782 -0
  55. package/lib/admin/AdminApiTypes.js +4 -0
  56. package/lib/admin/AdminAudit.d.ts +113 -0
  57. package/lib/admin/AdminAudit.js +266 -0
  58. package/lib/admin/AdminCli.d.ts +7 -0
  59. package/lib/admin/AdminCli.js +351 -0
  60. package/lib/admin/AdminClock.d.ts +51 -0
  61. package/lib/admin/AdminClock.js +10 -0
  62. package/lib/admin/AdminConsole.d.ts +39 -0
  63. package/lib/admin/AdminConsole.js +325 -0
  64. package/lib/admin/AdminConstants.d.ts +147 -0
  65. package/lib/admin/AdminConstants.js +167 -0
  66. package/lib/admin/AdminCrypto.d.ts +19 -0
  67. package/lib/admin/AdminCrypto.js +29 -0
  68. package/lib/admin/AdminExtensionsHost.d.ts +176 -0
  69. package/lib/admin/AdminExtensionsHost.js +307 -0
  70. package/lib/admin/AdminPasswords.d.ts +33 -0
  71. package/lib/admin/AdminPasswords.js +92 -0
  72. package/lib/admin/AdminPermissions.d.ts +28 -0
  73. package/lib/admin/AdminPermissions.js +74 -0
  74. package/lib/admin/AdminRecords.d.ts +196 -0
  75. package/lib/admin/AdminRecords.js +3 -0
  76. package/lib/admin/AdminRequestBodies.d.ts +47 -0
  77. package/lib/admin/AdminRequestBodies.js +84 -0
  78. package/lib/admin/AdminRequestGate.d.ts +38 -0
  79. package/lib/admin/AdminRequestGate.js +59 -0
  80. package/lib/admin/AdminResponses.d.ts +103 -0
  81. package/lib/admin/AdminResponses.js +164 -0
  82. package/lib/admin/AdminServices.d.ts +99 -0
  83. package/lib/admin/AdminServices.js +4 -0
  84. package/lib/admin/AdminStaticFiles.d.ts +29 -0
  85. package/lib/admin/AdminStaticFiles.js +97 -0
  86. package/lib/admin/AdminStore.d.ts +152 -0
  87. package/lib/admin/AdminStore.js +46 -0
  88. package/lib/admin/AlertChannels.d.ts +43 -0
  89. package/lib/admin/AlertChannels.js +120 -0
  90. package/lib/admin/AlertEngine.d.ts +231 -0
  91. package/lib/admin/AlertEngine.js +681 -0
  92. package/lib/admin/AlertRoutes.d.ts +8 -0
  93. package/lib/admin/AlertRoutes.js +180 -0
  94. package/lib/admin/ApiKeyRoutes.d.ts +8 -0
  95. package/lib/admin/ApiKeyRoutes.js +53 -0
  96. package/lib/admin/ApiKeys.d.ts +63 -0
  97. package/lib/admin/ApiKeys.js +167 -0
  98. package/lib/admin/AuditRoutes.d.ts +8 -0
  99. package/lib/admin/AuditRoutes.js +106 -0
  100. package/lib/admin/AuthToolRoutes.d.ts +13 -0
  101. package/lib/admin/AuthToolRoutes.js +164 -0
  102. package/lib/admin/BanList.d.ts +87 -0
  103. package/lib/admin/BanList.js +152 -0
  104. package/lib/admin/CborDecoder.d.ts +18 -0
  105. package/lib/admin/CborDecoder.js +201 -0
  106. package/lib/admin/ColyseusCloudClient.d.ts +313 -0
  107. package/lib/admin/ColyseusCloudClient.js +272 -0
  108. package/lib/admin/ColyseusCloudRoutes.d.ts +8 -0
  109. package/lib/admin/ColyseusCloudRoutes.js +91 -0
  110. package/lib/admin/ColyseusCloudService.d.ts +180 -0
  111. package/lib/admin/ColyseusCloudService.js +476 -0
  112. package/lib/admin/ColyseusCloudSettings.d.ts +16 -0
  113. package/lib/admin/ColyseusCloudSettings.js +29 -0
  114. package/lib/admin/ColyseusCloudViews.d.ts +105 -0
  115. package/lib/admin/ColyseusCloudViews.js +371 -0
  116. package/lib/admin/CommonPasswords.d.ts +6 -0
  117. package/lib/admin/CommonPasswords.js +19 -0
  118. package/lib/admin/ConfigOverrides.d.ts +174 -0
  119. package/lib/admin/ConfigOverrides.js +406 -0
  120. package/lib/admin/ConfigRoutes.d.ts +8 -0
  121. package/lib/admin/ConfigRoutes.js +91 -0
  122. package/lib/admin/ConsoleSessions.d.ts +112 -0
  123. package/lib/admin/ConsoleSessions.js +201 -0
  124. package/lib/admin/DedicatedRoutes.d.ts +21 -0
  125. package/lib/admin/DedicatedRoutes.js +254 -0
  126. package/lib/admin/ExtensionRoutes.d.ts +8 -0
  127. package/lib/admin/ExtensionRoutes.js +145 -0
  128. package/lib/admin/FileAdminStore.d.ts +157 -0
  129. package/lib/admin/FileAdminStore.js +349 -0
  130. package/lib/admin/FleetDirectory.d.ts +323 -0
  131. package/lib/admin/FleetDirectory.js +489 -0
  132. package/lib/admin/FleetLeader.d.ts +43 -0
  133. package/lib/admin/FleetLeader.js +89 -0
  134. package/lib/admin/FleetRoutes.d.ts +55 -0
  135. package/lib/admin/FleetRoutes.js +465 -0
  136. package/lib/admin/HealthWarnings.d.ts +10 -0
  137. package/lib/admin/HealthWarnings.js +95 -0
  138. package/lib/admin/LeaderboardRoutes.d.ts +8 -0
  139. package/lib/admin/LeaderboardRoutes.js +569 -0
  140. package/lib/admin/LogBuffer.d.ts +60 -0
  141. package/lib/admin/LogBuffer.js +92 -0
  142. package/lib/admin/LogRedaction.d.ts +15 -0
  143. package/lib/admin/LogRedaction.js +72 -0
  144. package/lib/admin/LogRoutes.d.ts +45 -0
  145. package/lib/admin/LogRoutes.js +307 -0
  146. package/lib/admin/MaintenanceMode.d.ts +41 -0
  147. package/lib/admin/MaintenanceMode.js +74 -0
  148. package/lib/admin/MaintenanceWindows.d.ts +91 -0
  149. package/lib/admin/MaintenanceWindows.js +207 -0
  150. package/lib/admin/MetricsRoutes.d.ts +8 -0
  151. package/lib/admin/MetricsRoutes.js +209 -0
  152. package/lib/admin/MetricsSampler.d.ts +124 -0
  153. package/lib/admin/MetricsSampler.js +211 -0
  154. package/lib/admin/OneTimeCodes.d.ts +60 -0
  155. package/lib/admin/OneTimeCodes.js +162 -0
  156. package/lib/admin/OpenIdConnect.d.ts +146 -0
  157. package/lib/admin/OpenIdConnect.js +335 -0
  158. package/lib/admin/Passkeys.d.ts +130 -0
  159. package/lib/admin/Passkeys.js +432 -0
  160. package/lib/admin/PlayerRoutes.d.ts +20 -0
  161. package/lib/admin/PlayerRoutes.js +676 -0
  162. package/lib/admin/RedisAdminStore.d.ts +114 -0
  163. package/lib/admin/RedisAdminStore.js +258 -0
  164. package/lib/admin/RolloutCoordinator.d.ts +99 -0
  165. package/lib/admin/RolloutCoordinator.js +321 -0
  166. package/lib/admin/ServerEvents.d.ts +57 -0
  167. package/lib/admin/ServerEvents.js +89 -0
  168. package/lib/admin/SessionRoutes.d.ts +113 -0
  169. package/lib/admin/SessionRoutes.js +386 -0
  170. package/lib/admin/UserRoutes.d.ts +8 -0
  171. package/lib/admin/UserRoutes.js +214 -0
  172. package/lib/cli.d.ts +2 -0
  173. package/lib/cli.js +221 -0
  174. package/lib/console/assets/index-CI5N7c-T.js +21 -0
  175. package/lib/console/assets/index-DPitvzLp.css +1 -0
  176. package/lib/console/favicon.svg +1 -0
  177. package/lib/console/index.html +16 -0
  178. package/lib/discord/DiscordOAuth.d.ts +57 -0
  179. package/lib/discord/DiscordOAuth.js +127 -0
  180. package/lib/discord/DiscordSessions.d.ts +79 -0
  181. package/lib/discord/DiscordSessions.js +135 -0
  182. package/lib/discord/DiscordSignInConstants.d.ts +54 -0
  183. package/lib/discord/DiscordSignInConstants.js +55 -0
  184. package/lib/discord/DiscordSignInRoutes.d.ts +51 -0
  185. package/lib/discord/DiscordSignInRoutes.js +359 -0
  186. package/lib/index.d.ts +24 -0
  187. package/lib/index.js +18 -0
  188. package/lib/leaderboards/FileLeaderboardRankStore.d.ts +293 -0
  189. package/lib/leaderboards/FileLeaderboardRankStore.js +808 -0
  190. package/lib/leaderboards/LeaderboardConstants.d.ts +63 -0
  191. package/lib/leaderboards/LeaderboardConstants.js +70 -0
  192. package/lib/leaderboards/LeaderboardError.d.ts +15 -0
  193. package/lib/leaderboards/LeaderboardError.js +20 -0
  194. package/lib/leaderboards/LeaderboardIdentity.d.ts +44 -0
  195. package/lib/leaderboards/LeaderboardIdentity.js +72 -0
  196. package/lib/leaderboards/LeaderboardPublicApi.d.ts +17 -0
  197. package/lib/leaderboards/LeaderboardPublicApi.js +389 -0
  198. package/lib/leaderboards/LeaderboardRankStore.d.ts +296 -0
  199. package/lib/leaderboards/LeaderboardRankStore.js +18 -0
  200. package/lib/leaderboards/LeaderboardRules.d.ts +122 -0
  201. package/lib/leaderboards/LeaderboardRules.js +472 -0
  202. package/lib/leaderboards/LeaderboardScheduler.d.ts +43 -0
  203. package/lib/leaderboards/LeaderboardScheduler.js +103 -0
  204. package/lib/leaderboards/LeaderboardService.d.ts +669 -0
  205. package/lib/leaderboards/LeaderboardService.js +1667 -0
  206. package/lib/leaderboards/LeaderboardTransfer.d.ts +35 -0
  207. package/lib/leaderboards/LeaderboardTransfer.js +580 -0
  208. package/lib/leaderboards/RedisLeaderboardRankStore.d.ts +282 -0
  209. package/lib/leaderboards/RedisLeaderboardRankStore.js +691 -0
  210. package/package.json +64 -4
  211. package/templates/init/Caddyfile +6 -0
  212. package/templates/init/DEPLOY.md +233 -0
  213. package/templates/init/Dockerfile +12 -0
  214. package/templates/init/app.json +21 -0
  215. package/templates/init/aws/README.md +118 -0
  216. package/templates/init/aws/ecs-fargate-alb.yml +207 -0
  217. package/templates/init/aws/ecs-fargate-nlb-quic.yml +210 -0
  218. package/templates/init/babylon-game-server.config.mjs +158 -0
  219. package/templates/init/colyseus-cloud/README.md +133 -0
  220. package/templates/init/colyseus-cloud/babylon-game-server.config.mjs +36 -0
  221. package/templates/init/colyseus-cloud/ecosystem.config.cjs +35 -0
  222. package/templates/init/colyseus-cloud/gitignore +6 -0
  223. package/templates/init/colyseus-cloud/package.json +15 -0
  224. package/templates/init/digitalocean-app.yaml +30 -0
  225. package/templates/init/docker-compose.full.yml +45 -0
  226. package/templates/init/docker-compose.websocket.yml +41 -0
  227. package/templates/init/fly.toml +56 -0
  228. package/templates/init/heroku.yml +11 -0
  229. package/templates/init/kubernetes/deployment.yaml +71 -0
  230. package/templates/init/kubernetes/redis.yaml +73 -0
  231. package/templates/init/kubernetes/service.yaml +24 -0
  232. package/templates/init/monitoring/grafana-dashboard.json +539 -0
  233. package/templates/init/monitoring/prometheus-alerts.yml +51 -0
  234. package/templates/init/monitoring/prometheus-scrape.yml +17 -0
  235. package/templates/init/railway.json +13 -0
  236. package/templates/init/render.yaml +19 -0
@@ -0,0 +1,386 @@
1
+ // The session routes (game-server-admin-console DR §6.11 sessions and notices rows, DR §6.6, §13 R17, R18): the
2
+ // fleet-wide session list, a session's page and inspector, close, close-all, lock and notices, plus the room admin
3
+ // client every room action goes through and this instance's describeSessions and sendNotice fleet commands.
4
+ import { ErrorCode, matchMaker, ServerError } from "colyseus";
5
+ import { AdminRequestError } from "./AdminApiRouter.js";
6
+ import { ADMIN_PAGE_SIZE, ROOM_CALL_TIMEOUT_MS, ROOM_STOP_TIMEOUT_MS } from "./AdminConstants.js";
7
+ import { hasPermission } from "./AdminPermissions.js";
8
+ import { bodyObject, booleanField, numberField, optionalStringField } from "./AdminRequestBodies.js";
9
+ import { createNoticePayload, noticeTextField, sendLocalNotice } from "./FleetRoutes.js";
10
+ /** The only SessionRoom methods the admin API may call through remoteRoomCall (D11). */
11
+ export const SESSION_ROOM_ADMIN_METHODS = {
12
+ describe: "adminDescribe",
13
+ inspect: "adminInspect",
14
+ kick: "adminKickPlayer",
15
+ close: "adminClose",
16
+ lock: "adminSetLocked",
17
+ notice: "adminSendNotice",
18
+ stop: "adminStopDedicated",
19
+ disconnectBanned: "adminDisconnectBanned"
20
+ };
21
+ /** HTTP status of a successful read or change. */
22
+ const HTTP_OK = 200;
23
+ /** HTTP status of an invalid request. */
24
+ const HTTP_BAD_REQUEST = 400;
25
+ /** HTTP status of an unknown session. */
26
+ const HTTP_NOT_FOUND = 404;
27
+ /** HTTP status of a room call that got no answer. */
28
+ const HTTP_GATEWAY_TIMEOUT = 504;
29
+ /** The text close-all must be confirmed with. */
30
+ const CLOSE_ALL_CONFIRMATION = "close all sessions";
31
+ /** Notice severities the API accepts. */
32
+ const NOTICE_SEVERITIES = new Set(["info", "warning", "critical"]);
33
+ /** Notice scopes the API accepts. */
34
+ const NOTICE_SCOPES = new Set(["fleet", "instance", "session", "player"]);
35
+ /** Session list fields the sort query may name (a leading "-" sorts descending). */
36
+ const SESSION_SORT_FIELDS = new Set(["sessionName", "gameMode", "players", "createdAt", "bytesPerSecond", "processId"]);
37
+ /** The sort of the session list when the query names none. */
38
+ const DEFAULT_SESSION_SORT = "sessionName";
39
+ /** Prefix of a descending sort. */
40
+ const DESCENDING_PREFIX = "-";
41
+ /** The playerId of "every player of the session". */
42
+ const EVERY_PLAYER = 0;
43
+ /** Every room action of the API, on whichever instance owns the room (R17). Only fixed method names are passed (D11). */
44
+ export class RoomAdminClient {
45
+ /**
46
+ * A session's page.
47
+ * @param roomId The room.
48
+ * @param includeAddresses False hides player addresses.
49
+ * @returns The page.
50
+ */
51
+ describe(roomId, includeAddresses) {
52
+ return this.call(roomId, SESSION_ROOM_ADMIN_METHODS.describe, [includeAddresses]);
53
+ }
54
+ /**
55
+ * A session's network objects.
56
+ * @param roomId The room.
57
+ * @returns The inspector view.
58
+ */
59
+ inspect(roomId) {
60
+ return this.call(roomId, SESSION_ROOM_ADMIN_METHODS.inspect, []);
61
+ }
62
+ /**
63
+ * Kicks a player (D12).
64
+ * @param roomId The room.
65
+ * @param playerId The player.
66
+ * @param message Shown to the player.
67
+ * @param allowRejoin True to skip the kick block.
68
+ * @returns False when the room has no connected player with that id.
69
+ */
70
+ kick(roomId, playerId, message, allowRejoin) {
71
+ return this.call(roomId, SESSION_ROOM_ADMIN_METHODS.kick, [playerId, message, allowRejoin]);
72
+ }
73
+ /**
74
+ * Closes a session, telling its players GameClosed (D12).
75
+ * @param roomId The room.
76
+ * @param message Shown to the players.
77
+ */
78
+ async close(roomId, message) {
79
+ await this.call(roomId, SESSION_ROOM_ADMIN_METHODS.close, [message]);
80
+ }
81
+ /**
82
+ * Locks or unlocks a session.
83
+ * @param roomId The room.
84
+ * @param locked True to lock.
85
+ */
86
+ async setLocked(roomId, locked) {
87
+ await this.call(roomId, SESSION_ROOM_ADMIN_METHODS.lock, [locked]);
88
+ }
89
+ /**
90
+ * Sends a notice to one player of a session or to all of them.
91
+ * @param roomId The room.
92
+ * @param notice The notice.
93
+ * @param playerId The player, or 0 for everyone.
94
+ * @returns How many players were targeted.
95
+ */
96
+ sendNotice(roomId, notice, playerId) {
97
+ return this.call(roomId, SESSION_ROOM_ADMIN_METHODS.notice, [notice, playerId]);
98
+ }
99
+ /**
100
+ * Stops a dedicated session, telling its players GameClosed (D23). The room answers before it disconnects (R17).
101
+ * @param roomId The room.
102
+ * @param message Shown to the players.
103
+ * @returns What a restart under the same name needs.
104
+ * @throws AdminRequestError 404 when the room runs no dedicated runner.
105
+ */
106
+ async stopDedicated(roomId, message) {
107
+ const restartOptions = await this.call(roomId, SESSION_ROOM_ADMIN_METHODS.stop, [message], ROOM_STOP_TIMEOUT_MS);
108
+ if (restartOptions == null)
109
+ throw new AdminRequestError(HTTP_NOT_FOUND, "no such dedicated session");
110
+ return restartOptions;
111
+ }
112
+ /**
113
+ * Calls a fixed admin method of a room on its instance.
114
+ * @param roomId The room.
115
+ * @param method The method (from SESSION_ROOM_ADMIN_METHODS).
116
+ * @param methodArguments Its arguments.
117
+ * @param timeoutMs How long its instance has to answer (ROOM_CALL_TIMEOUT_MS unless the call says).
118
+ * @returns The method's result.
119
+ * @throws AdminRequestError 404 for an unknown room, 504 when its instance does not answer.
120
+ */
121
+ async call(roomId, method, methodArguments, timeoutMs = ROOM_CALL_TIMEOUT_MS) {
122
+ if (await matchMaker.getRoomById(roomId) == null)
123
+ throw new AdminRequestError(HTTP_NOT_FOUND, "no such session");
124
+ try {
125
+ return await matchMaker.remoteRoomCall(roomId, method, methodArguments, timeoutMs);
126
+ }
127
+ catch (error) {
128
+ if (error instanceof ServerError && error.code === ErrorCode.MATCHMAKE_UNHANDLED)
129
+ throw new AdminRequestError(HTTP_GATEWAY_TIMEOUT, "the session's instance did not answer");
130
+ throw error;
131
+ }
132
+ }
133
+ }
134
+ /**
135
+ * Registers the sessions/* and notices routes.
136
+ * @param router The admin API router.
137
+ * @param services The shared services.
138
+ */
139
+ export function registerSessionRoutes(router, services) {
140
+ const routes = [
141
+ { method: "GET", pattern: "sessions", permission: "sessions.view", requiresStepUp: false, auditAction: null, handler: async (request) => ({ status: HTTP_OK, body: await listSessions(services, request) }) },
142
+ { method: "GET", pattern: "sessions/:roomId", permission: "sessions.view", requiresStepUp: false, auditAction: null, handler: async (request) => ({ status: HTTP_OK, body: await services.roomClient.describe(request.params.roomId, canViewAddresses(request)) }) },
143
+ { method: "GET", pattern: "sessions/:roomId/inspect", permission: "sessions.inspect", requiresStepUp: false, auditAction: null, handler: async (request) => ({ status: HTTP_OK, body: await services.roomClient.inspect(request.params.roomId) }) },
144
+ { method: "POST", pattern: "sessions/:roomId/close", permission: "sessions.manage", requiresStepUp: false, auditAction: "session.close", handler: async (request) => closeSession(services, request) },
145
+ { method: "POST", pattern: "sessions/close-all", permission: "sessions.manage", requiresStepUp: true, auditAction: "session.closeAll", handler: async (request) => closeAllSessions(services, request) },
146
+ { method: "POST", pattern: "sessions/:roomId/lock", permission: "sessions.manage", requiresStepUp: false, auditAction: "session.lock", handler: async (request) => lockSession(services, request) },
147
+ { method: "POST", pattern: "notices", permission: "sessions.manage", requiresStepUp: isFleetNotice, auditAction: "notice.send", handler: async (request) => sendNotice(services, request) }
148
+ ];
149
+ for (const route of routes)
150
+ router.register(route);
151
+ }
152
+ /**
153
+ * Registers this instance's describeSessions and sendNotice fleet commands (R15).
154
+ * @param services The shared services.
155
+ */
156
+ export function registerSessionCommands(services) {
157
+ services.fleet.handle("describeSessions", async () => Array.from(services.state.sessionRooms).map((room) => room.adminSummary()));
158
+ services.fleet.handle("sendNotice", async (command) => ({ targetedPlayers: sendLocalNotice(services.state, command.roomId, command.playerId, command.notice) }));
159
+ }
160
+ /**
161
+ * Every session of the fleet (R18), described by each instance in-process.
162
+ * @param services The shared services.
163
+ * @returns The sessions, unsorted.
164
+ */
165
+ export async function describeFleetSessions(services) {
166
+ const described = await services.fleet.runOnAll("describeSessions", null);
167
+ return described.results.flatMap((entry) => entry.result);
168
+ }
169
+ /**
170
+ * Whether the caller may see player addresses.
171
+ * @param request The request.
172
+ * @returns True with players.viewAddresses.
173
+ */
174
+ export function canViewAddresses(request) {
175
+ return request.caller != null && hasPermission(request.caller.permissions, "players.viewAddresses");
176
+ }
177
+ /**
178
+ * The page number of a list query (1-based).
179
+ * @param request The request.
180
+ * @returns The page, 1 when missing or invalid.
181
+ */
182
+ export function pageOf(request) {
183
+ const page = Number(request.query.get("page") ?? "1");
184
+ return (Number.isInteger(page) && page >= 1) ? page : 1;
185
+ }
186
+ /**
187
+ * One page of a list (ADMIN_PAGE_SIZE rows).
188
+ * @param items Every row, sorted.
189
+ * @param page The 1-based page.
190
+ * @returns The page and the total.
191
+ */
192
+ export function pageItems(items, page) {
193
+ const start = (page - 1) * ADMIN_PAGE_SIZE;
194
+ return { items: items.slice(start, start + ADMIN_PAGE_SIZE), total: items.length };
195
+ }
196
+ /**
197
+ * Whether a notice request is fleet-wide (step-up applies to fleet notices only, R8).
198
+ * @param request The request.
199
+ * @returns True for scope "fleet".
200
+ */
201
+ function isFleetNotice(request) {
202
+ return bodyObject(request.body).scope === "fleet";
203
+ }
204
+ /**
205
+ * GET sessions: filtered, sorted and paged on this instance (R18).
206
+ * @param services The shared services.
207
+ * @param request The request.
208
+ * @returns The page.
209
+ */
210
+ async function listSessions(services, request) {
211
+ const sessions = (await describeFleetSessions(services)).filter(sessionFilter(request.query));
212
+ sessions.sort(sessionComparator(request.query.get("sort") ?? DEFAULT_SESSION_SORT));
213
+ return pageItems(sessions, pageOf(request));
214
+ }
215
+ /**
216
+ * The filter of the session list query: search (session name or room id), gameMode, processId, dedicated, locked and
217
+ * minPlayers.
218
+ * @param query The query.
219
+ * @returns The filter.
220
+ */
221
+ function sessionFilter(query) {
222
+ const search = (query.get("search") ?? "").toLowerCase();
223
+ const gameMode = query.get("gameMode");
224
+ const processId = query.get("processId");
225
+ const dedicated = query.get("dedicated");
226
+ const locked = query.get("locked");
227
+ const minPlayers = Number(query.get("minPlayers") ?? "0");
228
+ return (session) => {
229
+ if (search !== "" && !session.sessionName.toLowerCase().includes(search) && !session.roomId.toLowerCase().includes(search))
230
+ return false;
231
+ if (gameMode != null && gameMode !== "" && session.gameMode !== gameMode)
232
+ return false;
233
+ if (processId != null && processId !== "" && session.processId !== processId)
234
+ return false;
235
+ if (dedicated != null && dedicated !== "" && (session.dedicatedScene != null) !== (dedicated === "true"))
236
+ return false;
237
+ if (locked != null && locked !== "" && session.locked !== (locked === "true"))
238
+ return false;
239
+ return !(Number.isFinite(minPlayers) && session.players < minPlayers);
240
+ };
241
+ }
242
+ /**
243
+ * The comparator of a sort query.
244
+ * @param sort A field of SESSION_SORT_FIELDS, with a leading "-" for descending.
245
+ * @returns The comparator (by session name for an unknown field).
246
+ */
247
+ function sessionComparator(sort) {
248
+ const descending = sort.startsWith(DESCENDING_PREFIX);
249
+ const requestedField = descending ? sort.substring(DESCENDING_PREFIX.length) : sort;
250
+ const field = (SESSION_SORT_FIELDS.has(requestedField) ? requestedField : DEFAULT_SESSION_SORT);
251
+ const direction = descending ? -1 : 1;
252
+ return (left, right) => direction * compareValues(left[field], right[field]);
253
+ }
254
+ /**
255
+ * Compares two list values: numbers by size, anything else as text (null last).
256
+ * @param left The first value.
257
+ * @param right The second value.
258
+ * @returns Negative, zero or positive.
259
+ */
260
+ function compareValues(left, right) {
261
+ if (typeof left === "number" && typeof right === "number")
262
+ return left - right;
263
+ if (left == null)
264
+ return (right == null) ? 0 : 1;
265
+ if (right == null)
266
+ return -1;
267
+ return String(left).localeCompare(String(right));
268
+ }
269
+ /**
270
+ * POST sessions/:roomId/close: the players are told GameClosed with the message (D12).
271
+ * @param services The shared services.
272
+ * @param request The request.
273
+ * @returns ok.
274
+ */
275
+ async function closeSession(services, request) {
276
+ const message = noticeTextField(bodyObject(request.body), "message") ?? "";
277
+ await services.roomClient.close(request.params.roomId, message);
278
+ return { status: HTTP_OK, body: { ok: true }, audit: { target: request.params.roomId, parameters: { message: message } } };
279
+ }
280
+ /**
281
+ * POST sessions/close-all: closes every session of the fleet, confirmed by typing "close all sessions".
282
+ * @param services The shared services.
283
+ * @param request The request.
284
+ * @returns How many sessions were closed.
285
+ * @throws AdminRequestError 400 without the confirmation.
286
+ */
287
+ async function closeAllSessions(services, request) {
288
+ const body = bodyObject(request.body);
289
+ if (body.confirmText !== CLOSE_ALL_CONFIRMATION)
290
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "type '" + CLOSE_ALL_CONFIRMATION + "' to confirm");
291
+ const message = noticeTextField(body, "message") ?? "";
292
+ const sessions = await describeFleetSessions(services);
293
+ let closed = 0;
294
+ for (const session of sessions) {
295
+ try {
296
+ await services.roomClient.close(session.roomId, message);
297
+ closed += 1;
298
+ }
299
+ catch (error) {
300
+ // A session that ended meanwhile, or whose instance did not answer, is not counted.
301
+ if (!(error instanceof AdminRequestError))
302
+ throw error;
303
+ }
304
+ }
305
+ return { status: HTTP_OK, body: { closed: closed }, audit: { target: "fleet", parameters: { message: message, closed: closed } } };
306
+ }
307
+ /**
308
+ * POST sessions/:roomId/lock.
309
+ * @param services The shared services.
310
+ * @param request The request.
311
+ * @returns ok.
312
+ */
313
+ async function lockSession(services, request) {
314
+ const locked = booleanField(bodyObject(request.body), "locked");
315
+ await services.roomClient.setLocked(request.params.roomId, locked);
316
+ return { status: HTTP_OK, body: { ok: true }, audit: { target: request.params.roomId, parameters: { locked: locked } } };
317
+ }
318
+ /**
319
+ * POST notices: to one player, a session, an instance or everyone (fleet scope through runOnAll).
320
+ * @param services The shared services.
321
+ * @param request The request.
322
+ * @returns How many players were targeted.
323
+ * @throws AdminRequestError 400 for a bad scope, severity, text or target.
324
+ */
325
+ async function sendNotice(services, request) {
326
+ const body = bodyObject(request.body);
327
+ const scope = String(body.scope ?? "");
328
+ if (!NOTICE_SCOPES.has(scope))
329
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "scope must be fleet, instance, session or player");
330
+ const notice = noticeOf(services, body);
331
+ const sent = { targetedPlayers: await deliverNotice(services, scope, body, notice) };
332
+ const target = (scope === "fleet") ? "fleet" : String(body.roomId ?? body.processId ?? "");
333
+ return { status: HTTP_OK, body: sent, audit: { target: target, parameters: { scope: scope, text: notice.text, severity: notice.severity, playerId: body.playerId ?? null } } };
334
+ }
335
+ /**
336
+ * The notice of a POST notices body.
337
+ * @param services The shared services.
338
+ * @param body The body.
339
+ * @returns The notice.
340
+ * @throws AdminRequestError 400 for an empty or long text, an unknown severity or a bad countdown.
341
+ */
342
+ function noticeOf(services, body) {
343
+ const text = noticeTextField(body, "text");
344
+ if (text == null || text === "")
345
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "text must be text");
346
+ const severity = String(body.severity ?? "info");
347
+ if (!NOTICE_SEVERITIES.has(severity))
348
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "severity must be info, warning or critical");
349
+ const countdownSeconds = (body.countdownSeconds == null) ? null : numberField(body, "countdownSeconds");
350
+ return createNoticePayload(services, text, severity, countdownSeconds);
351
+ }
352
+ /**
353
+ * Delivers a notice to its scope.
354
+ * @param services The shared services.
355
+ * @param scope "fleet", "instance", "session" or "player".
356
+ * @param body The body (processId, roomId, playerId).
357
+ * @param notice The notice.
358
+ * @returns How many players were targeted.
359
+ */
360
+ async function deliverNotice(services, scope, body, notice) {
361
+ if (scope === "fleet") {
362
+ const sent = await services.fleet.runOnAll("sendNotice", { roomId: null, playerId: EVERY_PLAYER, notice: notice });
363
+ return sent.results.reduce((total, entry) => total + entry.result.targetedPlayers, 0);
364
+ }
365
+ if (scope === "instance") {
366
+ const processId = requiredField(body, "processId");
367
+ const sent = await services.fleet.runOn(processId, "sendNotice", { roomId: null, playerId: EVERY_PLAYER, notice: notice });
368
+ return sent.targetedPlayers;
369
+ }
370
+ const roomId = requiredField(body, "roomId");
371
+ const playerId = (scope === "player") ? numberField(body, "playerId") : EVERY_PLAYER;
372
+ return services.roomClient.sendNotice(roomId, notice, playerId);
373
+ }
374
+ /**
375
+ * A required, non-empty text field.
376
+ * @param body The body.
377
+ * @param name The field.
378
+ * @returns The text.
379
+ * @throws AdminRequestError 400 when missing.
380
+ */
381
+ function requiredField(body, name) {
382
+ const value = optionalStringField(body, name);
383
+ if (value == null || value === "")
384
+ throw new AdminRequestError(HTTP_BAD_REQUEST, name + " must be text");
385
+ return value;
386
+ }
@@ -0,0 +1,8 @@
1
+ import { type AdminApiRouter } from "./AdminApiRouter.js";
2
+ import type { AdminServices } from "./AdminServices.js";
3
+ /**
4
+ * Registers every users/* route.
5
+ * @param router The admin API router.
6
+ * @param services The shared services.
7
+ */
8
+ export declare function registerUserRoutes(router: AdminApiRouter, services: AdminServices): void;
@@ -0,0 +1,214 @@
1
+ // The admin user routes (game-server-admin-console DR §6.11 users/* rows, DR §5 owner rules): only an owner manages
2
+ // owners, and the last enabled owner can never be removed, demoted or disabled.
3
+ import { AdminRequestError } from "./AdminApiRouter.js";
4
+ import { isAdminRole } from "./AdminPermissions.js";
5
+ import { bodyObject, booleanField, numberField, optionalStringField, stringField } from "./AdminRequestBodies.js";
6
+ /** HTTP status of a successful read or change. */
7
+ const HTTP_OK = 200;
8
+ /** HTTP status of a created account. */
9
+ const HTTP_CREATED = 201;
10
+ /** HTTP status of an invalid request. */
11
+ const HTTP_BAD_REQUEST = 400;
12
+ /** HTTP status of an owner rule refusal. */
13
+ const HTTP_FORBIDDEN = 403;
14
+ /** HTTP status of an unknown account. */
15
+ const HTTP_NOT_FOUND = 404;
16
+ /** HTTP status of the last-owner rule. */
17
+ const HTTP_CONFLICT = 409;
18
+ /** The permission every change here needs. */
19
+ const MANAGE_PERMISSION = "users.manage";
20
+ /** The owner role. */
21
+ const OWNER_ROLE = "owner";
22
+ /** The last-owner refusal (DR §5). */
23
+ const LAST_OWNER_MESSAGE = "the last owner cannot be removed";
24
+ /** The owner-rule refusal (DR §5). */
25
+ const OWNER_ONLY_MESSAGE = "only an owner can create, change or delete an owner";
26
+ /**
27
+ * Registers every users/* route.
28
+ * @param router The admin API router.
29
+ * @param services The shared services.
30
+ */
31
+ export function registerUserRoutes(router, services) {
32
+ const routes = [
33
+ { method: "GET", pattern: "users", permission: "users.view", requiresStepUp: false, auditAction: null, handler: async () => ({ status: HTTP_OK, body: await listUsers(services) }) },
34
+ { method: "POST", pattern: "users", permission: MANAGE_PERMISSION, requiresStepUp: true, auditAction: "user.create", handler: async (request) => createUser(services, request) },
35
+ { method: "PUT", pattern: "users/:userId", permission: MANAGE_PERMISSION, requiresStepUp: true, auditAction: "user.update", handler: async (request) => updateUser(services, request) },
36
+ { method: "POST", pattern: "users/:userId/reset-second-factor", permission: MANAGE_PERMISSION, requiresStepUp: true, auditAction: "user.resetSecondFactor", handler: async (request) => resetSecondFactor(services, request) },
37
+ { method: "POST", pattern: "users/:userId/unlock", permission: MANAGE_PERMISSION, requiresStepUp: false, auditAction: "user.unlock", handler: async (request) => unlockUser(services, request) },
38
+ { method: "POST", pattern: "users/:userId/end-sessions", permission: MANAGE_PERMISSION, requiresStepUp: false, auditAction: "user.endSessions", handler: async (request) => endSessions(services, request) },
39
+ { method: "DELETE", pattern: "users/:userId", permission: MANAGE_PERMISSION, requiresStepUp: true, auditAction: "user.delete", handler: async (request) => deleteUser(services, request) }
40
+ ];
41
+ for (const route of routes)
42
+ router.register(route);
43
+ }
44
+ /**
45
+ * GET users.
46
+ * @param services The shared services.
47
+ * @returns Every account, by username.
48
+ */
49
+ async function listUsers(services) {
50
+ const users = await services.store.listDocuments("users");
51
+ users.sort((left, right) => left.value.username.localeCompare(right.value.username));
52
+ return users.map((stored) => services.accounts.toUserView(stored));
53
+ }
54
+ /**
55
+ * POST users: an account with a temporary password; it enrols its authenticator at its first sign-in.
56
+ * @param services The shared services.
57
+ * @param request The request.
58
+ * @returns The new account.
59
+ */
60
+ async function createUser(services, request) {
61
+ const body = bodyObject(request.body);
62
+ const role = roleField(body);
63
+ if (role === OWNER_ROLE)
64
+ requireOwnerCaller(request);
65
+ const user = await services.accounts.createUser({
66
+ username: stringField(body, "username"),
67
+ displayName: optionalStringField(body, "displayName") ?? "",
68
+ email: optionalStringField(body, "email"),
69
+ role: role,
70
+ password: stringField(body, "temporaryPassword"),
71
+ oidcSubject: null
72
+ });
73
+ const stored = await requireUser(services, user.userId);
74
+ return { status: HTTP_CREATED, body: services.accounts.toUserView(stored), audit: { target: user.username, parameters: { role: role } } };
75
+ }
76
+ /**
77
+ * PUT users/:userId: display name, email, role or disabled, with the owner rules.
78
+ * @param services The shared services.
79
+ * @param request The request.
80
+ * @returns The updated account.
81
+ */
82
+ async function updateUser(services, request) {
83
+ const body = bodyObject(request.body);
84
+ const stored = await requireUser(services, request.params.userId);
85
+ const updated = Object.assign({}, stored.value);
86
+ if (body.displayName !== undefined)
87
+ updated.displayName = stringField(body, "displayName");
88
+ if (body.email !== undefined)
89
+ updated.email = optionalStringField(body, "email");
90
+ if (body.role !== undefined)
91
+ updated.role = roleField(body);
92
+ if (body.disabled !== undefined)
93
+ updated.disabled = booleanField(body, "disabled");
94
+ const touchesOwner = stored.value.role === OWNER_ROLE || updated.role === OWNER_ROLE;
95
+ if (touchesOwner)
96
+ requireOwnerCaller(request);
97
+ const losesOwner = stored.value.role === OWNER_ROLE && !stored.value.disabled && (updated.role !== OWNER_ROLE || updated.disabled);
98
+ if (losesOwner)
99
+ await requireAnotherOwner(services, stored.value.userId);
100
+ const saved = await services.store.putDocument("users", stored.id, updated, numberField(body, "expectedRevision"));
101
+ if (updated.disabled)
102
+ await services.sessions.endAllFor(updated.userId);
103
+ const changes = Object.assign({}, body);
104
+ delete changes.expectedRevision;
105
+ return { status: HTTP_OK, body: services.accounts.toUserView(saved), audit: { target: updated.username, parameters: changes } };
106
+ }
107
+ /**
108
+ * POST users/:userId/reset-second-factor.
109
+ * @param services The shared services.
110
+ * @param request The request.
111
+ * @returns ok.
112
+ */
113
+ async function resetSecondFactor(services, request) {
114
+ const stored = await requireUser(services, request.params.userId);
115
+ if (stored.value.role === OWNER_ROLE)
116
+ requireOwnerCaller(request);
117
+ await services.accounts.resetSecondFactor(stored.id);
118
+ await services.sessions.endAllFor(stored.id);
119
+ return okResult(stored.value.username);
120
+ }
121
+ /**
122
+ * POST users/:userId/unlock.
123
+ * @param services The shared services.
124
+ * @param request The request.
125
+ * @returns ok.
126
+ */
127
+ async function unlockUser(services, request) {
128
+ const stored = await requireUser(services, request.params.userId);
129
+ await services.accounts.unlock(stored.id);
130
+ return okResult(stored.value.username);
131
+ }
132
+ /**
133
+ * POST users/:userId/end-sessions.
134
+ * @param services The shared services.
135
+ * @param request The request.
136
+ * @returns How many sessions ended.
137
+ */
138
+ async function endSessions(services, request) {
139
+ const stored = await requireUser(services, request.params.userId);
140
+ if (stored.value.role === OWNER_ROLE)
141
+ requireOwnerCaller(request);
142
+ const ended = await services.sessions.endAllFor(stored.id);
143
+ return { status: HTTP_OK, body: { ended: ended }, audit: { target: stored.value.username, parameters: {} } };
144
+ }
145
+ /**
146
+ * DELETE users/:userId, with the owner rules.
147
+ * @param services The shared services.
148
+ * @param request The request.
149
+ * @returns ok.
150
+ */
151
+ async function deleteUser(services, request) {
152
+ const stored = await requireUser(services, request.params.userId);
153
+ if (stored.value.role === OWNER_ROLE) {
154
+ requireOwnerCaller(request);
155
+ if (!stored.value.disabled)
156
+ await requireAnotherOwner(services, stored.id);
157
+ }
158
+ await services.sessions.endAllFor(stored.id);
159
+ await services.store.deleteDocument("users", stored.id);
160
+ return okResult(stored.value.username);
161
+ }
162
+ /**
163
+ * An ok answer audited against an account.
164
+ * @param username The account's username.
165
+ * @returns The result.
166
+ */
167
+ function okResult(username) {
168
+ return { status: HTTP_OK, body: { ok: true }, audit: { target: username, parameters: {} } };
169
+ }
170
+ /**
171
+ * The role field of a body.
172
+ * @param body The body object.
173
+ * @returns The role.
174
+ * @throws AdminRequestError 400 for an unknown role.
175
+ */
176
+ function roleField(body) {
177
+ const role = stringField(body, "role");
178
+ if (!isAdminRole(role))
179
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "unknown role " + role);
180
+ return role;
181
+ }
182
+ /**
183
+ * Reads an account that must exist.
184
+ * @param services The shared services.
185
+ * @param userId The account id.
186
+ * @returns The stored account.
187
+ * @throws AdminRequestError 404 when it does not exist.
188
+ */
189
+ async function requireUser(services, userId) {
190
+ const stored = await services.accounts.getUser(userId);
191
+ if (stored == null)
192
+ throw new AdminRequestError(HTTP_NOT_FOUND, "no such admin user");
193
+ return stored;
194
+ }
195
+ /**
196
+ * Only a signed-in owner may create, change or delete an owner (an admin holds users.manage but cannot grant owner).
197
+ * @param request The request.
198
+ * @throws AdminRequestError 403 naming users.manage otherwise.
199
+ */
200
+ function requireOwnerCaller(request) {
201
+ const caller = request.caller;
202
+ if (caller?.user?.role !== OWNER_ROLE)
203
+ throw new AdminRequestError(HTTP_FORBIDDEN, OWNER_ONLY_MESSAGE, MANAGE_PERMISSION);
204
+ }
205
+ /**
206
+ * Refuses to remove the last enabled owner (DR §5).
207
+ * @param services The shared services.
208
+ * @param ownerId The owner being removed, demoted or disabled.
209
+ * @throws AdminRequestError 409 when no other enabled owner exists.
210
+ */
211
+ async function requireAnotherOwner(services, ownerId) {
212
+ if (!(await services.accounts.hasAnotherEnabledOwner(ownerId)))
213
+ throw new AdminRequestError(HTTP_CONFLICT, LAST_OWNER_MESSAGE);
214
+ }
package/lib/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};