@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,112 @@
1
+ import type { AdminStore } from "./AdminStore.js";
2
+ import type { AdminClock } from "./AdminClock.js";
3
+ import type { AdminUserRecord, ConsoleSessionRecord } from "./AdminRecords.js";
4
+ /** Where a sign-in comes from. */
5
+ export interface SignInContext {
6
+ /** The client address. */
7
+ clientAddress: string;
8
+ /** The browser's User-Agent header. */
9
+ userAgent: string;
10
+ }
11
+ /** A live session with its account. */
12
+ export interface ResolvedConsoleSession {
13
+ /** The session. */
14
+ record: ConsoleSessionRecord;
15
+ /** Its account. */
16
+ user: AdminUserRecord;
17
+ }
18
+ /** The console sessions of the admin store. */
19
+ export declare class ConsoleSessions {
20
+ /** The admin store. */
21
+ private readonly store;
22
+ /** The clock of timeouts. */
23
+ private readonly clock;
24
+ /** Idle timeout, in milliseconds (admin.sessionIdleMinutes). */
25
+ private readonly idleTimeoutMilliseconds;
26
+ /** Absolute timeout, in milliseconds (admin.sessionMaxHours). */
27
+ private readonly maximumLifetimeMilliseconds;
28
+ /**
29
+ * Creates the session service.
30
+ * @param store The admin store.
31
+ * @param clock The clock.
32
+ * @param sessionIdleMinutes Idle timeout, in minutes.
33
+ * @param sessionMaxHours Absolute timeout, in hours.
34
+ */
35
+ constructor(store: AdminStore, clock: AdminClock, sessionIdleMinutes: number, sessionMaxHours: number);
36
+ /** How long the browser keeps the cookie, in seconds (the absolute timeout). */
37
+ get cookieMaxAgeSeconds(): number;
38
+ /**
39
+ * Starts a session for an account that has just passed both factors.
40
+ * @param user The account.
41
+ * @param context Where the sign-in came from.
42
+ * @returns The stored session and the cookie value (only its hash is stored).
43
+ */
44
+ create(user: AdminUserRecord, context: SignInContext): Promise<{
45
+ record: ConsoleSessionRecord;
46
+ cookieValue: string;
47
+ }>;
48
+ /**
49
+ * The live session of a cookie: timeouts applied (an expired session is deleted), lastSeenAt touched at most once
50
+ * per SESSION_TOUCH_INTERVAL_MS, and null for a disabled or deleted account.
51
+ * @param cookieValue The cookie value.
52
+ * @returns The session and its account, or null.
53
+ */
54
+ resolve(cookieValue: string): Promise<ResolvedConsoleSession | null>;
55
+ /**
56
+ * Records a fresh second-factor check on a session (step-up).
57
+ * @param sessionId The session's public id.
58
+ * @returns The updated session, or null when it no longer exists.
59
+ */
60
+ markSecondFactor(sessionId: string): Promise<ConsoleSessionRecord | null>;
61
+ /**
62
+ * Ends one session.
63
+ * @param sessionId The session's public id.
64
+ */
65
+ end(sessionId: string): Promise<void>;
66
+ /**
67
+ * Ends every session of an account.
68
+ * @param userId The account.
69
+ * @returns How many were ended.
70
+ */
71
+ endAllFor(userId: string): Promise<number>;
72
+ /**
73
+ * The live sessions of an account.
74
+ * @param userId The account.
75
+ * @returns Its sessions, newest first.
76
+ */
77
+ listFor(userId: string): Promise<ConsoleSessionRecord[]>;
78
+ /**
79
+ * Deletes expired sessions and expired pending sign-ins (every SESSION_SWEEP_INTERVAL_MS).
80
+ * @returns How many records were deleted.
81
+ */
82
+ sweepExpired(): Promise<number>;
83
+ /**
84
+ * When a session ends at the latest.
85
+ * @param record The session.
86
+ * @returns ISO time of its absolute timeout.
87
+ */
88
+ expiresAt(record: ConsoleSessionRecord): string;
89
+ /**
90
+ * Whether a session passed its idle or absolute timeout.
91
+ * @param record The session.
92
+ * @param now The current time, in milliseconds.
93
+ * @returns True when it has expired.
94
+ */
95
+ private isExpired;
96
+ /**
97
+ * Finds a session by its public id.
98
+ * @param sessionId The public id.
99
+ * @returns The stored session, or null.
100
+ */
101
+ private findBySessionId;
102
+ /**
103
+ * Every stored session of an account.
104
+ * @param userId The account.
105
+ * @returns Its session documents.
106
+ */
107
+ private documentsOf;
108
+ /**
109
+ * @returns The clock's now as an ISO time.
110
+ */
111
+ private nowIso;
112
+ }
@@ -0,0 +1,201 @@
1
+ // Console sessions (game-server-admin-console D10): server-side records keyed by the SHA-256 of a random cookie value,
2
+ // with an idle and an absolute timeout, a CSRF token and the time of the last second-factor check.
3
+ import { SESSION_TOUCH_INTERVAL_MS } from "./AdminConstants.js";
4
+ import { randomToken, sha256Hex } from "./AdminCrypto.js";
5
+ /** Random bytes of a session cookie value. */
6
+ const COOKIE_VALUE_BYTES = 32;
7
+ /** Random bytes of a session's public id. */
8
+ const SESSION_ID_BYTES = 8;
9
+ /** Random bytes of a CSRF token. */
10
+ const CSRF_TOKEN_BYTES = 32;
11
+ /** Milliseconds per second. */
12
+ const MILLISECONDS_PER_SECOND = 1000;
13
+ /** Milliseconds per minute. */
14
+ const MILLISECONDS_PER_MINUTE = 60000;
15
+ /** Milliseconds per hour. */
16
+ const MILLISECONDS_PER_HOUR = 3600000;
17
+ /** The console sessions of the admin store. */
18
+ export class ConsoleSessions {
19
+ /** The admin store. */
20
+ store;
21
+ /** The clock of timeouts. */
22
+ clock;
23
+ /** Idle timeout, in milliseconds (admin.sessionIdleMinutes). */
24
+ idleTimeoutMilliseconds;
25
+ /** Absolute timeout, in milliseconds (admin.sessionMaxHours). */
26
+ maximumLifetimeMilliseconds;
27
+ /**
28
+ * Creates the session service.
29
+ * @param store The admin store.
30
+ * @param clock The clock.
31
+ * @param sessionIdleMinutes Idle timeout, in minutes.
32
+ * @param sessionMaxHours Absolute timeout, in hours.
33
+ */
34
+ constructor(store, clock, sessionIdleMinutes, sessionMaxHours) {
35
+ this.store = store;
36
+ this.clock = clock;
37
+ this.idleTimeoutMilliseconds = sessionIdleMinutes * MILLISECONDS_PER_MINUTE;
38
+ this.maximumLifetimeMilliseconds = sessionMaxHours * MILLISECONDS_PER_HOUR;
39
+ }
40
+ /** How long the browser keeps the cookie, in seconds (the absolute timeout). */
41
+ get cookieMaxAgeSeconds() {
42
+ return Math.floor(this.maximumLifetimeMilliseconds / MILLISECONDS_PER_SECOND);
43
+ }
44
+ /**
45
+ * Starts a session for an account that has just passed both factors.
46
+ * @param user The account.
47
+ * @param context Where the sign-in came from.
48
+ * @returns The stored session and the cookie value (only its hash is stored).
49
+ */
50
+ async create(user, context) {
51
+ const cookieValue = randomToken(COOKIE_VALUE_BYTES);
52
+ const now = this.nowIso();
53
+ const record = {
54
+ sessionId: randomToken(SESSION_ID_BYTES),
55
+ userId: user.userId,
56
+ csrfToken: randomToken(CSRF_TOKEN_BYTES),
57
+ createdAt: now,
58
+ lastSeenAt: now,
59
+ secondFactorAt: now,
60
+ clientAddress: context.clientAddress,
61
+ userAgent: context.userAgent
62
+ };
63
+ await this.store.putDocument("consoleSessions", sha256Hex(cookieValue), record, 0);
64
+ return { record: record, cookieValue: cookieValue };
65
+ }
66
+ /**
67
+ * The live session of a cookie: timeouts applied (an expired session is deleted), lastSeenAt touched at most once
68
+ * per SESSION_TOUCH_INTERVAL_MS, and null for a disabled or deleted account.
69
+ * @param cookieValue The cookie value.
70
+ * @returns The session and its account, or null.
71
+ */
72
+ async resolve(cookieValue) {
73
+ const documentId = sha256Hex(cookieValue);
74
+ const stored = await this.store.getDocument("consoleSessions", documentId);
75
+ if (stored == null)
76
+ return null;
77
+ let record = stored.value;
78
+ const now = this.clock.now();
79
+ if (this.isExpired(record, now)) {
80
+ await this.store.deleteDocument("consoleSessions", documentId);
81
+ return null;
82
+ }
83
+ const user = await this.store.getDocument("users", record.userId);
84
+ if (user == null || user.value.disabled)
85
+ return null;
86
+ if (now - Date.parse(record.lastSeenAt) >= SESSION_TOUCH_INTERVAL_MS) {
87
+ record = Object.assign({}, record, { lastSeenAt: new Date(now).toISOString() });
88
+ await this.store.putDocument("consoleSessions", documentId, record, null);
89
+ }
90
+ return { record: record, user: user.value };
91
+ }
92
+ /**
93
+ * Records a fresh second-factor check on a session (step-up).
94
+ * @param sessionId The session's public id.
95
+ * @returns The updated session, or null when it no longer exists.
96
+ */
97
+ async markSecondFactor(sessionId) {
98
+ const stored = await this.findBySessionId(sessionId);
99
+ if (stored == null)
100
+ return null;
101
+ const record = Object.assign({}, stored.value, { secondFactorAt: this.nowIso() });
102
+ await this.store.putDocument("consoleSessions", stored.id, record, null);
103
+ return record;
104
+ }
105
+ /**
106
+ * Ends one session.
107
+ * @param sessionId The session's public id.
108
+ */
109
+ async end(sessionId) {
110
+ const stored = await this.findBySessionId(sessionId);
111
+ if (stored != null)
112
+ await this.store.deleteDocument("consoleSessions", stored.id);
113
+ }
114
+ /**
115
+ * Ends every session of an account.
116
+ * @param userId The account.
117
+ * @returns How many were ended.
118
+ */
119
+ async endAllFor(userId) {
120
+ const sessions = await this.documentsOf(userId);
121
+ for (const stored of sessions)
122
+ await this.store.deleteDocument("consoleSessions", stored.id);
123
+ return sessions.length;
124
+ }
125
+ /**
126
+ * The live sessions of an account.
127
+ * @param userId The account.
128
+ * @returns Its sessions, newest first.
129
+ */
130
+ async listFor(userId) {
131
+ const now = this.clock.now();
132
+ return (await this.documentsOf(userId))
133
+ .map((stored) => stored.value)
134
+ .filter((record) => !this.isExpired(record, now))
135
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
136
+ }
137
+ /**
138
+ * Deletes expired sessions and expired pending sign-ins (every SESSION_SWEEP_INTERVAL_MS).
139
+ * @returns How many records were deleted.
140
+ */
141
+ async sweepExpired() {
142
+ const now = this.clock.now();
143
+ let deletedCount = 0;
144
+ for (const stored of await this.store.listDocuments("consoleSessions")) {
145
+ if (!this.isExpired(stored.value, now))
146
+ continue;
147
+ if (await this.store.deleteDocument("consoleSessions", stored.id))
148
+ deletedCount++;
149
+ }
150
+ for (const stored of await this.store.listDocuments("pendingSignIns")) {
151
+ if (Date.parse(stored.value.expiresAt) > now)
152
+ continue;
153
+ if (await this.store.deleteDocument("pendingSignIns", stored.id))
154
+ deletedCount++;
155
+ }
156
+ return deletedCount;
157
+ }
158
+ /**
159
+ * When a session ends at the latest.
160
+ * @param record The session.
161
+ * @returns ISO time of its absolute timeout.
162
+ */
163
+ expiresAt(record) {
164
+ return new Date(Date.parse(record.createdAt) + this.maximumLifetimeMilliseconds).toISOString();
165
+ }
166
+ /**
167
+ * Whether a session passed its idle or absolute timeout.
168
+ * @param record The session.
169
+ * @param now The current time, in milliseconds.
170
+ * @returns True when it has expired.
171
+ */
172
+ isExpired(record, now) {
173
+ const idleFor = now - Date.parse(record.lastSeenAt);
174
+ const aliveFor = now - Date.parse(record.createdAt);
175
+ return idleFor > this.idleTimeoutMilliseconds || aliveFor > this.maximumLifetimeMilliseconds;
176
+ }
177
+ /**
178
+ * Finds a session by its public id.
179
+ * @param sessionId The public id.
180
+ * @returns The stored session, or null.
181
+ */
182
+ async findBySessionId(sessionId) {
183
+ const sessions = await this.store.listDocuments("consoleSessions");
184
+ return sessions.find((stored) => stored.value.sessionId === sessionId) ?? null;
185
+ }
186
+ /**
187
+ * Every stored session of an account.
188
+ * @param userId The account.
189
+ * @returns Its session documents.
190
+ */
191
+ async documentsOf(userId) {
192
+ const sessions = await this.store.listDocuments("consoleSessions");
193
+ return sessions.filter((stored) => stored.value.userId === userId);
194
+ }
195
+ /**
196
+ * @returns The clock's now as an ISO time.
197
+ */
198
+ nowIso() {
199
+ return new Date(this.clock.now()).toISOString();
200
+ }
201
+ }
@@ -0,0 +1,21 @@
1
+ import type { ResolvedDedicatedScene } from "../DedicatedSceneRegistry.js";
2
+ import { type AdminApiRouter } from "./AdminApiRouter.js";
3
+ import type { DedicatedSceneView } from "./AdminApiTypes.js";
4
+ import type { AdminServices } from "./AdminServices.js";
5
+ /**
6
+ * Registers the dedicated/* routes.
7
+ * @param router The admin API router.
8
+ * @param services The shared services.
9
+ */
10
+ export declare function registerDedicatedRoutes(router: AdminApiRouter, services: AdminServices): void;
11
+ /**
12
+ * Registers this instance's checkDedicatedScene and startDedicated fleet commands (R15).
13
+ * @param services The shared services.
14
+ */
15
+ export declare function registerDedicatedCommands(services: AdminServices): void;
16
+ /**
17
+ * A configured scene as the console lists it.
18
+ * @param scene The resolved scene.
19
+ * @returns The row.
20
+ */
21
+ export declare function dedicatedSceneView(scene: ResolvedDedicatedScene): DedicatedSceneView;
@@ -0,0 +1,254 @@
1
+ // The dedicated session routes (game-server-admin-console DR §6.11 dedicated rows, D23, R17): the configured scenes and
2
+ // the running dedicated sessions of the fleet, start on a chosen instance, stop, restart under the same name and the
3
+ // scene check, plus this instance's checkDedicatedScene and startDedicated fleet commands (R15).
4
+ import { matchMaker } from "colyseus";
5
+ import { stat } from "node:fs/promises";
6
+ import { fileURLToPath } from "node:url";
7
+ import { getLogger } from "../GameServerLogger.js";
8
+ import { AdminRequestError } from "./AdminApiRouter.js";
9
+ import { ROOM_STOP_TIMEOUT_MS } from "./AdminConstants.js";
10
+ import { bodyObject, optionalStringField, stringField } from "./AdminRequestBodies.js";
11
+ import { noticeTextField } from "./FleetRoutes.js";
12
+ import { describeFleetSessions } from "./SessionRoutes.js";
13
+ /** HTTP status of a successful read or change. */
14
+ const HTTP_OK = 200;
15
+ /** HTTP status of an unknown scene or dedicated session. */
16
+ const HTTP_NOT_FOUND = 404;
17
+ /** HTTP status of a session name already in use, or a session that cannot be restarted. */
18
+ const HTTP_CONFLICT = 409;
19
+ /** HTTP status of a scene that failed to start. */
20
+ const HTTP_SERVICE_UNAVAILABLE = 503;
21
+ /** HTTP status of a stopped session that did not close in time. */
22
+ const HTTP_GATEWAY_TIMEOUT = 504;
23
+ /** How long one scene check request may take, in milliseconds. */
24
+ const SCENE_CHECK_TIMEOUT_MS = 10000;
25
+ /** Milliseconds between checks for a stopped room's disposal during a restart. */
26
+ const ROOM_DISPOSAL_POLL_MS = 100;
27
+ /** The URL scheme the scene check reads from disk. */
28
+ const FILE_URL_SCHEME = "file:";
29
+ /** Who the console's starts and restarts are recorded as. */
30
+ const ADMIN_STARTER = "admin";
31
+ /**
32
+ * Registers the dedicated/* routes.
33
+ * @param router The admin API router.
34
+ * @param services The shared services.
35
+ */
36
+ export function registerDedicatedRoutes(router, services) {
37
+ const routes = [
38
+ { method: "GET", pattern: "dedicated", permission: "dedicated.view", requiresStepUp: false, auditAction: null, handler: async () => ({ status: HTTP_OK, body: await buildDedicatedView(services) }) },
39
+ { method: "POST", pattern: "dedicated/start", permission: "dedicated.manage", requiresStepUp: false, auditAction: "dedicated.start", handler: async (request) => startDedicated(services, request) },
40
+ { method: "POST", pattern: "dedicated/:roomId/stop", permission: "dedicated.manage", requiresStepUp: false, auditAction: "dedicated.stop", handler: async (request) => stopDedicated(services, request) },
41
+ { method: "POST", pattern: "dedicated/:roomId/restart", permission: "dedicated.manage", requiresStepUp: false, auditAction: "dedicated.restart", handler: async (request) => restartDedicated(services, request) },
42
+ { method: "POST", pattern: "dedicated/scenes/:sceneName/check", permission: "dedicated.view", requiresStepUp: false, auditAction: null, handler: async (request) => checkSceneOn(services, request) }
43
+ ];
44
+ for (const route of routes)
45
+ router.register(route);
46
+ }
47
+ /**
48
+ * Registers this instance's checkDedicatedScene and startDedicated fleet commands (R15).
49
+ * @param services The shared services.
50
+ */
51
+ export function registerDedicatedCommands(services) {
52
+ services.fleet.handle("checkDedicatedScene", async (command) => checkDedicatedScene(services, command.sceneName));
53
+ services.fleet.handle("startDedicated", async (command) => startOnThisInstance(services, command.sceneName, command.sessionName, command.startedBy));
54
+ }
55
+ /**
56
+ * A configured scene as the console lists it.
57
+ * @param scene The resolved scene.
58
+ * @returns The row.
59
+ */
60
+ export function dedicatedSceneView(scene) {
61
+ return {
62
+ name: scene.name,
63
+ sceneRoot: scene.sceneRoot,
64
+ sceneFile: scene.sceneFile,
65
+ projectScripts: scene.projectScripts.slice(),
66
+ maxPlayers: scene.maxPlayers,
67
+ onDemand: scene.onDemand,
68
+ idleSeconds: scene.idleSeconds,
69
+ source: "config"
70
+ };
71
+ }
72
+ /**
73
+ * GET dedicated: this instance's configured scenes and every dedicated session of the fleet.
74
+ * @param services The shared services.
75
+ * @returns The page data.
76
+ */
77
+ async function buildDedicatedView(services) {
78
+ const scenes = services.state.registry.listScenes().map(dedicatedSceneView);
79
+ const dedicatedSummaries = (await describeFleetSessions(services)).filter((session) => session.dedicatedScene != null);
80
+ const sessions = await Promise.all(dedicatedSummaries.map((summary) => describeDedicatedSession(services, summary)));
81
+ return { scenes: scenes, sessions: sessions.filter((session) => session != null) };
82
+ }
83
+ /**
84
+ * One running dedicated session with its frame history.
85
+ * @param services The shared services.
86
+ * @param summary The session's list row.
87
+ * @returns The row, or null when the session ended meanwhile or its instance did not answer.
88
+ */
89
+ async function describeDedicatedSession(services, summary) {
90
+ let detail;
91
+ try {
92
+ detail = await services.roomClient.describe(summary.roomId, false);
93
+ }
94
+ catch (error) {
95
+ if (error instanceof AdminRequestError)
96
+ return null;
97
+ throw error;
98
+ }
99
+ if (detail.dedicated == null)
100
+ return null;
101
+ return {
102
+ roomId: detail.roomId,
103
+ sessionName: detail.sessionName,
104
+ sceneName: detail.dedicated.sceneName,
105
+ processId: detail.processId,
106
+ players: detail.players,
107
+ maxPlayers: detail.maxPlayers,
108
+ startedBy: detail.dedicated.startedBy,
109
+ frameStatistics: detail.dedicated.frameStatistics
110
+ };
111
+ }
112
+ /**
113
+ * POST dedicated/start: starts a configured scene under a session name on a chosen instance (this one for null).
114
+ * @param services The shared services.
115
+ * @param request The request.
116
+ * @returns The new room id.
117
+ * @throws AdminRequestError 409 when a session of that name runs anywhere in the fleet.
118
+ */
119
+ async function startDedicated(services, request) {
120
+ const body = bodyObject(request.body);
121
+ const sceneName = stringField(body, "sceneName");
122
+ const sessionName = stringField(body, "sessionName");
123
+ const processId = optionalStringField(body, "processId") ?? services.fleet.localProcessId;
124
+ const sessions = await describeFleetSessions(services);
125
+ if (sessions.some((session) => session.sessionName === sessionName))
126
+ throw new AdminRequestError(HTTP_CONFLICT, "a session with this name is running");
127
+ const started = await services.fleet.runOn(processId, "startDedicated", { sceneName: sceneName, sessionName: sessionName, startedBy: ADMIN_STARTER });
128
+ return { status: HTTP_OK, body: started, audit: { target: sessionName, parameters: { sceneName: sceneName, processId: processId, roomId: started.roomId } } };
129
+ }
130
+ /**
131
+ * POST dedicated/:roomId/stop: the players are told GameClosed with the message; nothing restarts the session.
132
+ * @param services The shared services.
133
+ * @param request The request.
134
+ * @returns ok.
135
+ */
136
+ async function stopDedicated(services, request) {
137
+ const message = noticeTextField(bodyObject(request.body), "message") ?? "";
138
+ await services.roomClient.stopDedicated(request.params.roomId, message);
139
+ return { status: HTTP_OK, body: { ok: true }, audit: { target: request.params.roomId, parameters: { message: message } } };
140
+ }
141
+ /**
142
+ * POST dedicated/:roomId/restart (R17): stops the session, waits until its room is gone, then starts the same scene
143
+ * under the same session name on the same instance.
144
+ * @param services The shared services.
145
+ * @param request The request.
146
+ * @returns The new room id.
147
+ * @throws AdminRequestError 404 for a session without a dedicated runner, 409 when its scene is not configured here.
148
+ */
149
+ async function restartDedicated(services, request) {
150
+ const roomId = request.params.roomId;
151
+ const message = noticeTextField(bodyObject(request.body), "message") ?? "";
152
+ const detail = await services.roomClient.describe(roomId, false);
153
+ if (detail.dedicated == null)
154
+ throw new AdminRequestError(HTTP_NOT_FOUND, "no such dedicated session");
155
+ if (services.state.registry.find(detail.dedicated.sceneName) == null)
156
+ throw new AdminRequestError(HTTP_CONFLICT, "this session's scene is not configured, so it cannot be restarted");
157
+ const restartOptions = await services.roomClient.stopDedicated(roomId, message);
158
+ await waitForRoomDisposal(services, roomId);
159
+ const started = await services.fleet.runOn(restartOptions.processId, "startDedicated", { sceneName: restartOptions.sceneName, sessionName: restartOptions.sessionName, startedBy: ADMIN_STARTER });
160
+ return { status: HTTP_OK, body: started, audit: { target: roomId, parameters: { message: message, sessionName: restartOptions.sessionName, roomId: started.roomId } } };
161
+ }
162
+ /**
163
+ * Waits until a stopped room has left the matchmaker, at most ROOM_STOP_TIMEOUT_MS.
164
+ * @param services The shared services (the clock).
165
+ * @param roomId The room.
166
+ * @throws AdminRequestError 504 when it is still there after ROOM_STOP_TIMEOUT_MS.
167
+ */
168
+ async function waitForRoomDisposal(services, roomId) {
169
+ const deadline = services.clock.now() + ROOM_STOP_TIMEOUT_MS;
170
+ while (await matchMaker.getRoomById(roomId) != null) {
171
+ if (services.clock.now() >= deadline)
172
+ throw new AdminRequestError(HTTP_GATEWAY_TIMEOUT, "the dedicated session did not stop in time");
173
+ await new Promise((resolve) => services.clock.setTimeout(resolve, ROOM_DISPOSAL_POLL_MS));
174
+ }
175
+ }
176
+ /**
177
+ * POST dedicated/scenes/:sceneName/check: checks the scene's files on a chosen instance (this one for null).
178
+ * @param services The shared services.
179
+ * @param request The request.
180
+ * @returns The check.
181
+ */
182
+ async function checkSceneOn(services, request) {
183
+ const processId = optionalStringField(bodyObject(request.body), "processId") ?? services.fleet.localProcessId;
184
+ const check = await services.fleet.runOn(processId, "checkDedicatedScene", { sceneName: request.params.sceneName });
185
+ return { status: HTTP_OK, body: check };
186
+ }
187
+ /**
188
+ * Starts a configured scene on this instance (the startDedicated command).
189
+ * @param services The shared services.
190
+ * @param sceneName The configured scene.
191
+ * @param sessionName The session name clients join.
192
+ * @param startedBy Who starts it.
193
+ * @returns The new room id.
194
+ * @throws AdminRequestError 404 for an unknown scene, 503 when the scene does not start (logged).
195
+ */
196
+ async function startOnThisInstance(services, sceneName, sessionName, startedBy) {
197
+ if (services.state.registry.find(sceneName) == null)
198
+ throw new AdminRequestError(HTTP_NOT_FOUND, "no dedicated scene named " + sceneName);
199
+ try {
200
+ return { roomId: await services.routeContext.startDedicatedSession(sessionName, sceneName, startedBy) };
201
+ }
202
+ catch (error) {
203
+ getLogger().error("the dedicated session did not start", { sessionName: sessionName, sceneName: sceneName, error: String(error) });
204
+ throw new AdminRequestError(HTTP_SERVICE_UNAVAILABLE, "the dedicated session did not start: " + String(error));
205
+ }
206
+ }
207
+ /**
208
+ * Checks that a configured scene's files load, without running any of them (the checkDedicatedScene command): the
209
+ * scene file and each project script, fetched over http(s) or read from disk for file:// URLs.
210
+ * @param services The shared services.
211
+ * @param sceneName The configured scene.
212
+ * @returns The check.
213
+ * @throws AdminRequestError 404 for an unknown scene.
214
+ */
215
+ async function checkDedicatedScene(services, sceneName) {
216
+ const scene = services.state.registry.find(sceneName);
217
+ if (scene == null)
218
+ throw new AdminRequestError(HTTP_NOT_FOUND, "no dedicated scene named " + sceneName);
219
+ const urls = [scene.sceneRootUrl + scene.sceneFile, ...scene.projectScripts];
220
+ const files = await Promise.all(urls.map((url) => checkSceneFile(url)));
221
+ return { sceneName: sceneName, ok: files.every((file) => file.ok), files: files };
222
+ }
223
+ /**
224
+ * Checks one file of a scene.
225
+ * @param url An http(s):// or file:// URL.
226
+ * @returns Whether it loads, with its HTTP status (null for files on disk and failed requests) and the error.
227
+ */
228
+ async function checkSceneFile(url) {
229
+ if (url.startsWith(FILE_URL_SCHEME))
230
+ return checkFileOnDisk(url);
231
+ try {
232
+ const response = await fetch(url, { method: "GET", signal: AbortSignal.timeout(SCENE_CHECK_TIMEOUT_MS) });
233
+ // Only the status matters; the body is never read, so nothing of it runs or stays in memory.
234
+ await response.body?.cancel();
235
+ return { url: url, ok: response.ok, status: response.status, error: response.ok ? null : "the server answered " + response.status };
236
+ }
237
+ catch (error) {
238
+ return { url: url, ok: false, status: null, error: (error instanceof Error) ? error.message : String(error) };
239
+ }
240
+ }
241
+ /**
242
+ * Checks that a file:// URL names a readable file.
243
+ * @param url The file:// URL.
244
+ * @returns Whether it is a file, with the error when it is not.
245
+ */
246
+ async function checkFileOnDisk(url) {
247
+ try {
248
+ const isFile = (await stat(fileURLToPath(url))).isFile();
249
+ return { url: url, ok: isFile, status: null, error: isFile ? null : "not a file" };
250
+ }
251
+ catch (error) {
252
+ return { url: url, ok: false, status: null, error: (error instanceof Error) ? error.message : String(error) };
253
+ }
254
+ }
@@ -0,0 +1,8 @@
1
+ import { type AdminApiRouter } from "./AdminApiRouter.js";
2
+ import type { AdminServices } from "./AdminServices.js";
3
+ /**
4
+ * Registers the extensions/* and docs routes.
5
+ * @param router The admin API router.
6
+ * @param services The shared services.
7
+ */
8
+ export declare function registerExtensionRoutes(router: AdminApiRouter, services: AdminServices): void;