@babylonjs-toolkit/server 0.0.0-stage → 9.35.1

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,380 @@
1
+ import { createRequire } from "node:module";
2
+ import { readFile } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import vm from "node:vm";
5
+ import { getLogger } from "./GameServerLogger.js";
6
+ /** The loaded runtime, or null before loadToolkitRuntime. */
7
+ let loadedRuntime = null;
8
+ /**
9
+ * Loads the browser runtime into Node (D25): babylonjs, its loaders and GUI, then the toolkit UMD bundle, which publishes
10
+ * TOOLKIT (networking included) on globalThis. The session logic and protocol the server runs are therefore the same code as the
11
+ * browser's. Loads once; later calls return the same globals.
12
+ * @returns The BABYLON and TOOLKIT namespaces.
13
+ */
14
+ export async function loadToolkitRuntime() {
15
+ if (loadedRuntime != null)
16
+ return loadedRuntime;
17
+ const requireModule = createRequire(import.meta.url);
18
+ const globalScope = globalThis;
19
+ const babylon = requireModule("babylonjs");
20
+ globalScope.BABYLON = babylon;
21
+ requireModule("babylonjs-loaders");
22
+ babylon.GUI = requireModule("babylonjs-gui");
23
+ requireModule("babylonjs-toolkit");
24
+ if (globalScope.TOOLKIT == null)
25
+ throw new Error("babylonjs-toolkit did not publish TOOLKIT on globalThis");
26
+ if (typeof globalScope.TOOLKIT.NETWORK_PROTOCOL_VERSION !== "number")
27
+ throw new Error(olderRuntimeMessage(requireModule));
28
+ loadedRuntime = { BABYLON: babylon, TOOLKIT: globalScope.TOOLKIT };
29
+ return loadedRuntime;
30
+ }
31
+ /**
32
+ * The startup error of a runtime that predates the network protocol version (§Error policy).
33
+ * @param requireModule Resolves packages the way the server does.
34
+ * @returns The message, naming the installed babylonjs-toolkit version.
35
+ */
36
+ function olderRuntimeMessage(requireModule) {
37
+ const runtimePackage = requireModule("babylonjs-toolkit/package.json");
38
+ return "babylonjs-toolkit " + (runtimePackage.version ?? "(unknown version)") + " is older than this server needs; install the version this server was released with";
39
+ }
40
+ /**
41
+ * The loaded runtime, for code that runs after createGameServer has loaded it (rooms).
42
+ * @returns The runtime globals.
43
+ * @throws Error when loadToolkitRuntime has not completed.
44
+ */
45
+ export function getToolkitRuntime() {
46
+ if (loadedRuntime == null)
47
+ throw new Error("The toolkit runtime is not loaded: await loadToolkitRuntime() first");
48
+ return loadedRuntime;
49
+ }
50
+ /** The SceneLoader entry points the toolkit calls (SceneManager, CoreFramework, NetworkSceneManager), with their argument positions. */
51
+ const SCENE_LOADER_ENTRY_POINTS = [
52
+ { name: "LoadAsync", rootUrlIndex: 0, pluginExtensionIndex: 4, nameIndex: 5, onErrorIndex: -1 },
53
+ { name: "AppendAsync", rootUrlIndex: 0, pluginExtensionIndex: 4, nameIndex: 5, onErrorIndex: -1 },
54
+ { name: "LoadAssetContainerAsync", rootUrlIndex: 0, pluginExtensionIndex: 4, nameIndex: 5, onErrorIndex: -1 },
55
+ { name: "ImportMeshAsync", rootUrlIndex: 1, pluginExtensionIndex: 5, nameIndex: 6, onErrorIndex: -1 },
56
+ { name: "LoadAssetContainer", rootUrlIndex: 0, pluginExtensionIndex: 6, nameIndex: 7, onErrorIndex: 5 },
57
+ { name: "ImportMesh", rootUrlIndex: 1, pluginExtensionIndex: 7, nameIndex: 8, onErrorIndex: 6 }
58
+ ];
59
+ /** Prefix of a data URL. */
60
+ const DATA_URL_PREFIX = "data:";
61
+ /** Prefix of a binary file as a data URL. */
62
+ const BINARY_DATA_URL_PREFIX = "data:application/octet-stream;base64,";
63
+ /** File extension of a binary glTF. */
64
+ const BINARY_GLTF_EXTENSION = ".glb";
65
+ /** Shortest time between two log lines about failed dedicated frames, in milliseconds. */
66
+ const FRAME_FAILURE_LOG_INTERVAL_MS = 10000;
67
+ /** Name and position of the camera added to a scene without one, as engine.html adds it (A19). */
68
+ const DEFAULT_CAMERA_NAME = "Default Camera";
69
+ const DEFAULT_CAMERA_HEIGHT = 5;
70
+ const DEFAULT_CAMERA_DISTANCE = -10;
71
+ /**
72
+ * Texture units a headless engine reports. A NullEngine draws nothing, so the toolkit's per-material sampler budget (16 on
73
+ * the NullEngine's invented limits) would only warn that materials "will not render".
74
+ */
75
+ const HEADLESS_TEXTURE_UNITS = 1024;
76
+ /** Runtime options of a headless scene (A18): no device pixel ratio, no input, no page loading screen, no bundle. */
77
+ const HEADLESS_RUNTIME_OPTIONS = { hardwareScalingLevel: 1, trackDevicePixelRatio: false, enableUserInput: false, showDefaultLoadingScreen: false, loadProjectScriptBundle: false };
78
+ /** Prefix of a file URL, which the fetch helpers read from disk. */
79
+ const FILE_URL_PREFIX = "file:";
80
+ /** Scene file URLs the scene loader adapts (HTTP and local folders, D19). */
81
+ const LOADABLE_SCENE_URL_PATTERN = /^(https?|file):/i;
82
+ /** Milliseconds per second. */
83
+ const MILLISECONDS_PER_SECOND = 1000;
84
+ /**
85
+ * URL prefixes the fetch helpers may load (D19), or null to allow every URL. It stays null when HeadlessRuntime is used on
86
+ * its own; createGameServer always sets it (to the dedicated scene registry's prefixes, possibly none) and close resets it.
87
+ */
88
+ let allowedUrlPrefixes = null;
89
+ /** Lower-cased file names of the scripts the Node script loader has run (the toolkit's bundle cache key). */
90
+ const loadedScriptNames = new Set();
91
+ /** Script loads in flight, by cache key, so concurrent loads of one bundle run it once. */
92
+ const pendingScriptLoads = new Map();
93
+ /** True once the Node loaders replaced Babylon's DOM-based ones. */
94
+ let nodeLoadersInstalled = false;
95
+ /** True once Havok is ready on globalThis. */
96
+ let havokInstalled = false;
97
+ /**
98
+ * Adds URL prefixes to the allow-list of the fetch helpers (D19). The first call turns the allow-list on: from then on
99
+ * only data: URLs and URLs under an allowed prefix load.
100
+ * @param prefixes The URL prefixes (scene roots and project scripts); none turns the allow-list on with only data: URLs.
101
+ */
102
+ export function allowScriptUrlPrefixes(prefixes) {
103
+ const allowed = allowedUrlPrefixes ?? [];
104
+ for (const prefix of prefixes) {
105
+ if (!allowed.includes(prefix))
106
+ allowed.push(prefix);
107
+ }
108
+ allowedUrlPrefixes = allowed;
109
+ }
110
+ /**
111
+ * Turns the allow-list off again (every URL allowed), as before any game server ran. createGameServer's close calls it.
112
+ */
113
+ export function resetScriptUrlAllowList() {
114
+ allowedUrlPrefixes = null;
115
+ }
116
+ /**
117
+ * Whether the fetch helpers may load a URL (D19).
118
+ * @param url The URL.
119
+ * @returns True for a data: URL, for any URL while the allow-list is off, else when the URL starts with an allowed prefix.
120
+ */
121
+ export function isUrlAllowed(url) {
122
+ if (url.startsWith(DATA_URL_PREFIX) || allowedUrlPrefixes == null)
123
+ return true;
124
+ return allowedUrlPrefixes.some((prefix) => url.startsWith(prefix));
125
+ }
126
+ /**
127
+ * The cache key of a script URL: its lower-cased file name, as the toolkit keys its project bundles (CanvasTools.ts).
128
+ * @param scriptUrl The script URL.
129
+ * @returns The key.
130
+ */
131
+ export function scriptCacheKey(scriptUrl) {
132
+ const withoutQuery = scriptUrl.split(/[?#]/)[0];
133
+ return withoutQuery.substring(withoutQuery.lastIndexOf("/") + 1).toLowerCase();
134
+ }
135
+ /**
136
+ * Loads a script in Node, as Babylon's LoadScriptAsync does in a page (T13 step 2): fetches it and runs it in this context,
137
+ * once per file name; a file name already loaded resolves at once.
138
+ * @param scriptUrl The script URL.
139
+ * @returns Resolves once it ran.
140
+ * @throws Error when the server answers with an error status (the toolkit then warns that the bundle was not found), or
141
+ * when the URL is not on the dedicated allow-list (D19).
142
+ */
143
+ export async function loadNodeScriptAsync(scriptUrl) {
144
+ const cacheKey = scriptCacheKey(scriptUrl);
145
+ if (loadedScriptNames.has(cacheKey))
146
+ return;
147
+ const pending = pendingScriptLoads.get(cacheKey);
148
+ if (pending != null)
149
+ return pending;
150
+ const load = fetchAndRunScript(scriptUrl, cacheKey);
151
+ pendingScriptLoads.set(cacheKey, load);
152
+ try {
153
+ await load;
154
+ }
155
+ finally {
156
+ pendingScriptLoads.delete(cacheKey);
157
+ }
158
+ }
159
+ /**
160
+ * Fetches a script and runs it in this context.
161
+ * @param scriptUrl The script URL.
162
+ * @param cacheKey Its cache key, recorded once it ran.
163
+ * @throws Error when the URL is not allowed, or answers with an error status.
164
+ */
165
+ async function fetchAndRunScript(scriptUrl, cacheKey) {
166
+ const source = await fetchText(scriptUrl);
167
+ vm.runInThisContext(source, { filename: scriptUrl });
168
+ loadedScriptNames.add(cacheKey);
169
+ }
170
+ /**
171
+ * Replaces Babylon's DOM-based loaders with Node ones, once (T13 step 2): BABYLON.Tools.LoadScriptAsync / LoadScript use
172
+ * the Node script loader, so the toolkit's project bundle auto-load works unchanged; BABYLON.SceneLoader's entry points
173
+ * fetch the scene file over HTTP and hand it over as data, and the glTF loader fetches its buffers the same way and loads
174
+ * no materials (no textures headless). Babylon's own versions use a script element and XMLHttpRequest, which Node lacks.
175
+ * @param babylon The BABYLON namespace.
176
+ */
177
+ export function installNodeLoaders(babylon) {
178
+ if (nodeLoadersInstalled)
179
+ return;
180
+ nodeLoadersInstalled = true;
181
+ babylon.Tools.LoadScriptAsync = (scriptUrl) => loadNodeScriptAsync(scriptUrl);
182
+ babylon.Tools.LoadScript = (scriptUrl, onSuccess, onError) => {
183
+ loadNodeScriptAsync(scriptUrl).then(() => onSuccess?.(), (error) => onError?.(String(error), error));
184
+ };
185
+ babylon.SceneLoader.OnPluginActivatedObservable.add((plugin) => {
186
+ if (plugin == null || plugin.name !== "gltf")
187
+ return;
188
+ plugin.skipMaterials = true;
189
+ plugin.preprocessUrlAsync = (url) => fetchAsDataUrl(url);
190
+ });
191
+ for (const entryPoint of SCENE_LOADER_ENTRY_POINTS)
192
+ adaptSceneLoaderEntryPoint(babylon.SceneLoader, entryPoint);
193
+ }
194
+ /**
195
+ * Wraps one SceneLoader entry point: an HTTP scene file is fetched first and passed as data, with its extension (which
196
+ * picks the loader plugin) and its file name (which the toolkit records for the scene).
197
+ * @param sceneLoader BABYLON.SceneLoader.
198
+ * @param entryPoint The entry point and its argument positions.
199
+ */
200
+ function adaptSceneLoaderEntryPoint(sceneLoader, entryPoint) {
201
+ const original = sceneLoader[entryPoint.name];
202
+ if (typeof original !== "function")
203
+ return;
204
+ const callOriginal = (args) => original.apply(sceneLoader, args);
205
+ if (entryPoint.onErrorIndex < 0) {
206
+ sceneLoader[entryPoint.name] = async (...args) => callOriginal(await withSceneData(args, entryPoint));
207
+ return;
208
+ }
209
+ sceneLoader[entryPoint.name] = (...args) => {
210
+ withSceneData(args, entryPoint).then((dataArgs) => callOriginal(dataArgs), (error) => {
211
+ const onError = args[entryPoint.onErrorIndex];
212
+ if (typeof onError === "function")
213
+ onError(null, String(error), error);
214
+ else
215
+ getLogger().error("headless scene load failed", { error: String(error) });
216
+ });
217
+ return null;
218
+ };
219
+ }
220
+ /**
221
+ * The arguments of a scene loader call with an HTTP or file:// scene file replaced by its data.
222
+ * @param args The call's arguments.
223
+ * @param entryPoint The entry point's argument positions.
224
+ * @returns The arguments to call the original entry point with.
225
+ */
226
+ async function withSceneData(args, entryPoint) {
227
+ const rootUrl = args[entryPoint.rootUrlIndex];
228
+ const sceneFile = args[entryPoint.rootUrlIndex + 1];
229
+ if (typeof rootUrl !== "string" || typeof sceneFile !== "string" || sceneFile.startsWith(DATA_URL_PREFIX))
230
+ return args;
231
+ const url = rootUrl + sceneFile;
232
+ if (!LOADABLE_SCENE_URL_PATTERN.test(url))
233
+ return args;
234
+ const dataArgs = args.slice();
235
+ const extension = fileExtension(sceneFile);
236
+ dataArgs[entryPoint.rootUrlIndex + 1] = (extension === BINARY_GLTF_EXTENSION) ? await fetchAsDataUrl(url) : DATA_URL_PREFIX + await fetchText(url);
237
+ if (dataArgs[entryPoint.pluginExtensionIndex] == null)
238
+ dataArgs[entryPoint.pluginExtensionIndex] = extension;
239
+ if (dataArgs[entryPoint.nameIndex] == null || dataArgs[entryPoint.nameIndex] === "")
240
+ dataArgs[entryPoint.nameIndex] = sceneFile;
241
+ return dataArgs;
242
+ }
243
+ /**
244
+ * The lower-cased extension of a file name, without its query.
245
+ * @param fileName The file name.
246
+ * @returns The extension with its dot (".gltf"), or "".
247
+ */
248
+ function fileExtension(fileName) {
249
+ const withoutQuery = fileName.split(/[?#]/)[0];
250
+ const dotIndex = withoutQuery.lastIndexOf(".");
251
+ return (dotIndex >= 0) ? withoutQuery.substring(dotIndex).toLowerCase() : "";
252
+ }
253
+ /**
254
+ * Fetches a file's text: over HTTP, or from disk for a file:// URL.
255
+ * @param url The URL.
256
+ * @returns The text.
257
+ * @throws Error when the URL is not on the dedicated allow-list, or on an error status.
258
+ */
259
+ async function fetchText(url) {
260
+ const bytes = await fetchBytes(url);
261
+ return bytes.toString("utf8");
262
+ }
263
+ /**
264
+ * Fetches a file as a base64 data URL, which Babylon loads without a request; a data URL passes through.
265
+ * @param url The URL.
266
+ * @returns The data URL.
267
+ * @throws Error when the URL is not on the dedicated allow-list, or on an error status.
268
+ */
269
+ async function fetchAsDataUrl(url) {
270
+ if (url.startsWith(DATA_URL_PREFIX))
271
+ return url;
272
+ const bytes = await fetchBytes(url);
273
+ return BINARY_DATA_URL_PREFIX + bytes.toString("base64");
274
+ }
275
+ /**
276
+ * Fetches a file's bytes, after checking the allow-list (D19): over HTTP, or from disk for a file:// URL.
277
+ * @param url The URL.
278
+ * @returns The bytes.
279
+ * @throws Error("URL is not on the dedicated allow-list: <url>") without making a request, or on an error status.
280
+ */
281
+ async function fetchBytes(url) {
282
+ if (!isUrlAllowed(url))
283
+ throw new Error("URL is not on the dedicated allow-list: " + url);
284
+ if (url.startsWith(FILE_URL_PREFIX))
285
+ return readFile(fileURLToPath(url.split(/[?#]/)[0]));
286
+ const response = await fetch(url);
287
+ if (!response.ok)
288
+ throw new Error(url + " answered " + response.status + " " + response.statusText);
289
+ return Buffer.from(await response.arrayBuffer());
290
+ }
291
+ /**
292
+ * Makes Havok available the way the toolkit's physics setup expects (A18): globalThis.HavokPhysics() resolves the Havok
293
+ * module (its wasm read from the package), and globalThis.HK holds it, as the browser page sets it before loading a scene.
294
+ */
295
+ async function installHavok() {
296
+ if (havokInstalled)
297
+ return;
298
+ const requireModule = createRequire(import.meta.url);
299
+ const havokFactory = requireModule("@babylonjs/havok");
300
+ const wasmBinary = await readFile(requireModule.resolve("@babylonjs/havok/lib/umd/HavokPhysics.wasm"));
301
+ const globalScope = globalThis;
302
+ const havok = await havokFactory({ wasmBinary: wasmBinary });
303
+ globalScope.HavokPhysics = () => Promise.resolve(havok);
304
+ globalScope.HK = havok;
305
+ havokInstalled = true;
306
+ }
307
+ /**
308
+ * Loads an exported toolkit scene headless (T13 step 2): the same .gltf the browser loads, fetched over HTTP, on a
309
+ * NullEngine with Havok, the toolkit runtime initialised without a page (A18) and no materials or textures; the scene's
310
+ * project script bundle auto-loads through the Node script loader. As engine.html does, a scene without a camera gets a
311
+ * default one and the render loop starts; it resolves once the toolkit has created the scene's components and the scene
312
+ * is ready (TOOLKIT.SceneManager.WhenSceneReady).
313
+ * @param sceneRootUrl The folder URL of the scene: http(s):// (for example "http://localhost:8877/scenes/") or file://.
314
+ * @param sceneFile The scene file (for example "networktestbed.gltf").
315
+ * @param onFrameRendered Receives each frame's duration in seconds (the dedicated frame metrics, D16).
316
+ * @returns The engine and the ready, rendering scene.
317
+ * @throws Error when the toolkit does not report the scene ready.
318
+ */
319
+ export async function loadHeadlessScene(sceneRootUrl, sceneFile, onFrameRendered) {
320
+ const runtime = await loadToolkitRuntime();
321
+ const babylon = runtime.BABYLON;
322
+ const toolkit = runtime.TOOLKIT;
323
+ installNodeLoaders(babylon);
324
+ await installHavok();
325
+ const engine = new babylon.NullEngine();
326
+ engine.getCaps().maxTexturesImageUnits = HEADLESS_TEXTURE_UNITS;
327
+ await toolkit.SceneManager.InitializeRuntime(engine, HEADLESS_RUNTIME_OPTIONS);
328
+ const loadScene = babylon.SceneLoader["LoadAsync"];
329
+ const scene = await loadScene(sceneRootUrl, sceneFile, engine);
330
+ await scene.whenReadyAsync();
331
+ // As engine.html does: a scene renders only with a camera.
332
+ if (scene.cameras.length === 0)
333
+ new babylon.FreeCamera(DEFAULT_CAMERA_NAME, new babylon.Vector3(0, DEFAULT_CAMERA_HEIGHT, DEFAULT_CAMERA_DISTANCE), scene);
334
+ const reportFrameFailure = createFrameFailureReporter();
335
+ engine.runRenderLoop(() => renderTimed(scene, onFrameRendered, reportFrameFailure));
336
+ const isReady = await toolkit.SceneManager.WhenSceneReady(scene);
337
+ if (!isReady) {
338
+ engine.dispose();
339
+ throw new Error("[GameServer] the headless scene " + sceneRootUrl + sceneFile + " did not get ready");
340
+ }
341
+ return { engine: engine, scene: scene };
342
+ }
343
+ /**
344
+ * Renders one frame, reporting how long it took when asked. A frame that throws stays inside this function: the render
345
+ * loop runs outside every request, so the error would otherwise end the process, and with it every session on it. A
346
+ * client can cause one, because the runner decodes what clients send during the frame. The runner drops a frame before
347
+ * decoding it, so the next render carries on.
348
+ * @param scene The scene.
349
+ * @param onFrameRendered Receives the frame's duration in seconds, or undefined.
350
+ * @param reportFailure Told about a frame that threw.
351
+ */
352
+ function renderTimed(scene, onFrameRendered, reportFailure) {
353
+ const startedAt = performance.now();
354
+ try {
355
+ scene.render();
356
+ }
357
+ catch (error) {
358
+ reportFailure(error);
359
+ }
360
+ onFrameRendered?.((performance.now() - startedAt) / MILLISECONDS_PER_SECOND);
361
+ }
362
+ /**
363
+ * Makes the function that logs failed frames. A scene script that throws fails every frame, so it logs at most once per
364
+ * FRAME_FAILURE_LOG_INTERVAL_MS and counts the rest.
365
+ * @returns The reporter of one scene.
366
+ */
367
+ function createFrameFailureReporter() {
368
+ let lastLoggedAt = Number.NEGATIVE_INFINITY;
369
+ let failuresSinceLog = 0;
370
+ return (error) => {
371
+ failuresSinceLog += 1;
372
+ const now = performance.now();
373
+ if (now - lastLoggedAt < FRAME_FAILURE_LOG_INTERVAL_MS)
374
+ return;
375
+ const failure = (error instanceof Error) ? (error.stack ?? error.message) : String(error);
376
+ getLogger().error("a dedicated frame failed", { error: failure, failedFrames: failuresSinceLog });
377
+ lastLoggedAt = now;
378
+ failuresSinceLog = 0;
379
+ };
380
+ }
@@ -0,0 +1,34 @@
1
+ import { type AuthContext } from "colyseus";
2
+ import { type AuthIdentity } from "./Authentication.js";
3
+ import { type FusionConnectRequest } from "./HeadlessRuntime.js";
4
+ /** What the gate hands Colyseus for `client.auth`: who the player is and where it connects from. */
5
+ export interface JoinGateResult {
6
+ /** The verified identity, or null when auth is off. */
7
+ identity: AuthIdentity | null;
8
+ /** The client's address (D11). */
9
+ clientAddress: string;
10
+ }
11
+ /** Which room a join is for: a session, or the lobby (which skips the session-only checks). */
12
+ export type JoinKind = "session" | "lobby";
13
+ /** The client address used when the request carries none (D11). */
14
+ export declare const UNKNOWN_CLIENT_ADDRESS: string;
15
+ /**
16
+ * The one gate every join passes at matchmaking (D10), called by the static onAuth of SessionRoom ("session") and
17
+ * GameServerLobbyRoom ("lobby"). In order: draining, maintenance (sessions), origin, address bans, per-address rate,
18
+ * then for sessions the protocol version, session name and on-demand dedicated scene, then the token verifier when
19
+ * auth is on, and finally the checks that need the verified identity (user bans, kick blocks, maintenance allow lists).
20
+ * @param token The bearer token the client sent, if any.
21
+ * @param options The join options (a session's NetworkConnectRequest; the lobby's are empty).
22
+ * @param context Colyseus' matchmaking context: headers and the client address.
23
+ * @param kind Which room the join is for.
24
+ * @returns The identity and client address, which Colyseus stores as client.auth.
25
+ * @throws ServerError(status, "<Reason>: <detail>") when the join is refused.
26
+ */
27
+ export declare function authorizeJoin(token: string | undefined, options: Partial<FusionConnectRequest> | undefined, context: AuthContext, kind: JoinKind): Promise<JoinGateResult>;
28
+ /**
29
+ * The dedicated scene a Client-mode join asks the server to start on demand (D19): the NETWORK_DEDICATED_SCENE_PROPERTY
30
+ * session property, when it is non-empty text.
31
+ * @param request The join options.
32
+ * @returns The scene name, or null when the join names none (or is not a Client join).
33
+ */
34
+ export declare function requestedDedicatedScene(request: Partial<FusionConnectRequest>): string | null;
@@ -0,0 +1,221 @@
1
+ import { ErrorCode, ServerError } from "colyseus";
2
+ import { isOriginAllowed, sessionNameProblem } from "./AbuseLimits.js";
3
+ import { AuthRefusal } from "./Authentication.js";
4
+ import { getLogger } from "./GameServerLogger.js";
5
+ import { getGameServerState } from "./GameServerState.js";
6
+ import { getToolkitRuntime } from "./HeadlessRuntime.js";
7
+ import { normalizeAddress } from "./NetworkAddresses.js";
8
+ import { needsVerification, verifyPlayerToken } from "./PlayerTokens.js";
9
+ import { banRefusalDetail } from "./admin/BanList.js";
10
+ /** The client address used when the request carries none (D11). */
11
+ export const UNKNOWN_CLIENT_ADDRESS = "unknown";
12
+ /** HTTP status of a join refused because the server is draining or full. */
13
+ const SERVICE_UNAVAILABLE_STATUS = 503;
14
+ /** HTTP status of a join from an origin that is not allowed. */
15
+ const FORBIDDEN_STATUS = 403;
16
+ /** HTTP status of a join over the per-address rate. */
17
+ const TOO_MANY_REQUESTS_STATUS = 429;
18
+ /** HTTP status of a Client join naming a dedicated scene the server cannot start on demand (D6). */
19
+ const NOT_FOUND_STATUS = 404;
20
+ /** HTTP status of a join with a wrong protocol version or a bad session name. */
21
+ const BAD_REQUEST_STATUS = 400;
22
+ /**
23
+ * The one gate every join passes at matchmaking (D10), called by the static onAuth of SessionRoom ("session") and
24
+ * GameServerLobbyRoom ("lobby"). In order: draining, maintenance (sessions), origin, address bans, per-address rate,
25
+ * then for sessions the protocol version, session name and on-demand dedicated scene, then the token verifier when
26
+ * auth is on, and finally the checks that need the verified identity (user bans, kick blocks, maintenance allow lists).
27
+ * @param token The bearer token the client sent, if any.
28
+ * @param options The join options (a session's NetworkConnectRequest; the lobby's are empty).
29
+ * @param context Colyseus' matchmaking context: headers and the client address.
30
+ * @param kind Which room the join is for.
31
+ * @returns The identity and client address, which Colyseus stores as client.auth.
32
+ * @throws ServerError(status, "<Reason>: <detail>") when the join is refused.
33
+ */
34
+ export async function authorizeJoin(token, options, context, kind) {
35
+ const state = getGameServerState();
36
+ const clientAddress = context.ip ?? UNKNOWN_CLIENT_ADDRESS;
37
+ const origin = context.headers.get("origin");
38
+ const request = options ?? {};
39
+ const refusal = checkAdmission(state, clientAddress, origin, request, kind);
40
+ if (refusal != null)
41
+ refuse(state, refusal, clientAddress, origin, request);
42
+ const identity = await verifyToken(state, token ?? "", kind, request, clientAddress, origin);
43
+ const lateRefusal = await checkAuthenticated(state, clientAddress, identity, kind, request);
44
+ if (lateRefusal != null)
45
+ refuse(state, lateRefusal, clientAddress, origin, request);
46
+ return { identity: identity, clientAddress: clientAddress };
47
+ }
48
+ /**
49
+ * The checks that need the verified identity (game-server-admin-console D12, D15, R19, R20): a user-id ban, then for
50
+ * session joins a kick block of the user id or the address, then maintenance with a user-id allow list (the join
51
+ * passes only when its address or its user id is allowed).
52
+ * @param state The server state.
53
+ * @param clientAddress The client's address.
54
+ * @param identity The verified identity, or null when auth is off.
55
+ * @param kind Which room the join is for.
56
+ * @param request The join options.
57
+ * @returns The refusal, or null when the join may go on.
58
+ */
59
+ async function checkAuthenticated(state, clientAddress, identity, kind, request) {
60
+ const userId = identity?.userId ?? null;
61
+ const userBan = (userId != null) ? state.banList?.userBan(userId) ?? null : null;
62
+ if (userBan != null)
63
+ return banRefusal(userBan);
64
+ if (kind !== "session")
65
+ return null;
66
+ if (await state.kickBlocks.isBlocked(request.sessionName ?? "", userId, clientAddress)) {
67
+ return { status: FORBIDDEN_STATUS, reasonName: "ConnectionRefused", detail: "you were removed from this session" };
68
+ }
69
+ const maintenance = state.maintenance;
70
+ if (maintenance == null || isMaintenanceAddress(maintenance, clientAddress))
71
+ return null;
72
+ if (userId != null && maintenance.allowUserIds.includes(userId))
73
+ return null;
74
+ return maintenanceRefusal(maintenance);
75
+ }
76
+ /**
77
+ * The refusal of a banned joiner (D15): 403 ConnectionRefused with the ban's end time.
78
+ * @param record The ban.
79
+ * @returns The refusal.
80
+ */
81
+ function banRefusal(record) {
82
+ return { status: FORBIDDEN_STATUS, reasonName: "ConnectionRefused", detail: banRefusalDetail(record) };
83
+ }
84
+ /**
85
+ * Whether maintenance still admits an address (its allowAddresses, compared after normalizing IPv4-mapped forms).
86
+ * @param maintenance The maintenance in force.
87
+ * @param clientAddress The client's address.
88
+ * @returns True when the address is allowed.
89
+ */
90
+ function isMaintenanceAddress(maintenance, clientAddress) {
91
+ const normalized = normalizeAddress(clientAddress);
92
+ return maintenance.allowAddresses.some((allowed) => normalizeAddress(allowed) === normalized);
93
+ }
94
+ /**
95
+ * The refusal of a session join during maintenance (D14): 503 GameClosed with the maintenance message.
96
+ * @param maintenance The maintenance in force.
97
+ * @returns The refusal.
98
+ */
99
+ function maintenanceRefusal(maintenance) {
100
+ return { status: SERVICE_UNAVAILABLE_STATUS, reasonName: "GameClosed", detail: maintenance.message };
101
+ }
102
+ /**
103
+ * The checks before authentication (D10 steps 1–6).
104
+ * @param state The server state.
105
+ * @param clientAddress The client's address.
106
+ * @param origin The request's Origin, or null.
107
+ * @param request The join options.
108
+ * @param kind Which room the join is for.
109
+ * @returns The first refusal, or null when the join may go on to authentication.
110
+ */
111
+ function checkAdmission(state, clientAddress, origin, request, kind) {
112
+ if (state.draining)
113
+ return { status: SERVICE_UNAVAILABLE_STATUS, reasonName: "GameClosed", detail: "the server is shutting down" };
114
+ // Maintenance refuses here only when no user id could be allowed later; otherwise checkAuthenticated decides (R20).
115
+ const maintenance = state.maintenance;
116
+ if (kind === "session" && maintenance != null && maintenance.allowUserIds.length === 0 && !isMaintenanceAddress(maintenance, clientAddress))
117
+ return maintenanceRefusal(maintenance);
118
+ if (!isOriginAllowed(origin, state.config.limits))
119
+ return { status: FORBIDDEN_STATUS, reasonName: "ConnectionRefused", detail: "the origin " + (origin ?? "(none)") + " is not allowed" };
120
+ const addressBan = state.banList?.addressBan(clientAddress) ?? null;
121
+ if (addressBan != null)
122
+ return banRefusal(addressBan);
123
+ if (!state.rateLimiter.tryConsume(clientAddress))
124
+ return { status: TOO_MANY_REQUESTS_STATUS, reasonName: "ConnectionRefused", detail: "too many join requests from this address; try again in a minute" };
125
+ if (kind === "lobby")
126
+ return null;
127
+ const versionDetail = protocolVersionProblem(request.protocolVersion);
128
+ if (versionDetail != null)
129
+ return { status: BAD_REQUEST_STATUS, reasonName: "IncompatibleConfiguration", detail: versionDetail };
130
+ const nameDetail = sessionNameProblem(request.sessionName ?? "", state.config.limits);
131
+ if (nameDetail != null)
132
+ return { status: BAD_REQUEST_STATUS, reasonName: "InvalidArguments", detail: nameDetail };
133
+ const sceneName = requestedDedicatedScene(request);
134
+ if (sceneName != null && state.registry.find(sceneName)?.onDemand !== true)
135
+ return { status: NOT_FOUND_STATUS, reasonName: "GameNotFound", detail: "no on-demand dedicated scene named " + sceneName };
136
+ return null;
137
+ }
138
+ /**
139
+ * The dedicated scene a Client-mode join asks the server to start on demand (D19): the NETWORK_DEDICATED_SCENE_PROPERTY
140
+ * session property, when it is non-empty text.
141
+ * @param request The join options.
142
+ * @returns The scene name, or null when the join names none (or is not a Client join).
143
+ */
144
+ export function requestedDedicatedScene(request) {
145
+ const toolkit = getToolkitRuntime().TOOLKIT;
146
+ if (request.gameMode !== toolkit.GameMode.Client)
147
+ return null;
148
+ const sceneName = request.sessionProperties?.[toolkit.NETWORK_DEDICATED_SCENE_PROPERTY];
149
+ return (typeof sceneName === "string" && sceneName !== "") ? sceneName : null;
150
+ }
151
+ /**
152
+ * Compares the client's network protocol version with the server's (D5); a missing version counts as 0.
153
+ * @param clientVersion The version the join sent, if any.
154
+ * @returns A readable problem naming both versions and which side to update, or null when they match.
155
+ */
156
+ function protocolVersionProblem(clientVersion) {
157
+ const serverVersion = getToolkitRuntime().TOOLKIT.NETWORK_PROTOCOL_VERSION;
158
+ const sentVersion = (typeof clientVersion === "number") ? clientVersion : null;
159
+ if (sentVersion === serverVersion)
160
+ return null;
161
+ const sideToUpdate = (sentVersion == null || sentVersion < serverVersion) ? "game's Babylon Toolkit runtime" : "Babylon Game Server";
162
+ return "this server speaks network protocol " + serverVersion + " but the game sent " + (sentVersion ?? "none") + "; update the " + sideToUpdate;
163
+ }
164
+ /**
165
+ * Runs the token verifier when auth is on (D13).
166
+ * @param state The server state.
167
+ * @param token The bearer token ("" when none was sent).
168
+ * @param kind Which room the join is for.
169
+ * @param request The join options.
170
+ * @param clientAddress The client's address.
171
+ * @param origin The request's Origin, or null.
172
+ * @returns The verified identity, or null when auth is off.
173
+ * @throws ServerError(525, "<Reason>: <detail>") when the verifier refuses.
174
+ */
175
+ async function verifyToken(state, token, kind, request, clientAddress, origin) {
176
+ if (!needsVerification(state, token))
177
+ return null;
178
+ const authRequest = {
179
+ token: token,
180
+ kind: kind,
181
+ sessionName: (kind === "session") ? request.sessionName ?? null : null,
182
+ gameMode: (kind === "session" && typeof request.gameMode === "number") ? request.gameMode : null,
183
+ clientAddress: clientAddress,
184
+ origin: origin
185
+ };
186
+ try {
187
+ return await verifyPlayerToken(state, authRequest);
188
+ }
189
+ catch (error) {
190
+ state.metrics?.countAuthFailure(state.config.auth.mode);
191
+ return refuse(state, verifierRefusal(error, clientAddress), clientAddress, origin, request);
192
+ }
193
+ }
194
+ /**
195
+ * The refusal of a verifier failure: the verifier's own reason, or CustomAuthenticationFailed when the verifier itself
196
+ * broke (logged as an error, since that is a server or hook bug).
197
+ * @param error What the verifier threw.
198
+ * @param clientAddress The client's address, for the log.
199
+ * @returns The refusal, with status 525 (ErrorCode.AUTH_FAILED).
200
+ */
201
+ function verifierRefusal(error, clientAddress) {
202
+ if (error instanceof AuthRefusal)
203
+ return { status: ErrorCode.AUTH_FAILED, reasonName: error.reasonName, detail: error.detail };
204
+ getLogger().error("token verifier failed", { clientAddress: clientAddress, error: String(error) });
205
+ return { status: ErrorCode.AUTH_FAILED, reasonName: "CustomAuthenticationFailed", detail: "the token could not be verified" };
206
+ }
207
+ /**
208
+ * Counts, logs and throws a refusal.
209
+ * @param state The server state.
210
+ * @param refusal The refusal.
211
+ * @param clientAddress The client's address.
212
+ * @param origin The request's Origin, or null.
213
+ * @param request The join options.
214
+ * @returns Never returns.
215
+ * @throws ServerError(status, "<Reason>: <detail>").
216
+ */
217
+ function refuse(state, refusal, clientAddress, origin, request) {
218
+ state.metrics?.countJoin("refused", refusal.reasonName);
219
+ getLogger().info("join refused", { reason: refusal.reasonName, detail: refusal.detail, clientAddress: clientAddress, origin: origin, sessionName: request.sessionName });
220
+ throw new ServerError(refusal.status, refusal.reasonName + ": " + refusal.detail);
221
+ }
@@ -0,0 +1,33 @@
1
+ import { BlockList } from "node:net";
2
+ /**
3
+ * Turns an IPv4-mapped IPv6 address (`::ffff:a.b.c.d`) into its IPv4 form, so lists and checks see one spelling.
4
+ * @param address The client address.
5
+ * @returns The normalized address.
6
+ */
7
+ export declare function normalizeAddress(address: string): string;
8
+ /**
9
+ * Whether an address is this machine's loopback interface (127.0.0.1, ::1 or ::ffff:127.0.0.1).
10
+ * @param address The client address.
11
+ * @returns True for a loopback address.
12
+ */
13
+ export declare function isLoopbackAddress(address: string): boolean;
14
+ /**
15
+ * Adds one address or CIDR range to a block list.
16
+ * @param blockList The list to add to.
17
+ * @param entry An address (`a.b.c.d`, IPv6) or a range (`a.b.c.d/n`).
18
+ * @returns False when the entry is not an address or range.
19
+ */
20
+ export declare function addNetworkEntry(blockList: BlockList, entry: string): boolean;
21
+ /**
22
+ * Builds a block list from address and CIDR entries, skipping invalid ones (config validation reports those).
23
+ * @param entries The addresses and ranges.
24
+ * @returns The list.
25
+ */
26
+ export declare function createNetworkList(entries: string[]): BlockList;
27
+ /**
28
+ * Whether an address falls inside a block list.
29
+ * @param blockList The list.
30
+ * @param address The client address.
31
+ * @returns True when the address is listed or inside a listed range; false for text that is not an address.
32
+ */
33
+ export declare function isAddressInList(blockList: BlockList, address: string): boolean;