@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,90 @@
1
+ import { BlockList, isIPv4, isIPv6 } from "node:net";
2
+ /** Prefix Node gives an IPv4 address seen on a dual-stack socket. */
3
+ const IPV4_MAPPED_PREFIX = "::ffff:";
4
+ /** Addresses that mean "this machine" (D9). */
5
+ const LOOPBACK_ADDRESSES = ["127.0.0.1", "::1"];
6
+ /** Separator of an address and its prefix length in CIDR notation. */
7
+ const CIDR_SEPARATOR = "/";
8
+ /** Longest IPv4 prefix length. */
9
+ const IPV4_MAX_PREFIX = 32;
10
+ /** Longest IPv6 prefix length. */
11
+ const IPV6_MAX_PREFIX = 128;
12
+ /**
13
+ * Turns an IPv4-mapped IPv6 address (`::ffff:a.b.c.d`) into its IPv4 form, so lists and checks see one spelling.
14
+ * @param address The client address.
15
+ * @returns The normalized address.
16
+ */
17
+ export function normalizeAddress(address) {
18
+ const lowerCase = address.trim().toLowerCase();
19
+ if (lowerCase.startsWith(IPV4_MAPPED_PREFIX) && isIPv4(lowerCase.substring(IPV4_MAPPED_PREFIX.length)))
20
+ return lowerCase.substring(IPV4_MAPPED_PREFIX.length);
21
+ return lowerCase;
22
+ }
23
+ /**
24
+ * Whether an address is this machine's loopback interface (127.0.0.1, ::1 or ::ffff:127.0.0.1).
25
+ * @param address The client address.
26
+ * @returns True for a loopback address.
27
+ */
28
+ export function isLoopbackAddress(address) {
29
+ return LOOPBACK_ADDRESSES.includes(normalizeAddress(address));
30
+ }
31
+ /**
32
+ * Adds one address or CIDR range to a block list.
33
+ * @param blockList The list to add to.
34
+ * @param entry An address (`a.b.c.d`, IPv6) or a range (`a.b.c.d/n`).
35
+ * @returns False when the entry is not an address or range.
36
+ */
37
+ export function addNetworkEntry(blockList, entry) {
38
+ const separatorIndex = entry.indexOf(CIDR_SEPARATOR);
39
+ const address = normalizeAddress(separatorIndex >= 0 ? entry.substring(0, separatorIndex) : entry);
40
+ const family = addressFamily(address);
41
+ if (family == null)
42
+ return false;
43
+ if (separatorIndex < 0) {
44
+ blockList.addAddress(address, family);
45
+ return true;
46
+ }
47
+ const prefixText = entry.substring(separatorIndex + 1);
48
+ const prefix = Number(prefixText);
49
+ const maxPrefix = (family === "ipv4") ? IPV4_MAX_PREFIX : IPV6_MAX_PREFIX;
50
+ if (prefixText === "" || !Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix)
51
+ return false;
52
+ blockList.addSubnet(address, prefix, family);
53
+ return true;
54
+ }
55
+ /**
56
+ * Builds a block list from address and CIDR entries, skipping invalid ones (config validation reports those).
57
+ * @param entries The addresses and ranges.
58
+ * @returns The list.
59
+ */
60
+ export function createNetworkList(entries) {
61
+ const blockList = new BlockList();
62
+ for (const entry of entries)
63
+ addNetworkEntry(blockList, entry);
64
+ return blockList;
65
+ }
66
+ /**
67
+ * Whether an address falls inside a block list.
68
+ * @param blockList The list.
69
+ * @param address The client address.
70
+ * @returns True when the address is listed or inside a listed range; false for text that is not an address.
71
+ */
72
+ export function isAddressInList(blockList, address) {
73
+ const normalized = normalizeAddress(address);
74
+ const family = addressFamily(normalized);
75
+ if (family == null)
76
+ return false;
77
+ return blockList.check(normalized, family);
78
+ }
79
+ /**
80
+ * The IP family of an address.
81
+ * @param address The normalized address.
82
+ * @returns "ipv4", "ipv6", or null when it is not an IP address.
83
+ */
84
+ function addressFamily(address) {
85
+ if (isIPv4(address))
86
+ return "ipv4";
87
+ if (isIPv6(address))
88
+ return "ipv6";
89
+ return null;
90
+ }
@@ -0,0 +1,17 @@
1
+ import type { AuthIdentity, AuthRequest } from "./Authentication.js";
2
+ import type { GameServerStateValues } from "./GameServerState.js";
3
+ /**
4
+ * Verifies a player token: this server's Discord sessions first, then the configured auth (or anonymous when auth is off).
5
+ * @param state The server state.
6
+ * @param request The token and what the server knows about the call.
7
+ * @returns The verified identity, or null for an anonymous player on a server whose auth is off.
8
+ * @throws AuthRefusal (or whatever the configured verifier throws) when the token is refused.
9
+ */
10
+ export declare function verifyPlayerToken(state: GameServerStateValues, request: AuthRequest): Promise<AuthIdentity | null>;
11
+ /**
12
+ * Whether a token goes to a verifier at all (a Discord session always does; anything else only when auth is on).
13
+ * @param state The server state.
14
+ * @param token The token ("" when none was sent).
15
+ * @returns True when the token must be verified.
16
+ */
17
+ export declare function needsVerification(state: GameServerStateValues, token: string): boolean;
@@ -0,0 +1,24 @@
1
+ // PlayerTokens.ts — the one place that decides which verifier sees a player token (discord-sign-in D10).
2
+ /**
3
+ * Verifies a player token: this server's Discord sessions first, then the configured auth (or anonymous when auth is off).
4
+ * @param state The server state.
5
+ * @param request The token and what the server knows about the call.
6
+ * @returns The verified identity, or null for an anonymous player on a server whose auth is off.
7
+ * @throws AuthRefusal (or whatever the configured verifier throws) when the token is refused.
8
+ */
9
+ export async function verifyPlayerToken(state, request) {
10
+ if (state.discordSessions != null && state.discordSessions.isSessionToken(request.token))
11
+ return await state.discordSessions.verify(request.token);
12
+ if (state.verifier == null)
13
+ return null;
14
+ return await state.verifier(request);
15
+ }
16
+ /**
17
+ * Whether a token goes to a verifier at all (a Discord session always does; anything else only when auth is on).
18
+ * @param state The server state.
19
+ * @param token The token ("" when none was sent).
20
+ * @returns True when the token must be verified.
21
+ */
22
+ export function needsVerification(state, token) {
23
+ return (state.discordSessions != null && state.discordSessions.isSessionToken(token)) || state.verifier != null;
24
+ }
@@ -0,0 +1,73 @@
1
+ import type { FusionConnectRequest, FusionSessionLogicApi, FusionSessionServerEndpoint, FusionWelcome } from "./HeadlessRuntime.js";
2
+ /** Callbacks a transport delivers into, as TOOLKIT.NetworkTransportHandlers. */
3
+ export interface RoomTransportHandlers {
4
+ /** A simulation frame arrived from a player. */
5
+ onFrame(sourcePlayerId: number, frame: Uint8Array): void;
6
+ /** A control message arrived. */
7
+ onControl(name: string, payload: object): void;
8
+ /** The connection closed without a disconnect call. */
9
+ onDisconnected(reason: number): void;
10
+ }
11
+ /**
12
+ * The dedicated server runner's transport (A17, T13 step 3), as TOOLKIT.INetworkTransport: the runner runs inside the
13
+ * session room's process, so instead of a socket it talks straight to the room's FusionSessionLogic. It is also the
14
+ * session's server endpoint: frames clients send to the host reach the runner through receiveFrame.
15
+ */
16
+ export declare class RoomServerTransport implements FusionSessionServerEndpoint {
17
+ /** The session rules of the room the server runs in. */
18
+ private readonly logic;
19
+ /** Where received frames and control messages go. */
20
+ private handlers;
21
+ /** True between connect and disconnect. */
22
+ private connected;
23
+ /** The stats overlay's name of this connection. */
24
+ readonly transportName: string;
25
+ /**
26
+ * Creates the transport of a room's dedicated runner.
27
+ * @param logic The room's session logic.
28
+ */
29
+ constructor(logic: FusionSessionLogicApi);
30
+ /** True between connect and disconnect. */
31
+ get isConnected(): boolean;
32
+ /** No latency simulator: the server is in the room's process. */
33
+ get conditions(): null;
34
+ /**
35
+ * Makes the runner the session's dedicated server (player 0).
36
+ * @param request The runner's connect request.
37
+ * @returns The runner's welcome.
38
+ */
39
+ connect(request: FusionConnectRequest): Promise<FusionWelcome>;
40
+ /**
41
+ * Sends a frame from the server: ToPlayer to that client, any other route to every client.
42
+ * @param route The route.
43
+ * @param targetPlayerId The target for ToPlayer.
44
+ * @param frame The frame.
45
+ * @param channel The TOOLKIT.RpcChannel the runner sends it on; the room keeps it for datagram delivery (D7).
46
+ */
47
+ sendFrame(route: number, targetPlayerId: number, frame: Uint8Array, channel?: number): void;
48
+ /**
49
+ * Sends a control message from the server (a connect result is applied by the session itself).
50
+ * @param name The control message name.
51
+ * @param payload The payload.
52
+ */
53
+ sendControl(name: string, payload: object): void;
54
+ /**
55
+ * Sets where received frames and control messages go.
56
+ * @param handlers The callbacks.
57
+ */
58
+ setHandlers(handlers: RoomTransportHandlers): void;
59
+ /** Stops delivering; the room ends with its server. */
60
+ disconnect(): Promise<void>;
61
+ /**
62
+ * A frame a player sent to the server (the session endpoint).
63
+ * @param sourcePlayerId The sending player.
64
+ * @param frame The frame.
65
+ */
66
+ receiveFrame(sourcePlayerId: number, frame: Uint8Array): void;
67
+ /**
68
+ * A control message for the server (connect requests, players joining and leaving).
69
+ * @param name The control message name.
70
+ * @param payload The payload.
71
+ */
72
+ receiveControl(name: string, payload: object): void;
73
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The dedicated server runner's transport (A17, T13 step 3), as TOOLKIT.INetworkTransport: the runner runs inside the
3
+ * session room's process, so instead of a socket it talks straight to the room's FusionSessionLogic. It is also the
4
+ * session's server endpoint: frames clients send to the host reach the runner through receiveFrame.
5
+ */
6
+ export class RoomServerTransport {
7
+ /** The session rules of the room the server runs in. */
8
+ logic;
9
+ /** Where received frames and control messages go. */
10
+ handlers = null;
11
+ /** True between connect and disconnect. */
12
+ connected = false;
13
+ /** The stats overlay's name of this connection. */
14
+ transportName = "Room";
15
+ /**
16
+ * Creates the transport of a room's dedicated runner.
17
+ * @param logic The room's session logic.
18
+ */
19
+ constructor(logic) {
20
+ this.logic = logic;
21
+ }
22
+ /** True between connect and disconnect. */
23
+ get isConnected() {
24
+ return this.connected;
25
+ }
26
+ /** No latency simulator: the server is in the room's process. */
27
+ get conditions() {
28
+ return null;
29
+ }
30
+ /**
31
+ * Makes the runner the session's dedicated server (player 0).
32
+ * @param request The runner's connect request.
33
+ * @returns The runner's welcome.
34
+ */
35
+ connect(request) {
36
+ const welcome = this.logic.attachDedicatedServer(this, request);
37
+ this.connected = true;
38
+ return Promise.resolve(welcome);
39
+ }
40
+ /**
41
+ * Sends a frame from the server: ToPlayer to that client, any other route to every client.
42
+ * @param route The route.
43
+ * @param targetPlayerId The target for ToPlayer.
44
+ * @param frame The frame.
45
+ * @param channel The TOOLKIT.RpcChannel the runner sends it on; the room keeps it for datagram delivery (D7).
46
+ */
47
+ sendFrame(route, targetPlayerId, frame, channel) {
48
+ if (this.connected)
49
+ this.logic.sendFromServer(route, targetPlayerId, frame, channel);
50
+ }
51
+ /**
52
+ * Sends a control message from the server (a connect result is applied by the session itself).
53
+ * @param name The control message name.
54
+ * @param payload The payload.
55
+ */
56
+ sendControl(name, payload) {
57
+ if (this.connected)
58
+ this.logic.sendControlFromServer(name, payload, 0);
59
+ }
60
+ /**
61
+ * Sets where received frames and control messages go.
62
+ * @param handlers The callbacks.
63
+ */
64
+ setHandlers(handlers) {
65
+ this.handlers = handlers;
66
+ }
67
+ /** Stops delivering; the room ends with its server. */
68
+ disconnect() {
69
+ this.connected = false;
70
+ return Promise.resolve();
71
+ }
72
+ /**
73
+ * A frame a player sent to the server (the session endpoint).
74
+ * @param sourcePlayerId The sending player.
75
+ * @param frame The frame.
76
+ */
77
+ receiveFrame(sourcePlayerId, frame) {
78
+ if (this.connected && this.handlers != null)
79
+ this.handlers.onFrame(sourcePlayerId, frame);
80
+ }
81
+ /**
82
+ * A control message for the server (connect requests, players joining and leaving).
83
+ * @param name The control message name.
84
+ * @param payload The payload.
85
+ */
86
+ receiveControl(name, payload) {
87
+ if (this.connected && this.handlers != null)
88
+ this.handlers.onControl(name, payload);
89
+ }
90
+ }
@@ -0,0 +1,133 @@
1
+ import { type Endpoint } from "colyseus";
2
+ import type { GameServerUrls } from "./GameServer.js";
3
+ import type { GameServerMetrics } from "./GameServerMetrics.js";
4
+ import type { GameServerStateValues } from "./GameServerState.js";
5
+ import type { DedicatedStartedBy } from "./admin/AdminApiTypes.js";
6
+ /** What an endpoint handler reads from its request. */
7
+ export interface EndpointRequestContext {
8
+ /** Reads a request header. */
9
+ getHeader(name: string): string | null;
10
+ /** The path parameters (":roomId"). */
11
+ params?: Record<string, unknown>;
12
+ /** The parsed JSON body of a POST. */
13
+ body?: unknown;
14
+ }
15
+ /** One session of the admin list (D15). */
16
+ export interface AdminSessionSummary {
17
+ /** The Colyseus room id (the close route's :roomId). */
18
+ roomId: string;
19
+ /** The session name players join by. */
20
+ sessionName: string;
21
+ /** The session's TOOLKIT.GameMode name, or null when the room has not published one. */
22
+ gameMode: string | null;
23
+ /** Clients connected now. */
24
+ players: number;
25
+ /** Most clients the room accepts. */
26
+ maxPlayers: number;
27
+ /** True when a dedicated Server-mode runner hosts it. */
28
+ isDedicated: boolean;
29
+ }
30
+ /** The connections a game server accepts, as TOOLKIT.GameServerTransports. */
31
+ export interface GameServerTransports {
32
+ /** Always true. */
33
+ webSocket: boolean;
34
+ /** The HTTP/3 endpoint when UDP is enabled, else null. */
35
+ webTransport: WebTransportEndpointInfo | null;
36
+ }
37
+ /** The HTTP/3 endpoint of a game server, as TOOLKIT.GameServerWebTransport. */
38
+ export interface WebTransportEndpointInfo {
39
+ /** The UDP port (the same number as the TCP port). */
40
+ port: number;
41
+ /** SHA-256 hash of a pinned development certificate (serverCertificateHashes), or null for a CA-issued one. */
42
+ certificateHash: number[] | null;
43
+ }
44
+ /** The game server's public description (GET /game-server/info), as TOOLKIT.GameServerInfo. */
45
+ export interface GameServerInfo {
46
+ /** Always "Babylon Game Server". */
47
+ serverName: string;
48
+ /** The server package version. */
49
+ serverVersion: string;
50
+ /** The server's TOOLKIT.NETWORK_PROTOCOL_VERSION. */
51
+ protocolVersion: number;
52
+ /** The connections the server accepts. */
53
+ transports: GameServerTransports;
54
+ /** True when joins need an auth token. */
55
+ authRequired: boolean;
56
+ /** True when players can sign in with Discord (discord-sign-in D20). */
57
+ discordSignIn: boolean;
58
+ }
59
+ /**
60
+ * What the routes read. The routes are made before the server listens (defineServer takes them), so they read the
61
+ * server through these closures, which createGameServer fills in after listen.
62
+ */
63
+ export interface GameServerRouteContext {
64
+ /** @returns The process's game server state. */
65
+ state(): GameServerStateValues;
66
+ /** @returns The port the server listens on (the free port picked for 0). */
67
+ listeningPort(): number;
68
+ /** @returns The SHA-256 hash of the pinned HTTP/3 development certificate, or null. */
69
+ webTransportCertificateHash(): number[] | null;
70
+ /** @returns True between a successful listen and the end of a drain. */
71
+ isListening(): boolean;
72
+ /** @returns Where players and tools reach the server (valid once it listens). */
73
+ urls(): GameServerUrls;
74
+ /** @returns The Prometheus metrics, or null when metrics.enabled is false. */
75
+ metrics(): GameServerMetrics | null;
76
+ /** @returns True when Redis answers, or when no Redis is configured. */
77
+ isRedisReachable(): Promise<boolean>;
78
+ /** Drains the server (D17). */
79
+ drain(): Promise<void>;
80
+ /** @returns ISO time a running drain closes the remaining sessions, or null while no drain runs. */
81
+ drainDeadline(): string | null;
82
+ /**
83
+ * Starts a dedicated session of a configured scene.
84
+ * @param sessionName The session name.
85
+ * @param sceneName The configured scene.
86
+ * @param startedBy Who starts it, as the admin console shows it.
87
+ * @returns The room id.
88
+ */
89
+ startDedicatedSession(sessionName: string, sceneName: string, startedBy: DedicatedStartedBy): Promise<string>;
90
+ /**
91
+ * Audits a successful legacy admin call as the legacy token; the admin console sets it (game-server-admin-console T3).
92
+ * @param action "legacy.sessions", "legacy.close", "legacy.dedicated" or "legacy.drain".
93
+ * @param target The room id or session name it acted on, or null.
94
+ */
95
+ auditLegacyAdmin?(action: string, target: string | null): void;
96
+ }
97
+ /** The server package version, read once from package.json. */
98
+ export declare const SERVER_VERSION: string;
99
+ /**
100
+ * The HTTP endpoints of the game server (D15): health, readiness, info, Prometheus metrics and the admin API. They are
101
+ * Colyseus router endpoints, so they answer the same on the WebSocket and HTTP/3 transports and get core's CORS.
102
+ * @param context What the routes read.
103
+ * @returns The endpoints, keyed by name, for createRouter.
104
+ */
105
+ export declare function createGameServerRoutes(context: GameServerRouteContext): Record<string, Endpoint>;
106
+ /**
107
+ * Why the server is not ready, as the readiness endpoint and the fleet heartbeat report it: draining, Redis not
108
+ * answering, not listening, and maintenance that leaves readiness (game-server-admin-console D14).
109
+ * @param context What the routes read.
110
+ * @returns The reasons; empty when ready.
111
+ */
112
+ export declare function readinessReasons(context: GameServerRouteContext): Promise<string[]>;
113
+ /**
114
+ * Whether a request carries the admin bearer token (constant-time compare). The admin console also accepts it as a
115
+ * full-access key (game-server-admin-console D18).
116
+ * @param state The server state.
117
+ * @param request The request.
118
+ * @returns True when an admin token is configured and the request's bearer token equals it.
119
+ */
120
+ export declare function hasAdminToken(state: GameServerStateValues, request: EndpointRequestContext): boolean;
121
+ /**
122
+ * A JSON answer.
123
+ * @param status The HTTP status.
124
+ * @param body The value sent as JSON.
125
+ * @returns The response.
126
+ */
127
+ export declare function jsonResponse(status: number, body: unknown): Response;
128
+ /**
129
+ * The public description of the running server: name, version, protocol version, transports and whether auth is on.
130
+ * @param context What the routes read.
131
+ * @returns The info.
132
+ */
133
+ export declare function buildServerInfo(context: GameServerRouteContext): GameServerInfo;
@@ -0,0 +1,247 @@
1
+ import { CloseCode, createEndpoint, matchMaker } from "colyseus";
2
+ import { createRequire } from "node:module";
3
+ import { secretsMatch } from "./Authentication.js";
4
+ import { getLogger } from "./GameServerLogger.js";
5
+ import { getToolkitRuntime } from "./HeadlessRuntime.js";
6
+ /** The product name the info endpoint reports. */
7
+ const SERVER_NAME = "Babylon Game Server";
8
+ /** Path of the public info endpoint (TOOLKIT.GAME_SERVER_INFO_PATH). */
9
+ const INFO_PATH = "/game-server/info";
10
+ /** Path of the liveness endpoint. */
11
+ const HEALTH_PATH = "/game-server/health";
12
+ /** Path of the readiness endpoint. */
13
+ const READY_PATH = "/game-server/ready";
14
+ /** Path of the Prometheus endpoint. */
15
+ const METRICS_PATH = "/metrics";
16
+ /** Path of the admin session list. */
17
+ const ADMIN_SESSIONS_PATH = "/game-server/admin/sessions";
18
+ /** Path of the admin session close. */
19
+ const ADMIN_CLOSE_SESSION_PATH = "/game-server/admin/sessions/:roomId/close";
20
+ /** Path of the admin dedicated session start. */
21
+ const ADMIN_DEDICATED_PATH = "/game-server/admin/dedicated";
22
+ /** Path of the admin drain. */
23
+ const ADMIN_DRAIN_PATH = "/game-server/admin/drain";
24
+ /** The Colyseus room type of sessions. */
25
+ const SESSION_ROOM_NAME = "fusion_session";
26
+ /** Scheme word of a bearer Authorization header. */
27
+ const BEARER_PREFIX = "Bearer ";
28
+ /** Lowest status that is not a success. */
29
+ const FIRST_FAILURE_STATUS = 400;
30
+ /** HTTP statuses the endpoints answer with. */
31
+ const HTTP_OK = 200;
32
+ const HTTP_CREATED = 201;
33
+ const HTTP_ACCEPTED = 202;
34
+ const HTTP_BAD_REQUEST = 400;
35
+ const HTTP_NOT_FOUND = 404;
36
+ const HTTP_UNAUTHORIZED = 401;
37
+ const HTTP_FORBIDDEN = 403;
38
+ const HTTP_SERVICE_UNAVAILABLE = 503;
39
+ /** The server package version, read once from package.json. */
40
+ export const SERVER_VERSION = createRequire(import.meta.url)("../package.json").version;
41
+ /**
42
+ * The HTTP endpoints of the game server (D15): health, readiness, info, Prometheus metrics and the admin API. They are
43
+ * Colyseus router endpoints, so they answer the same on the WebSocket and HTTP/3 transports and get core's CORS.
44
+ * @param context What the routes read.
45
+ * @returns The endpoints, keyed by name, for createRouter.
46
+ */
47
+ export function createGameServerRoutes(context) {
48
+ return {
49
+ gameServerHealth: createEndpoint(HEALTH_PATH, { method: "GET" }, async () => jsonResponse(HTTP_OK, { status: "ok" })),
50
+ gameServerReady: createEndpoint(READY_PATH, { method: "GET" }, async () => readinessResponse(context)),
51
+ gameServerInfo: createEndpoint(INFO_PATH, { method: "GET" }, async () => jsonResponse(HTTP_OK, buildServerInfo(context))),
52
+ gameServerMetrics: createEndpoint(METRICS_PATH, { method: "GET" }, async (request) => metricsResponse(context, request)),
53
+ gameServerAdminSessions: createEndpoint(ADMIN_SESSIONS_PATH, { method: "GET" }, async (request) => requireAdmin(context, request) ?? audited(context, "legacy.sessions", null, jsonResponse(HTTP_OK, await listSessions()))),
54
+ gameServerAdminCloseSession: createEndpoint(ADMIN_CLOSE_SESSION_PATH, { method: "POST" }, async (request) => requireAdmin(context, request) ?? audited(context, "legacy.close", String(request.params?.roomId ?? ""), await closeSession(String(request.params?.roomId ?? "")))),
55
+ gameServerAdminDedicated: createEndpoint(ADMIN_DEDICATED_PATH, { method: "POST" }, async (request) => requireAdmin(context, request) ?? audited(context, "legacy.dedicated", dedicatedSessionNameOf(request.body), await startDedicated(context, request.body))),
56
+ gameServerAdminDrain: createEndpoint(ADMIN_DRAIN_PATH, { method: "POST" }, async (request) => requireAdmin(context, request) ?? audited(context, "legacy.drain", null, startDrain(context)))
57
+ };
58
+ }
59
+ /**
60
+ * The readiness answer: ready unless the server drains, Redis does not answer, it is not listening, or maintenance
61
+ * leaves readiness.
62
+ * @param context What the routes read.
63
+ * @returns 200 { ready: true }, or 503 { ready: false, reasons }.
64
+ */
65
+ async function readinessResponse(context) {
66
+ const reasons = await readinessReasons(context);
67
+ if (reasons.length === 0)
68
+ return jsonResponse(HTTP_OK, { ready: true });
69
+ return jsonResponse(HTTP_SERVICE_UNAVAILABLE, { ready: false, reasons: reasons });
70
+ }
71
+ /**
72
+ * Why the server is not ready, as the readiness endpoint and the fleet heartbeat report it: draining, Redis not
73
+ * answering, not listening, and maintenance that leaves readiness (game-server-admin-console D14).
74
+ * @param context What the routes read.
75
+ * @returns The reasons; empty when ready.
76
+ */
77
+ export async function readinessReasons(context) {
78
+ const state = context.state();
79
+ const reasons = [];
80
+ if (state.draining)
81
+ reasons.push("draining");
82
+ if (!(await context.isRedisReachable()))
83
+ reasons.push("redis unreachable");
84
+ if (!context.isListening())
85
+ reasons.push("not listening");
86
+ if (state.maintenance?.leaveReadiness === true)
87
+ reasons.push("maintenance");
88
+ return reasons;
89
+ }
90
+ /**
91
+ * The Prometheus answer: 404 when metrics are off, 403 without the admin token when metrics.requireAdminToken is on.
92
+ * @param context What the routes read.
93
+ * @param request The request.
94
+ * @returns The exposition text, or an error answer.
95
+ */
96
+ async function metricsResponse(context, request) {
97
+ const metrics = context.metrics();
98
+ if (metrics == null)
99
+ return jsonResponse(HTTP_NOT_FOUND, { error: "metrics are disabled" });
100
+ const state = context.state();
101
+ if (state.config.metrics.requireAdminToken && !hasAdminToken(state, request))
102
+ return jsonResponse(HTTP_FORBIDDEN, { error: "forbidden" });
103
+ return new Response(await metrics.render(), { status: HTTP_OK, headers: { "content-type": metrics.contentType } });
104
+ }
105
+ /**
106
+ * The admin check of an admin route (D15): the admin API exists only with a configured token.
107
+ * @param context What the routes read.
108
+ * @param request The request.
109
+ * @returns 404 when no admin token is configured, 401 on a missing or wrong bearer token, or null to go on.
110
+ */
111
+ function requireAdmin(context, request) {
112
+ const state = context.state();
113
+ if (state.config.admin.token == null)
114
+ return jsonResponse(HTTP_NOT_FOUND, { error: "not found" });
115
+ if (!hasAdminToken(state, request))
116
+ return jsonResponse(HTTP_UNAUTHORIZED, { error: "unauthorized" });
117
+ return null;
118
+ }
119
+ /**
120
+ * Passes a legacy admin answer through, auditing it when it succeeded (the answer itself is unchanged).
121
+ * @param context What the routes read.
122
+ * @param action The audit action.
123
+ * @param target The room id or session name, or null.
124
+ * @param response The answer.
125
+ * @returns The same answer.
126
+ */
127
+ function audited(context, action, target, response) {
128
+ if (response.status < FIRST_FAILURE_STATUS)
129
+ context.auditLegacyAdmin?.(action, target);
130
+ return response;
131
+ }
132
+ /**
133
+ * The session name of a dedicated start body, for the audit target.
134
+ * @param body The parsed body.
135
+ * @returns The session name, or null.
136
+ */
137
+ function dedicatedSessionNameOf(body) {
138
+ return isDedicatedStartBody(body) ? body.sessionName : null;
139
+ }
140
+ /**
141
+ * Whether a request carries the admin bearer token (constant-time compare). The admin console also accepts it as a
142
+ * full-access key (game-server-admin-console D18).
143
+ * @param state The server state.
144
+ * @param request The request.
145
+ * @returns True when an admin token is configured and the request's bearer token equals it.
146
+ */
147
+ export function hasAdminToken(state, request) {
148
+ const adminToken = state.config.admin.token;
149
+ const authorization = request.getHeader("authorization");
150
+ if (adminToken == null || authorization == null || !authorization.startsWith(BEARER_PREFIX))
151
+ return false;
152
+ return secretsMatch(authorization.substring(BEARER_PREFIX.length).trim(), adminToken);
153
+ }
154
+ /**
155
+ * Every session room the matchmaker knows (every instance when scaled out through Redis).
156
+ * @returns The sessions.
157
+ */
158
+ async function listSessions() {
159
+ const rooms = await matchMaker.query({ name: SESSION_ROOM_NAME });
160
+ return rooms.map((room) => ({
161
+ roomId: room.roomId,
162
+ sessionName: String(room.metadata?.sessionName ?? ""),
163
+ gameMode: room.metadata?.gameMode ?? null,
164
+ players: room.clients,
165
+ maxPlayers: room.maxClients,
166
+ isDedicated: room.metadata?.isDedicated === true
167
+ }));
168
+ }
169
+ /**
170
+ * Starts a dedicated session of a configured scene (D19).
171
+ * @param context What the routes read.
172
+ * @param body The request body.
173
+ * @returns 201 { roomId }; 400 without string sceneName and sessionName; 404 for an unknown scene; 503 when the scene
174
+ * fails to start (logged).
175
+ */
176
+ async function startDedicated(context, body) {
177
+ if (!isDedicatedStartBody(body))
178
+ return jsonResponse(HTTP_BAD_REQUEST, { error: "the body needs string sceneName and sessionName" });
179
+ if (context.state().registry.find(body.sceneName) == null)
180
+ return jsonResponse(HTTP_NOT_FOUND, { error: "no dedicated scene named " + body.sceneName });
181
+ try {
182
+ const roomId = await context.startDedicatedSession(body.sessionName, body.sceneName, "admin");
183
+ return jsonResponse(HTTP_CREATED, { roomId: roomId });
184
+ }
185
+ catch (error) {
186
+ getLogger().error("the dedicated session did not start", { sessionName: body.sessionName, sceneName: body.sceneName, error: String(error) });
187
+ return jsonResponse(HTTP_SERVICE_UNAVAILABLE, { error: "the dedicated session did not start: " + String(error) });
188
+ }
189
+ }
190
+ /**
191
+ * Whether a request body names a scene and a session.
192
+ * @param body The parsed body.
193
+ * @returns True when sceneName and sessionName are both text.
194
+ */
195
+ function isDedicatedStartBody(body) {
196
+ if (body == null || typeof body !== "object")
197
+ return false;
198
+ const fields = body;
199
+ return typeof fields.sceneName === "string" && typeof fields.sessionName === "string";
200
+ }
201
+ /**
202
+ * Starts a drain without exiting the process (the CLI's signal handlers exit; the admin route does not).
203
+ * @param context What the routes read.
204
+ * @returns 202 { draining: true } at once; the drain goes on in the background.
205
+ */
206
+ function startDrain(context) {
207
+ context.drain().catch((error) => getLogger().error("drain failed", { error: String(error) }));
208
+ return jsonResponse(HTTP_ACCEPTED, { draining: true });
209
+ }
210
+ /**
211
+ * Closes a session room: its clients are disconnected with CloseCode.CONSENTED.
212
+ * @param roomId The room id.
213
+ * @returns 200 { closed: roomId }, or 404 when no such room exists.
214
+ */
215
+ async function closeSession(roomId) {
216
+ const room = await matchMaker.getRoomById(roomId);
217
+ if (room == null)
218
+ return jsonResponse(HTTP_NOT_FOUND, { error: "no such session" });
219
+ await matchMaker.remoteRoomCall(roomId, "disconnect", [CloseCode.CONSENTED]);
220
+ return jsonResponse(HTTP_OK, { closed: roomId });
221
+ }
222
+ /**
223
+ * A JSON answer.
224
+ * @param status The HTTP status.
225
+ * @param body The value sent as JSON.
226
+ * @returns The response.
227
+ */
228
+ export function jsonResponse(status, body) {
229
+ return new Response(JSON.stringify(body), { status: status, headers: { "content-type": "application/json" } });
230
+ }
231
+ /**
232
+ * The public description of the running server: name, version, protocol version, transports and whether auth is on.
233
+ * @param context What the routes read.
234
+ * @returns The info.
235
+ */
236
+ export function buildServerInfo(context) {
237
+ const state = context.state();
238
+ const webTransport = state.config.udp.enabled ? { port: context.listeningPort(), certificateHash: context.webTransportCertificateHash() } : null;
239
+ return {
240
+ serverName: SERVER_NAME,
241
+ serverVersion: SERVER_VERSION,
242
+ protocolVersion: getToolkitRuntime().TOOLKIT.NETWORK_PROTOCOL_VERSION,
243
+ transports: { webSocket: true, webTransport: webTransport },
244
+ authRequired: state.config.auth.enabled,
245
+ discordSignIn: state.discordSessions != null
246
+ };
247
+ }