@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,64 @@
1
+ import type { IncomingMessage, Server as HttpServer } from "node:http";
2
+ import type { LimitsConfig } from "./GameServerConfig.js";
3
+ /**
4
+ * Limits how many join requests one client address may make per minute (D14), in fixed one-minute windows.
5
+ */
6
+ export declare class JoinRateLimiter {
7
+ /** Most join requests per address per window; the console can change it live (game-server-admin-console D20). */
8
+ private requestsPerMinute;
9
+ /** The clock, in milliseconds. */
10
+ private readonly nowMilliseconds;
11
+ /** The current window of each address seen recently. */
12
+ private readonly windows;
13
+ /**
14
+ * Creates a limiter.
15
+ * @param requestsPerMinute Most join requests one address may make per minute.
16
+ * @param nowMilliseconds The clock (Date.now when omitted; tests pass their own).
17
+ */
18
+ constructor(requestsPerMinute: number, nowMilliseconds?: () => number);
19
+ /**
20
+ * Changes the most join requests one address may make per minute, from the next request on.
21
+ * @param requestsPerMinute The new limit (more than 0).
22
+ */
23
+ setRequestsPerMinute(requestsPerMinute: number): void;
24
+ /**
25
+ * Records a join request from an address, when the address still has room in its window.
26
+ * @param clientAddress The client's address (D11).
27
+ * @returns True when this address may make another join request now.
28
+ */
29
+ tryConsume(clientAddress: string): boolean;
30
+ /**
31
+ * Forgets addresses whose window ended more than a window ago, so the map stays small.
32
+ * @param now The current time, in milliseconds.
33
+ */
34
+ private pruneIdleWindows;
35
+ }
36
+ /**
37
+ * Whether a join from an origin may proceed (D14).
38
+ * @param origin The request's Origin header, or null when it sent none.
39
+ * @param limits The limits config.
40
+ * @returns True when the allow-list is empty, the origin is listed, or a missing origin is allowed.
41
+ */
42
+ export declare function isOriginAllowed(origin: string | null, limits: LimitsConfig): boolean;
43
+ /**
44
+ * Checks a session name against the length and character rules (D14).
45
+ * @param sessionName The requested session name.
46
+ * @param limits The limits config.
47
+ * @returns A readable problem, or null when the name is acceptable.
48
+ */
49
+ export declare function sessionNameProblem(sessionName: string, limits: LimitsConfig): string | null;
50
+ /**
51
+ * Makes the client address Colyseus reports the socket's own address unless the server sits behind a trusted proxy
52
+ * (D11). Colyseus builds the matchmaking context's ip from forwarding headers only, so without this any client could
53
+ * pick its address by sending X-Forwarded-For. Call it after the server listens: the listeners are prepended, so they
54
+ * run before Colyseus' router.
55
+ * @param httpServer The transport's HTTP(S) server.
56
+ * @param trustProxy True to keep the forwarding headers a proxy set.
57
+ */
58
+ export declare function installClientAddressHeaders(httpServer: HttpServer, trustProxy: boolean): void;
59
+ /**
60
+ * Replaces a request's forwarding headers with its socket address. It also removes x-forwarded-proto, so a client
61
+ * without a trusted proxy cannot claim HTTPS and pass the admin console's transport rule (D9).
62
+ * @param request The incoming request or upgrade.
63
+ */
64
+ export declare function rewriteClientAddressHeaders(request: IncomingMessage): void;
@@ -0,0 +1,121 @@
1
+ /** Length of one join-rate window, in milliseconds (D14: a fixed one-minute window). */
2
+ const JOIN_RATE_WINDOW_MS = 60000;
3
+ /** Windows an idle address's entry is kept before it is pruned. */
4
+ const PRUNE_AFTER_WINDOWS = 2;
5
+ /** Characters a session name may not contain: C0 controls and DEL. */
6
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
7
+ /** Forwarding headers Colyseus reads the client address from (`@colyseus/core` Transport.ts). */
8
+ const FORWARDING_HEADERS = ["x-forwarded-for", "x-client-ip", "x-real-ip"];
9
+ /** The header the server rewrites to the socket's address, which Colyseus reads first. */
10
+ const REAL_IP_HEADER = "x-real-ip";
11
+ /** The header a proxy names the original scheme in; only a trusted proxy may set it (game-server-admin-console D9). */
12
+ const FORWARDED_PROTO_HEADER = "x-forwarded-proto";
13
+ /**
14
+ * Limits how many join requests one client address may make per minute (D14), in fixed one-minute windows.
15
+ */
16
+ export class JoinRateLimiter {
17
+ /** Most join requests per address per window; the console can change it live (game-server-admin-console D20). */
18
+ requestsPerMinute;
19
+ /** The clock, in milliseconds. */
20
+ nowMilliseconds;
21
+ /** The current window of each address seen recently. */
22
+ windows = new Map();
23
+ /**
24
+ * Creates a limiter.
25
+ * @param requestsPerMinute Most join requests one address may make per minute.
26
+ * @param nowMilliseconds The clock (Date.now when omitted; tests pass their own).
27
+ */
28
+ constructor(requestsPerMinute, nowMilliseconds) {
29
+ this.requestsPerMinute = requestsPerMinute;
30
+ this.nowMilliseconds = nowMilliseconds ?? Date.now;
31
+ }
32
+ /**
33
+ * Changes the most join requests one address may make per minute, from the next request on.
34
+ * @param requestsPerMinute The new limit (more than 0).
35
+ */
36
+ setRequestsPerMinute(requestsPerMinute) {
37
+ this.requestsPerMinute = requestsPerMinute;
38
+ }
39
+ /**
40
+ * Records a join request from an address, when the address still has room in its window.
41
+ * @param clientAddress The client's address (D11).
42
+ * @returns True when this address may make another join request now.
43
+ */
44
+ tryConsume(clientAddress) {
45
+ const now = this.nowMilliseconds();
46
+ this.pruneIdleWindows(now);
47
+ const window = this.windows.get(clientAddress);
48
+ if (window == null || now - window.windowStartMilliseconds >= JOIN_RATE_WINDOW_MS) {
49
+ this.windows.set(clientAddress, { windowStartMilliseconds: now, count: 1 });
50
+ return true;
51
+ }
52
+ if (window.count >= this.requestsPerMinute)
53
+ return false;
54
+ window.count++;
55
+ return true;
56
+ }
57
+ /**
58
+ * Forgets addresses whose window ended more than a window ago, so the map stays small.
59
+ * @param now The current time, in milliseconds.
60
+ */
61
+ pruneIdleWindows(now) {
62
+ const oldestKept = now - PRUNE_AFTER_WINDOWS * JOIN_RATE_WINDOW_MS;
63
+ for (const [clientAddress, window] of this.windows) {
64
+ if (window.windowStartMilliseconds < oldestKept)
65
+ this.windows.delete(clientAddress);
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Whether a join from an origin may proceed (D14).
71
+ * @param origin The request's Origin header, or null when it sent none.
72
+ * @param limits The limits config.
73
+ * @returns True when the allow-list is empty, the origin is listed, or a missing origin is allowed.
74
+ */
75
+ export function isOriginAllowed(origin, limits) {
76
+ if (limits.allowedOrigins.length === 0)
77
+ return true;
78
+ if (origin == null)
79
+ return limits.allowMissingOrigin;
80
+ return limits.allowedOrigins.includes(origin);
81
+ }
82
+ /**
83
+ * Checks a session name against the length and character rules (D14).
84
+ * @param sessionName The requested session name.
85
+ * @param limits The limits config.
86
+ * @returns A readable problem, or null when the name is acceptable.
87
+ */
88
+ export function sessionNameProblem(sessionName, limits) {
89
+ if (sessionName.length > limits.maxSessionNameLength)
90
+ return "session name is longer than " + limits.maxSessionNameLength + " characters";
91
+ if (CONTROL_CHARACTER_PATTERN.test(sessionName))
92
+ return "session name contains control characters";
93
+ return null;
94
+ }
95
+ /**
96
+ * Makes the client address Colyseus reports the socket's own address unless the server sits behind a trusted proxy
97
+ * (D11). Colyseus builds the matchmaking context's ip from forwarding headers only, so without this any client could
98
+ * pick its address by sending X-Forwarded-For. Call it after the server listens: the listeners are prepended, so they
99
+ * run before Colyseus' router.
100
+ * @param httpServer The transport's HTTP(S) server.
101
+ * @param trustProxy True to keep the forwarding headers a proxy set.
102
+ */
103
+ export function installClientAddressHeaders(httpServer, trustProxy) {
104
+ if (trustProxy)
105
+ return;
106
+ httpServer.prependListener("request", rewriteClientAddressHeaders);
107
+ httpServer.prependListener("upgrade", rewriteClientAddressHeaders);
108
+ }
109
+ /**
110
+ * Replaces a request's forwarding headers with its socket address. It also removes x-forwarded-proto, so a client
111
+ * without a trusted proxy cannot claim HTTPS and pass the admin console's transport rule (D9).
112
+ * @param request The incoming request or upgrade.
113
+ */
114
+ export function rewriteClientAddressHeaders(request) {
115
+ for (const headerName of FORWARDING_HEADERS)
116
+ delete request.headers[headerName];
117
+ delete request.headers[FORWARDED_PROTO_HEADER];
118
+ const remoteAddress = request.socket.remoteAddress;
119
+ if (remoteAddress != null)
120
+ request.headers[REAL_IP_HEADER] = remoteAddress;
121
+ }
@@ -0,0 +1,86 @@
1
+ import { type AuthConfig } from "./GameServerConfig.js";
2
+ /** A verified player. */
3
+ export interface AuthIdentity {
4
+ /** The player's user id from the token or verifier, or null when it names none. */
5
+ userId: string | null;
6
+ /** Everything the verifier learned about the player (JWT claims, webhook answer). Never contains the token. */
7
+ claims: Record<string, unknown>;
8
+ }
9
+ /** What a verifier sees. */
10
+ export interface AuthRequest {
11
+ /** The bearer token the client sent. */
12
+ token: string;
13
+ /** "session" for a session join, "lobby" for the lobby. */
14
+ kind: "session" | "lobby";
15
+ /** The requested session name, or null for the lobby. */
16
+ sessionName: string | null;
17
+ /** The joiner's TOOLKIT.GameMode, or null for the lobby. */
18
+ gameMode: number | null;
19
+ /** The client's address (D11). */
20
+ clientAddress: string;
21
+ /** The request's Origin header, or null. */
22
+ origin: string | null;
23
+ }
24
+ /** A verifier's refusal: the ShutdownReason name and a readable detail (D6). */
25
+ export declare class AuthRefusal extends Error {
26
+ /** The TOOLKIT.ShutdownReason name the client sees. */
27
+ readonly reasonName: string;
28
+ /** The readable detail after the reason name. */
29
+ readonly detail: string;
30
+ /**
31
+ * Creates a refusal whose message is "<reasonName>: <detail>".
32
+ * @param reasonName The ShutdownReason name.
33
+ * @param detail A readable detail.
34
+ */
35
+ constructor(reasonName: string, detail: string);
36
+ }
37
+ /** Verifies a token; resolves the identity or rejects with AuthRefusal. */
38
+ export type TokenVerifier = (request: AuthRequest) => Promise<AuthIdentity>;
39
+ /** The custom verifier of auth mode "custom" (GameServerHooks.verifyToken). */
40
+ export type CustomTokenVerifier = (request: AuthRequest) => Promise<AuthIdentity | null>;
41
+ /** What the admin console's auth tools can do with a verifier's keys (game-server-admin-console T12). */
42
+ export interface TokenKeyControl {
43
+ /** Fetches the JWKS again now; null outside JWKS mode. */
44
+ reloadKeys: (() => Promise<void>) | null;
45
+ /**
46
+ * How old the loaded JWKS is.
47
+ * @returns Seconds since it was last loaded, or null before any load and outside JWKS mode.
48
+ */
49
+ keyCacheAgeSeconds: () => number | null;
50
+ }
51
+ /** A token verifier with its key controls (what createTokenVerifier returns). */
52
+ export type ManagedTokenVerifier = TokenVerifier & TokenKeyControl;
53
+ /** The outcome of a token test (the console's token tester). */
54
+ export interface TokenTestResult {
55
+ /** True when the gate would accept the token. */
56
+ accepted: boolean;
57
+ /** The verified identity, or null when refused. */
58
+ identity: AuthIdentity | null;
59
+ /** The refusal the gate would answer with ("<ShutdownReason>: <detail>"), or null. */
60
+ refusal: string | null;
61
+ }
62
+ /**
63
+ * The token verifier of an auth config (D13): shared secrets, JWTs (secret or JWKS), a webhook, or the custom hook. It
64
+ * refuses a missing token itself, so each mode only checks a token that was sent.
65
+ * @param auth The auth config.
66
+ * @param customVerifier The custom hook (hooks.verifyToken), used by mode "custom".
67
+ * @returns The verifier.
68
+ * @throws GameServerConfigError when mode is "custom" and no verifyToken hook was given.
69
+ */
70
+ export declare function createTokenVerifier(auth: AuthConfig, customVerifier: CustomTokenVerifier | null): ManagedTokenVerifier;
71
+ /**
72
+ * Tests a player token the way the join gate would (the console's token tester, game-server-admin-console T12): it
73
+ * calls the verifier directly, so nothing is counted in the metrics and no log line carries the token.
74
+ * @param verifier The server's verifier (auth must be on).
75
+ * @param request The token and what the gate would know about the join.
76
+ * @returns Accepted with the identity, or the refusal the gate would answer with.
77
+ */
78
+ export declare function testPlayerToken(verifier: TokenVerifier, request: AuthRequest): Promise<TokenTestResult>;
79
+ /**
80
+ * Compares a sent secret with the expected one in constant time (through SHA-256 digests, so lengths never leak).
81
+ * The admin API uses it for its bearer token (D15).
82
+ * @param sentText The secret the request sent.
83
+ * @param expectedText The configured secret.
84
+ * @returns True when they are equal.
85
+ */
86
+ export declare function secretsMatch(sentText: string, expectedText: string): boolean;
@@ -0,0 +1,272 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { createRemoteJWKSet, errors as joseErrors, jwtVerify } from "jose";
3
+ import { GameServerConfigError } from "./GameServerConfig.js";
4
+ /** A verifier's refusal: the ShutdownReason name and a readable detail (D6). */
5
+ export class AuthRefusal extends Error {
6
+ /** The TOOLKIT.ShutdownReason name the client sees. */
7
+ reasonName;
8
+ /** The readable detail after the reason name. */
9
+ detail;
10
+ /**
11
+ * Creates a refusal whose message is "<reasonName>: <detail>".
12
+ * @param reasonName The ShutdownReason name.
13
+ * @param detail A readable detail.
14
+ */
15
+ constructor(reasonName, detail) {
16
+ super(reasonName + ": " + detail);
17
+ this.name = "AuthRefusal";
18
+ this.reasonName = reasonName;
19
+ this.detail = detail;
20
+ }
21
+ }
22
+ /** The key controls of a mode without remote keys. */
23
+ const NO_KEY_CONTROL = { reloadKeys: null, keyCacheAgeSeconds: () => null };
24
+ /** Milliseconds per second. */
25
+ const MILLISECONDS_PER_SECOND = 1000;
26
+ /** The refusal reason the gate uses when a verifier itself breaks. */
27
+ const VERIFIER_FAILURE_REFUSAL = "CustomAuthenticationFailed: the token could not be verified";
28
+ /** ShutdownReason name of a missing or wrong token. */
29
+ const INVALID_AUTHENTICATION = "InvalidAuthentication";
30
+ /** ShutdownReason name of an expired token. */
31
+ const TICKET_EXPIRED = "AuthenticationTicketExpired";
32
+ /** ShutdownReason name of a refusal by the webhook or the custom verifier. */
33
+ const CUSTOM_AUTHENTICATION_FAILED = "CustomAuthenticationFailed";
34
+ /** Milliseconds a fetched JWKS stays cached (D13). */
35
+ const JWKS_CACHE_MAX_AGE_MS = 600000;
36
+ /** Milliseconds between JWKS refetches for an unknown key id (D13). */
37
+ const JWKS_COOLDOWN_MS = 30000;
38
+ /** jose error codes that mean the JWKS itself could not be fetched or read. */
39
+ const JWKS_UNAVAILABLE_CODES = [joseErrors.JWKSTimeout.code, joseErrors.JWKSInvalid.code, joseErrors.JOSEError.code];
40
+ /**
41
+ * The token verifier of an auth config (D13): shared secrets, JWTs (secret or JWKS), a webhook, or the custom hook. It
42
+ * refuses a missing token itself, so each mode only checks a token that was sent.
43
+ * @param auth The auth config.
44
+ * @param customVerifier The custom hook (hooks.verifyToken), used by mode "custom".
45
+ * @returns The verifier.
46
+ * @throws GameServerConfigError when mode is "custom" and no verifyToken hook was given.
47
+ */
48
+ export function createTokenVerifier(auth, customVerifier) {
49
+ const modeVerifier = createModeVerifier(auth, customVerifier);
50
+ const verifier = async (request) => {
51
+ if (request.token === "")
52
+ throw new AuthRefusal(INVALID_AUTHENTICATION, "no token was sent");
53
+ return modeVerifier.verify(request);
54
+ };
55
+ return Object.assign(verifier, { reloadKeys: modeVerifier.keys.reloadKeys, keyCacheAgeSeconds: modeVerifier.keys.keyCacheAgeSeconds });
56
+ }
57
+ /**
58
+ * Tests a player token the way the join gate would (the console's token tester, game-server-admin-console T12): it
59
+ * calls the verifier directly, so nothing is counted in the metrics and no log line carries the token.
60
+ * @param verifier The server's verifier (auth must be on).
61
+ * @param request The token and what the gate would know about the join.
62
+ * @returns Accepted with the identity, or the refusal the gate would answer with.
63
+ */
64
+ export async function testPlayerToken(verifier, request) {
65
+ try {
66
+ const identity = await verifier(request);
67
+ return { accepted: true, identity: identity, refusal: null };
68
+ }
69
+ catch (error) {
70
+ const refusal = (error instanceof AuthRefusal) ? error.reasonName + ": " + error.detail : VERIFIER_FAILURE_REFUSAL;
71
+ return { accepted: false, identity: null, refusal: refusal };
72
+ }
73
+ }
74
+ /**
75
+ * The verifier of the configured mode.
76
+ * @param auth The auth config.
77
+ * @param customVerifier The custom hook, or null.
78
+ * @returns The verifier of a token that was sent.
79
+ * @throws GameServerConfigError when mode is "custom" without a hook.
80
+ */
81
+ function createModeVerifier(auth, customVerifier) {
82
+ switch (auth.mode) {
83
+ case "shared-secret":
84
+ return { verify: createSharedSecretVerifier(auth.sharedSecret.tokens), keys: NO_KEY_CONTROL };
85
+ case "jwt":
86
+ return createJwtVerifier(auth.jwt);
87
+ case "webhook":
88
+ return { verify: createWebhookVerifier(auth.webhook), keys: NO_KEY_CONTROL };
89
+ case "custom":
90
+ if (customVerifier == null)
91
+ throw new GameServerConfigError(["auth.mode \"custom\" needs a verifyToken hook (createGameServer({ hooks: { verifyToken } }))"]);
92
+ return { verify: createCustomVerifier(customVerifier), keys: NO_KEY_CONTROL };
93
+ }
94
+ }
95
+ /**
96
+ * Shared-secret mode: the token must equal one of the configured tokens, compared in constant time.
97
+ * @param tokens The accepted tokens.
98
+ * @returns The verifier.
99
+ */
100
+ function createSharedSecretVerifier(tokens) {
101
+ const tokenDigests = tokens.map(sha256Digest);
102
+ return async (request) => {
103
+ const sentDigest = sha256Digest(request.token);
104
+ // Compare against every token so the time taken never says which one (or whether any) matched.
105
+ let matchedIndex = -1;
106
+ tokenDigests.forEach((tokenDigest, keyIndex) => {
107
+ if (timingSafeEqual(sentDigest, tokenDigest) && matchedIndex < 0)
108
+ matchedIndex = keyIndex;
109
+ });
110
+ if (matchedIndex < 0)
111
+ throw new AuthRefusal(INVALID_AUTHENTICATION, "the token is not valid");
112
+ return { userId: null, claims: { method: "shared-secret", keyIndex: matchedIndex } };
113
+ };
114
+ }
115
+ /**
116
+ * JWT mode: jose's jwtVerify against a shared secret (HS*) or a remote JWKS, with the configured issuer, audience and
117
+ * algorithms.
118
+ * @param jwt The JWT config.
119
+ * @returns The verifier and its key controls.
120
+ */
121
+ function createJwtVerifier(jwt) {
122
+ const verifyOptions = {};
123
+ if (jwt.issuer != null)
124
+ verifyOptions.issuer = jwt.issuer;
125
+ if (jwt.audience != null)
126
+ verifyOptions.audience = jwt.audience;
127
+ if (jwt.algorithms.length > 0)
128
+ verifyOptions.algorithms = jwt.algorithms;
129
+ const signatureCheck = createJwtSignatureCheck(jwt, verifyOptions);
130
+ const usesJwks = jwt.jwksUrl != null;
131
+ const verify = async (request) => {
132
+ let payload;
133
+ try {
134
+ payload = await signatureCheck.verify(request.token);
135
+ }
136
+ catch (error) {
137
+ throw jwtRefusal(error, usesJwks);
138
+ }
139
+ const userId = (typeof payload.sub === "string") ? payload.sub : null;
140
+ return { userId: userId, claims: payload };
141
+ };
142
+ return { verify: verify, keys: signatureCheck.keys };
143
+ }
144
+ /**
145
+ * The jwtVerify call of a JWT config: against the remote JWKS when jwksUrl is set, else the shared secret (validation
146
+ * guarantees one of them).
147
+ * @param jwt The JWT config.
148
+ * @param verifyOptions The issuer, audience and algorithm checks.
149
+ * @returns The check and its key controls (reload and cache age with a JWKS).
150
+ */
151
+ function createJwtSignatureCheck(jwt, verifyOptions) {
152
+ if (jwt.jwksUrl != null)
153
+ return createJwksSignatureCheck(new URL(jwt.jwksUrl), verifyOptions);
154
+ const secretKey = new TextEncoder().encode(jwt.secret ?? "");
155
+ return { verify: async (token) => (await jwtVerify(token, secretKey, verifyOptions)).payload, keys: NO_KEY_CONTROL };
156
+ }
157
+ /**
158
+ * The jwtVerify call against a remote JWKS, which jose caches. The load time is noted whenever jose's key set object
159
+ * changes (a first fetch, a cache refresh or a reload), so the console can show the cache's age.
160
+ * @param jwksUrl The JWKS URL.
161
+ * @param verifyOptions The issuer, audience and algorithm checks.
162
+ * @returns The check with reload and cache age.
163
+ */
164
+ function createJwksSignatureCheck(jwksUrl, verifyOptions) {
165
+ const remoteKeys = createRemoteJWKSet(jwksUrl, { cacheMaxAge: JWKS_CACHE_MAX_AGE_MS, cooldownDuration: JWKS_COOLDOWN_MS });
166
+ let loadedAt = null;
167
+ let seenKeySet = undefined;
168
+ const noteLoadedKeys = () => {
169
+ const keySet = remoteKeys.jwks();
170
+ if (keySet == null || keySet === seenKeySet)
171
+ return;
172
+ seenKeySet = keySet;
173
+ loadedAt = Date.now();
174
+ };
175
+ const verify = async (token) => {
176
+ try {
177
+ return (await jwtVerify(token, remoteKeys, verifyOptions)).payload;
178
+ }
179
+ finally {
180
+ noteLoadedKeys();
181
+ }
182
+ };
183
+ const reloadKeys = async () => {
184
+ await remoteKeys.reload();
185
+ noteLoadedKeys();
186
+ };
187
+ const keyCacheAgeSeconds = () => (loadedAt == null) ? null : (Date.now() - loadedAt) / MILLISECONDS_PER_SECOND;
188
+ return { verify: verify, keys: { reloadKeys: reloadKeys, keyCacheAgeSeconds: keyCacheAgeSeconds } };
189
+ }
190
+ /**
191
+ * The refusal of a failed JWT check (D13 refusal texts).
192
+ * @param error What jwtVerify threw.
193
+ * @param usesJwks True when keys come from a JWKS URL (a fetch failure then means the keys could not be loaded).
194
+ * @returns The refusal.
195
+ */
196
+ function jwtRefusal(error, usesJwks) {
197
+ if (error instanceof joseErrors.JWTExpired)
198
+ return new AuthRefusal(TICKET_EXPIRED, "the token has expired");
199
+ const isJoseError = error instanceof joseErrors.JOSEError;
200
+ const errorCode = isJoseError ? error.code : "";
201
+ const keysUnavailable = usesJwks && (!isJoseError || JWKS_UNAVAILABLE_CODES.includes(errorCode));
202
+ if (keysUnavailable)
203
+ return new AuthRefusal(INVALID_AUTHENTICATION, "the identity provider's keys could not be loaded");
204
+ const detailCode = isJoseError ? errorCode : "ERR_JWT_INVALID";
205
+ return new AuthRefusal(INVALID_AUTHENTICATION, "the token is not valid (" + detailCode + ")");
206
+ }
207
+ /**
208
+ * Webhook mode: POSTs the token and what the server knows about the join to a URL; a 2xx JSON object approves.
209
+ * @param webhook The webhook config (url is set: validation requires it).
210
+ * @returns The verifier.
211
+ */
212
+ function createWebhookVerifier(webhook) {
213
+ const webhookUrl = webhook.url ?? "";
214
+ return async (request) => {
215
+ const body = await postToWebhook(webhookUrl, webhook.timeoutMs, request);
216
+ if (body == null)
217
+ throw new AuthRefusal(CUSTOM_AUTHENTICATION_FAILED, "the auth webhook did not approve the player");
218
+ const userId = (typeof body.userId === "string") ? body.userId : null;
219
+ return { userId: userId, claims: body };
220
+ };
221
+ }
222
+ /**
223
+ * Asks the webhook about a join.
224
+ * @param webhookUrl The webhook URL.
225
+ * @param timeoutMs Milliseconds to wait for the answer.
226
+ * @param request The join.
227
+ * @returns The answer's JSON object, or null when it did not approve (not 2xx, timeout, not a JSON object).
228
+ */
229
+ async function postToWebhook(webhookUrl, timeoutMs, request) {
230
+ const payload = JSON.stringify({ token: request.token, kind: request.kind, sessionName: request.sessionName, gameMode: request.gameMode, clientAddress: request.clientAddress, origin: request.origin });
231
+ try {
232
+ const response = await fetch(webhookUrl, { method: "POST", headers: { "content-type": "application/json" }, body: payload, signal: AbortSignal.timeout(timeoutMs) });
233
+ if (!response.ok)
234
+ return null;
235
+ const body = await response.json();
236
+ return (body != null && typeof body === "object" && !Array.isArray(body)) ? body : null;
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ }
242
+ /**
243
+ * Custom mode: the developer's hook decides.
244
+ * @param customVerifier The hook.
245
+ * @returns The verifier.
246
+ */
247
+ function createCustomVerifier(customVerifier) {
248
+ return async (request) => {
249
+ const identity = await customVerifier(request);
250
+ if (identity == null)
251
+ throw new AuthRefusal(CUSTOM_AUTHENTICATION_FAILED, "the custom verifier did not approve the player");
252
+ return identity;
253
+ };
254
+ }
255
+ /**
256
+ * Compares a sent secret with the expected one in constant time (through SHA-256 digests, so lengths never leak).
257
+ * The admin API uses it for its bearer token (D15).
258
+ * @param sentText The secret the request sent.
259
+ * @param expectedText The configured secret.
260
+ * @returns True when they are equal.
261
+ */
262
+ export function secretsMatch(sentText, expectedText) {
263
+ return timingSafeEqual(sha256Digest(sentText), sha256Digest(expectedText));
264
+ }
265
+ /**
266
+ * The SHA-256 digest of a text, so tokens of any length compare in constant time.
267
+ * @param text The text.
268
+ * @returns The 32-byte digest.
269
+ */
270
+ function sha256Digest(text) {
271
+ return createHash("sha256").update(text, "utf8").digest();
272
+ }
@@ -0,0 +1,86 @@
1
+ import type { AdminClock } from "./admin/AdminClock.js";
2
+ /** The store lease whose holder runs the boot dedicated sessions. */
3
+ export declare const BOOT_SESSION_LEASE_NAME: string;
4
+ /** What a BootSessionOwner needs from the running server. */
5
+ export interface BootSessionOwnerDependencies {
6
+ /**
7
+ * Takes the boot session lease, or renews it when this holder has it.
8
+ * @param holderId The would-be holder.
9
+ * @param ttlMilliseconds The lease length.
10
+ * @returns True when this holder now holds it.
11
+ * @throws Error when the store cannot answer.
12
+ */
13
+ tryAcquireLease(holderId: string, ttlMilliseconds: number): Promise<boolean>;
14
+ /**
15
+ * Starts the boot dedicated sessions that can start now. It is called again on a later tick until it returns true,
16
+ * so it must skip what an earlier call started.
17
+ * @returns True when all of them run; false when some must wait, for example for a draining process's session of
18
+ * the same name to end.
19
+ * @throws Error when a session cannot start.
20
+ */
21
+ startSessions(): Promise<boolean>;
22
+ /**
23
+ * This process's id, unique across the deploy. Called only after the server listens.
24
+ * @returns The id.
25
+ */
26
+ holderId(): string;
27
+ /**
28
+ * Whether this process drains, and so must let the lease lapse.
29
+ * @returns True once a drain started.
30
+ */
31
+ isDraining(): boolean;
32
+ /** The time and timers. */
33
+ clock: AdminClock;
34
+ }
35
+ /**
36
+ * Holds the boot session lease and starts the boot dedicated sessions on the process that gets it. A process that does
37
+ * not hold the lease keeps asking; a draining holder stops renewing, so a process of the next generation takes over
38
+ * within LEADER_LEASE_TTL_MS.
39
+ */
40
+ export declare class BootSessionOwner {
41
+ /** What it needs from the running server. */
42
+ private readonly dependencies;
43
+ /** The renewal timer, while started. */
44
+ private renewTimer;
45
+ /** What the last look at the lease found; logged only when it changes. */
46
+ private leaseState;
47
+ /** True once this process runs every boot session. */
48
+ private areSessionsStarted;
49
+ /** True while a start runs: a scene can load for longer than a tick, and the lease must keep renewing meanwhile. */
50
+ private isStarting;
51
+ /**
52
+ * Prepares the owner; nothing runs until start().
53
+ * @param dependencies What it needs from the running server.
54
+ */
55
+ constructor(dependencies: BootSessionOwnerDependencies);
56
+ /**
57
+ * Runs the first attempt and keeps trying every LEADER_RENEW_INTERVAL_MS. The timer starts first, so a slow scene
58
+ * load cannot let the lease lapse.
59
+ * @throws Error when this process gets the lease and a boot session cannot start; the owner is stopped first.
60
+ */
61
+ start(): Promise<void>;
62
+ /** Stops the timer. The lease runs out by itself after LEADER_LEASE_TTL_MS. */
63
+ stop(): void;
64
+ /**
65
+ * One timer tick: renews the lease and starts the sessions when due, unless this process drains. A failure never
66
+ * reaches the running process: it is logged, and the next tick tries again.
67
+ */
68
+ tick(): Promise<void>;
69
+ /**
70
+ * Renews the lease, then starts the sessions when this process holds it and has not started them.
71
+ * @throws Error when a session cannot start.
72
+ */
73
+ private attempt;
74
+ /**
75
+ * Takes or renews the lease; a store failure counts as not holding it.
76
+ * @returns True while this process holds the lease.
77
+ */
78
+ private renewLease;
79
+ /**
80
+ * Logs a change of the lease state, so a process that waits for hours writes one line.
81
+ * @param leaseState What this look found.
82
+ * @param holderId This process's id.
83
+ * @param failure Why the store could not answer, when it could not.
84
+ */
85
+ private reportLeaseState;
86
+ }