@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,51 @@
1
+ import { type Endpoint } from "colyseus";
2
+ import type { GameServerStateValues } from "../GameServerState.js";
3
+ import type { AdminClock } from "../admin/AdminClock.js";
4
+ import type { AdminStore } from "../admin/AdminStore.js";
5
+ import type { LeaderboardService } from "../leaderboards/LeaderboardService.js";
6
+ import { type DiscordEndpoints } from "./DiscordOAuth.js";
7
+ /** What the Discord routes read. */
8
+ export interface DiscordSignInRouteContext {
9
+ /** The server state (config, discordSessions, limits). */
10
+ state: () => GameServerStateValues;
11
+ /** The admin store (challenges, handoffs). */
12
+ store: AdminStore;
13
+ /** The leaderboards (claims at the session exchange). */
14
+ leaderboards: LeaderboardService;
15
+ /** The clock. */
16
+ clock: AdminClock;
17
+ /** Discord's addresses. */
18
+ endpoints: DiscordEndpoints;
19
+ }
20
+ /** One verified Discord player of a network session (D17). */
21
+ export interface DiscordSessionPlayerView {
22
+ /** The player id in the session. */
23
+ playerId: number;
24
+ /** discord:<id>. */
25
+ userId: string;
26
+ /** The verified display name. */
27
+ displayName: string;
28
+ /** The avatar hash, or null. */
29
+ avatar: string | null;
30
+ }
31
+ /** The answer of POST /game-server/discord/session. */
32
+ export interface DiscordSessionResponse {
33
+ /** The session and its profile. */
34
+ session: {
35
+ token: string;
36
+ expiresAt: string;
37
+ userId: string;
38
+ discordId: string;
39
+ displayName: string;
40
+ avatarHash: string | null;
41
+ };
42
+ /** True when this browser's anonymous history was moved onto the Discord player. */
43
+ merged: boolean;
44
+ }
45
+ /**
46
+ * The Discord sign-in endpoints (discord-sign-in D13). Every route answers 404 { reason: "discordSignInOff" } while the
47
+ * state has no Discord sessions.
48
+ * @param context What the routes read.
49
+ * @returns The endpoints, keyed by name, for createRouter.
50
+ */
51
+ export declare function createDiscordSignInRoutes(context: DiscordSignInRouteContext): Record<string, Endpoint>;
@@ -0,0 +1,359 @@
1
+ // discord/DiscordSignInRoutes.ts — the Discord sign-in endpoints (discord-sign-in D13, D14, D17): start, Discord's
2
+ // callback, the handoff-for-session exchange and the verified players of one network session.
3
+ import { createHash } from "node:crypto";
4
+ import { createEndpoint, matchMaker } from "colyseus";
5
+ import { isOriginAllowed } from "../AbuseLimits.js";
6
+ import { SESSION_ROOM_NAME } from "../DedicatedSession.js";
7
+ import { getLogger } from "../GameServerLogger.js";
8
+ import { jsonResponse } from "../ServerEndpoints.js";
9
+ import { requestOf } from "../admin/AdminApiRouter.js";
10
+ import { randomToken, sha256Hex } from "../admin/AdminCrypto.js";
11
+ import { readJsonBody } from "../leaderboards/LeaderboardPublicApi.js";
12
+ import { LeaderboardError } from "../leaderboards/LeaderboardError.js";
13
+ import { DiscordApiError, exchangeDiscordCode, fetchDiscordProfile } from "./DiscordOAuth.js";
14
+ import { CHALLENGE_ID_PREFIX, CHALLENGE_LIFETIME_MS, DISCORD_CALLBACK_PATH, DISCORD_COLLECTION, DISCORD_ISSUER, DISCORD_ROOM_CALL_TIMEOUT_MS, DISCORD_SCOPE, DISCORD_SESSION_PATH, DISCORD_SESSION_PLAYERS_PATH, DISCORD_START_PATH, HANDOFF_ID_PREFIX, HANDOFF_LIFETIME_MS, RANDOM_VALUE_BYTES, RETURN_COOKIE_MAX_AGE_SECONDS, RETURN_COOKIE_NAME, RETURN_COOKIE_PATH } from "./DiscordSignInConstants.js";
15
+ /** HTTP statuses the routes answer with. */
16
+ const HTTP_OK = 200;
17
+ const HTTP_FOUND = 302;
18
+ const HTTP_BAD_REQUEST = 400;
19
+ const HTTP_UNAUTHORIZED = 401;
20
+ const HTTP_FORBIDDEN = 403;
21
+ const HTTP_NOT_FOUND = 404;
22
+ /** The text page of a failed callback that has no return address to go back to. */
23
+ const FAILED_SIGN_IN_TEXT = "Sign-in failed. Go back to the game and try again.";
24
+ /** The authorization response's error for a player who pressed Cancel at Discord. */
25
+ const ACCESS_DENIED_ERROR = "access_denied";
26
+ /** The return query's names and the outcomes of signin=. */
27
+ const SIGN_IN_QUERY_NAME = "signin";
28
+ const CODE_QUERY_NAME = "code";
29
+ const SIGN_IN_CANCELLED = "cancelled";
30
+ const SIGN_IN_FAILED = "failed";
31
+ const SIGN_IN_BUSY = "busy";
32
+ /**
33
+ * The Discord sign-in endpoints (discord-sign-in D13). Every route answers 404 { reason: "discordSignInOff" } while the
34
+ * state has no Discord sessions.
35
+ * @param context What the routes read.
36
+ * @returns The endpoints, keyed by name, for createRouter.
37
+ */
38
+ export function createDiscordSignInRoutes(context) {
39
+ return {
40
+ discordSignInStart: createEndpoint(DISCORD_START_PATH, { method: "GET", disableBody: true }, async (routerContext) => startSignIn(context, requestOf(routerContext))),
41
+ discordSignInCallback: createEndpoint(DISCORD_CALLBACK_PATH, { method: "GET", disableBody: true }, async (routerContext) => finishAtDiscordCallback(context, requestOf(routerContext))),
42
+ discordSignInSession: createEndpoint(DISCORD_SESSION_PATH, { method: "POST", disableBody: true }, async (routerContext) => exchangeHandoff(context, requestOf(routerContext))),
43
+ discordSignInSessionPlayers: createEndpoint(DISCORD_SESSION_PLAYERS_PATH, { method: "GET", disableBody: true }, async (routerContext) => readSessionPlayers(context, requestOf(routerContext)))
44
+ };
45
+ }
46
+ // ---- Route handlers ----------------------------------------------------------------------------------------------------
47
+ /**
48
+ * GET start?returnTo=<url>: stores a challenge and sends the browser to Discord's consent page (prompt=none, so a
49
+ * player who already approved passes straight through), with the return cookie for refused-state answers.
50
+ * @param context What the routes read.
51
+ * @param request The Fetch request.
52
+ * @returns 302 to Discord, 400 returnNotAllowed, or 404 while sign-in is off.
53
+ */
54
+ async function startSignIn(context, request) {
55
+ const state = context.state();
56
+ if (state.discordSessions == null)
57
+ return signInOffResponse();
58
+ const config = state.config.discordSignIn;
59
+ const returnTo = validReturnTo(new URL(request.url).searchParams.get("returnTo"), config.returnOrigins);
60
+ if (returnTo == null)
61
+ return jsonResponse(HTTP_BAD_REQUEST, { error: "returnTo is not an allowed game address", reason: "returnNotAllowed" });
62
+ await sweepExpired(context.store, context.clock);
63
+ const challengeState = randomToken(RANDOM_VALUE_BYTES);
64
+ const codeVerifier = randomToken(RANDOM_VALUE_BYTES);
65
+ const challenge = { kind: "challenge", codeVerifier: codeVerifier, returnTo: returnTo.href, expiresAt: new Date(context.clock.now() + CHALLENGE_LIFETIME_MS).toISOString() };
66
+ await context.store.putDocument(DISCORD_COLLECTION, CHALLENGE_ID_PREFIX + sha256Hex(challengeState), challenge, 0);
67
+ const authorizeUrl = new URL(context.endpoints.authorizeUrl);
68
+ authorizeUrl.searchParams.set("response_type", "code");
69
+ authorizeUrl.searchParams.set("client_id", config.clientId ?? "");
70
+ authorizeUrl.searchParams.set("redirect_uri", config.redirectUri ?? "");
71
+ authorizeUrl.searchParams.set("scope", DISCORD_SCOPE);
72
+ authorizeUrl.searchParams.set("state", challengeState);
73
+ authorizeUrl.searchParams.set("code_challenge", createHash("sha256").update(codeVerifier).digest("base64url"));
74
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
75
+ authorizeUrl.searchParams.set("prompt", "none");
76
+ return redirectResponse(authorizeUrl.href, returnCookie(returnTo.href, isSecureServer(state)));
77
+ }
78
+ /**
79
+ * GET callback?code&state&iss&error: Discord's redirect back. Consumes the challenge, checks Discord's issuer, trades
80
+ * the code, reads the user and hands the game a single-use code.
81
+ * @param context What the routes read.
82
+ * @param request The Fetch request.
83
+ * @returns 302 to the game's return address (code= or signin=), 400 text without a return address, or 404 while off.
84
+ */
85
+ async function finishAtDiscordCallback(context, request) {
86
+ const state = context.state();
87
+ if (state.discordSessions == null)
88
+ return signInOffResponse();
89
+ const config = state.config.discordSignIn;
90
+ const isSecure = isSecureServer(state);
91
+ const clearedCookie = clearedReturnCookie(isSecure);
92
+ const query = new URL(request.url).searchParams;
93
+ const challenge = await consumeRecord(context.store, context.clock, CHALLENGE_ID_PREFIX, query.get("state"), "challenge");
94
+ if (challenge == null) {
95
+ const cookieReturn = validReturnTo(decodedCookie(readCookie(request, RETURN_COOKIE_NAME)), config.returnOrigins);
96
+ if (cookieReturn == null)
97
+ return new Response(FAILED_SIGN_IN_TEXT, { status: HTTP_BAD_REQUEST, headers: { "content-type": "text/plain; charset=utf-8" } });
98
+ return redirectWithQuery(cookieReturn.href, SIGN_IN_QUERY_NAME, SIGN_IN_FAILED, clearedCookie);
99
+ }
100
+ const discordError = query.get("error");
101
+ if (discordError != null)
102
+ return redirectWithQuery(challenge.returnTo, SIGN_IN_QUERY_NAME, (discordError === ACCESS_DENIED_ERROR) ? SIGN_IN_CANCELLED : SIGN_IN_FAILED, clearedCookie);
103
+ const issuer = query.get("iss");
104
+ if (issuer != null && issuer !== DISCORD_ISSUER)
105
+ return redirectWithQuery(challenge.returnTo, SIGN_IN_QUERY_NAME, SIGN_IN_FAILED, clearedCookie);
106
+ const code = query.get("code");
107
+ if (code == null || code === "")
108
+ return redirectWithQuery(challenge.returnTo, SIGN_IN_QUERY_NAME, SIGN_IN_FAILED, clearedCookie);
109
+ let profile;
110
+ try {
111
+ const accessToken = await exchangeDiscordCode(context.endpoints, config, code, challenge.codeVerifier);
112
+ profile = await fetchDiscordProfile(context.endpoints, accessToken);
113
+ }
114
+ catch (error) {
115
+ if (!(error instanceof DiscordApiError))
116
+ throw error;
117
+ getLogger().warn("a Discord sign-in failed at Discord", { kind: error.kind, error: error.message });
118
+ return redirectWithQuery(challenge.returnTo, SIGN_IN_QUERY_NAME, (error.kind === "busy") ? SIGN_IN_BUSY : SIGN_IN_FAILED, clearedCookie);
119
+ }
120
+ const handoffCode = randomToken(RANDOM_VALUE_BYTES);
121
+ const handoff = { kind: "handoff", returnOrigin: new URL(challenge.returnTo).origin, profile: profile, expiresAt: new Date(context.clock.now() + HANDOFF_LIFETIME_MS).toISOString() };
122
+ await context.store.putDocument(DISCORD_COLLECTION, HANDOFF_ID_PREFIX + sha256Hex(handoffCode), handoff, 0);
123
+ return redirectWithQuery(challenge.returnTo, CODE_QUERY_NAME, handoffCode, clearedCookie);
124
+ }
125
+ /**
126
+ * POST session { code, playerKey? }: trades a handoff code for this server's session and gives the player their
127
+ * leaderboard player (merging the browser's anonymous history on a first sign-in, D16).
128
+ * @param context What the routes read.
129
+ * @param request The Fetch request.
130
+ * @returns 200 DiscordSessionResponse, 400 invalidRequest, 401 handoffInvalid, 403 originNotAllowed, or 404 while off.
131
+ */
132
+ async function exchangeHandoff(context, request) {
133
+ const discordSessions = context.state().discordSessions;
134
+ if (discordSessions == null)
135
+ return signInOffResponse();
136
+ const body = await readBodyOrNull(request);
137
+ const code = body?.code;
138
+ if (body == null || typeof code !== "string" || code === "")
139
+ return jsonResponse(HTTP_BAD_REQUEST, { error: "the body needs a code", reason: "invalidRequest" });
140
+ const handoff = await consumeRecord(context.store, context.clock, HANDOFF_ID_PREFIX, code, "handoff");
141
+ if (handoff == null)
142
+ return jsonResponse(HTTP_UNAUTHORIZED, { error: "the sign-in code is not valid", reason: "handoffInvalid" });
143
+ if (request.headers.get("origin") !== handoff.returnOrigin)
144
+ return jsonResponse(HTTP_FORBIDDEN, { error: "origin not allowed", reason: "originNotAllowed" });
145
+ const grant = await discordSessions.issue(handoff.profile);
146
+ const playerKey = body.playerKey;
147
+ const anonymousPlayerKey = (typeof playerKey === "string" && playerKey !== "") ? playerKey : null;
148
+ const merged = await claimLeaderboardPlayer(context.leaderboards, grant, anonymousPlayerKey);
149
+ const response = {
150
+ session: { token: grant.token, expiresAt: grant.expiresAt, userId: grant.userId, discordId: grant.profile.discordId, displayName: grant.profile.displayName, avatarHash: grant.profile.avatarHash },
151
+ merged: merged
152
+ };
153
+ return jsonResponse(HTTP_OK, response);
154
+ }
155
+ /**
156
+ * GET session-players?sessionName=<name>: the verified Discord players of one network session (D17), asked of the
157
+ * session's room wherever it runs.
158
+ * @param context What the routes read.
159
+ * @param request The Fetch request.
160
+ * @returns 200 { players }, 400 invalidRequest, 403 originNotAllowed, or 404 while off.
161
+ */
162
+ async function readSessionPlayers(context, request) {
163
+ const state = context.state();
164
+ if (state.discordSessions == null)
165
+ return signInOffResponse();
166
+ const sessionName = new URL(request.url).searchParams.get("sessionName");
167
+ if (sessionName == null || sessionName === "")
168
+ return jsonResponse(HTTP_BAD_REQUEST, { error: "sessionName is required", reason: "invalidRequest" });
169
+ if (!isOriginAllowed(request.headers.get("origin"), state.config.limits))
170
+ return jsonResponse(HTTP_FORBIDDEN, { error: "origin not allowed", reason: "originNotAllowed" });
171
+ const rooms = await matchMaker.query({ name: SESSION_ROOM_NAME, sessionName: sessionName });
172
+ if (rooms.length === 0)
173
+ return jsonResponse(HTTP_OK, { players: [] });
174
+ try {
175
+ const players = await matchMaker.remoteRoomCall(rooms[0].roomId, "discordPlayers", [], DISCORD_ROOM_CALL_TIMEOUT_MS);
176
+ return jsonResponse(HTTP_OK, { players: players });
177
+ }
178
+ catch (error) {
179
+ getLogger().warn("the verified players of a session could not be read", { sessionName: sessionName, error: String(error) });
180
+ return jsonResponse(HTTP_OK, { players: [] });
181
+ }
182
+ }
183
+ // ---- Helpers -----------------------------------------------------------------------------------------------------------
184
+ /**
185
+ * The answer of every route while Discord sign-in is off.
186
+ * @returns 404 discordSignInOff.
187
+ */
188
+ function signInOffResponse() {
189
+ return jsonResponse(HTTP_NOT_FOUND, { error: "Discord sign-in is off on this server", reason: "discordSignInOff" });
190
+ }
191
+ /**
192
+ * Whether the server's own callback is https (the return cookie is then Secure).
193
+ * @param state The server state.
194
+ * @returns True when the redirect address starts with https:.
195
+ */
196
+ function isSecureServer(state) {
197
+ return (state.config.discordSignIn.redirectUri ?? "").startsWith("https:");
198
+ }
199
+ /**
200
+ * A return address the routes may redirect to (D13: no open redirect).
201
+ * @param text The requested address, or null.
202
+ * @param returnOrigins The configured game origins.
203
+ * @returns The parsed URL when it is absolute http(s), its origin is listed, and it has no query, fragment or user info;
204
+ * else null.
205
+ */
206
+ function validReturnTo(text, returnOrigins) {
207
+ if (text == null || text === "")
208
+ return null;
209
+ let parsed;
210
+ try {
211
+ parsed = new URL(text);
212
+ }
213
+ catch {
214
+ return null;
215
+ }
216
+ const isHttp = parsed.protocol === "http:" || parsed.protocol === "https:";
217
+ const isBare = parsed.search === "" && parsed.hash === "" && parsed.username === "" && parsed.password === "";
218
+ return (isHttp && isBare && returnOrigins.includes(parsed.origin)) ? parsed : null;
219
+ }
220
+ /**
221
+ * Takes a single-use record (D14): read, delete, and only a successful delete counts; the record must be live.
222
+ * @param store The admin store.
223
+ * @param clock The clock.
224
+ * @param idPrefix CHALLENGE_ID_PREFIX or HANDOFF_ID_PREFIX.
225
+ * @param secret The state or code the browser carried, or null.
226
+ * @param kind The record kind the id must hold.
227
+ * @returns The record, or null when missing, already taken, of another kind or expired.
228
+ */
229
+ async function consumeRecord(store, clock, idPrefix, secret, kind) {
230
+ if (secret == null || secret === "")
231
+ return null;
232
+ const documentId = idPrefix + sha256Hex(secret);
233
+ const stored = await store.getDocument(DISCORD_COLLECTION, documentId);
234
+ if (stored == null || !(await store.deleteDocument(DISCORD_COLLECTION, documentId)))
235
+ return null;
236
+ const isLive = stored.value.kind === kind && Date.parse(stored.value.expiresAt) > clock.now();
237
+ return isLive ? stored.value : null;
238
+ }
239
+ /**
240
+ * Deletes every challenge and handoff whose expiry has passed (run at each start; the session key has no expiry).
241
+ * @param store The admin store.
242
+ * @param clock The clock.
243
+ */
244
+ async function sweepExpired(store, clock) {
245
+ const documents = await store.listDocuments(DISCORD_COLLECTION);
246
+ const now = clock.now();
247
+ for (const document of documents) {
248
+ const expiresAt = document.value.expiresAt;
249
+ if (typeof expiresAt === "string" && Date.parse(expiresAt) <= now)
250
+ await store.deleteDocument(DISCORD_COLLECTION, document.id);
251
+ }
252
+ }
253
+ /**
254
+ * The return cookie set at start (D13).
255
+ * @param returnTo The validated return address.
256
+ * @param isSecure True to add Secure.
257
+ * @returns The Set-Cookie value.
258
+ */
259
+ function returnCookie(returnTo, isSecure) {
260
+ return RETURN_COOKIE_NAME + "=" + encodeURIComponent(returnTo) + "; Path=" + RETURN_COOKIE_PATH + "; Max-Age=" + RETURN_COOKIE_MAX_AGE_SECONDS + "; HttpOnly; SameSite=Lax" + (isSecure ? "; Secure" : "");
261
+ }
262
+ /**
263
+ * The Set-Cookie value that clears the return cookie.
264
+ * @param isSecure True to add Secure.
265
+ * @returns The Set-Cookie value.
266
+ */
267
+ function clearedReturnCookie(isSecure) {
268
+ return RETURN_COOKIE_NAME + "=; Path=" + RETURN_COOKIE_PATH + "; Max-Age=0; HttpOnly; SameSite=Lax" + (isSecure ? "; Secure" : "");
269
+ }
270
+ /**
271
+ * One cookie of a request.
272
+ * @param request The Fetch request.
273
+ * @param name The cookie name.
274
+ * @returns Its raw value, or null when the request does not carry it.
275
+ */
276
+ function readCookie(request, name) {
277
+ const header = request.headers.get("cookie");
278
+ if (header == null)
279
+ return null;
280
+ for (const part of header.split(";")) {
281
+ const separatorIndex = part.indexOf("=");
282
+ if (separatorIndex < 0)
283
+ continue;
284
+ if (part.slice(0, separatorIndex).trim() === name)
285
+ return part.slice(separatorIndex + 1).trim();
286
+ }
287
+ return null;
288
+ }
289
+ /**
290
+ * A cookie value decoded from encodeURIComponent.
291
+ * @param value The raw value, or null.
292
+ * @returns The decoded text, or null when absent or malformed.
293
+ */
294
+ function decodedCookie(value) {
295
+ if (value == null)
296
+ return null;
297
+ try {
298
+ return decodeURIComponent(value);
299
+ }
300
+ catch {
301
+ return null;
302
+ }
303
+ }
304
+ /**
305
+ * A 302 to the game's return address with one query value, clearing the return cookie.
306
+ * @param returnTo The validated return address.
307
+ * @param name "code" or "signin".
308
+ * @param value The value.
309
+ * @param cookie The Set-Cookie value.
310
+ * @returns The redirect.
311
+ */
312
+ function redirectWithQuery(returnTo, name, value, cookie) {
313
+ const location = new URL(returnTo);
314
+ location.searchParams.set(name, value);
315
+ return redirectResponse(location.href, cookie);
316
+ }
317
+ /**
318
+ * A 302 with a cookie.
319
+ * @param location The address.
320
+ * @param cookie The Set-Cookie value.
321
+ * @returns The redirect.
322
+ */
323
+ function redirectResponse(location, cookie) {
324
+ return new Response(null, { status: HTTP_FOUND, headers: { location: location, "set-cookie": cookie } });
325
+ }
326
+ /**
327
+ * Reads the exchange body.
328
+ * @param request The Fetch request.
329
+ * @returns The JSON object, or null when it is too large, not JSON or not an object.
330
+ */
331
+ async function readBodyOrNull(request) {
332
+ try {
333
+ return await readJsonBody(request);
334
+ }
335
+ catch (error) {
336
+ if (error instanceof LeaderboardError)
337
+ return null;
338
+ throw error;
339
+ }
340
+ }
341
+ /**
342
+ * Gives the signed-in user their leaderboard player; a refused claim is logged and the sign-in still succeeds.
343
+ * @param leaderboards The leaderboard service.
344
+ * @param grant The issued session.
345
+ * @param anonymousPlayerKey The browser's anonymous key, or null.
346
+ * @returns True when the anonymous history was merged.
347
+ */
348
+ async function claimLeaderboardPlayer(leaderboards, grant, anonymousPlayerKey) {
349
+ try {
350
+ const claim = await leaderboards.claimUserPlayer({ userId: grant.userId, displayName: grant.profile.displayName, avatar: grant.profile.avatarHash, anonymousPlayerKey: anonymousPlayerKey });
351
+ return claim.merged;
352
+ }
353
+ catch (error) {
354
+ if (!(error instanceof LeaderboardError))
355
+ throw error;
356
+ getLogger().warn("the leaderboard claim of a Discord sign-in failed", { userId: grant.userId, reason: error.reason });
357
+ return false;
358
+ }
359
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The public API of the Babylon Game Server package (`@babylonjs-toolkit/server`).
3
+ */
4
+ export { createGameServer, type GameServer, type GameServerOptions, type GameServerUrls } from "./GameServer.js";
5
+ export { type GameServerHooks, type GameServerMetricsSink, type PlayerEvent, type SessionEvent } from "./GameServerState.js";
6
+ export { AuthRefusal, type AuthIdentity, type AuthRequest, type TokenVerifier } from "./Authentication.js";
7
+ export { UNKNOWN_CLIENT_ADDRESS, type JoinGateResult, type JoinKind } from "./JoinGate.js";
8
+ export { GameServerMetrics } from "./GameServerMetrics.js";
9
+ export { buildServerInfo, jsonResponse, type AdminSessionSummary, type GameServerInfo, type GameServerRouteContext, type GameServerTransports, type WebTransportEndpointInfo } from "./ServerEndpoints.js";
10
+ export { reloadTlsNow, TLS_RELOAD_POLL_MS, type TlsMaterial } from "./TlsCertificates.js";
11
+ export { DedicatedSession, type DedicatedSessionOptions } from "./DedicatedSession.js";
12
+ export { DedicatedSceneRegistry, type ResolvedDedicatedScene } from "./DedicatedSceneRegistry.js";
13
+ export { allowScriptUrlPrefixes, isUrlAllowed } from "./HeadlessRuntime.js";
14
+ export { GameServerLogger, getLogger, setLogger, type LogFields, type LogLineFormat } from "./GameServerLogger.js";
15
+ /** Builds extra HTTP endpoints for `GameServerOptions.routes` (re-exported from Colyseus, D15). */
16
+ export { createEndpoint } from "colyseus";
17
+ export { CONFIG_OPTIONS, DEFAULT_GAME_SERVER_CONFIG, GameServerConfigError, loadGameServerConfig, resolveGameServerConfig, type AdminConfig, type AuthConfig, type AuthMode, type ConfigOptionDefinition, type DedicatedBootSession, type DedicatedConfig, type DedicatedSceneConfig, type DeepPartial, type DiscordSignInConfig, type GameServerConfig, type GameServerConfigSources, type GameServerLogLevel, type LimitsConfig, type MetricsConfig, type RedisConfig, type TlsConfig, type UdpConfig } from "./GameServerConfig.js";
18
+ export { DiscordSessions, type DiscordProfile, type DiscordSessionGrant } from "./discord/DiscordSessions.js";
19
+ export type { DiscordTestingOptions } from "./discord/DiscordOAuth.js";
20
+ export type { DiscordSessionPlayerView, DiscordSessionResponse } from "./discord/DiscordSignInRoutes.js";
21
+ /** Developer extensions of the admin console: custom actions, panels and admin event hooks (game-server-admin-console D25). */
22
+ export { type AdminActionContext, type AdminActionDefinition, type AdminEventHooks, type AdminExtensions, type AdminPanelDefinition } from "./admin/AdminExtensionsHost.js";
23
+ export { type AdminActionField, type AdminActionResult, type AdminActionTarget, type AdminPanelContent, type AdminPermission, type AdminRole } from "./admin/AdminApiTypes.js";
24
+ export { ADMIN_PERMISSIONS, ROLE_PERMISSIONS } from "./admin/AdminPermissions.js";
package/lib/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The public API of the Babylon Game Server package (`@babylonjs-toolkit/server`).
3
+ */
4
+ export { createGameServer } from "./GameServer.js";
5
+ export { AuthRefusal } from "./Authentication.js";
6
+ export { UNKNOWN_CLIENT_ADDRESS } from "./JoinGate.js";
7
+ export { GameServerMetrics } from "./GameServerMetrics.js";
8
+ export { buildServerInfo, jsonResponse } from "./ServerEndpoints.js";
9
+ export { reloadTlsNow, TLS_RELOAD_POLL_MS } from "./TlsCertificates.js";
10
+ export { DedicatedSession } from "./DedicatedSession.js";
11
+ export { DedicatedSceneRegistry } from "./DedicatedSceneRegistry.js";
12
+ export { allowScriptUrlPrefixes, isUrlAllowed } from "./HeadlessRuntime.js";
13
+ export { GameServerLogger, getLogger, setLogger } from "./GameServerLogger.js";
14
+ /** Builds extra HTTP endpoints for `GameServerOptions.routes` (re-exported from Colyseus, D15). */
15
+ export { createEndpoint } from "colyseus";
16
+ export { CONFIG_OPTIONS, DEFAULT_GAME_SERVER_CONFIG, GameServerConfigError, loadGameServerConfig, resolveGameServerConfig } from "./GameServerConfig.js";
17
+ export { DiscordSessions } from "./discord/DiscordSessions.js";
18
+ export { ADMIN_PERMISSIONS, ROLE_PERMISSIONS } from "./admin/AdminPermissions.js";