@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,202 @@
1
+ import type { AdminCallerKind, AdminPermission } from "./AdminApiTypes.js";
2
+ import type { AdminUserRecord, ConsoleSessionRecord } from "./AdminRecords.js";
3
+ import { AdminRequestGate } from "./AdminRequestGate.js";
4
+ import { type AdminResult } from "./AdminResponses.js";
5
+ import type { AdminServices } from "./AdminServices.js";
6
+ export type { AdminResult } from "./AdminResponses.js";
7
+ /** What the router endpoint passes the handler (the better-call context, narrowed). */
8
+ export interface RouterEndpointContext {
9
+ /** The Fetch request (the router always sets it; better-call types it optional for direct calls). */
10
+ request?: Request;
11
+ /** The router's path parameters. */
12
+ params?: Record<string, unknown>;
13
+ }
14
+ /** Who is calling (after authentication). */
15
+ export interface AdminCaller {
16
+ kind: AdminCallerKind;
17
+ /** userId, keyId, "legacy", "cli" or "system". */
18
+ id: string;
19
+ /** Display name, key name, "legacy admin token", "CLI" or "system". */
20
+ name: string;
21
+ /** What the caller may do. */
22
+ permissions: ReadonlySet<AdminPermission>;
23
+ /** The console session, for cookie callers. */
24
+ session: ConsoleSessionRecord | null;
25
+ /** The account, for cookie callers. */
26
+ user: AdminUserRecord | null;
27
+ }
28
+ /** One API request after parsing. */
29
+ export interface AdminRequest {
30
+ method: "GET" | "POST" | "PUT" | "DELETE";
31
+ /** The path after ADMIN_API_PREFIX, without query, e.g. "sessions/abc/close". */
32
+ apiPath: string;
33
+ /** The named parts of the matched pattern, e.g. { roomId: "abc" }. */
34
+ params: Record<string, string>;
35
+ query: URLSearchParams;
36
+ headers: Headers;
37
+ /** The parsed JSON body of a POST or PUT, or null. */
38
+ body: unknown;
39
+ clientAddress: string;
40
+ /** scheme://host of the console, from the Host header and the D9 scheme rule. */
41
+ consoleOrigin: string;
42
+ /** True for TLS, or HTTPS at a trusted proxy (R9). */
43
+ isSecure: boolean;
44
+ /** True when the D9 rule accepts this connection. */
45
+ isTransportSafe: boolean;
46
+ /** The authenticated caller, or null. */
47
+ caller: AdminCaller | null;
48
+ }
49
+ /** One route of the table (R8: fields may depend on the request). */
50
+ export interface AdminRouteDefinition {
51
+ method: AdminRequest["method"];
52
+ /** Pattern after ADMIN_API_PREFIX with :name parts, e.g. "sessions/:roomId/close". */
53
+ pattern: string;
54
+ /** "public" routes need no caller (sign-in, setup, methods); "signedIn" needs any caller. */
55
+ permission: AdminPermission | "public" | "signedIn" | ((request: AdminRequest) => AdminPermission);
56
+ /** Requires a second-factor check within STEP_UP_WINDOW_MS (spec 1 dangerous actions). */
57
+ requiresStepUp: boolean | ((request: AdminRequest) => boolean);
58
+ /** Audit action name, e.g. "session.close"; null = not audited (reads). */
59
+ auditAction: string | null | ((request: AdminRequest) => string);
60
+ /** False for routes API keys may not call (auth/*, account/*). Default true. */
61
+ allowsApiKeys?: boolean;
62
+ /**
63
+ * Runs the route.
64
+ * @param request The parsed, authorized request.
65
+ * @returns The result the router turns into a response.
66
+ */
67
+ handler(request: AdminRequest): Promise<AdminResult>;
68
+ }
69
+ /** Thrown by handlers for an expected refusal; the router turns it into { error, detail? } with the status. */
70
+ export declare class AdminRequestError extends Error {
71
+ /** HTTP status. */
72
+ readonly status: number;
73
+ /** Extra detail (for example the missing permission name), or null. */
74
+ readonly detail: string | null;
75
+ /**
76
+ * Creates the refusal.
77
+ * @param status HTTP status.
78
+ * @param message The error sentence shown to the admin.
79
+ * @param detail Extra detail, or null.
80
+ */
81
+ constructor(status: number, message: string, detail?: string | null);
82
+ }
83
+ /** The admin API dispatcher (D6). */
84
+ export declare class AdminApiRouter {
85
+ /** The shared services. */
86
+ private readonly services;
87
+ /** The network restriction and transport rule. */
88
+ private readonly gate;
89
+ /** Whether the API answers at all (D30, R6). */
90
+ private readonly isApiAvailable;
91
+ /** Every registered route. */
92
+ private readonly routes;
93
+ /**
94
+ * Creates an empty route table.
95
+ * @param services The shared services.
96
+ * @param gate The network restriction and transport rule.
97
+ * @param isApiAvailable Whether the API answers at all (D30, R6).
98
+ */
99
+ constructor(services: AdminServices, gate: AdminRequestGate, isApiAvailable: () => Promise<boolean>);
100
+ /**
101
+ * Adds a route.
102
+ * @param route The route.
103
+ * @throws Error when the same method and pattern are already registered.
104
+ */
105
+ register(route: AdminRouteDefinition): void;
106
+ /**
107
+ * Runs one API request through the pipeline (DR §6.3) and builds its single-buffer response (D5).
108
+ * @param context The router context.
109
+ * @returns The response.
110
+ */
111
+ handle(context: RouterEndpointContext): Promise<Response>;
112
+ /**
113
+ * The pipeline up to the response: each step answers or goes on.
114
+ * @param httpRequest The Fetch request.
115
+ * @param isSecure True for a secure request (R9).
116
+ * @param consoleOrigin The console origin.
117
+ * @returns The result to answer with.
118
+ */
119
+ private run;
120
+ /**
121
+ * Audits a call of an audited route (DR §6.3 step 11): successes and refusals of a known caller, and every call of
122
+ * a public route (sign-in attempts). Calls refused before authentication are not audited.
123
+ * @param route The route.
124
+ * @param request The request.
125
+ * @param result The answer.
126
+ */
127
+ private audit;
128
+ /**
129
+ * Finds the route of a method and path; among several matches the one with the most literal segments wins.
130
+ * @param method The HTTP method.
131
+ * @param apiPath The path after ADMIN_API_PREFIX.
132
+ * @returns The route and its parameters, "wrongMethod" when only other methods match, or "none".
133
+ */
134
+ private match;
135
+ /**
136
+ * Finds the caller (DR §6.3 step 6): a bearer value (the legacy admin token, a full-access key, D18), else the
137
+ * session cookie (timeouts applied, D10), else nobody.
138
+ * @param request The request.
139
+ * @returns The caller, or null.
140
+ */
141
+ private authenticate;
142
+ /**
143
+ * The caller of a bearer value: an API key (`bgsk_`), the legacy admin token, or nobody.
144
+ * @param bearer The bearer value.
145
+ * @param request The request.
146
+ * @returns The caller, or null.
147
+ */
148
+ private authenticateBearer;
149
+ /**
150
+ * Checks that the caller may run the route (DR §6.3 steps 6–8): a caller for non-public routes, signed-in admins
151
+ * only where API keys are not allowed, the CSRF token and Origin of cookie callers, then the permission.
152
+ * @param route The route.
153
+ * @param request The request with its caller.
154
+ * @throws AdminRequestError 401 without a caller, 403 for a key on an account route, a CSRF failure or a missing
155
+ * permission.
156
+ */
157
+ private authorize;
158
+ /**
159
+ * The CSRF and Origin rule of cookie callers (D10, DR §6.3 step 7): a state-changing request must send the
160
+ * session's CSRF token and no Origin, or the console's own.
161
+ * @param request The request.
162
+ * @param caller The cookie caller.
163
+ * @throws AdminRequestError 403 when the token is missing or wrong or the Origin is foreign.
164
+ */
165
+ private checkCsrf;
166
+ /**
167
+ * The step-up rule of dangerous routes (DR §6.3 step 9): a signed-in admin must have passed a second factor within
168
+ * STEP_UP_WINDOW_MS; API keys and the legacy token count as stepped up.
169
+ * @param route The route.
170
+ * @param request The request with its caller.
171
+ * @throws AdminRequestError 403 with detail "stepUp" when the check is too old.
172
+ */
173
+ private checkStepUp;
174
+ /**
175
+ * Turns a thrown error into its answer (DR §6.3 step 10).
176
+ * @param error What the pipeline or handler threw.
177
+ * @param route The matched route (for the log).
178
+ * @returns The error result.
179
+ */
180
+ private errorToResult;
181
+ }
182
+ /**
183
+ * The Fetch request of a router context.
184
+ * @param context The router context.
185
+ * @returns The request.
186
+ * @throws Error when the endpoint was called without one (only a direct, non-router call can do that).
187
+ */
188
+ export declare function requestOf(context: RouterEndpointContext): Request;
189
+ /**
190
+ * The bearer token of a request.
191
+ * @param headers The request headers.
192
+ * @returns The token, or null when there is no bearer Authorization header.
193
+ */
194
+ export declare function bearerTokenOf(headers: Headers): string | null;
195
+ /**
196
+ * An error result: { error, detail? }.
197
+ * @param status HTTP status.
198
+ * @param message The error sentence.
199
+ * @param detail Extra detail, or null.
200
+ * @returns The result.
201
+ */
202
+ export declare function errorResult(status: number, message: string, detail?: string | null): AdminResult;
@@ -0,0 +1,465 @@
1
+ // The admin API dispatcher (game-server-admin-console D6, DR §6.3): one router endpoint hands every
2
+ // /game-server/admin/api/* request here, and every security check runs in this one pipeline.
3
+ import { getLogger } from "../GameServerLogger.js";
4
+ import { hasAdminToken } from "../ServerEndpoints.js";
5
+ import { ADMIN_API_PREFIX, ADMIN_MAX_BODY_BYTES, STEP_UP_WINDOW_MS } from "./AdminConstants.js";
6
+ import { textsMatch } from "./AdminCrypto.js";
7
+ import { ROLE_PERMISSIONS, hasPermission } from "./AdminPermissions.js";
8
+ import { AdminRequestGate } from "./AdminRequestGate.js";
9
+ import { buildAdminResponse, consoleOriginOf, sessionCookieName } from "./AdminResponses.js";
10
+ import { AdminConflictError } from "./AdminStore.js";
11
+ import { API_KEY_PREFIX } from "./ApiKeys.js";
12
+ /** HTTP statuses of the pipeline's own answers. */
13
+ const HTTP_BAD_REQUEST = 400;
14
+ const HTTP_UNAUTHORIZED = 401;
15
+ const HTTP_FORBIDDEN = 403;
16
+ const HTTP_NOT_FOUND = 404;
17
+ const HTTP_METHOD_NOT_ALLOWED = 405;
18
+ const HTTP_CONFLICT = 409;
19
+ const HTTP_PAYLOAD_TOO_LARGE = 413;
20
+ const HTTP_INTERNAL_ERROR = 500;
21
+ /** Scheme word of a bearer Authorization header. */
22
+ const BEARER_PREFIX = "Bearer ";
23
+ /** Methods whose body is read as JSON. */
24
+ const BODY_METHODS = new Set(["POST", "PUT"]);
25
+ /** Methods that change state, so cookie callers must send the CSRF token (D10). */
26
+ const STATE_CHANGING_METHODS = new Set(["POST", "PUT", "DELETE"]);
27
+ /** The header that carries the session's CSRF token (D10). */
28
+ const CSRF_HEADER = "x-admin-csrf";
29
+ /** Methods the API accepts. */
30
+ const API_METHODS = new Set(["GET", "POST", "PUT", "DELETE"]);
31
+ /** Marks a named pattern segment. */
32
+ const PARAMETER_MARKER = ":";
33
+ /** Lowest status that is not a success. */
34
+ const FIRST_FAILURE_STATUS = 400;
35
+ /** The actor of an audited public call without a caller (a sign-in attempt). */
36
+ const ANONYMOUS_ACTOR = { kind: "user", id: "anonymous", name: "anonymous" };
37
+ /** Thrown by handlers for an expected refusal; the router turns it into { error, detail? } with the status. */
38
+ export class AdminRequestError extends Error {
39
+ /** HTTP status. */
40
+ status;
41
+ /** Extra detail (for example the missing permission name), or null. */
42
+ detail;
43
+ /**
44
+ * Creates the refusal.
45
+ * @param status HTTP status.
46
+ * @param message The error sentence shown to the admin.
47
+ * @param detail Extra detail, or null.
48
+ */
49
+ constructor(status, message, detail = null) {
50
+ super(message);
51
+ this.name = "AdminRequestError";
52
+ this.status = status;
53
+ this.detail = detail;
54
+ }
55
+ }
56
+ /** The admin API dispatcher (D6). */
57
+ export class AdminApiRouter {
58
+ /** The shared services. */
59
+ services;
60
+ /** The network restriction and transport rule. */
61
+ gate;
62
+ /** Whether the API answers at all (D30, R6). */
63
+ isApiAvailable;
64
+ /** Every registered route. */
65
+ routes = [];
66
+ /**
67
+ * Creates an empty route table.
68
+ * @param services The shared services.
69
+ * @param gate The network restriction and transport rule.
70
+ * @param isApiAvailable Whether the API answers at all (D30, R6).
71
+ */
72
+ constructor(services, gate, isApiAvailable) {
73
+ this.services = services;
74
+ this.gate = gate;
75
+ this.isApiAvailable = isApiAvailable;
76
+ }
77
+ /**
78
+ * Adds a route.
79
+ * @param route The route.
80
+ * @throws Error when the same method and pattern are already registered.
81
+ */
82
+ register(route) {
83
+ const duplicate = this.routes.some((existing) => existing.definition.method === route.method && existing.definition.pattern === route.pattern);
84
+ if (duplicate)
85
+ throw new Error("admin route " + route.method + " " + route.pattern + " is registered twice");
86
+ const segments = route.pattern.split("/");
87
+ const literalCount = segments.filter((segment) => !segment.startsWith(PARAMETER_MARKER)).length;
88
+ this.routes.push({ definition: route, segments: segments, literalCount: literalCount });
89
+ }
90
+ /**
91
+ * Runs one API request through the pipeline (DR §6.3) and builds its single-buffer response (D5).
92
+ * @param context The router context.
93
+ * @returns The response.
94
+ */
95
+ async handle(context) {
96
+ const httpRequest = requestOf(context);
97
+ const headers = httpRequest.headers;
98
+ const isSecure = this.gate.isSecureRequest(headers);
99
+ const consoleOrigin = consoleOriginOf(headers, isSecure);
100
+ const result = await this.run(httpRequest, isSecure, consoleOrigin);
101
+ return buildAdminResponse(result, { consoleOrigin: consoleOrigin });
102
+ }
103
+ /**
104
+ * The pipeline up to the response: each step answers or goes on.
105
+ * @param httpRequest The Fetch request.
106
+ * @param isSecure True for a secure request (R9).
107
+ * @param consoleOrigin The console origin.
108
+ * @returns The result to answer with.
109
+ */
110
+ async run(httpRequest, isSecure, consoleOrigin) {
111
+ const headers = httpRequest.headers;
112
+ const clientAddress = AdminRequestGate.clientAddressOf(headers);
113
+ if (!this.gate.isNetworkAllowed(clientAddress))
114
+ return errorResult(HTTP_NOT_FOUND, "not found");
115
+ if (!(await this.isApiAvailable()))
116
+ return errorResult(HTTP_NOT_FOUND, "not found");
117
+ const url = new URL(httpRequest.url);
118
+ const apiPath = url.pathname.startsWith(ADMIN_API_PREFIX) ? url.pathname.substring(ADMIN_API_PREFIX.length) : "";
119
+ const method = httpRequest.method.toUpperCase();
120
+ const match = this.match(method, apiPath);
121
+ if (match.kind === "none")
122
+ return errorResult(HTTP_NOT_FOUND, "no such admin route");
123
+ if (match.kind === "wrongMethod")
124
+ return errorResult(HTTP_METHOD_NOT_ALLOWED, "this admin route does not take " + method);
125
+ const isTransportSafe = this.gate.isTransportSafe(headers, clientAddress);
126
+ if (!isTransportSafe)
127
+ return errorResult(HTTP_FORBIDDEN, "the console needs HTTPS");
128
+ let request = null;
129
+ let result;
130
+ try {
131
+ const body = await readJsonBody(httpRequest, method);
132
+ request = {
133
+ method: method,
134
+ apiPath: apiPath,
135
+ params: match.params,
136
+ query: url.searchParams,
137
+ headers: headers,
138
+ body: body,
139
+ clientAddress: clientAddress,
140
+ consoleOrigin: consoleOrigin,
141
+ isSecure: isSecure,
142
+ isTransportSafe: isTransportSafe,
143
+ caller: null
144
+ };
145
+ request.caller = await this.authenticate(request);
146
+ this.authorize(match.route.definition, request);
147
+ this.checkStepUp(match.route.definition, request);
148
+ result = await match.route.definition.handler(request);
149
+ }
150
+ catch (error) {
151
+ result = this.errorToResult(error, match.route.definition);
152
+ }
153
+ if (request != null)
154
+ await this.audit(match.route.definition, request, result);
155
+ return result;
156
+ }
157
+ /**
158
+ * Audits a call of an audited route (DR §6.3 step 11): successes and refusals of a known caller, and every call of
159
+ * a public route (sign-in attempts). Calls refused before authentication are not audited.
160
+ * @param route The route.
161
+ * @param request The request.
162
+ * @param result The answer.
163
+ */
164
+ async audit(route, request, result) {
165
+ if (route.auditAction == null)
166
+ return;
167
+ if (request.caller == null && route.permission !== "public")
168
+ return;
169
+ const action = (typeof route.auditAction === "function") ? route.auditAction(request) : route.auditAction;
170
+ const succeeded = result.status < FIRST_FAILURE_STATUS;
171
+ const caller = request.caller;
172
+ const callerActor = (caller != null) ? { kind: caller.kind, id: caller.id, name: caller.name } : ANONYMOUS_ACTOR;
173
+ const actor = (succeeded ? result.audit?.actor : undefined) ?? callerActor;
174
+ const paramValues = Object.values(request.params);
175
+ await this.services.audit.recordSafely({
176
+ actor: actor,
177
+ clientAddress: request.clientAddress,
178
+ processId: this.services.processId(),
179
+ action: action,
180
+ target: result.audit?.target ?? paramValues[0] ?? null,
181
+ parameters: result.audit?.parameters ?? Object.assign({}, request.params),
182
+ result: { ok: succeeded, detail: succeeded ? null : errorTextOf(result) }
183
+ });
184
+ }
185
+ /**
186
+ * Finds the route of a method and path; among several matches the one with the most literal segments wins.
187
+ * @param method The HTTP method.
188
+ * @param apiPath The path after ADMIN_API_PREFIX.
189
+ * @returns The route and its parameters, "wrongMethod" when only other methods match, or "none".
190
+ */
191
+ match(method, apiPath) {
192
+ if (!API_METHODS.has(method))
193
+ return { kind: "wrongMethod" };
194
+ const pathSegments = apiPath.split("/");
195
+ let best = null;
196
+ let pathMatched = false;
197
+ for (const route of this.routes) {
198
+ const params = matchSegments(route.segments, pathSegments);
199
+ if (params == null)
200
+ continue;
201
+ pathMatched = true;
202
+ if (route.definition.method !== method)
203
+ continue;
204
+ if (best == null || route.literalCount > best.route.literalCount)
205
+ best = { route: route, params: params };
206
+ }
207
+ if (best != null)
208
+ return { kind: "found", route: best.route, params: best.params };
209
+ return pathMatched ? { kind: "wrongMethod" } : { kind: "none" };
210
+ }
211
+ /**
212
+ * Finds the caller (DR §6.3 step 6): a bearer value (the legacy admin token, a full-access key, D18), else the
213
+ * session cookie (timeouts applied, D10), else nobody.
214
+ * @param request The request.
215
+ * @returns The caller, or null.
216
+ */
217
+ async authenticate(request) {
218
+ const bearer = bearerTokenOf(request.headers);
219
+ if (bearer != null)
220
+ return this.authenticateBearer(bearer, request);
221
+ const cookieValue = cookieValueOf(request.headers, sessionCookieName(request.isSecure));
222
+ if (cookieValue == null || cookieValue === "")
223
+ return null;
224
+ const resolved = await this.services.sessions.resolve(cookieValue);
225
+ if (resolved == null)
226
+ return null;
227
+ return { kind: "user", id: resolved.user.userId, name: resolved.user.displayName, permissions: ROLE_PERMISSIONS[resolved.user.role], session: resolved.record, user: resolved.user };
228
+ }
229
+ /**
230
+ * The caller of a bearer value: an API key (`bgsk_`), the legacy admin token, or nobody.
231
+ * @param bearer The bearer value.
232
+ * @param request The request.
233
+ * @returns The caller, or null.
234
+ */
235
+ async authenticateBearer(bearer, request) {
236
+ if (bearer.startsWith(API_KEY_PREFIX))
237
+ return this.services.apiKeys.verify(bearer, request.clientAddress);
238
+ const sentToken = { getHeader: (name) => request.headers.get(name) };
239
+ return hasAdminToken(this.services.state, sentToken) ? this.services.apiKeys.legacyCaller() : null;
240
+ }
241
+ /**
242
+ * Checks that the caller may run the route (DR §6.3 steps 6–8): a caller for non-public routes, signed-in admins
243
+ * only where API keys are not allowed, the CSRF token and Origin of cookie callers, then the permission.
244
+ * @param route The route.
245
+ * @param request The request with its caller.
246
+ * @throws AdminRequestError 401 without a caller, 403 for a key on an account route, a CSRF failure or a missing
247
+ * permission.
248
+ */
249
+ authorize(route, request) {
250
+ if (route.permission === "public")
251
+ return;
252
+ const caller = request.caller;
253
+ if (caller == null)
254
+ throw new AdminRequestError(HTTP_UNAUTHORIZED, "sign in first");
255
+ if (route.allowsApiKeys === false && caller.kind !== "user")
256
+ throw new AdminRequestError(HTTP_FORBIDDEN, "this route needs a signed-in admin, not an API key");
257
+ if (caller.kind === "user")
258
+ this.checkCsrf(request, caller);
259
+ if (route.permission === "signedIn")
260
+ return;
261
+ const required = (typeof route.permission === "function") ? route.permission(request) : route.permission;
262
+ if (!hasPermission(caller.permissions, required))
263
+ throw new AdminRequestError(HTTP_FORBIDDEN, "you do not have permission for this", required);
264
+ }
265
+ /**
266
+ * The CSRF and Origin rule of cookie callers (D10, DR §6.3 step 7): a state-changing request must send the
267
+ * session's CSRF token and no Origin, or the console's own.
268
+ * @param request The request.
269
+ * @param caller The cookie caller.
270
+ * @throws AdminRequestError 403 when the token is missing or wrong or the Origin is foreign.
271
+ */
272
+ checkCsrf(request, caller) {
273
+ if (!STATE_CHANGING_METHODS.has(request.method) || caller.session == null)
274
+ return;
275
+ const sentToken = request.headers.get(CSRF_HEADER);
276
+ const origin = request.headers.get("origin");
277
+ const tokenMatches = sentToken != null && textsMatch(sentToken, caller.session.csrfToken);
278
+ const originMatches = origin == null || origin === request.consoleOrigin;
279
+ if (!tokenMatches || !originMatches)
280
+ throw new AdminRequestError(HTTP_FORBIDDEN, "refused: missing or wrong CSRF token");
281
+ }
282
+ /**
283
+ * The step-up rule of dangerous routes (DR §6.3 step 9): a signed-in admin must have passed a second factor within
284
+ * STEP_UP_WINDOW_MS; API keys and the legacy token count as stepped up.
285
+ * @param route The route.
286
+ * @param request The request with its caller.
287
+ * @throws AdminRequestError 403 with detail "stepUp" when the check is too old.
288
+ */
289
+ checkStepUp(route, request) {
290
+ const requiresStepUp = (typeof route.requiresStepUp === "function") ? route.requiresStepUp(request) : route.requiresStepUp;
291
+ const session = request.caller?.session ?? null;
292
+ if (!requiresStepUp || request.caller?.kind !== "user" || session == null)
293
+ return;
294
+ const sinceSecondFactor = this.services.clock.now() - Date.parse(session.secondFactorAt);
295
+ if (sinceSecondFactor > STEP_UP_WINDOW_MS)
296
+ throw new AdminRequestError(HTTP_FORBIDDEN, "confirm with your second factor", "stepUp");
297
+ }
298
+ /**
299
+ * Turns a thrown error into its answer (DR §6.3 step 10).
300
+ * @param error What the pipeline or handler threw.
301
+ * @param route The matched route (for the log).
302
+ * @returns The error result.
303
+ */
304
+ errorToResult(error, route) {
305
+ if (error instanceof AdminRequestError)
306
+ return errorResult(error.status, error.message, error.detail);
307
+ if (error instanceof AdminConflictError)
308
+ return { status: HTTP_CONFLICT, body: { error: "changed by someone else", current: error.current } };
309
+ getLogger().error("admin route failed", { route: route.method + " " + route.pattern, error: String(error) });
310
+ return errorResult(HTTP_INTERNAL_ERROR, "internal error");
311
+ }
312
+ }
313
+ /**
314
+ * The Fetch request of a router context.
315
+ * @param context The router context.
316
+ * @returns The request.
317
+ * @throws Error when the endpoint was called without one (only a direct, non-router call can do that).
318
+ */
319
+ export function requestOf(context) {
320
+ if (context.request == null)
321
+ throw new Error("the admin endpoint was called without a request");
322
+ return context.request;
323
+ }
324
+ /**
325
+ * Matches a pattern's segments against a path's.
326
+ * @param patternSegments The pattern ("sessions", ":roomId", "close").
327
+ * @param pathSegments The request path's segments.
328
+ * @returns The named parameters (URI-decoded), or null when the path does not match.
329
+ */
330
+ function matchSegments(patternSegments, pathSegments) {
331
+ if (patternSegments.length !== pathSegments.length)
332
+ return null;
333
+ const params = {};
334
+ for (let i = 0; i < patternSegments.length; i++) {
335
+ const patternSegment = patternSegments[i];
336
+ const pathSegment = pathSegments[i];
337
+ if (!patternSegment.startsWith(PARAMETER_MARKER)) {
338
+ if (patternSegment !== pathSegment)
339
+ return null;
340
+ continue;
341
+ }
342
+ if (pathSegment === "")
343
+ return null;
344
+ const decoded = decodeSegment(pathSegment);
345
+ if (decoded == null)
346
+ return null;
347
+ params[patternSegment.substring(PARAMETER_MARKER.length)] = decoded;
348
+ }
349
+ return params;
350
+ }
351
+ /**
352
+ * URI-decodes a path segment.
353
+ * @param segment The raw segment.
354
+ * @returns The decoded text, or null when it is malformed.
355
+ */
356
+ function decodeSegment(segment) {
357
+ try {
358
+ return decodeURIComponent(segment);
359
+ }
360
+ catch {
361
+ return null;
362
+ }
363
+ }
364
+ /**
365
+ * The bearer token of a request.
366
+ * @param headers The request headers.
367
+ * @returns The token, or null when there is no bearer Authorization header.
368
+ */
369
+ export function bearerTokenOf(headers) {
370
+ const authorization = headers.get("authorization");
371
+ if (authorization == null || !authorization.startsWith(BEARER_PREFIX))
372
+ return null;
373
+ return authorization.substring(BEARER_PREFIX.length).trim();
374
+ }
375
+ /**
376
+ * The error sentence of a failed result, with its detail.
377
+ * @param result The result.
378
+ * @returns "<status> <error> (<detail>)".
379
+ */
380
+ function errorTextOf(result) {
381
+ const body = (result.body != null && typeof result.body === "object") ? result.body : {};
382
+ const detail = (typeof body.detail === "string") ? " (" + body.detail + ")" : "";
383
+ return result.status + " " + String(body.error ?? "") + detail;
384
+ }
385
+ /**
386
+ * A cookie's value from a request's Cookie header.
387
+ * @param headers The request headers.
388
+ * @param cookieName The cookie name.
389
+ * @returns The value, or null when the cookie is not sent.
390
+ */
391
+ function cookieValueOf(headers, cookieName) {
392
+ const cookieHeader = headers.get("cookie");
393
+ if (cookieHeader == null)
394
+ return null;
395
+ for (const part of cookieHeader.split(";")) {
396
+ const separatorIndex = part.indexOf("=");
397
+ if (separatorIndex < 0)
398
+ continue;
399
+ if (part.substring(0, separatorIndex).trim() === cookieName)
400
+ return part.substring(separatorIndex + 1).trim();
401
+ }
402
+ return null;
403
+ }
404
+ /**
405
+ * Reads a POST or PUT body as JSON, at most ADMIN_MAX_BODY_BYTES (DR §6.3 step 5). Other methods have no body.
406
+ * @param httpRequest The Fetch request.
407
+ * @param method The HTTP method.
408
+ * @returns The parsed body, or null when there is none.
409
+ * @throws AdminRequestError 413 when too large, 400 when not JSON.
410
+ */
411
+ async function readJsonBody(httpRequest, method) {
412
+ if (!BODY_METHODS.has(method))
413
+ return null;
414
+ const text = await readBodyText(httpRequest, ADMIN_MAX_BODY_BYTES);
415
+ if (text.trim() === "")
416
+ return null;
417
+ try {
418
+ return JSON.parse(text);
419
+ }
420
+ catch {
421
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "the body must be JSON");
422
+ }
423
+ }
424
+ /**
425
+ * Reads a request body, stopping as soon as it grows past the cap.
426
+ * @param httpRequest The Fetch request.
427
+ * @param maximumBytes The cap.
428
+ * @returns The body text.
429
+ * @throws AdminRequestError 413 past the cap.
430
+ */
431
+ async function readBodyText(httpRequest, maximumBytes) {
432
+ const tooLarge = new AdminRequestError(HTTP_PAYLOAD_TOO_LARGE, "the body is larger than " + maximumBytes + " bytes");
433
+ if (Number(httpRequest.headers.get("content-length") ?? 0) > maximumBytes)
434
+ throw tooLarge;
435
+ if (httpRequest.body == null)
436
+ return "";
437
+ const reader = httpRequest.body.getReader();
438
+ const chunks = [];
439
+ let totalBytes = 0;
440
+ for (;;) {
441
+ const { done, value } = await reader.read();
442
+ if (done)
443
+ break;
444
+ totalBytes += value.byteLength;
445
+ if (totalBytes > maximumBytes) {
446
+ await reader.cancel();
447
+ throw tooLarge;
448
+ }
449
+ chunks.push(value);
450
+ }
451
+ return Buffer.concat(chunks).toString("utf8");
452
+ }
453
+ /**
454
+ * An error result: { error, detail? }.
455
+ * @param status HTTP status.
456
+ * @param message The error sentence.
457
+ * @param detail Extra detail, or null.
458
+ * @returns The result.
459
+ */
460
+ export function errorResult(status, message, detail = null) {
461
+ const body = { error: message };
462
+ if (detail != null)
463
+ body.detail = detail;
464
+ return { status: status, body: body };
465
+ }